Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
- name: Assert size budgets
run: bash scripts/assert-size.sh
working-directory: packages/codec
- name: Freeze-gate (golden vector oracle check)
run: pnpm -C packages/codec check-vectors
- run: |
for dir in packages/codec packages/types packages/networks; do
(cd "$dir" && npm pack --dry-run)
Expand Down Expand Up @@ -67,7 +69,7 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ignromanov/voidpay
ref: e4926b7f7b08ca4f72b707df8796bfd4a4b0a3b3
ref: c658fff
token: ${{ secrets.VOIDPAY_READ_TOKEN }}
path: vl-app
sparse-checkout: |
Expand Down
1 change: 1 addition & 0 deletions packages/codec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"test:wasm": "wasm-pack test --node",
"test:pack": "node scripts/test-pack.mjs",
"generate-vectors": "vitest run --config scripts/generate-vectors.config.ts",
"check-vectors": "vitest run --config scripts/check-vectors.config.ts --coverage.enabled=false",
"test:ts-rust-parity": "tsx scripts/ts-rust-parity.ts",
"lint": "cargo clippy --all-targets --all-features --locked -- -D warnings && cargo fmt --manifest-path Cargo.toml -- --check && eslint src",
"size": "ls -la pkg/void_layer_codec_bg.wasm"
Expand Down
30 changes: 30 additions & 0 deletions packages/codec/scripts/check-vectors.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Vitest config for the freeze-gate vector checker.
* Used by: pnpm check-vectors (CI freeze-gate)
*
* Includes the run-check-vectors.test.ts wrapper which:
* - Runs the positive gate (codec output == frozen oracle)
* - Runs the negative test (mutated vector is caught)
*
* Coverage is disabled — this is a gate script, not a coverage target.
*/
import { defineConfig } from 'vitest/config'
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'
import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)

export default defineConfig({
plugins: [wasm(), topLevelAwait()],
test: {
environment: 'node',
include: ['scripts/run-check-vectors.test.ts', 'scripts/run-freeze-write.test.ts'],
coverage: { enabled: false },
},
resolve: {
alias: {
'brotli-wasm': require.resolve('brotli-wasm'),
},
},
})
213 changes: 213 additions & 0 deletions packages/codec/scripts/check-vectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/**
* Golden vector freeze-gate — @void-layer/codec
*
* Modes:
* --check (default / CI): re-derive vectors from the codec WASM and diff
* against the committed v4-codec.json. Returns a non-zero exit code
* on ANY mismatch. Never overwrites the file.
* --write (deliberate local re-capture only): same derivation, then writes
* the result to disk. Use only after a reviewed, intentional update
* to the frozen oracle (new TLV type, dict code, etc.).
*
* The frozen oracle was captured from vl/app master c658fff (release v1.1.2).
* Any change to canonical_hex or wire_hex is a regression against that deployed
* reference and must be reviewed by Kai before --write is used.
*
* Run (from packages/codec root):
* pnpm check-vectors # check mode (CI default)
* pnpm check-vectors -- --write # write mode (local only)
*/

import * as fs from 'node:fs'
import * as path from 'node:path'
import * as crypto from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { buildAllVectors } from './scenarios/all-vectors.js'
import { demoinvoiceVectors } from './scenarios/demo-invoices.js'
import type { NonMalformedVector } from './scenarios/non-malformed.js'
import type { MalformedVector } from './scenarios/malformed.js'

const _filename = fileURLToPath(import.meta.url)
const _dirname = path.dirname(_filename)
const VECTORS_DIR = path.resolve(_dirname, '../vectors')
const OUT_PATH = path.join(VECTORS_DIR, 'v4-codec.json')

// Provenance constants — update only on deliberate oracle re-capture.
const CAPTURED_FROM_VL_APP_SHA = 'c658fff'
const FROZEN_AT = '2026-05-30'

type Vector = NonMalformedVector | MalformedVector

/**
* Compute sha256 of the canonical vector payload (the "vectors" array JSON,
* without the header fields). This is the content_hash stored in the frozen JSON.
*/
export function computeContentHash(vectors: unknown[]): string {
const payload = JSON.stringify(vectors)
return crypto.createHash('sha256').update(payload).digest('hex')
}

async function deriveVectors(): Promise<Vector[]> {
return [
...(await buildAllVectors()),
...(await demoinvoiceVectors()),
]
}

/**
* Run the freeze-gate check. Returns a list of mismatch descriptions.
* Empty list = PASS. Non-empty = FAIL.
*
* Exported so the vitest runner can assert without process.exit().
*/
export async function runFreezeCheck(committedPath: string = OUT_PATH): Promise<string[]> {
const derived = await deriveVectors()
const mismatches: string[] = []

if (!fs.existsSync(committedPath)) {
return ['vectors/v4-codec.json does not exist — run pnpm check-vectors -- --write']
}

const committed = JSON.parse(fs.readFileSync(committedPath, 'utf-8')) as {
vectors: Vector[]
content_hash?: string
frozen?: boolean
captured_from_vl_app_sha?: string
}

const committedVectors = committed.vectors ?? []
const derivedByName = new Map(derived.map((v) => [v.name, v]))
const committedByName = new Map(committedVectors.map((v) => [v.name, v]))

// Check every derived vector against committed
for (const [name, derivedV] of derivedByName) {
const committedV = committedByName.get(name)
if (!committedV) {
mismatches.push(`MISSING_IN_COMMITTED: vector=${name}`)
continue
}

const dv = derivedV as NonMalformedVector
const cv = committedV as NonMalformedVector

if ('canonical_hex' in dv && 'canonical_hex' in cv) {
if (dv.canonical_hex !== cv.canonical_hex) {
mismatches.push(
`CANONICAL_MISMATCH vector=${name}\n` +
` committed: ${cv.canonical_hex}\n` +
` derived: ${dv.canonical_hex}`,
)
}
}
if ('wire_hex' in dv && 'wire_hex' in cv) {
if (dv.wire_hex !== cv.wire_hex) {
mismatches.push(
`WIRE_MISMATCH vector=${name}\n` +
` committed: ${cv.wire_hex}\n` +
` derived: ${dv.wire_hex}`,
)
}
}
if ('receipt_hash_hex' in dv && 'receipt_hash_hex' in cv) {
if (dv.receipt_hash_hex !== cv.receipt_hash_hex) {
mismatches.push(
`RECEIPT_HASH_MISMATCH vector=${name}\n` +
` committed: ${cv.receipt_hash_hex}\n` +
` derived: ${dv.receipt_hash_hex}`,
)
}
}
}

// Check for vectors in committed but not in derived.
// decode_* vectors are static forward-compat fixtures not re-derived from the
// codec scenario builders — intentionally present only in the JSON oracle.
for (const name of committedByName.keys()) {
if (!derivedByName.has(name) && !name.startsWith('decode_')) {
mismatches.push(`EXTRA_IN_COMMITTED: vector=${name} (not in derived set — stale?)`)
}
}

// Integrity gate: sha256 is computed over the COMMITTED vectors array as
// stored in the frozen JSON — not over WASM-derived output. decode-only
// fixtures (roundtrip===false) are part of the frozen oracle and must be
// included in the hash. This detects accidental corruption (bitrot, bad
// merge, unintended manual edit). It is NOT a tamper-proof security
// boundary — a malicious editor can edit the vectors and re-stamp the hash.
// Real cross-impl byte identity is enforced by the codec-drift gate (check a).
if (committed.content_hash) {
const expectedHash = computeContentHash(committedVectors)
if (committed.content_hash !== expectedHash) {
mismatches.push(
`CONTENT_HASH_MISMATCH (integrity check failed — vectors edited without re-stamping hash)\n` +
` committed: ${committed.content_hash}\n` +
` recomputed: ${expectedHash}`,
)
}
}

return mismatches
}

/**
* Write mode: re-derive and stamp the oracle with provenance headers.
* Only called with explicit --write flag.
*/
export async function runWrite(): Promise<void> {
const derived = await deriveVectors()
const content_hash = computeContentHash(derived)
const output = {
schema_version: 1,
frozen: true,
captured_from_vl_app_sha: CAPTURED_FROM_VL_APP_SHA,
frozen_at: FROZEN_AT,
content_hash,
generated_by: '@void-layer/codec v0.1.0',
vectors: derived,
}
fs.mkdirSync(VECTORS_DIR, { recursive: true })
fs.writeFileSync(OUT_PATH, JSON.stringify(output, null, 2) + '\n')
console.log(`[freeze-gate] wrote ${derived.length} vectors → ${OUT_PATH}`)
console.log(`[freeze-gate] content_hash: ${content_hash}`)
}

// ---------------------------------------------------------------------------
// CLI entry point (direct invocation only — not called when imported as module)
// ---------------------------------------------------------------------------

async function main(): Promise<void> {
const args = process.argv.slice(2)
const writeMode = args.includes('--write')

if (writeMode) {
console.log(`\n[freeze-gate] mode=write`)
console.log(`[freeze-gate] oracle: vectors/v4-codec.json`)
console.log(`[freeze-gate] captured_from_vl_app_sha: ${CAPTURED_FROM_VL_APP_SHA}\n`)
await runWrite()
return
}

console.log(`\n[freeze-gate] mode=check`)
console.log(`[freeze-gate] oracle: vectors/v4-codec.json`)
console.log(`[freeze-gate] captured_from_vl_app_sha: ${CAPTURED_FROM_VL_APP_SHA}\n`)

const mismatches = await runFreezeCheck()

if (mismatches.length > 0) {
console.error('[freeze-gate] FAIL: v4-codec.json has drifted from codec output\n')
for (const m of mismatches) console.error(` ${m}\n`)
console.error('\nTo re-capture: pnpm check-vectors -- --write (requires Kai review)')
process.exit(1)
}

console.log(`[freeze-gate] PASS — all vectors match committed oracle`)
}

// Only run main() when this file is the direct entrypoint, not when imported as a module.
// Vitest imports this as a module to call runFreezeCheck()/runWrite() directly.
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error('[freeze-gate] fatal:', err)
process.exit(1)
})
}
88 changes: 88 additions & 0 deletions packages/codec/scripts/run-check-vectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Vitest wrapper — runs the freeze-gate checker as a test so vitest's
* module resolver (brotli-wasm alias, wasm plugin) are active.
*
* CI usage:
* pnpm -C packages/codec exec vitest run scripts/run-check-vectors.test.ts --coverage.enabled=false
*
* This test FAILS if any derived vector byte differs from the committed oracle.
* A failure here means @void-layer/codec output has drifted from the frozen
* vl/app reference (captured at c658fff). Do NOT suppress — escalate to Kai.
*
* NEGATIVE TEST: a deliberately mutated single byte also lives here to prove
* the gate has teeth (see "freeze-gate rejects mutated vector").
*/
import { test, expect } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
import { runFreezeCheck } from './check-vectors.js'

const _dirname = path.dirname(fileURLToPath(import.meta.url))
const VECTORS_PATH = path.resolve(_dirname, '../vectors/v4-codec.json')

// ---------------------------------------------------------------------------
// Positive test — codec output must match frozen oracle
// ---------------------------------------------------------------------------

test(
'freeze-gate: codec output matches committed v4-codec.json (oracle integrity)',
async () => {
const mismatches = await runFreezeCheck()
if (mismatches.length > 0) {
// Surface all mismatches in one assertion so CI output is actionable.
throw new Error(
`[freeze-gate] ${mismatches.length} mismatch(es) detected:\n\n` +
mismatches.map((m) => ` ${m}`).join('\n\n') +
'\n\nTo re-capture: pnpm check-vectors -- --write (requires Kai review)',
)
}
},
120_000,
)

// ---------------------------------------------------------------------------
// Negative test — proves the gate has teeth: a mutated byte must be caught
// ---------------------------------------------------------------------------

test('freeze-gate rejects mutated vector (gate has teeth)', async () => {
if (!fs.existsSync(VECTORS_PATH)) {
// Skip gracefully if oracle is absent (fresh checkout pre-write)
return
}

const original = fs.readFileSync(VECTORS_PATH, 'utf-8')
const parsed = JSON.parse(original) as {
vectors: Array<{ name: string; canonical_hex?: string; [k: string]: unknown }>
}

// Find first non-malformed vector with a canonical_hex to mutate
const target = parsed.vectors.find((v) => typeof v.canonical_hex === 'string')
if (!target || typeof target.canonical_hex !== 'string') {
throw new Error('No non-malformed vector found to mutate — test is invalid')
}

// Flip one nibble at position 4 (after the magic+version bytes)
const original_hex = target.canonical_hex
const mutated_hex =
original_hex.slice(0, 4) +
// XOR the nibble at position 4 with 0x1 to ensure it changes
(parseInt(original_hex[4]!, 16) ^ 1).toString(16) +
original_hex.slice(5)
target.canonical_hex = mutated_hex

// Write mutated content to a temp file — never overwrite the live oracle.
// A process kill between write and restore would otherwise corrupt v4-codec.json.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codec-freeze-gate-'))
const mutatedPath = path.join(tmpDir, 'v4-codec.json')
fs.writeFileSync(mutatedPath, JSON.stringify(parsed, null, 2))

try {
const mismatches = await runFreezeCheck(mutatedPath)
expect(mismatches.length).toBeGreaterThan(0)
expect(mismatches.some((m) => m.includes('CANONICAL_MISMATCH'))).toBe(true)
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true })
}
})
32 changes: 32 additions & 0 deletions packages/codec/scripts/run-freeze-write.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* One-shot oracle freeze writer.
*
* Rewrites vectors/v4-codec.json with provenance headers and the current
* codec's derived bytes. Run ONLY when making a deliberate, reviewed update
* to the frozen oracle (e.g. adding a new TLV type, new dict code).
*
* Usage (from packages/codec root):
* FREEZE_GATE_WRITE=1 pnpm exec vitest run scripts/run-freeze-write.test.ts --config scripts/check-vectors.config.ts
*
* After running, verify with:
* pnpm check-vectors
*
* Requires Kai review before merging any oracle update.
*/
import { test } from 'vitest'
import { runWrite } from './check-vectors.js'

test(
'freeze-write: stamp v4-codec.json with provenance headers',
async () => {
if (!process.env['FREEZE_GATE_WRITE']) {
console.log(
'[freeze-write] Skipped — set FREEZE_GATE_WRITE=1 to actually write the oracle',
)
return
}
await runWrite()
console.log('[freeze-write] Oracle written successfully')
},
120_000,
)
Loading