OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-protocol / forward-route-scope.js

94 lines · javascript ref:
import b4a from 'b4a'
import { protocolError } from './errors.js'
import { blake2b256 } from './hashes.js'
import {
  AUXILIARY_SIGNATURE_DOMAIN_ID,
  DOMAIN_PURPOSE,
  DOMAIN_RECIPE,
  domainRegistryEntry
} from './registry.js'

const MAX_U64 = (1n << 64n) - 1n

export const FORWARD_ROUTE_SCOPE_MAX_RELAY_COUNT = 4
export const FORWARD_ROUTE_SCOPE_GENESIS_DOMAIN = 'hiverelay.blind.forward-scope-genesis.v1'
export const FORWARD_ROUTE_SCOPE_HOP_DOMAIN = 'hiverelay.blind.forward-scope-hop.v1'

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

function bytes (value, length, field) {
  if (!value || typeof value.byteLength !== 'number') fail(`${field} must be bytes`)
  const output = b4a.isBuffer(value)
    ? value
    : ArrayBuffer.isView(value)
      ? b4a.from(value.buffer, value.byteOffset, value.byteLength)
      : b4a.from(value)
  if (output.byteLength !== length) fail(`${field} must be exactly ${length} bytes`)
  return output
}

function unsigned (value, length, maximum, field) {
  if (!Number.isSafeInteger(value) || value < 0 || value > maximum) fail(`${field} is outside its unsigned range`)
  const output = b4a.alloc(length)
  for (let index = length - 1; index >= 0; index--) {
    output[index] = value & 0xff
    value = Math.floor(value / 0x100)
  }
  return output
}

function u64 (value, field) {
  if (typeof value === 'number') {
    if (!Number.isSafeInteger(value) || value < 0) fail(`${field} is outside u64`)
    value = BigInt(value)
  }
  if (typeof value !== 'bigint' || value < 0n || value > MAX_U64) fail(`${field} is outside u64`)
  const output = b4a.alloc(8)
  for (let index = 7; index >= 0; index--) {
    output[index] = Number(value & 0xffn)
    value >>= 8n
  }
  return output
}

export function forwardRouteScopeGenesisHash (value) {
  if (!value || typeof value !== 'object') fail('route-scope genesis input must be an object')
  return blake2b256(b4a.concat([
    b4a.from(FORWARD_ROUTE_SCOPE_GENESIS_DOMAIN, 'ascii'),
    bytes(value.rootRouteId, 16, 'rootRouteId'),
    bytes(value.rootCircuitNonce, 32, 'rootCircuitNonce'),
    bytes(value.rootRequestCommitment, 32, 'rootRequestCommitment'),
    unsigned(value.maxRelayCount, 1, FORWARD_ROUTE_SCOPE_MAX_RELAY_COUNT, 'maxRelayCount'),
    unsigned(value.expiresEpoch, 4, 0xffffffff, 'expiresEpoch')
  ]))
}

export function forwardRouteScopeHopHash (value) {
  if (!value || typeof value !== 'object') fail('route-scope hop hash input must be an object')
  return blake2b256(b4a.concat([
    b4a.from(FORWARD_ROUTE_SCOPE_HOP_DOMAIN, 'ascii'),
    bytes(value.previousScopeHash, 32, 'previousScopeHash'),
    unsigned(value.hopIndex, 1, FORWARD_ROUTE_SCOPE_MAX_RELAY_COUNT - 1, 'hopIndex'),
    bytes(value.relayPublicKey, 32, 'relayPublicKey'),
    u64(value.descriptorSequence, 'descriptorSequence'),
    bytes(value.descriptorHash, 32, 'descriptorHash')
  ]))
}

export function forwardRouteScopeSignaturePayload (scopeHash) {
  scopeHash = bytes(scopeHash, 32, 'scopeHash')
  const domainId = AUXILIARY_SIGNATURE_DOMAIN_ID.FORWARD_ROUTE_SCOPE
  const domain = domainRegistryEntry(domainId)
  if (!domain || domain.purpose !== DOMAIN_PURPOSE.AUXILIARY_SIGNATURE ||
      domain.recipeId !== DOMAIN_RECIPE.ED25519_DOMAIN_LEN64_PAYLOAD) {
    fail('forward route-scope signature domain is absent from the draft registry')
  }
  return b4a.concat([
    b4a.from(domain.exactAsciiBytes, 'ascii'),
    u64(scopeHash.byteLength, 'scopeHash length'),
    scopeHash
  ])
}