OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-protocol / abi-registry-v3.js

310 lines · javascript ref:
import b4a from 'b4a'
import c from 'compact-encoding'
import { protocolError } from './errors.js'
import { RELEASE_PROFILE_V2 } from './wire-v2.js'
import {
  FORWARD_HTTPS_DOMAIN_V3,
  FORWARD_HTTPS_SUCCESSOR_TRANSPORT_VARIANTS_V3,
  WIRE_V3_HASH_DOMAIN_PURPOSE,
  WIRE_V3_HASH_RECIPES,
  WIRE_V3_PROTOCOL,
  WIRE_V3_SCHEMA_DECLARATIONS
} from './wire-v3.js'

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

function list (encoding) {
  return {
    preencode (state, values) {
      c.uint.preencode(state, values.length)
      for (const value of values) encoding.preencode(state, value)
    },
    encode (state, values) {
      c.uint.encode(state, values.length)
      for (const value of values) encoding.encode(state, value)
    },
    decode (state) {
      const length = c.uint.decode(state)
      const values = []
      for (let i = 0; i < length; i++) values.push(encoding.decode(state))
      return values
    }
  }
}

const uintList = list(c.uint)

const schemaEncoding = {
  preencode (state, value) {
    c.uint.preencode(state, value.schemaId)
    c.string.preencode(state, value.schemaName)
    c.buffer.preencode(state, value.canonicalDeclarationBytes)
  },
  encode (state, value) {
    c.uint.encode(state, value.schemaId)
    c.string.encode(state, value.schemaName)
    c.buffer.encode(state, value.canonicalDeclarationBytes)
  },
  decode (state) {
    return {
      schemaId: c.uint.decode(state),
      schemaName: c.string.decode(state),
      canonicalDeclarationBytes: b4a.from(c.buffer.decode(state))
    }
  }
}

const profileEncoding = {
  preencode (state, value) {
    c.uint.preencode(state, value.profileId)
    c.string.preencode(state, value.canonicalName)
    c.uint.preencode(state, value.operationBits)
    c.uint.preencode(state, value.isDefault ? 1 : 0)
  },
  encode (state, value) {
    c.uint.encode(state, value.profileId)
    c.string.encode(state, value.canonicalName)
    c.uint.encode(state, value.operationBits)
    c.uint.encode(state, value.isDefault ? 1 : 0)
  },
  decode (state) {
    const value = {
      profileId: c.uint.decode(state),
      canonicalName: c.string.decode(state),
      operationBits: c.uint.decode(state),
      isDefault: c.uint.decode(state)
    }
    if (value.isDefault !== 0 && value.isDefault !== 1) fail('profile isDefault is outside 0..1')
    value.isDefault = value.isDefault === 1
    return value
  }
}

const variantEncoding = {
  preencode (state, value) {
    c.string.preencode(state, value.family)
    c.string.preencode(state, value.operation)
    c.uint.preencode(state, value.requestKind)
    c.uint.preencode(state, value.routeKind)
    c.uint.preencode(state, value.releaseProfileId)
    c.uint.preencode(state, value.requestSchemaId)
    c.uint.preencode(state, value.resultSchemaId)
    c.uint.preencode(state, value.requestBytes)
    c.uint.preencode(state, value.resultBytes)
  },
  encode (state, value) {
    c.string.encode(state, value.family)
    c.string.encode(state, value.operation)
    c.uint.encode(state, value.requestKind)
    c.uint.encode(state, value.routeKind)
    c.uint.encode(state, value.releaseProfileId)
    c.uint.encode(state, value.requestSchemaId)
    c.uint.encode(state, value.resultSchemaId)
    c.uint.encode(state, value.requestBytes)
    c.uint.encode(state, value.resultBytes)
  },
  decode (state) {
    return {
      family: c.string.decode(state),
      operation: c.string.decode(state),
      requestKind: c.uint.decode(state),
      routeKind: c.uint.decode(state),
      releaseProfileId: c.uint.decode(state),
      requestSchemaId: c.uint.decode(state),
      resultSchemaId: c.uint.decode(state),
      requestBytes: c.uint.decode(state),
      resultBytes: c.uint.decode(state)
    }
  }
}

const domainEncoding = {
  preencode (state, value) {
    c.uint.preencode(state, value.domainId)
    c.uint.preencode(state, value.purposeId)
    c.uint.preencode(state, value.recipeId)
    c.string.preencode(state, value.purpose)
    c.string.preencode(state, value.name)
    c.string.preencode(state, value.exactAsciiBytes)
  },
  encode (state, value) {
    c.uint.encode(state, value.domainId)
    c.uint.encode(state, value.purposeId)
    c.uint.encode(state, value.recipeId)
    c.string.encode(state, value.purpose)
    c.string.encode(state, value.name)
    c.string.encode(state, value.exactAsciiBytes)
  },
  decode (state) {
    return {
      domainId: c.uint.decode(state),
      purposeId: c.uint.decode(state),
      recipeId: c.uint.decode(state),
      purpose: c.string.decode(state),
      name: c.string.decode(state),
      exactAsciiBytes: c.string.decode(state)
    }
  }
}

const hashRecipeEncoding = {
  preencode (state, value) {
    c.uint.preencode(state, value.recipeId)
    c.string.preencode(state, value.name)
    c.string.preencode(state, value.canonicalPreimage)
  },
  encode (state, value) {
    c.uint.encode(state, value.recipeId)
    c.string.encode(state, value.name)
    c.string.encode(state, value.canonicalPreimage)
  },
  decode (state) {
    return {
      recipeId: c.uint.decode(state),
      name: c.string.decode(state),
      canonicalPreimage: c.string.decode(state)
    }
  }
}

const schemasEncoding = list(schemaEncoding)
const profilesEncoding = list(profileEncoding)
const variantsEncoding = list(variantEncoding)
const domainsEncoding = list(domainEncoding)
const hashRecipesEncoding = list(hashRecipeEncoding)

export const wireAbiV3Encoding = {
  preencode (state, value) {
    c.string.preencode(state, value.magic)
    c.uint.preencode(state, value.formatVersion)
    c.string.preencode(state, value.protocolFamily)
    c.uint.preencode(state, value.protocolMajor)
    c.uint.preencode(state, value.protocolMinor)
    c.buffer.preencode(state, value.baseWireV2AbiHash)
    c.uint.preencode(state, value.baseSchemaCount)
    uintList.preencode(state, value.compatibilityOnlySchemaIds)
    schemasEncoding.preencode(state, value.additionalSchemas)
    profilesEncoding.preencode(state, value.releaseProfiles)
    variantsEncoding.preencode(state, value.successorTransportVariants)
    domainsEncoding.preencode(state, value.additionalDomains)
    c.uint.preencode(state, value.hashDomainPurposeId)
    hashRecipesEncoding.preencode(state, value.hashRecipes)
    c.uint.preencode(state, value.forwardReadinessOperationBits)
  },
  encode (state, value) {
    c.string.encode(state, value.magic)
    c.uint.encode(state, value.formatVersion)
    c.string.encode(state, value.protocolFamily)
    c.uint.encode(state, value.protocolMajor)
    c.uint.encode(state, value.protocolMinor)
    c.buffer.encode(state, value.baseWireV2AbiHash)
    c.uint.encode(state, value.baseSchemaCount)
    uintList.encode(state, value.compatibilityOnlySchemaIds)
    schemasEncoding.encode(state, value.additionalSchemas)
    profilesEncoding.encode(state, value.releaseProfiles)
    variantsEncoding.encode(state, value.successorTransportVariants)
    domainsEncoding.encode(state, value.additionalDomains)
    c.uint.encode(state, value.hashDomainPurposeId)
    hashRecipesEncoding.encode(state, value.hashRecipes)
    c.uint.encode(state, value.forwardReadinessOperationBits)
  },
  decode (state) {
    return {
      magic: c.string.decode(state),
      formatVersion: c.uint.decode(state),
      protocolFamily: c.string.decode(state),
      protocolMajor: c.uint.decode(state),
      protocolMinor: c.uint.decode(state),
      baseWireV2AbiHash: b4a.from(c.buffer.decode(state)),
      baseSchemaCount: c.uint.decode(state),
      compatibilityOnlySchemaIds: uintList.decode(state),
      additionalSchemas: schemasEncoding.decode(state),
      releaseProfiles: profilesEncoding.decode(state),
      successorTransportVariants: variantsEncoding.decode(state),
      additionalDomains: domainsEncoding.decode(state),
      hashDomainPurposeId: c.uint.decode(state),
      hashRecipes: hashRecipesEncoding.decode(state),
      forwardReadinessOperationBits: c.uint.decode(state)
    }
  }
}

const declarationBytes = schema => b4a.from(JSON.stringify({ name: schema.schemaName, fields: schema.fields }), 'utf8')

export function createWireAbiV3Value (baseWireV2AbiHash) {
  if (!baseWireV2AbiHash || baseWireV2AbiHash.byteLength !== 32) fail('baseWireV2AbiHash must be exactly 32 bytes')
  return {
    magic: 'hiverelay-blind-wire-abi-v3',
    formatVersion: WIRE_V3_PROTOCOL.abiFormatVersion,
    protocolFamily: 'hiverelay-blind-wire',
    protocolMajor: WIRE_V3_PROTOCOL.major,
    protocolMinor: WIRE_V3_PROTOCOL.minor,
    baseWireV2AbiHash: b4a.from(baseWireV2AbiHash),
    baseSchemaCount: 75,
    compatibilityOnlySchemaIds: [74, 75],
    additionalSchemas: WIRE_V3_SCHEMA_DECLARATIONS.map(schema => ({
      schemaId: schema.schemaId,
      schemaName: schema.schemaName,
      canonicalDeclarationBytes: declarationBytes(schema)
    })),
    releaseProfiles: Object.entries(RELEASE_PROFILE_V2).map(([canonicalName, profile]) => ({
      profileId: profile.id,
      canonicalName,
      operationBits: profile.operationBits,
      isDefault: profile.isDefault
    })),
    successorTransportVariants: FORWARD_HTTPS_SUCCESSOR_TRANSPORT_VARIANTS_V3.map(value => ({ ...value })),
    additionalDomains: Object.values(FORWARD_HTTPS_DOMAIN_V3).map(value => ({ ...value })),
    hashDomainPurposeId: WIRE_V3_HASH_DOMAIN_PURPOSE.purposeId,
    hashRecipes: WIRE_V3_HASH_RECIPES.map(value => ({ ...value })),
    forwardReadinessOperationBits: 0
  }
}

export function encodeWireAbiV3 (value) {
  const state = { start: 0, end: 0, buffer: null }
  wireAbiV3Encoding.preencode(state, value)
  state.buffer = b4a.alloc(state.end)
  state.start = 0
  wireAbiV3Encoding.encode(state, value)
  if (state.start !== state.end) fail('WIRE v3 ABI encoder did not fill its allocation')
  return state.buffer
}

function exactIds (values, expected, field) {
  if (values.length !== expected.length || values.some((value, index) => value !== expected[index])) fail(`${field} fixed order is invalid`)
}

export function decodeWireAbiV3 (input) {
  const state = { start: 0, end: input.byteLength, buffer: input }
  const value = wireAbiV3Encoding.decode(state)
  if (state.start !== state.end) fail('trailing bytes after WIRE v3 ABI')
  if (!b4a.equals(encodeWireAbiV3(value), input)) fail('WIRE v3 ABI is not canonical')
  if (value.magic !== 'hiverelay-blind-wire-abi-v3' || value.formatVersion !== 3 ||
      value.protocolFamily !== 'hiverelay-blind-wire' || value.protocolMajor !== 1 || value.protocolMinor !== 2 ||
      value.baseWireV2AbiHash.byteLength !== 32 || value.baseSchemaCount !== 75 ||
      value.hashDomainPurposeId !== 4 || value.forwardReadinessOperationBits !== 0) {
    fail('WIRE v3 ABI fixed header is invalid')
  }
  exactIds(value.compatibilityOnlySchemaIds, [74, 75], 'compatibility-only schema IDs')
  exactIds(value.additionalSchemas.map(schema => schema.schemaId), [76, 77], 'additional schema IDs')
  const profiles = value.releaseProfiles
  if (profiles.length !== 2 || profiles[0].profileId !== 1 || profiles[0].canonicalName !== 'LIMITED_PUBLIC_TEST_V1' ||
      profiles[0].operationBits !== 131071 || profiles[0].isDefault !== true || profiles[1].profileId !== 2 ||
      profiles[1].canonicalName !== 'LIMITED_PUBLIC_TEST_FORWARD_ONE_HOP_V1' || profiles[1].operationBits !== 4063231 ||
      profiles[1].isDefault !== false) {
    fail('releaseProfiles fixed logical rows or order are invalid')
  }
  exactIds(value.successorTransportVariants.map(variant => variant.requestKind), [1, 2, 3, 4], 'successor transport variants')
  exactIds(value.additionalDomains.map(domain => domain.domainId), [18, 113, 114, 115, 215, 216, 217, 218, 219], 'additional domains')
  exactIds(value.hashRecipes.map(recipe => recipe.recipeId), [3, 4], 'hash recipes')
  for (const domain of value.additionalDomains.slice(5)) {
    if (domain.purposeId !== 4 || domain.purpose !== 'HASH_DOMAIN' || (domain.recipeId !== 3 && domain.recipeId !== 4)) {
      fail('WIRE v3 HASH domain semantics are invalid')
    }
  }
  return value
}