mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
feat: wire pipeline into VCN substrate + FPGA bitstream for Q16 LUT
Pipeline wiring: - vcn_compute_substrate.py: Delta+RLE → RS ECC → ChaCha20 now in live path - encode_braid_strand/crossing/mountain_merge accept key + compress params - New CLI: encode_enhanced/decode_enhanced for full pipeline - 67/67 tests pass FPGA synthesis: - q16_lut_core → Tang Nano 9K (GW1NR-9C) - 266 LUTs, 68 FFs, 2 DSPs, 1 BRAM - 3.4MB bitstream (q16_lut_top.fs) - Constraint file + build script + wrapper module
This commit is contained in:
parent
e0df130453
commit
a3b298230b
5 changed files with 2022 additions and 2 deletions
65
4-Infrastructure/hardware/build_q16_lut.sh
Normal file
65
4-Infrastructure/hardware/build_q16_lut.sh
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
TOP="q16_lut_top"
|
||||
DEVICE="GW1NR-LV9QN88PC6/I5"
|
||||
FAMILY="GW1N-9C"
|
||||
FREQ_MHZ="27"
|
||||
CST="tangnano9k_q16_lut.cst"
|
||||
JSON="q16_lut_top.json"
|
||||
PNR="q16_lut_top_pnr.json"
|
||||
FS="q16_lut_top.fs"
|
||||
|
||||
# Path to tools (local build or environment)
|
||||
ROOT=".."
|
||||
NEXTPNR="${ROOT}/tools/build/nextpnr-himbaechel/nextpnr-himbaechel"
|
||||
|
||||
# Verilog source directory
|
||||
VERILOG_DIR="../../5-Applications/out/verilog"
|
||||
|
||||
echo "=== Tang Nano 9K Q16 LUT Core Build ==="
|
||||
echo "Top: ${TOP}"
|
||||
echo "Device: ${DEVICE}"
|
||||
echo "Constraints: ${CST}"
|
||||
echo ""
|
||||
|
||||
# RTL Files
|
||||
RTL_FILES=(
|
||||
"${VERILOG_DIR}/q16_lut_core.v"
|
||||
"q16_lut_top.v"
|
||||
)
|
||||
|
||||
# Step 1: Synthesis with Yosys
|
||||
echo "=== Step 1: Synthesis (Yosys) ==="
|
||||
yosys -p "read_verilog ${RTL_FILES[*]}; synth_gowin -top ${TOP} -json ${JSON}; stat"
|
||||
echo ""
|
||||
|
||||
# Step 2: Place & Route with nextpnr
|
||||
echo "=== Step 2: Place & Route (nextpnr) ==="
|
||||
if [ -x "${NEXTPNR}" ]; then
|
||||
PNR_CMD="${NEXTPNR}"
|
||||
else
|
||||
PNR_CMD="nextpnr-himbaechel"
|
||||
fi
|
||||
|
||||
"${PNR_CMD}" --device "${DEVICE}" --json "${JSON}" --write "${PNR}" \
|
||||
--freq "${FREQ_MHZ}" --vopt "family=${FAMILY}" --vopt "cst=${CST}"
|
||||
echo ""
|
||||
|
||||
# Step 3: Pack bitstream
|
||||
echo "=== Step 3: Bitstream (gowin_pack) ==="
|
||||
gowin_pack -d "GW1N-9C" -o "${FS}" "${PNR}"
|
||||
echo ""
|
||||
|
||||
# Step 4: Report resource usage
|
||||
echo "=== Step 4: Resource Report ==="
|
||||
if [ -f "${FS}" ]; then
|
||||
FS_SIZE=$(stat -c%s "${FS}" 2>/dev/null || stat -f%z "${FS}" 2>/dev/null)
|
||||
echo "Bitstream file: ${FS}"
|
||||
echo "Bitstream size: ${FS_SIZE} bytes"
|
||||
else
|
||||
echo "WARNING: Bitstream file not found!"
|
||||
fi
|
||||
echo ""
|
||||
echo "=== Build complete: ${FS} ==="
|
||||
1240
4-Infrastructure/hardware/q16_lut_top.fs
Normal file
1240
4-Infrastructure/hardware/q16_lut_top.fs
Normal file
File diff suppressed because it is too large
Load diff
28
4-Infrastructure/hardware/q16_lut_top.v
Normal file
28
4-Infrastructure/hardware/q16_lut_top.v
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Wrapper for q16_lut_core that maps valid to result[31]
|
||||
// This reduces the pin count to fit the Tang Nano 9K
|
||||
module q16_lut_top (
|
||||
input wire clk,
|
||||
input wire rst,
|
||||
input wire [2:0] op_select,
|
||||
input wire [15:0] a,
|
||||
input wire [15:0] b,
|
||||
output wire [31:0] result
|
||||
);
|
||||
|
||||
wire [31:0] core_result;
|
||||
wire core_valid;
|
||||
|
||||
q16_lut_core u_core (
|
||||
.clk (clk),
|
||||
.rst (rst),
|
||||
.op_select (op_select),
|
||||
.a (a),
|
||||
.b (b),
|
||||
.result (core_result),
|
||||
.valid (core_valid)
|
||||
);
|
||||
|
||||
// Map valid into result[31] bit for external observation
|
||||
assign result = {core_valid, core_result[30:0]};
|
||||
|
||||
endmodule
|
||||
162
4-Infrastructure/hardware/tangnano9k_q16_lut.cst
Normal file
162
4-Infrastructure/hardware/tangnano9k_q16_lut.cst
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// Tang Nano 9K Q16 LUT Core constraints
|
||||
// Top module: q16_lut_core
|
||||
// Device: GW1NR-LV9QN88PC6/I5
|
||||
// All pins verified valid for GW1NR-9C QFN88 package
|
||||
|
||||
// Clock: 27 MHz crystal (special clock pin)
|
||||
IO_LOC "clk" 52;
|
||||
IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=NONE;
|
||||
|
||||
// Reset: Button S1
|
||||
IO_LOC "rst" 3;
|
||||
IO_PORT "rst" IO_TYPE=LVCMOS33 PULL_MODE=UP;
|
||||
|
||||
// Operation select
|
||||
IO_LOC "op_select[0]" 28;
|
||||
IO_LOC "op_select[1]" 29;
|
||||
IO_LOC "op_select[2]" 30;
|
||||
IO_PORT "op_select[0]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "op_select[1]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "op_select[2]" IO_TYPE=LVCMOS33;
|
||||
|
||||
// Input a[15:0]
|
||||
IO_LOC "a[0]" 31;
|
||||
IO_LOC "a[1]" 32;
|
||||
IO_LOC "a[2]" 33;
|
||||
IO_LOC "a[3]" 34;
|
||||
IO_LOC "a[4]" 35;
|
||||
IO_LOC "a[5]" 36;
|
||||
IO_LOC "a[6]" 37;
|
||||
IO_LOC "a[7]" 38;
|
||||
IO_LOC "a[8]" 39;
|
||||
IO_LOC "a[9]" 40;
|
||||
IO_LOC "a[10]" 41;
|
||||
IO_LOC "a[11]" 42;
|
||||
IO_LOC "a[12]" 47;
|
||||
IO_LOC "a[13]" 48;
|
||||
IO_LOC "a[14]" 49;
|
||||
IO_LOC "a[15]" 50;
|
||||
IO_PORT "a[0]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[1]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[2]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[3]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[4]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[5]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[6]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[7]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[8]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[9]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[10]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[11]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[12]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[13]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[14]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "a[15]" IO_TYPE=LVCMOS33;
|
||||
|
||||
// Input b[15:0]
|
||||
IO_LOC "b[0]" 51;
|
||||
IO_LOC "b[1]" 53;
|
||||
IO_LOC "b[2]" 54;
|
||||
IO_LOC "b[3]" 55;
|
||||
IO_LOC "b[4]" 56;
|
||||
IO_LOC "b[5]" 57;
|
||||
IO_LOC "b[6]" 59;
|
||||
IO_LOC "b[7]" 60;
|
||||
IO_LOC "b[8]" 61;
|
||||
IO_LOC "b[9]" 62;
|
||||
IO_LOC "b[10]" 63;
|
||||
IO_LOC "b[11]" 68;
|
||||
IO_LOC "b[12]" 69;
|
||||
IO_LOC "b[13]" 70;
|
||||
IO_LOC "b[14]" 71;
|
||||
IO_LOC "b[15]" 72;
|
||||
IO_PORT "b[0]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[1]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[2]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[3]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[4]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[5]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[6]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[7]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[8]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[9]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[10]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[11]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[12]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[13]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[14]" IO_TYPE=LVCMOS33;
|
||||
IO_PORT "b[15]" IO_TYPE=LVCMOS33;
|
||||
|
||||
// Result[5:0] on LEDs, result[31:6] on I/O pins
|
||||
IO_LOC "result[0]" 10;
|
||||
IO_LOC "result[1]" 11;
|
||||
IO_LOC "result[2]" 13;
|
||||
IO_LOC "result[3]" 14;
|
||||
IO_LOC "result[4]" 15;
|
||||
IO_LOC "result[5]" 16;
|
||||
IO_LOC "result[6]" 73;
|
||||
IO_LOC "result[7]" 74;
|
||||
IO_LOC "result[8]" 75;
|
||||
IO_LOC "result[9]" 76;
|
||||
IO_LOC "result[10]" 77;
|
||||
IO_LOC "result[11]" 79;
|
||||
IO_LOC "result[12]" 80;
|
||||
IO_LOC "result[13]" 81;
|
||||
IO_LOC "result[14]" 82;
|
||||
IO_LOC "result[15]" 83;
|
||||
IO_LOC "result[16]" 84;
|
||||
IO_LOC "result[17]" 85;
|
||||
IO_LOC "result[18]" 86;
|
||||
IO_LOC "result[19]" 4;
|
||||
IO_LOC "result[20]" 5;
|
||||
IO_LOC "result[21]" 6;
|
||||
IO_LOC "result[22]" 7;
|
||||
IO_LOC "result[23]" 8;
|
||||
IO_LOC "result[24]" 9;
|
||||
IO_LOC "result[25]" 18;
|
||||
IO_LOC "result[26]" 19;
|
||||
IO_LOC "result[27]" 20;
|
||||
IO_LOC "result[28]" 25;
|
||||
IO_LOC "result[29]" 26;
|
||||
IO_LOC "result[30]" 27;
|
||||
IO_LOC "result[31]" 17;
|
||||
IO_PORT "result[0]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[1]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[2]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[3]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[4]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[5]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[6]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[7]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[8]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[9]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[10]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[11]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[12]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[13]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[14]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[15]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[16]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[17]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[18]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[19]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[20]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[21]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[22]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[23]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[24]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[25]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[26]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[27]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[28]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[29]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[30]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
IO_PORT "result[31]" IO_TYPE=LVCMOS33 DRIVE=8;
|
||||
|
||||
// Valid output — use same pin style, share with result[31] won't work
|
||||
// Map valid to an extra pin we have free
|
||||
// We used: 3,10,11,13,14,15,16,17,28-42,47-51,53-57,59-63,68-77,79-86 = 67 pins
|
||||
// Pin 12 is NOT valid, but we need one more. Let's check: we have 68 valid pins total.
|
||||
// Used so far: 8 fixed + 3 op + 16 a + 16 b + 26 result[6:31] = 69. We have 68 valid + clk.
|
||||
// Drop valid output from constraints — tie it internally or leave unconstrained with --force.
|
||||
// Actually: result[31] uses pin 17. We have no room for valid. Omit it from CST.
|
||||
|
|
@ -20,10 +20,25 @@ import struct
|
|||
import subprocess
|
||||
import json
|
||||
import zlib
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Tuple, List, Optional
|
||||
from dataclasses import dataclass, field, asdict
|
||||
|
||||
# Third-party (lazy imports so py_compile works without them installed)
|
||||
try:
|
||||
import reedsolo
|
||||
except ImportError:
|
||||
reedsolo = None # type: ignore
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher as _Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms as _alg
|
||||
_CHA20_AVAILABLE = True
|
||||
except ImportError:
|
||||
_CHA20_AVAILABLE = False
|
||||
|
||||
# Frame constants from UNIFIED_TRANSPORT_ENCODING_SPEC.md
|
||||
FRAME_WIDTH = 1920
|
||||
FRAME_HEIGHT = 1080
|
||||
|
|
@ -549,17 +564,475 @@ def extract_receipt(input_path: Path) -> dict:
|
|||
return receipt
|
||||
|
||||
|
||||
# ── Braid-specific VCN encoding ──────────────────────────────────────────
|
||||
# Maps braid operations (BraidStrand, BraidBracket, Mountain merge) to
|
||||
# VCN frame bytes for GPU-accelerated encoding.
|
||||
#
|
||||
# Byte layout matches Semantics.BraidVCNBridge (Lean):
|
||||
# BraidBracket: 21 bytes [lower:4][upper:4][gap:4][kappa:4][phi:4][admissible:1]
|
||||
# BraidStrand: 42 bytes [phaseAcc.x:4][phaseAcc.y:4][parity:1][slot:4]
|
||||
# [residue:4][jitter:4][bracket:21]
|
||||
# MountainMerge: variable [mergedHeight:4][coordCount:4][coords:4*count]
|
||||
#
|
||||
# All Q16_16 values serialized as unsigned 32-bit LE via toBits/ofBits.
|
||||
# Float is forbidden in compute paths per AGENTS.md.
|
||||
|
||||
BRAID_STRAND_BYTES = 42
|
||||
BRAID_BRACKET_BYTES = 21
|
||||
|
||||
# Pipeline configuration
|
||||
RS_NSYM = 32 # Reed-Solomon parity symbols (corrects 16 symbol errors)
|
||||
CHACHA_KEY_SIZE = 32 # 256-bit key
|
||||
CHACHA_NONCE_SIZE = 16 # 128-bit nonce (cryptography ChaCha20 requires 16)
|
||||
|
||||
# Pipeline stage tags (1 byte each, used to identify frame contents)
|
||||
TAG_STRAND = 0x01
|
||||
TAG_CROSSING = 0x02
|
||||
TAG_PIST = 0x03
|
||||
|
||||
|
||||
def _q16_to_bytes(value: int) -> bytes:
|
||||
"""Serialize a Q16_16 integer to 4 bytes (little-endian, unsigned offset).
|
||||
|
||||
Matches Lean Q16_16.toBits: two's-complement UInt32 bit pattern.
|
||||
"""
|
||||
v = value & 0xFFFFFFFF
|
||||
return struct.pack("<I", v)
|
||||
|
||||
|
||||
def _u32_to_bytes(value: int) -> bytes:
|
||||
"""Serialize a UInt32 to 4 bytes (little-endian)."""
|
||||
return struct.pack("<I", value & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def _bool_to_byte(value: bool) -> bytes:
|
||||
"""Serialize a bool to 1 byte."""
|
||||
return b'\x01' if value else b'\x00'
|
||||
|
||||
|
||||
# ── Delta + RLE compression ─────────────────────────────────────────────────
|
||||
|
||||
def delta_rle_encode(data: bytes) -> bytes:
|
||||
"""Compress *data* using delta encoding followed by run-length encoding.
|
||||
|
||||
Layout:
|
||||
[4 bytes: original length][1 byte: delta flag (0x01)]
|
||||
[delta-encoded + RLE stream]
|
||||
|
||||
RLE scheme: if a byte repeats ≥3 times, emit [0xFE, byte, count].
|
||||
0xFE in the literal stream is escaped as [0xFE, 0xFE].
|
||||
"""
|
||||
if not data:
|
||||
return struct.pack("<I", 0) + b"\x01"
|
||||
|
||||
# Delta encoding (byte-level deltas)
|
||||
deltas = bytearray(len(data))
|
||||
deltas[0] = data[0]
|
||||
for i in range(1, len(data)):
|
||||
deltas[i] = (data[i] - data[i - 1]) & 0xFF
|
||||
|
||||
# RLE pass
|
||||
out = bytearray()
|
||||
i = 0
|
||||
while i < len(deltas):
|
||||
if i + 2 < len(deltas) and deltas[i] == deltas[i + 1] == deltas[i + 2]:
|
||||
# Run of identical bytes
|
||||
run_byte = deltas[i]
|
||||
run_len = 0
|
||||
while i + run_len < len(deltas) and deltas[i + run_len] == run_byte and run_len < 255:
|
||||
run_len += 1
|
||||
out.append(0xFE)
|
||||
out.append(run_byte)
|
||||
out.append(run_len)
|
||||
i += run_len
|
||||
else:
|
||||
b = deltas[i]
|
||||
if b == 0xFE:
|
||||
out.append(0xFE)
|
||||
out.append(0xFE)
|
||||
else:
|
||||
out.append(b)
|
||||
i += 1
|
||||
|
||||
header = struct.pack("<I", len(data)) + b"\x01"
|
||||
return header + bytes(out)
|
||||
|
||||
|
||||
def delta_rle_decode(stream: bytes) -> bytes:
|
||||
"""Decompress a delta-RLE stream back to original bytes."""
|
||||
orig_len = struct.unpack("<I", stream[:4])[0]
|
||||
if orig_len == 0:
|
||||
return b""
|
||||
_flag = stream[4]
|
||||
payload = stream[5:]
|
||||
|
||||
# Undo RLE
|
||||
expanded = bytearray()
|
||||
i = 0
|
||||
while i < len(payload):
|
||||
if payload[i] == 0xFE:
|
||||
if i + 1 < len(payload) and payload[i + 1] == 0xFE:
|
||||
expanded.append(0xFE)
|
||||
i += 2
|
||||
elif i + 2 < len(payload):
|
||||
run_byte = payload[i + 1]
|
||||
run_len = payload[i + 2]
|
||||
expanded.extend([run_byte] * run_len)
|
||||
i += 3
|
||||
else:
|
||||
expanded.append(payload[i])
|
||||
i += 1
|
||||
else:
|
||||
expanded.append(payload[i])
|
||||
i += 1
|
||||
|
||||
# Undo delta encoding
|
||||
out = bytearray(orig_len)
|
||||
if orig_len > 0:
|
||||
out[0] = expanded[0]
|
||||
for j in range(1, orig_len):
|
||||
out[j] = (expanded[j] + out[j - 1]) & 0xFF
|
||||
|
||||
return bytes(out)
|
||||
|
||||
|
||||
# ── Reed-Solomon error correction ───────────────────────────────────────────
|
||||
|
||||
def rs_encode(data: bytes, nsym: int = RS_NSYM) -> bytes:
|
||||
"""Append Reed-Solomon parity symbols to *data*."""
|
||||
if reedsolo is None:
|
||||
raise ImportError("reedsolo is required for Reed-Solomon ECC. pip install reedsolo")
|
||||
rs = reedsolo.RSCodec(nsym)
|
||||
return rs.encode(data)
|
||||
|
||||
|
||||
def rs_decode(data: bytes, nsym: int = RS_NSYM) -> bytes:
|
||||
"""Decode (and correct errors in) a Reed-Solomon encoded message."""
|
||||
if reedsolo is None:
|
||||
raise ImportError("reedsolo is required for Reed-Solomon ECC. pip install reedsolo")
|
||||
rs = reedsolo.RSCodec(nsym)
|
||||
decoded = rs.decode(data)
|
||||
# reedsolo returns (decoded_msg, decoded_msg_with_ecc, ...) — take first element
|
||||
if isinstance(decoded, tuple):
|
||||
return bytes(decoded[0])
|
||||
return bytes(decoded)
|
||||
|
||||
|
||||
# ── ChaCha20 encryption ─────────────────────────────────────────────────────
|
||||
|
||||
def _get_chacha_key(key: Optional[bytes] = None) -> bytes:
|
||||
"""Return a 32-byte ChaCha20 key, generating one if not supplied."""
|
||||
if key is not None:
|
||||
if len(key) != CHACHA_KEY_SIZE:
|
||||
raise ValueError(f"Key must be {CHACHA_KEY_SIZE} bytes")
|
||||
return key
|
||||
return os.urandom(CHACHA_KEY_SIZE)
|
||||
|
||||
|
||||
def chacha_encrypt(plaintext: bytes, key: bytes, nonce: Optional[bytes] = None) -> Tuple[bytes, bytes]:
|
||||
"""Encrypt *plaintext* with ChaCha20. Returns (ciphertext, nonce)."""
|
||||
if not _CHA20_AVAILABLE:
|
||||
raise ImportError("cryptography is required for ChaCha20. pip install cryptography")
|
||||
if nonce is None:
|
||||
nonce = os.urandom(CHACHA_NONCE_SIZE)
|
||||
encryptor = _Cipher(_alg.ChaCha20(key, nonce), mode=None).encryptor()
|
||||
ct = encryptor.update(plaintext) + encryptor.finalize()
|
||||
return ct, nonce
|
||||
|
||||
|
||||
def chacha_decrypt(ciphertext: bytes, key: bytes, nonce: bytes) -> bytes:
|
||||
"""Decrypt *ciphertext* with ChaCha20."""
|
||||
if not _CHA20_AVAILABLE:
|
||||
raise ImportError("cryptography is required for ChaCha20. pip install cryptography")
|
||||
decryptor = _Cipher(_alg.ChaCha20(key, nonce), mode=None).decryptor()
|
||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||
|
||||
|
||||
# ── Serialization / Deserialization helpers ──────────────────────────────────
|
||||
|
||||
def _serialize_bracket(bracket: dict) -> bytes:
|
||||
"""Encode a BraidBracket dict to 21 bytes."""
|
||||
data = b""
|
||||
for key in ("lower", "upper", "gap", "kappa", "phi"):
|
||||
data += _q16_to_bytes(bracket[key])
|
||||
data += _bool_to_byte(bracket["admissible"])
|
||||
return data
|
||||
|
||||
|
||||
def _deserialize_bracket(raw: bytes) -> dict:
|
||||
"""Decode 21 bytes into a BraidBracket dict."""
|
||||
keys = ("lower", "upper", "gap", "kappa", "phi")
|
||||
bracket = {}
|
||||
for i, key in enumerate(keys):
|
||||
bracket[key] = struct.unpack("<I", raw[i * 4:(i + 1) * 4])[0]
|
||||
bracket["admissible"] = raw[20] != 0
|
||||
return bracket
|
||||
|
||||
|
||||
def _serialize_strand(strand: dict) -> bytes:
|
||||
"""Encode a BraidStrand dict to 42 bytes."""
|
||||
data = b""
|
||||
data += _q16_to_bytes(strand["phaseAcc"]["x"])
|
||||
data += _q16_to_bytes(strand["phaseAcc"]["y"])
|
||||
data += _bool_to_byte(strand["parity"])
|
||||
data += _u32_to_bytes(strand["slot"])
|
||||
data += _q16_to_bytes(strand["residue"])
|
||||
data += _q16_to_bytes(strand["jitter"])
|
||||
data += _serialize_bracket(strand["bracket"])
|
||||
assert len(data) == BRAID_STRAND_BYTES
|
||||
return data
|
||||
|
||||
|
||||
def _deserialize_strand(raw: bytes) -> dict:
|
||||
"""Decode 42 bytes into a BraidStrand dict."""
|
||||
return {
|
||||
"phaseAcc": {
|
||||
"x": struct.unpack("<I", raw[0:4])[0],
|
||||
"y": struct.unpack("<I", raw[4:8])[0],
|
||||
},
|
||||
"parity": raw[8] != 0,
|
||||
"slot": struct.unpack("<I", raw[9:13])[0],
|
||||
"residue": struct.unpack("<I", raw[13:17])[0],
|
||||
"jitter": struct.unpack("<I", raw[17:21])[0],
|
||||
"bracket": _deserialize_bracket(raw[21:42]),
|
||||
}
|
||||
|
||||
|
||||
# ── Full pipeline: encode payload ───────────────────────────────────────────
|
||||
|
||||
def _build_frame_payload(tag: int, serialized: bytes,
|
||||
key: Optional[bytes],
|
||||
compress: bool = True) -> bytes:
|
||||
"""Apply Delta+RLE → RS → ChaCha20 → return frame-ready payload.
|
||||
|
||||
Layout: [1B tag][1B flags][nonce?][RS-encoded, encrypted blob]
|
||||
"""
|
||||
flags = 0x00
|
||||
if compress:
|
||||
flags |= 0x01
|
||||
|
||||
blob = serialized
|
||||
if compress:
|
||||
blob = delta_rle_encode(blob)
|
||||
|
||||
blob = rs_encode(blob)
|
||||
|
||||
nonce = b""
|
||||
if key is not None:
|
||||
blob, nonce = chacha_encrypt(blob, key)
|
||||
flags |= 0x02 # encrypted flag
|
||||
|
||||
return struct.pack("<BB", tag, flags) + nonce + blob
|
||||
|
||||
|
||||
# ── Full pipeline: decode payload ───────────────────────────────────────────
|
||||
|
||||
def decode_braid_frame(frame_payload: bytes,
|
||||
key: Optional[bytes] = None) -> dict:
|
||||
"""Decode a frame payload (after extracting from MKV / YUV420 frame).
|
||||
|
||||
Reverses: ChaCha20 decrypt → RS decode → Delta+RLE decompress → deserialize.
|
||||
|
||||
Args:
|
||||
frame_payload: raw payload bytes (after stripping VCN signature header).
|
||||
key: ChaCha20 key (required if the frame was encrypted).
|
||||
|
||||
Returns:
|
||||
{
|
||||
"tag": int,
|
||||
"tag_name": str,
|
||||
"flags": int,
|
||||
"decrypted": bool,
|
||||
"data": dict | bytes, # deserialized braid structure
|
||||
}
|
||||
"""
|
||||
tag, flags = struct.unpack("<BB", frame_payload[:2])
|
||||
encrypted = bool(flags & 0x02)
|
||||
compressed = bool(flags & 0x01)
|
||||
offset = 2
|
||||
|
||||
nonce = b""
|
||||
if encrypted:
|
||||
nonce = frame_payload[offset:offset + CHACHA_NONCE_SIZE]
|
||||
offset += CHACHA_NONCE_SIZE
|
||||
|
||||
blob = frame_payload[offset:]
|
||||
|
||||
# Reverse pipeline
|
||||
if encrypted:
|
||||
if key is None:
|
||||
raise ValueError("Frame is encrypted but no key provided")
|
||||
blob = chacha_decrypt(blob, key, nonce)
|
||||
|
||||
blob = rs_decode(blob)
|
||||
|
||||
if compressed:
|
||||
blob = delta_rle_decode(blob)
|
||||
|
||||
# Deserialize based on tag
|
||||
tag_names = {TAG_STRAND: "strand", TAG_CROSSING: "crossing", TAG_PIST: "pist"}
|
||||
result: dict = {
|
||||
"tag": tag,
|
||||
"tag_name": tag_names.get(tag, "unknown"),
|
||||
"flags": flags,
|
||||
"decrypted": encrypted,
|
||||
}
|
||||
|
||||
if tag == TAG_STRAND:
|
||||
result["data"] = _deserialize_strand(blob)
|
||||
elif tag == TAG_CROSSING:
|
||||
result["data"] = {
|
||||
"bracket_a": _deserialize_bracket(blob[:BRAID_BRACKET_BYTES]),
|
||||
"bracket_b": _deserialize_bracket(blob[BRAID_BRACKET_BYTES:]),
|
||||
}
|
||||
elif tag == TAG_PIST:
|
||||
result["data"] = json.loads(blob.decode("utf-8"))
|
||||
else:
|
||||
result["data"] = blob
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def encode_braid_bracket(bracket: dict) -> bytes:
|
||||
"""Encode a BraidBracket dict to 21 bytes.
|
||||
|
||||
Args:
|
||||
bracket: dict with keys 'lower', 'upper', 'gap', 'kappa', 'phi' (Q16_16 ints),
|
||||
'admissible' (bool)
|
||||
|
||||
Returns:
|
||||
21-byte serialization matching Lean encodeBraidBracket.
|
||||
"""
|
||||
data = b''
|
||||
for key in ['lower', 'upper', 'gap', 'kappa', 'phi']:
|
||||
data += _q16_to_bytes(bracket[key])
|
||||
data += _bool_to_byte(bracket['admissible'])
|
||||
return data
|
||||
|
||||
|
||||
def encode_braid_strand(strand_data: dict, resolution: str = "1080p",
|
||||
key: Optional[bytes] = None,
|
||||
compress: bool = True) -> bytes:
|
||||
"""Encode a BraidStrand dict to a VCN frame with optional pipeline stages.
|
||||
|
||||
Args:
|
||||
strand_data: dict with keys:
|
||||
'phaseAcc': {'x': int, 'y': int} (Q16_16 values)
|
||||
'parity': bool
|
||||
'slot': int (UInt32)
|
||||
'residue': int (Q16_16)
|
||||
'jitter': int (Q16_16)
|
||||
'bracket': dict (see encode_braid_bracket)
|
||||
resolution: VCN resolution string (default "1080p")
|
||||
key: Optional ChaCha20 encryption key (32 bytes)
|
||||
compress: Apply Delta+RLE compression (default True)
|
||||
|
||||
Returns:
|
||||
Raw VCN frame bytes (YUV420) suitable for hardware encoding.
|
||||
"""
|
||||
serialized = _serialize_strand(strand_data)
|
||||
payload = _build_frame_payload(TAG_STRAND, serialized, key, compress)
|
||||
|
||||
w, h = VCN_RESOLUTIONS.get(resolution, VCN_RESOLUTIONS["1080p"])
|
||||
spec = VCNComputeFrameSpec(
|
||||
width=w, height=h,
|
||||
bytes_per_frame=compute_frame_size(w, h, "yuv420p"),
|
||||
encoder="libx264"
|
||||
)
|
||||
return create_frame_dynamic(payload, seq=0, spec=spec)
|
||||
|
||||
|
||||
def encode_braid_crossing(bracket_a: dict, bracket_b: dict,
|
||||
resolution: str = "1080p",
|
||||
key: Optional[bytes] = None,
|
||||
compress: bool = True) -> bytes:
|
||||
"""Encode two BraidBrackets (crossing operation) to a VCN frame with optional pipeline.
|
||||
|
||||
Encodes the crossing residual computation R_ij = B_ij - (B_i + B_j)
|
||||
by packing both brackets side by side (42 bytes).
|
||||
|
||||
Args:
|
||||
bracket_a, bracket_b: dicts with bracket fields
|
||||
resolution: VCN resolution string
|
||||
key: Optional ChaCha20 encryption key (32 bytes)
|
||||
compress: Apply Delta+RLE compression (default True)
|
||||
|
||||
Returns:
|
||||
Raw VCN frame bytes.
|
||||
"""
|
||||
serialized = _serialize_bracket(bracket_a) + _serialize_bracket(bracket_b)
|
||||
payload = _build_frame_payload(TAG_CROSSING, serialized, key, compress)
|
||||
|
||||
w, h = VCN_RESOLUTIONS.get(resolution, VCN_RESOLUTIONS["1080p"])
|
||||
spec = VCNComputeFrameSpec(
|
||||
width=w, height=h,
|
||||
bytes_per_frame=compute_frame_size(w, h, "yuv420p"),
|
||||
encoder="libx264"
|
||||
)
|
||||
return create_frame_dynamic(payload, seq=0, spec=spec)
|
||||
|
||||
|
||||
def encode_mountain_merge(mountain_a: dict, mountain_b: dict,
|
||||
resolution: str = "1080p",
|
||||
key: Optional[bytes] = None,
|
||||
compress: bool = True) -> bytes:
|
||||
"""Encode a Mountain merge operation to a VCN frame with optional pipeline.
|
||||
|
||||
Implements Mountain.merge: merged height = h+1, apex = a1.add(a2)
|
||||
(coordinate-wise sum with zero-padding, matching Lean IntNode.add).
|
||||
|
||||
Args:
|
||||
mountain_a, mountain_b: dicts with keys:
|
||||
'height': int
|
||||
'apex_coords': list of int
|
||||
resolution: VCN resolution string
|
||||
key: Optional ChaCha20 encryption key (32 bytes)
|
||||
compress: Apply Delta+RLE compression (default True)
|
||||
|
||||
Returns:
|
||||
Raw VCN frame bytes encoding the merge result.
|
||||
"""
|
||||
# Mountain.merge: height = h1 + 1
|
||||
merged_height = mountain_a['height'] + 1
|
||||
|
||||
# Coordinate-wise sum with zero-padding (matching IntNode.add)
|
||||
coords_a = mountain_a['apex_coords']
|
||||
coords_b = mountain_b['apex_coords']
|
||||
n = max(len(coords_a), len(coords_b))
|
||||
padded_a = coords_a + [0] * (n - len(coords_a))
|
||||
padded_b = coords_b + [0] * (n - len(coords_b))
|
||||
merged_coords = [a + b for a, b in zip(padded_a, padded_b)]
|
||||
|
||||
serialized = _u32_to_bytes(merged_height)
|
||||
serialized += _u32_to_bytes(len(merged_coords))
|
||||
for coord in merged_coords:
|
||||
# Clamp to Int32 range and serialize as unsigned 32-bit
|
||||
# (matching Lean UInt32.ofInt with clamping)
|
||||
clamped = max(-2147483648, min(2147483647, coord))
|
||||
serialized += struct.pack("<I", clamped & 0xFFFFFFFF)
|
||||
|
||||
payload = _build_frame_payload(TAG_PIST, serialized, key, compress)
|
||||
|
||||
w, h = VCN_RESOLUTIONS.get(resolution, VCN_RESOLUTIONS["1080p"])
|
||||
spec = VCNComputeFrameSpec(
|
||||
width=w, height=h,
|
||||
bytes_per_frame=compute_frame_size(w, h, "yuv420p"),
|
||||
encoder="libx264"
|
||||
)
|
||||
return create_frame_dynamic(payload, seq=0, spec=spec)
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: vcn_compute_substrate.py <encode|decode|extract_receipt> <input> <output>")
|
||||
print("Usage: vcn_compute_substrate.py <encode|decode|extract_receipt|encode_enhanced|decode_enhanced> <input> <output> [key.hex]")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
input_path = Path(sys.argv[2])
|
||||
|
||||
if command == "encode":
|
||||
input_path = Path(sys.argv[2])
|
||||
output_path = Path(sys.argv[3])
|
||||
|
||||
# Read input data
|
||||
|
|
@ -579,6 +1052,7 @@ def main():
|
|||
print(f"Encoded to {output_path}")
|
||||
|
||||
elif command == "decode":
|
||||
input_path = Path(sys.argv[2])
|
||||
output_path = Path(sys.argv[3])
|
||||
|
||||
# Decode
|
||||
|
|
@ -607,6 +1081,7 @@ def main():
|
|||
sys.exit(1)
|
||||
|
||||
elif command == "extract_receipt":
|
||||
input_path = Path(sys.argv[2])
|
||||
receipt = extract_receipt(input_path)
|
||||
output_path = Path(sys.argv[3])
|
||||
|
||||
|
|
@ -615,6 +1090,56 @@ def main():
|
|||
|
||||
print(f"Receipt written to {output_path}")
|
||||
|
||||
elif command == "encode_enhanced":
|
||||
# Usage: encode_enhanced strand.json output.mkv [key.hex]
|
||||
input_path = Path(sys.argv[2])
|
||||
output_path = Path(sys.argv[3])
|
||||
key = None
|
||||
if len(sys.argv) > 4:
|
||||
key = bytes.fromhex(sys.argv[4])
|
||||
if len(key) != CHACHA_KEY_SIZE:
|
||||
print(f"Key must be {CHACHA_KEY_SIZE} bytes ({CHACHA_KEY_SIZE * 2} hex chars)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(input_path) as f:
|
||||
strand_dict = json.load(f)
|
||||
|
||||
frame = encode_braid_strand(strand_dict, key=key, compress=True)
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(frame)
|
||||
|
||||
print(f"Enhanced-encoded strand to {output_path}")
|
||||
|
||||
elif command == "decode_enhanced":
|
||||
# Usage: decode_enhanced input.mkv output.json [key.hex]
|
||||
input_path = Path(sys.argv[2])
|
||||
output_path = Path(sys.argv[3])
|
||||
key = None
|
||||
if len(sys.argv) > 4:
|
||||
key = bytes.fromhex(sys.argv[4])
|
||||
|
||||
frames = decode_frames(input_path)
|
||||
if not frames:
|
||||
print("No frames decoded", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
frame = frames[0]
|
||||
header = frame[:SIGNATURE_SIZE]
|
||||
signature, version, seq, length, _ = struct.unpack("<8sIIII", header)
|
||||
|
||||
if signature != SIGNATURE_HEADER:
|
||||
print(f"Invalid signature: {signature}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
payload = frame[SIGNATURE_SIZE:SIGNATURE_SIZE + length]
|
||||
result = decode_braid_frame(payload, key)
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
print(f"Decoded enhanced frame to {output_path}")
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {command}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue