OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / verify-multiprocess-blind-relay-report.mjs

763 lines · javascript ref:
#!/usr/bin/env node
import { createHash, timingSafeEqual } from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import {
  REAL_BLIND_RELAY_CANONICALIZATION,
  canonicalRealBlindRelayReportJson,
  verifyRealBlindRelayReport
} from './verify-real-blind-relay-report.mjs'

export const LOCAL_MULTIPROCESS_BLIND_CHILD_SCHEMA =
  'hiverelay.blind.local-multiprocess-child.v1'
export const LOCAL_MULTIPROCESS_BLIND_REPORT_SCHEMA =
  'hiverelay.blind.local-multiprocess-relay-lab.v1'
export const LOCAL_MULTIPROCESS_BLIND_MANDATORY_BLOCKERS = Object.freeze([
  'CHECKSUM_ONLY_NOT_AUTHENTICATED_EVIDENCE',
  'SAME_HOST_LOCAL_PROCESSES',
  'SAME_UID_TEST_TOPOLOGY',
  'LOGICAL_OPERATOR_SCOPES_NOT_INDEPENDENT_OWNERSHIP',
  'SYNTHETIC_ADMISSION_NO_ECONOMIC_SETTLEMENT',
  'PRODUCTION_RELEASE_GATE_BYPASSED',
  'INDEPENDENT_OPERATOR_FAILURE_DOMAINS_UNMEASURED',
  'MULTI_HOST_NETWORK_UNMEASURED',
  'PUBLIC_CA_TLS_VALIDATION_UNMEASURED',
  'INTERNET_LATENCY_AND_PACKET_LOSS_UNMEASURED',
  'MULTI_RELAY_REPLICATION_REPAIR_PROTOCOL_UNMEASURED',
  'RELAY_CHURN_AND_PARTITION_UNMEASURED',
  'LONG_DURATION_SOAK_UNMEASURED',
  'CRASH_KILL_DURING_COMMIT_UNMEASURED',
  'DISK_FULL_AND_CORRUPTION_INJECTION_UNMEASURED',
  'RESOURCE_SATURATION_UNMEASURED',
  'SIGNED_RELEASE_AND_ROLLBACK_UNMEASURED'
])

const EMPTY_SHA256 = createHash('sha256').update('').digest('hex')
const CHILD_ROOT_KEYS = Object.freeze([
  'evidenceDigest',
  'generatedAt',
  'identity',
  'labReport',
  'process',
  'schema',
  'source'
])
const REPORT_ROOT_KEYS = Object.freeze([
  'blockers',
  'children',
  'correctnessGateReady',
  'evidenceBinding',
  'evidenceClass',
  'evidenceDigest',
  'gates',
  'generatedAt',
  'localGateReady',
  'processExecution',
  'releaseReady',
  'schema',
  'scope',
  'sourceCommit',
  'summary',
  'workload'
])

function fail (message) {
  const error = new Error(message)
  error.code = 'BLIND_LOCAL_MULTIPROCESS_REPORT_INVALID'
  throw error
}

function record (value, field) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${field} must be an object`)
  return value
}

function exactKeys (value, expectedKeys, field) {
  record(value, field)
  const actual = Object.keys(value).sort()
  const expected = [...expectedKeys].sort()
  if (JSON.stringify(actual) !== JSON.stringify(expected)) {
    fail(`${field} fields are inconsistent; expected ${expected.join(',')}, got ${actual.join(',')}`)
  }
}

function safeInteger (value, minimum, maximum, field) {
  if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
    fail(`${field} must be an integer within ${minimum}..${maximum}`)
  }
  return value
}

function finiteNumber (value, minimum, field) {
  if (!Number.isFinite(value) || value < minimum) fail(`${field} must be a finite number >= ${minimum}`)
  return value
}

function boolean (value, field) {
  if (typeof value !== 'boolean') fail(`${field} must be boolean`)
  return value
}

function lowerHex (value, bytes, field) {
  if (typeof value !== 'string' || !new RegExp(`^[0-9a-f]{${bytes * 2}}$`).test(value)) {
    fail(`${field} must be ${bytes}-byte lowercase hex`)
  }
  return value
}

function scopeName (value, field) {
  if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(value)) {
    fail(`${field} must match ^[a-z0-9][a-z0-9-]{0,63}$`)
  }
  return value
}

function isoMillis (value, field) {
  if (typeof value !== 'string' || Number.isNaN(Date.parse(value)) ||
      new Date(value).toISOString() !== value) {
    fail(`${field} must be a canonical ISO timestamp`)
  }
  return Date.parse(value)
}

function exactArray (actual, expected, field) {
  if (!Array.isArray(actual) || JSON.stringify(actual) !== JSON.stringify(expected)) {
    fail(`${field} is inconsistent with measured evidence`)
  }
}

function exactObject (actual, expected, field) {
  exactKeys(actual, Object.keys(expected), field)
  for (const [key, value] of Object.entries(expected)) {
    if (Array.isArray(value)) exactArray(actual[key], value, `${field}.${key}`)
    else if (value && typeof value === 'object') exactObject(actual[key], value, `${field}.${key}`)
    else if (actual[key] !== value) fail(`${field}.${key} is inconsistent with measured evidence`)
  }
}

function uniqueCount (values) {
  return new Set(values).size
}

function checksum (value) {
  return createHash('sha256').update(value).digest('hex')
}

function reportBody (report) {
  record(report, 'report')
  const body = { ...report }
  delete body.evidenceDigest
  return body
}

function digestReport (report) {
  return checksum(canonicalRealBlindRelayReportJson(reportBody(report)))
}

function assertDigest (report, field = 'evidenceDigest') {
  lowerHex(report.evidenceDigest, 32, field)
  const expected = digestReport(report)
  if (!timingSafeEqual(Buffer.from(report.evidenceDigest, 'hex'), Buffer.from(expected, 'hex'))) {
    fail(`${field} checksum mismatch`)
  }
}

export function sealLocalMultiprocessBlindChild (body) {
  record(body, 'child body')
  if (Object.hasOwn(body, 'evidenceDigest')) fail('child body must not contain evidenceDigest before sealing')
  const report = { ...body }
  return Object.freeze({ ...report, evidenceDigest: digestReport(report) })
}

export function sealLocalMultiprocessBlindReport (body) {
  record(body, 'aggregate body')
  if (Object.hasOwn(body, 'evidenceDigest')) fail('aggregate body must not contain evidenceDigest before sealing')
  const report = { ...body }
  return Object.freeze({ ...report, evidenceDigest: digestReport(report) })
}

function validateEvidenceBinding (binding) {
  exactObject(binding, {
    algorithm: 'sha256',
    canonicalization: REAL_BLIND_RELAY_CANONICALIZATION,
    checksumOnly: true,
    signed: false,
    authenticityProven: false
  }, 'evidenceBinding')
}

export function verifyLocalMultiprocessBlindChild (child, options = {}) {
  exactKeys(child, CHILD_ROOT_KEYS, 'child')
  if (child.schema !== LOCAL_MULTIPROCESS_BLIND_CHILD_SCHEMA) {
    fail(`unsupported child schema ${JSON.stringify(child.schema)}`)
  }
  const generatedAtMillis = isoMillis(child.generatedAt, 'child.generatedAt')
  assertDigest(child, 'child.evidenceDigest')

  exactKeys(child.source, ['gitCommit', 'trackedWorktreeClean'], 'child.source')
  lowerHex(child.source.gitCommit, 20, 'child.source.gitCommit')
  boolean(child.source.trackedWorktreeClean, 'child.source.trackedWorktreeClean')

  exactKeys(child.process, [
    'finishedAt',
    'gid',
    'identityScope',
    'logicalOperatorScope',
    'parentPid',
    'pid',
    'rootPathSha256',
    'startedAt',
    'uid',
    'wallMs'
  ], 'child.process')
  const pid = safeInteger(child.process.pid, 1, Number.MAX_SAFE_INTEGER, 'child.process.pid')
  safeInteger(child.process.parentPid, 1, Number.MAX_SAFE_INTEGER, 'child.process.parentPid')
  safeInteger(child.process.uid, 0, Number.MAX_SAFE_INTEGER, 'child.process.uid')
  safeInteger(child.process.gid, 0, Number.MAX_SAFE_INTEGER, 'child.process.gid')
  scopeName(child.process.logicalOperatorScope, 'child.process.logicalOperatorScope')
  const identityScope = scopeName(child.process.identityScope, 'child.process.identityScope')
  lowerHex(child.process.rootPathSha256, 32, 'child.process.rootPathSha256')
  const startedAtMillis = isoMillis(child.process.startedAt, 'child.process.startedAt')
  const finishedAtMillis = isoMillis(child.process.finishedAt, 'child.process.finishedAt')
  if (finishedAtMillis < startedAtMillis) fail('child process finished before it started')
  if (generatedAtMillis !== finishedAtMillis) fail('child.generatedAt must equal child.process.finishedAt')
  if (child.process.wallMs !== finishedAtMillis - startedAtMillis) {
    fail('child.process.wallMs is inconsistent with its timestamps')
  }

  exactKeys(child.identity, [
    'identityScope',
    'relayCount',
    'relayPublicKeys',
    'storeIds',
    'uniqueRelayPublicKeys',
    'uniqueStoreIds'
  ], 'child.identity')
  if (child.identity.identityScope !== identityScope) fail('child identity scope does not match process scope')
  const relayCount = safeInteger(child.identity.relayCount, 2, 8, 'child.identity.relayCount')
  for (const [field, values] of [
    ['relayPublicKeys', child.identity.relayPublicKeys],
    ['storeIds', child.identity.storeIds]
  ]) {
    if (!Array.isArray(values) || values.length !== relayCount) {
      fail(`child.identity.${field} must contain one value per relay`)
    }
    values.forEach((value, index) => lowerHex(value, 32, `child.identity.${field}[${index}]`))
  }
  const uniqueRelayPublicKeys = uniqueCount(child.identity.relayPublicKeys)
  const uniqueStoreIds = uniqueCount(child.identity.storeIds)
  if (child.identity.uniqueRelayPublicKeys !== uniqueRelayPublicKeys ||
      child.identity.uniqueStoreIds !== uniqueStoreIds) {
    fail('child identity uniqueness counts are inconsistent')
  }
  if (uniqueRelayPublicKeys !== relayCount || uniqueStoreIds !== relayCount) {
    fail('child relay signing keys and store IDs must be unique')
  }

  const labVerification = verifyRealBlindRelayReport(child.labReport, { requireCorrectness: true })
  if (child.labReport.scope.relayInstances !== relayCount) {
    fail('child identity count does not match the real relay report')
  }
  if (options.expectedPid != null && pid !== options.expectedPid) fail('child PID differs from supervisor observation')
  if (options.expectedParentPid != null && child.process.parentPid !== options.expectedParentPid) {
    fail('child parent PID differs from supervisor PID')
  }
  if (options.expectedGitCommit != null && child.source.gitCommit !== options.expectedGitCommit) {
    fail('child source commit differs from supervisor source commit')
  }
  if (options.requireCleanSource === true && child.source.trackedWorktreeClean !== true) {
    fail('child tracked source is not clean')
  }
  return Object.freeze({
    verified: true,
    checksumVerified: true,
    checksumOnly: true,
    authenticityProven: false,
    authorizesRelease: false,
    correctnessReady: labVerification.correctnessReady,
    performanceReady: labVerification.performanceReady,
    pid,
    gitCommit: child.source.gitCommit,
    trackedWorktreeClean: child.source.trackedWorktreeClean
  })
}

function latencyAggregate (children, metric) {
  const rows = children.map(child => child.envelope.labReport.metrics[metric])
  const count = rows.reduce((sum, row) => sum + row.count, 0)
  const weightedMean = rows.reduce((sum, row) => sum + row.meanMs * row.count, 0) / count
  return {
    count,
    weightedMeanMs: Number(weightedMean.toFixed(3)),
    maximumChildP99Ms: Number(Math.max(...rows.map(row => row.p99Ms)).toFixed(3)),
    maximumObservedMs: Number(Math.max(...rows.map(row => row.maxMs)).toFixed(3)),
    minimumChildOperationsPerSecond: Number(
      Math.min(...rows.map(row => row.operationsPerSecond)).toFixed(3)
    )
  }
}

function expectedSummary (children) {
  const reports = children.map(child => child.envelope.labReport)
  const beforeRows = reports.flatMap(report => report.integrity.exactStoreCountsBeforeRestart)
  const afterRows = reports.flatMap(report => report.integrity.exactStoreCountsAfterRestart)
  const relayPublicKeys = children.flatMap(child => child.envelope.identity.relayPublicKeys)
  const storeIds = children.flatMap(child => child.envelope.identity.storeIds)
  return {
    childLabEvidenceDigests: reports.map(report => report.evidenceDigest),
    childEnvelopeEvidenceDigests: children.map(child => child.envelope.evidenceDigest),
    sourceCommits: children.map(child => child.envelope.source.gitCommit),
    totalRelayInstances: reports.reduce((sum, report) => sum + report.scope.relayInstances, 0),
    totalAttemptedCellWrites: reports.reduce((sum, report) => sum + report.load.attemptedCellWrites, 0),
    totalPublicReadChecks: reports.reduce((sum, report) =>
      sum + report.integrity.contentChecksBeforeRestart, 0),
    totalRecoveredReadChecks: reports.reduce((sum, report) =>
      sum + report.integrity.contentChecksAfterRestart, 0),
    totalRelaysStopped: reports.reduce((sum, report) => sum + report.recovery.relaysStopped, 0),
    totalRelaysRestarted: reports.reduce((sum, report) => sum + report.recovery.relaysRestarted, 0),
    totalExpectedStoreRecordsBeforeRestart: beforeRows.reduce((sum, row) =>
      sum + row.expectedCellRecords, 0),
    totalActualStoreRecordsBeforeRestart: beforeRows.reduce((sum, row) =>
      sum + row.actualCellRecords, 0),
    totalExpectedStoreRecordsAfterRestart: afterRows.reduce((sum, row) =>
      sum + row.expectedCellRecords, 0),
    totalActualStoreRecordsAfterRestart: afterRows.reduce((sum, row) =>
      sum + row.actualCellRecords, 0),
    allStoreCountsExact: [...beforeRows, ...afterRows].every(row => row.exact === true),
    globalUniqueRelaySigningKeys: uniqueCount(relayPublicKeys),
    globalUniqueStoreIds: uniqueCount(storeIds),
    latencies: {
      stagedCellPut: latencyAggregate(children, 'stagedCellPut'),
      publicCellGet: latencyAggregate(children, 'publicCellGet'),
      recoveredPublicCellGet: latencyAggregate(children, 'recoveredPublicCellGet')
    },
    childBlockers: [...new Set(reports.flatMap(report => report.blockers))].sort()
  }
}

function expectedCorrectnessChecks (report, childResults, summary) {
  const processCount = report.workload.processCount
  const allKeys = report.children.flatMap(child => child.envelope.identity.relayPublicKeys)
  const allStoreIds = report.children.flatMap(child => child.envelope.identity.storeIds)
  return {
    allChildEnvelopesVerified: childResults.length === processCount &&
      childResults.every(result => result.verified === true),
    allChildCorrectnessGatesReady: report.children.every(child =>
      child.envelope.labReport.correctnessGateReady === true),
    allChildrenExitedZero: report.children.every(child => child.exitCode === 0),
    noChildSignals: report.children.every(child => child.signal === null),
    uniqueChildPids: report.processExecution.uniqueChildPids === processCount,
    parentPidIsolationObserved: report.children.every(child =>
      child.envelope.process.parentPid === report.processExecution.supervisorPid &&
      child.observedPid === child.envelope.process.pid &&
      child.observedPid !== report.processExecution.supervisorPid),
    sameUidAndGidObserved: report.children.every(child =>
      child.envelope.process.uid === report.processExecution.supervisorUid &&
      child.envelope.process.gid === report.processExecution.supervisorGid),
    concurrentExecutionObserved: report.processExecution.concurrentOverlapObserved === true &&
      report.processExecution.concurrentOverlapMillis > 0,
    uniqueLogicalOperatorScopes: uniqueCount(report.children.map(child =>
      child.envelope.process.logicalOperatorScope)) === processCount,
    uniqueIdentityScopes: uniqueCount(report.children.map(child =>
      child.envelope.process.identityScope)) === processCount,
    globalRelaySigningKeysUnique: uniqueCount(allKeys) === summary.totalRelayInstances,
    globalStoreIdsUnique: uniqueCount(allStoreIds) === summary.totalRelayInstances,
    exactWrites: summary.totalAttemptedCellWrites === report.workload.expectedCellWrites,
    exactReadsBeforeRestart: summary.totalPublicReadChecks === report.workload.expectedCellWrites,
    exactReadsAfterRestart: summary.totalRecoveredReadChecks === report.workload.expectedCellWrites,
    exactRestarts: summary.totalRelaysStopped === report.workload.totalRelayInstances &&
      summary.totalRelaysRestarted === report.workload.totalRelayInstances,
    exactStoreCounts: summary.allStoreCountsExact === true &&
      summary.totalActualStoreRecordsBeforeRestart ===
        summary.totalExpectedStoreRecordsBeforeRestart &&
      summary.totalActualStoreRecordsAfterRestart ===
        summary.totalExpectedStoreRecordsAfterRestart,
    noChildRuntimeErrors: report.children.every(child =>
      child.envelope.labReport.runtimeErrors.length === 0),
    sourceCommitConsistent: report.children.every(child =>
      child.envelope.source.gitCommit === report.sourceCommit)
  }
}

function expectedReproducibilityChecks (report) {
  return {
    allTrackedSourceClean: report.children.every(child =>
      child.envelope.source.trackedWorktreeClean === true),
    childStdoutDigestsExact: report.children.every(child =>
      child.stdoutSha256 === checksum(JSON.stringify(child.envelope) + '\n')),
    childStderrEmpty: report.children.every(child =>
      child.stderrBytes === 0 && child.stderrSha256 === EMPTY_SHA256),
    exactChildCommitRecorded: report.children.every(child =>
      child.envelope.source.gitCommit === report.sourceCommit),
    exactChildEvidenceDigestsRecorded: JSON.stringify(report.summary.childLabEvidenceDigests) ===
      JSON.stringify(report.children.map(child => child.envelope.labReport.evidenceDigest))
  }
}

export function localMultiprocessBlindExpectedBlockers (report, correctnessReady, reproducibilityReady) {
  const childBlockers = report?.summary?.childBlockers || []
  const blockers = [...LOCAL_MULTIPROCESS_BLIND_MANDATORY_BLOCKERS]
  for (const blocker of childBlockers) if (!blockers.includes(blocker)) blockers.push(blocker)
  if (!correctnessReady) blockers.push('LOCAL_MULTIPROCESS_CORRECTNESS_GATE_NOT_MET')
  if (!reproducibilityReady) blockers.push('LOCAL_MULTIPROCESS_REPRODUCIBILITY_GATE_NOT_MET')
  if (report?.children?.some(child => child.envelope.source.trackedWorktreeClean !== true)) {
    blockers.push('TRACKED_SOURCE_DIRTY')
  }
  return Object.freeze(blockers)
}

export function buildLocalMultiprocessBlindReport (input) {
  record(input, 'aggregate input')
  if (!Array.isArray(input.children) || input.children.length < 3 || input.children.length > 8) {
    fail('aggregate input requires 3..8 child process records')
  }
  const processCount = input.children.length
  const relaysPerProcess = safeInteger(input.relaysPerProcess, 2, 8, 'relaysPerProcess')
  const recordsPerRelay = safeInteger(input.recordsPerRelay, 1, 250000, 'recordsPerRelay')
  const concurrencyPerProcess = safeInteger(input.concurrencyPerProcess, 1, 128,
    'concurrencyPerProcess')
  const contentBytes = safeInteger(input.contentBytes, 32, 4000, 'contentBytes')
  const sourceCommit = lowerHex(input.sourceCommit, 20, 'sourceCommit')
  const supervisorPid = safeInteger(input.supervisorPid, 1, Number.MAX_SAFE_INTEGER, 'supervisorPid')
  const supervisorUid = safeInteger(input.supervisorUid, 0, Number.MAX_SAFE_INTEGER, 'supervisorUid')
  const supervisorGid = safeInteger(input.supervisorGid, 0, Number.MAX_SAFE_INTEGER, 'supervisorGid')
  const earliestSpawn = Math.min(...input.children.map(child => child.observedSpawnUnixMillis))
  const latestSpawn = Math.max(...input.children.map(child => child.observedSpawnUnixMillis))
  const earliestExit = Math.min(...input.children.map(child => child.observedExitUnixMillis))
  const latestExit = Math.max(...input.children.map(child => child.observedExitUnixMillis))
  const overlap = Math.max(0, earliestExit - latestSpawn)
  const processExecution = {
    supervisorPid,
    supervisorUid,
    supervisorGid,
    uniqueChildPids: uniqueCount(input.children.map(child => child.observedPid)),
    observedWallMillis: latestExit - earliestSpawn,
    concurrentOverlapObserved: overlap > 0,
    concurrentOverlapMillis: overlap
  }
  const workload = {
    processCount,
    relaysPerProcess,
    recordsPerRelay,
    concurrencyPerProcess,
    contentBytes,
    totalRelayInstances: processCount * relaysPerProcess,
    expectedCellWrites: processCount * relaysPerProcess * recordsPerRelay
  }
  const summary = expectedSummary(input.children)
  const partial = {
    schema: LOCAL_MULTIPROCESS_BLIND_REPORT_SCHEMA,
    generatedAt: input.generatedAt || new Date().toISOString(),
    evidenceClass: 'MEASURED_LOCAL_MULTI_PROCESS_REAL_HTTP_IPC_FILESYSTEM',
    evidenceBinding: {
      algorithm: 'sha256',
      canonicalization: REAL_BLIND_RELAY_CANONICALIZATION,
      checksumOnly: true,
      signed: false,
      authenticityProven: false
    },
    releaseReady: false,
    sourceCommit,
    scope: {
      childProcessCount: processCount,
      logicalOperatorScopeProcesses: processCount,
      crossChildProcessIsolationMeasured: true,
      sameHost: true,
      sameUid: true,
      hostIsolationMeasured: false,
      claimedIndependentOperators: false,
      realIndependentOwnershipMeasured: false,
      multiHostNetworkMeasured: false,
      publicCaTlsValidationMeasured: false,
      syntheticAdmissionAdapter: true,
      economicSettlementMeasured: false,
      soakMeasured: false,
      transport: 'ephemeral self-signed loopback TLS plus authenticated same-UID Unix IPC'
    },
    workload,
    processExecution,
    summary,
    children: input.children
  }
  const childResults = input.children.map(child => verifyLocalMultiprocessBlindChild(child.envelope, {
    expectedPid: child.observedPid,
    expectedParentPid: supervisorPid,
    expectedGitCommit: sourceCommit
  }))
  const correctnessChecks = expectedCorrectnessChecks(partial, childResults, summary)
  const reproducibilityChecks = expectedReproducibilityChecks(partial)
  const correctnessReady = Object.values(correctnessChecks).every(Boolean)
  const reproducibilityReady = Object.values(reproducibilityChecks).every(Boolean)
  partial.gates = {
    correctness: {
      ready: correctnessReady,
      claimClass: 'LOCAL_MULTI_PROCESS_CORRECTNESS_ONLY',
      checks: correctnessChecks
    },
    reproducibility: {
      ready: reproducibilityReady,
      claimClass: 'COMMIT_BOUND_CHECKSUM_ONLY',
      checks: reproducibilityChecks
    }
  }
  partial.correctnessGateReady = correctnessReady
  partial.localGateReady = correctnessReady && reproducibilityReady
  partial.blockers = localMultiprocessBlindExpectedBlockers(
    partial,
    correctnessReady,
    reproducibilityReady
  )
  const report = sealLocalMultiprocessBlindReport(partial)
  verifyLocalMultiprocessBlindReport(report)
  return report
}

export function verifyLocalMultiprocessBlindReport (report, options = {}) {
  exactKeys(report, REPORT_ROOT_KEYS, 'report')
  if (report.schema !== LOCAL_MULTIPROCESS_BLIND_REPORT_SCHEMA) {
    fail(`unsupported aggregate schema ${JSON.stringify(report.schema)}`)
  }
  isoMillis(report.generatedAt, 'generatedAt')
  if (report.evidenceClass !== 'MEASURED_LOCAL_MULTI_PROCESS_REAL_HTTP_IPC_FILESYSTEM') {
    fail('evidenceClass exceeds or changes the local multi-process boundary')
  }
  validateEvidenceBinding(report.evidenceBinding)
  if (report.releaseReady !== false) fail('local multi-process evidence cannot authorize release')
  lowerHex(report.sourceCommit, 20, 'sourceCommit')
  assertDigest(report)

  exactKeys(report.scope, [
    'childProcessCount',
    'claimedIndependentOperators',
    'economicSettlementMeasured',
    'hostIsolationMeasured',
    'logicalOperatorScopeProcesses',
    'multiHostNetworkMeasured',
    'crossChildProcessIsolationMeasured',
    'publicCaTlsValidationMeasured',
    'realIndependentOwnershipMeasured',
    'sameHost',
    'sameUid',
    'soakMeasured',
    'syntheticAdmissionAdapter',
    'transport'
  ], 'scope')
  const processCount = safeInteger(report.scope.childProcessCount, 3, 8, 'scope.childProcessCount')
  exactObject(report.scope, {
    childProcessCount: processCount,
    logicalOperatorScopeProcesses: processCount,
    crossChildProcessIsolationMeasured: true,
    sameHost: true,
    sameUid: true,
    hostIsolationMeasured: false,
    claimedIndependentOperators: false,
    realIndependentOwnershipMeasured: false,
    multiHostNetworkMeasured: false,
    publicCaTlsValidationMeasured: false,
    syntheticAdmissionAdapter: true,
    economicSettlementMeasured: false,
    soakMeasured: false,
    transport: 'ephemeral self-signed loopback TLS plus authenticated same-UID Unix IPC'
  }, 'scope')

  exactKeys(report.workload, [
    'concurrencyPerProcess',
    'contentBytes',
    'expectedCellWrites',
    'processCount',
    'recordsPerRelay',
    'relaysPerProcess',
    'totalRelayInstances'
  ], 'workload')
  if (report.workload.processCount !== processCount) fail('workload process count is inconsistent')
  const relaysPerProcess = safeInteger(report.workload.relaysPerProcess, 2, 8,
    'workload.relaysPerProcess')
  const recordsPerRelay = safeInteger(report.workload.recordsPerRelay, 1, 250000,
    'workload.recordsPerRelay')
  safeInteger(report.workload.concurrencyPerProcess, 1, 128, 'workload.concurrencyPerProcess')
  safeInteger(report.workload.contentBytes, 32, 4000, 'workload.contentBytes')
  const totalRelayInstances = processCount * relaysPerProcess
  const expectedCellWrites = totalRelayInstances * recordsPerRelay
  if (report.workload.totalRelayInstances !== totalRelayInstances ||
      report.workload.expectedCellWrites !== expectedCellWrites) {
    fail('workload totals are inconsistent')
  }

  exactKeys(report.processExecution, [
    'concurrentOverlapMillis',
    'concurrentOverlapObserved',
    'observedWallMillis',
    'supervisorGid',
    'supervisorPid',
    'supervisorUid',
    'uniqueChildPids'
  ], 'processExecution')
  const supervisorPid = safeInteger(report.processExecution.supervisorPid, 1, Number.MAX_SAFE_INTEGER,
    'processExecution.supervisorPid')
  safeInteger(report.processExecution.supervisorUid, 0, Number.MAX_SAFE_INTEGER,
    'processExecution.supervisorUid')
  safeInteger(report.processExecution.supervisorGid, 0, Number.MAX_SAFE_INTEGER,
    'processExecution.supervisorGid')
  const uniqueChildPids = safeInteger(report.processExecution.uniqueChildPids, 1, processCount,
    'processExecution.uniqueChildPids')
  finiteNumber(report.processExecution.observedWallMillis, Number.EPSILON,
    'processExecution.observedWallMillis')
  finiteNumber(report.processExecution.concurrentOverlapMillis, 0,
    'processExecution.concurrentOverlapMillis')
  boolean(report.processExecution.concurrentOverlapObserved,
    'processExecution.concurrentOverlapObserved')

  if (!Array.isArray(report.children) || report.children.length !== processCount) {
    fail('children must contain one child record per process')
  }
  const seenIndexes = new Set()
  const childResults = []
  for (const child of report.children) {
    exactKeys(child, [
      'envelope',
      'exitCode',
      'observedExitUnixMillis',
      'observedPid',
      'observedSpawnUnixMillis',
      'processIndex',
      'signal',
      'stderrBytes',
      'stderrSha256',
      'stdoutSha256'
    ], 'children[]')
    const processIndex = safeInteger(child.processIndex, 0, processCount - 1, 'children[].processIndex')
    if (seenIndexes.has(processIndex)) fail('children contains a duplicate processIndex')
    seenIndexes.add(processIndex)
    const observedPid = safeInteger(child.observedPid, 1, Number.MAX_SAFE_INTEGER,
      `children[${processIndex}].observedPid`)
    const spawnMillis = safeInteger(child.observedSpawnUnixMillis, 0, Number.MAX_SAFE_INTEGER,
      `children[${processIndex}].observedSpawnUnixMillis`)
    const exitMillis = safeInteger(child.observedExitUnixMillis, spawnMillis, Number.MAX_SAFE_INTEGER,
      `children[${processIndex}].observedExitUnixMillis`)
    if (child.exitCode !== 0) fail(`children[${processIndex}] did not exit zero`)
    if (child.signal !== null) fail(`children[${processIndex}] exited due to a signal`)
    safeInteger(child.stderrBytes, 0, 64 * 1024 * 1024, `children[${processIndex}].stderrBytes`)
    lowerHex(child.stdoutSha256, 32, `children[${processIndex}].stdoutSha256`)
    lowerHex(child.stderrSha256, 32, `children[${processIndex}].stderrSha256`)
    const result = verifyLocalMultiprocessBlindChild(child.envelope, {
      expectedPid: observedPid,
      expectedParentPid: supervisorPid,
      expectedGitCommit: report.sourceCommit
    })
    const lab = child.envelope.labReport
    if (lab.scope.relayInstances !== relaysPerProcess ||
        lab.load.logicalRecords !== recordsPerRelay ||
        lab.load.concurrency !== report.workload.concurrencyPerProcess ||
        lab.load.contentBytesPerRecord !== report.workload.contentBytes) {
      fail(`children[${processIndex}] workload differs from the aggregate workload`)
    }
    const childStart = Date.parse(child.envelope.process.startedAt)
    const childFinish = Date.parse(child.envelope.process.finishedAt)
    if (childStart < spawnMillis || childFinish > exitMillis + 10) {
      fail(`children[${processIndex}] timestamps fall outside its observed process lifetime`)
    }
    childResults.push(result)
  }
  if (seenIndexes.size !== processCount) fail('children process indexes are incomplete')
  const observedUniquePids = uniqueCount(report.children.map(child => child.observedPid))
  if (uniqueChildPids !== observedUniquePids) fail('uniqueChildPids is inconsistent')
  const earliestSpawn = Math.min(...report.children.map(child => child.observedSpawnUnixMillis))
  const latestSpawn = Math.max(...report.children.map(child => child.observedSpawnUnixMillis))
  const earliestExit = Math.min(...report.children.map(child => child.observedExitUnixMillis))
  const latestExit = Math.max(...report.children.map(child => child.observedExitUnixMillis))
  const overlap = Math.max(0, earliestExit - latestSpawn)
  const wall = latestExit - earliestSpawn
  if (report.processExecution.concurrentOverlapMillis !== overlap ||
      report.processExecution.concurrentOverlapObserved !== (overlap > 0) ||
      report.processExecution.observedWallMillis !== wall) {
    fail('process execution timing summary is inconsistent')
  }

  const summary = expectedSummary(report.children)
  exactObject(report.summary, summary, 'summary')
  if (summary.totalRelayInstances !== totalRelayInstances) fail('summary relay count is inconsistent')

  exactKeys(report.gates, ['correctness', 'reproducibility'], 'gates')
  exactKeys(report.gates.correctness, ['checks', 'claimClass', 'ready'], 'gates.correctness')
  exactKeys(report.gates.reproducibility, ['checks', 'claimClass', 'ready'], 'gates.reproducibility')
  if (report.gates.correctness.claimClass !== 'LOCAL_MULTI_PROCESS_CORRECTNESS_ONLY') {
    fail('gates.correctness.claimClass exceeds the local boundary')
  }
  if (report.gates.reproducibility.claimClass !== 'COMMIT_BOUND_CHECKSUM_ONLY') {
    fail('gates.reproducibility.claimClass exceeds checksum evidence')
  }
  const correctnessChecks = expectedCorrectnessChecks(report, childResults, summary)
  const reproducibilityChecks = expectedReproducibilityChecks(report)
  exactObject(report.gates.correctness.checks, correctnessChecks, 'gates.correctness.checks')
  exactObject(report.gates.reproducibility.checks, reproducibilityChecks,
    'gates.reproducibility.checks')
  const correctnessReady = Object.values(correctnessChecks).every(Boolean)
  const reproducibilityReady = Object.values(reproducibilityChecks).every(Boolean)
  if (report.gates.correctness.ready !== correctnessReady ||
      report.correctnessGateReady !== correctnessReady) {
    fail('aggregate correctness gate result is inconsistent')
  }
  if (report.gates.reproducibility.ready !== reproducibilityReady) {
    fail('aggregate reproducibility gate result is inconsistent')
  }
  if (report.localGateReady !== (correctnessReady && reproducibilityReady)) {
    fail('localGateReady must require correctness and reproducibility')
  }
  exactArray(report.blockers,
    localMultiprocessBlindExpectedBlockers(report, correctnessReady, reproducibilityReady),
    'blockers')
  if (options.requireCorrectness === true && !correctnessReady) {
    fail('aggregate correctness gate is not ready')
  }
  if (options.requireLocal === true && !report.localGateReady) {
    fail('aggregate local multi-process gate is not ready')
  }
  return Object.freeze({
    verified: true,
    checksumVerified: true,
    checksumOnly: true,
    contentAddress: `sha256:${report.evidenceDigest}`,
    authenticityProven: false,
    authorizesRelease: false,
    claimsIndependentOperators: false,
    correctnessReady,
    reproducibilityReady,
    localGateReady: report.localGateReady,
    releaseReady: false
  })
}

function parseCli (argv) {
  const options = {}
  for (let index = 0; index < argv.length; index++) {
    const value = argv[index]
    if (value === '--input') options.input = argv[++index]
    else if (value === '--require-correctness') options.requireCorrectness = true
    else if (value === '--require-local') options.requireLocal = true
    else if (value === '--pretty') options.pretty = true
    else fail(`unknown argument ${value}`)
  }
  if (!options.input) fail('--input is required')
  return options
}

async function main () {
  const options = parseCli(process.argv.slice(2))
  const input = path.resolve(options.input)
  const stat = await fs.stat(input)
  if (!stat.isFile() || stat.size > 64 * 1024 * 1024) {
    fail('input must be a report file no larger than 64 MiB')
  }
  const report = JSON.parse(await fs.readFile(input, 'utf8'))
  const result = verifyLocalMultiprocessBlindReport(report, options)
  process.stdout.write(JSON.stringify(result, null, options.pretty ? 2 : 0) + '\n')
}

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || '')) {
  main().catch(error => {
    process.stderr.write(`[verify-blind-local-multiprocess-report] ${error.code || 'ERROR'}: ${error.message}\n`)
    process.exitCode = 1
  })
}