OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / circuit-relay-encoding.test.js

170 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import c from 'compact-encoding'
import {
  CIRCUIT_ID_BYTES,
  MAX_CIRCUIT_DATA_MSG_BYTES,
  MAX_CIRCUIT_STATUS_MESSAGE_BYTES,
  circuitConnectEncoding,
  circuitDataEncoding,
  circuitStatusEncoding,
  circuitReadyEncoding,
  circuitCloseEncoding
} from 'p2p-hiverelay/core/protocol/relay-circuit.js'
import { ERR } from 'p2p-hiverelay/core/protocol/messages.js'

function encodeFrame (encoding, msg) {
  const state = { start: 0, end: 0, buffer: null }
  encoding.preencode(state, msg)
  state.buffer = b4a.alloc(state.end)
  encoding.encode(state, msg)
  return state.buffer
}

function declaredDataFrame (circuitId, len, payload = null) {
  const state = { start: 0, end: 0, buffer: null }
  c.fixed(CIRCUIT_ID_BYTES).preencode(state, circuitId)
  c.uint.preencode(state, len)
  state.end += payload ? payload.length : 0
  state.buffer = b4a.alloc(state.end)
  c.fixed(CIRCUIT_ID_BYTES).encode(state, circuitId)
  c.uint.encode(state, len)
  if (payload) payload.copy(state.buffer, state.start)
  return state.buffer
}

function declaredStatusFrame (code, len, payload = null) {
  const state = { start: 0, end: 0, buffer: null }
  c.uint.preencode(state, code)
  c.uint.preencode(state, len)
  state.end += payload ? payload.length : 0
  state.buffer = b4a.alloc(state.end)
  c.uint.encode(state, code)
  c.uint.encode(state, len)
  if (payload) payload.copy(state.buffer, state.start)
  return state.buffer
}

const circuitId = b4a.alloc(CIRCUIT_ID_BYTES, 0x11)
const targetPubkey = b4a.alloc(32, 0x22)
const sourcePubkey = b4a.alloc(32, 0x33)

test('circuit-relay encodings: round-trip valid frames', (t) => {
  const connectFrame = encodeFrame(circuitConnectEncoding, { targetPubkey, sourcePubkey })
  t.alike(circuitConnectEncoding.decode({ buffer: connectFrame, start: 0, end: connectFrame.length }), { targetPubkey, sourcePubkey })

  const data = b4a.from('hello')
  const dataFrame = encodeFrame(circuitDataEncoding, { circuitId, data })
  t.alike(circuitDataEncoding.decode({ buffer: dataFrame, start: 0, end: dataFrame.length }), { circuitId, data })

  const statusFrame = encodeFrame(circuitStatusEncoding, { code: 0, message: 'ok' })
  t.alike(circuitStatusEncoding.decode({ buffer: statusFrame, start: 0, end: statusFrame.length }), { code: 0, message: 'ok' })

  const readyFrame = encodeFrame(circuitReadyEncoding, { circuitId, remotePubkey: targetPubkey })
  t.alike(circuitReadyEncoding.decode({ buffer: readyFrame, start: 0, end: readyFrame.length }), { circuitId, remotePubkey: targetPubkey })

  const closeFrame = encodeFrame(circuitCloseEncoding, { circuitId, reason: 2 })
  t.alike(circuitCloseEncoding.decode({ buffer: closeFrame, start: 0, end: closeFrame.length }), { circuitId, reason: 2 })
})

test('circuit-relay encodings: reject bad outbound messages before allocation growth', (t) => {
  const badConnect = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    circuitConnectEncoding.preencode(badConnect, { targetPubkey, sourcePubkey: b4a.alloc(31) })
  }, /sourcePubkey/, 'connect preencode rejects invalid source')
  t.is(badConnect.end, 0, 'bad connect does not grow state.end')

  const badData = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    circuitDataEncoding.preencode(badData, { circuitId, data: b4a.alloc(MAX_CIRCUIT_DATA_MSG_BYTES + 1) })
  }, /frame too large/, 'data preencode rejects oversize')
  t.is(badData.end, 0, 'bad data does not grow state.end')

  const badStatus = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    circuitStatusEncoding.preencode(badStatus, { code: -1, message: 'bad' })
  }, /status code/, 'status preencode rejects invalid code')
  t.is(badStatus.end, 0, 'bad status does not grow state.end')

  const badReady = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    circuitReadyEncoding.preencode(badReady, { circuitId, remotePubkey: b4a.alloc(31) })
  }, /remotePubkey/, 'ready preencode rejects invalid remote pubkey')
  t.is(badReady.end, 0, 'bad ready does not grow state.end')

  const badClose = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    circuitCloseEncoding.preencode(badClose, { circuitId, reason: -1 })
  }, /close reason/, 'close preencode rejects invalid reason')
  t.is(badClose.end, 0, 'bad close does not grow state.end')
})

test('circuit-relay encodings: reject oversized declared inbound frames before decode materializes them', (t) => {
  const dataFrame = declaredDataFrame(circuitId, MAX_CIRCUIT_DATA_MSG_BYTES + 1)
  let dataOut = null
  t.execution(() => {
    dataOut = circuitDataEncoding.decode({ buffer: dataFrame, start: 0, end: dataFrame.length })
  }, 'oversized declared data frame does not throw')
  t.alike(dataOut.circuitId, circuitId)
  t.is(dataOut.data, null)
  t.is(dataOut.error, 'frame too large')

  const statusFrame = declaredStatusFrame(0, MAX_CIRCUIT_STATUS_MESSAGE_BYTES + 1)
  let statusOut = null
  t.execution(() => {
    statusOut = circuitStatusEncoding.decode({ buffer: statusFrame, start: 0, end: statusFrame.length })
  }, 'oversized declared status frame does not throw')
  t.is(statusOut.code, ERR.INVALID_REQUEST)
  t.is(statusOut.error, 'status message too large')
})

test('circuit-relay encodings: reject malformed and truncated frames without throwing', (t) => {
  const connectFrame = b4a.alloc(63)
  let connectOut = null
  t.execution(() => {
    connectOut = circuitConnectEncoding.decode({ buffer: connectFrame, start: 0, end: connectFrame.length })
  }, 'truncated connect frame does not throw')
  t.alike(connectOut.targetPubkey, connectFrame.subarray(0, 32))
  t.is(connectOut.sourcePubkey, null)
  t.is(connectOut.error, 'malformed connect')

  const dataFrame = declaredDataFrame(circuitId, 32)
  let dataOut = null
  t.execution(() => {
    dataOut = circuitDataEncoding.decode({ buffer: dataFrame, start: 0, end: dataFrame.length })
  }, 'truncated data frame does not throw')
  t.alike(dataOut.circuitId, circuitId)
  t.is(dataOut.data, null)
  t.is(dataOut.error, 'malformed data')

  const readyFrame = b4a.alloc(CIRCUIT_ID_BYTES + 31)
  let readyOut = null
  t.execution(() => {
    readyOut = circuitReadyEncoding.decode({ buffer: readyFrame, start: 0, end: readyFrame.length })
  }, 'truncated ready frame does not throw')
  t.is(readyOut.remotePubkey, null)
  t.is(readyOut.error, 'malformed ready')
})

test('circuit-relay encodings: reject invalid decode state without throwing', (t) => {
  const frame = encodeFrame(circuitDataEncoding, { circuitId, data: b4a.from('ok') })
  const cases = [
    { name: 'missing state', state: null },
    { name: 'missing buffer', state: { start: 0, end: 0 } },
    { name: 'negative start', state: { buffer: frame, start: -1, end: frame.length } },
    { name: 'end before start', state: { buffer: frame, start: 4, end: 2 } },
    { name: 'end past buffer', state: { buffer: frame, start: 0, end: frame.length + 1 } },
    { name: 'unsafe start', state: { buffer: frame, start: Number.MAX_SAFE_INTEGER + 1, end: frame.length } }
  ]

  for (const { name, state } of cases) {
    let result = null
    t.execution(() => {
      result = circuitDataEncoding.decode(state)
    }, `${name} does not throw`)
    t.is(result.circuitId, null, `${name} circuitId is null`)
    t.is(result.data, null, `${name} data is null`)
    t.is(result.error, 'malformed data', `${name} error`)
  }
})