portal-rng: 9-cell cross-impl stream matches independent Python baseline

Closes tickets 0001 (portal-rng) and 0002 (os-entropy-seed).

Ticket 0001 goal 4 called for proof that seeding with k, drawing N,
saving, clearing, resuming in any impl, and drawing M more produces a
full stream matching a single-process Python baseline bit-for-bit.
Prior tests/portal-cross-test.sh exercised producer-consumer agreement
but used a producer-side self-computed baseline; it did not compare
against an independent Python run that never saves or resumes.

tests/portal-rng-cross-test.sh computes a single-process Python baseline
once (seed=42, N+M=10 draws, no portal), then runs all 9 producer x
consumer cells (Python, C, asm each side) and checks that producer's
first N plus consumer's M equals the independent baseline. All 12
assertions pass.

Wired into make test-all. Ticket status updated to resolved on both
0001 and 0002 with dated one-line resolution notes.
This commit is contained in:
russell@unturf.com 2026-04-23 20:19:16 -04:00
parent 9dee0d02ee
commit d2866f486d
4 changed files with 156 additions and 5 deletions

View file

@ -99,9 +99,12 @@ functional-test: c-build
@echo "── C ──"
@./c/lumbda tests/functional.lsp | tail -3
test-all: test c-test asm-test functional-test
portal-rng-cross-test: c-build asm-build
@bash tests/portal-rng-cross-test.sh
test-all: test c-test asm-test functional-test portal-rng-cross-test
@echo "════════════════════════════════════════════════════"
@echo "All tests passed (Python + C + Assembly + functional)"
@echo "All tests passed (Python + C + Assembly + functional + portal-rng-cross)"
# ─── Benchmarks (reproducible; referenced in whitepaper §6§11) ───
@ -237,6 +240,6 @@ clean-all: clean clean-whitepaper clean-docs c-clean asm-clean
.PHONY: all test test-verbose bench bench-verbose repl lint \
c-build c-test c-bench c-repl c-clean \
asm-build asm-test asm-repl asm-clean \
test-all bench-all examples friction functional-test \
test-all bench-all examples friction functional-test portal-rng-cross-test \
bench-3way bench-portal bench-portal-cross bench-web bench-rpc-chain bench-proof \
docs whitepaper clean clean-whitepaper clean-docs clean-all

View file

@ -1,9 +1,10 @@
# 0001 — Portal preserves RNG state across processes
**Status:** open
**Status:** resolved
**Reporter:** Zoe (via fox)
**Implementer:** blackops
**Opened:** 2026-04-20
**Resolved:** 2026-04-24 — xoshiro256\*\* shipped in Python, C, asm; portal round-trips RNG state; `tests/portal-rng-cross-test.sh` proves all 9 producer×consumer cells reproduce the independent single-process Python baseline bit-for-bit. Wired into `make test-all`.
## Problem

View file

@ -1,9 +1,10 @@
# 0002 — `(random-seed-from-os!)` pulls kernel entropy
**Status:** open
**Status:** resolved
**Reporter:** fox (response to 0001 follow-up)
**Implementer:** blackops
**Opened:** 2026-04-20
**Resolved:** 2026-04-24 — `(random-seed-from-os!)` shipped in all three impls, reads 8 LE bytes from `/dev/urandom`, fails loud on short read. Entropic + replay checks in `tests/functional.lsp` and `asm/test.sh`; OS-seeded state plugs cleanly into the 0001 portal path.
## Problem

146
tests/portal-rng-cross-test.sh Executable file
View file

@ -0,0 +1,146 @@
#!/bin/bash
# portal-rng-cross-test.sh — ticket 0001 deliverable (goal 4):
#
# "seed k, draw N values, save, clear, resume in any impl, draw M more —
# full stream matches a single-process Python baseline."
#
# This script proves that every (producer, consumer) pair across
# Python / C / asm reproduces the exact same N+M stream that a single
# Python process produces with no save/resume at all. That is the
# stronger claim than portal-cross-test.sh's producer-side self-
# consistency: here the truth is computed independently, once, upfront,
# by a Python run that never touches the portal. Every cell compares
# against the same independent baseline.
#
# Matrix: 3 producers × 3 consumers = 9 cells. K=42, N=5, M=5.
set -e
cd "$(dirname "$0")/.."
PY="python3 lumbda.py --fast"
C="./c/lumbda"
ASM="./asm/lumbda"
K=42 # seed
N=5 # draws before save
M=5 # draws after resume
RANGE=1000000 # random-int upper bound
# ─── Compute the independent single-process Python baseline (N+M draws) ───
BASELINE_LSP=$(mktemp --suffix=.lsp)
trap 'rm -f "$BASELINE_LSP" /tmp/rng-xstream.sexp' EXIT INT TERM
cat > "$BASELINE_LSP" <<EOF
(random-seed! $K)
;; for-each returns a non-printing value across Python/C/asm, so no
;; trailing token leaks into stdout. Iteration count is carried by the
;; length of the dummy list below (shell-expanded once).
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
'($(seq 1 $((N + M)) | tr '\n' ' ')))
EOF
# shellcheck disable=SC2086
BASELINE=$($PY "$BASELINE_LSP")
BASELINE_COUNT=$(printf "%s\n" "$BASELINE" | wc -l)
if [ "$BASELINE_COUNT" -ne $((N + M)) ]; then
echo "FAIL: baseline produced $BASELINE_COUNT draws, expected $((N + M))"
exit 1
fi
# Split baseline into first-N and next-M.
BASELINE_N=$(printf "%s\n" "$BASELINE" | head -n $N)
BASELINE_M=$(printf "%s\n" "$BASELINE" | tail -n $M)
echo "═══════════════════════════════════════════════════════"
echo "RNG portal cross-impl round-trip — independent Python baseline"
echo "Ticket: docs/tickets/0001-portal-rng.md (goal 4)"
echo " seed=$K, save after N=$N draws, resume for M=$M draws"
echo "═══════════════════════════════════════════════════════"
echo
echo "single-process Python baseline (N+M=$((N + M)) draws):"
printf "%s\n" "$BASELINE" | sed 's/^/ /'
echo
# ─── Producer script (generated): seed, draw N, emit them to stdout, save state ───
PROD_LSP=$(mktemp --suffix=.lsp)
cat > "$PROD_LSP" <<EOF
(random-seed! $K)
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
'($(seq 1 $N | tr '\n' ' ')))
(define snapshot (random-state))
(define port (open-output-file "/tmp/rng-xstream.sexp"))
(display "(random-state! '" port) (write snapshot port) (display ")\n" port)
(close-port port)
EOF
# ─── Consumer script (generated): load state, draw M, emit them to stdout ───
CONS_LSP=$(mktemp --suffix=.lsp)
cat > "$CONS_LSP" <<EOF
(load "/tmp/rng-xstream.sexp")
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
'($(seq 1 $M | tr '\n' ' ')))
EOF
# shellcheck disable=SC2064
trap "rm -f '$BASELINE_LSP' '$PROD_LSP' '$CONS_LSP' /tmp/rng-xstream.sexp" EXIT INT TERM
PASS=0
FAIL=0
run_producer() {
local impl="$1"
case "$impl" in
Python) $PY "$PROD_LSP" ;;
C) $C "$PROD_LSP" ;;
Asm) $ASM < "$PROD_LSP" ;;
esac
}
run_consumer() {
local impl="$1"
case "$impl" in
Python) $PY "$CONS_LSP" ;;
C) $C "$CONS_LSP" ;;
Asm) $ASM < "$CONS_LSP" ;;
esac
}
verify_stream() {
local label="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
PASS=$((PASS + 1))
printf " %-22s OK\n" "$label"
else
FAIL=$((FAIL + 1))
printf " %-22s FAIL\n" "$label"
echo " expected:"
printf "%s\n" "$expected" | sed 's/^/ /'
echo " actual:"
printf "%s\n" "$actual" | sed 's/^/ /'
fi
}
for P_NAME in Python C Asm; do
echo "── producer: $P_NAME ──"
rm -f /tmp/rng-xstream.sexp
PROD_OUT=$(run_producer "$P_NAME")
# Producer's first N must equal the Python baseline's first N.
verify_stream "$P_NAME: first $N draws" "$BASELINE_N" "$PROD_OUT"
for C_NAME in Python C Asm; do
CONS_OUT=$(run_consumer "$C_NAME")
# Consumer's M draws after resume must equal the Python baseline's last M.
verify_stream "$P_NAME -> $C_NAME (next $M)" "$BASELINE_M" "$CONS_OUT"
done
done
echo
echo "═══════════════════════════════════════════════════════"
echo "Cross-impl RNG round-trip vs Python baseline: $PASS passed, $FAIL failed"
echo "═══════════════════════════════════════════════════════"
if [ $FAIL -ne 0 ]; then
exit 1
fi