feat(poi): canonical Freivalds verifier (luxfi/crypto/poi)

The Proof-of-Inference verifier primitive in its proper home, beside
keccak.go (which the aivmbridge already imports). Freivalds matmul check
over F_p (p=2^61-1) on the exact int8 i32 accumulator: an honest A*B
passes; a single tampered output entry (incl. a buried off-by-one in a
16x24 matmul) is caught; signed entries reduced correctly; keccak-derived
challenges deterministic; wrong dims fail closed. All tests pass.

This is the canonical verifier the chain consumes (chains/aivm settlement,
the computeattest precompile). hanzo-engine/src/poi.rs is the Rust
prover-side mirror; luxcpp/crypto is the C++ native mirror. The forward-
pass commitment + transcript binding are not here (engine/chain side); no
end-to-end proof is emitted yet.
This commit is contained in:
zeekay
2026-06-23 13:21:45 -07:00
parent 9a71af427f
commit 597f94e22c
2 changed files with 258 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
// Package poi is the canonical Proof-of-Inference verifier primitive.
//
// Proof-of-AI binds reward to genuine computation: an LLM forward pass is ~95% matrix
// multiplications C = A·B, and Freivalds (1977) verifies one for a random vector r via
// A·(B·r) == C·r in O(t·k + k·n + t·n) — an order cheaper than recomputing — catching a
// fabricated C (a claimed output the prover never multiplied for) with probability >= 1 - 1/p
// per vector. We work over the prime field F_p, p = 2^61-1, on the EXACT INTEGER accumulator
// of the engine's int8 matmul (i8·i8 -> i32), so the check is bit-exact with ZERO false-reject
// across CPU/GPU/backends — the determinism the whole scheme relies on. Soundness error is
// 1/p ~ 2^-61 per challenge vector; k=2 vectors give <= 2^-122.
//
// This is the CANONICAL verifier the chain consumes (chains/aivm settlement, the aivmbridge /
// computeattest precompile). It sits beside crypto.Keccak256 so a future compute_proof.go and
// the on-chain transcript verification share one hash and one field. The engine's Rust
// hanzo-engine/src/poi.rs is the PROVER-side mirror and must match this byte-for-byte (same
// pattern as the receipt wire spec mirrored Go aivmbridge <-> Solidity AICoinMiner); a luxcpp
// C++ mirror provides a native/precompile-speed verifier.
package poi
import (
"encoding/binary"
"math/bits"
"github.com/luxfi/crypto"
)
// P is the Mersenne prime field modulus p = 2^61 - 1. It bounds the int8 accumulator and the
// Freivalds intermediates within 61 bits, so a single 128-bit multiply never overflows.
const P uint64 = (1 << 61) - 1
// Mat is a dense row-major integer matrix (the int8 path's exact i32/i64 accumulators).
type Mat struct {
Rows int
Cols int
Data []int64 // element (i,j) = Data[i*Cols+j]
}
// NewMat builds a matrix, panicking on a length mismatch (programmer error).
func NewMat(rows, cols int, data []int64) Mat {
if len(data) != rows*cols {
panic("poi: Mat data length must be rows*cols")
}
return Mat{Rows: rows, Cols: cols, Data: data}
}
func (m Mat) at(i, j int) int64 { return m.Data[i*m.Cols+j] }
// toField reduces a signed integer into [0, p). Handles negative int8 weights/activations.
func toField(x int64) uint64 {
r := x % int64(P)
if r < 0 {
r += int64(P)
}
return uint64(r)
}
// fadd returns (a + b) mod p for a, b < p (sum < 2p < 2^62, no overflow).
func fadd(a, b uint64) uint64 {
s := a + b
if s >= P {
return s - P
}
return s
}
// fmul returns (a * b) mod p via a 128-bit multiply (both < p < 2^61, so the high word < p
// and bits.Div64 is well-defined).
func fmul(a, b uint64) uint64 {
hi, lo := bits.Mul64(a, b)
_, rem := bits.Div64(hi, lo, P)
return rem
}
// matvec computes m·v over F_p: m is an INTEGER matrix (reduced per entry), v a field vector of
// length m.Cols. Returns a field vector of length m.Rows.
func matvec(m Mat, v []uint64) []uint64 {
out := make([]uint64, m.Rows)
for i := 0; i < m.Rows; i++ {
var acc uint64
for j := 0; j < m.Cols; j++ {
acc = fadd(acc, fmul(toField(m.at(i, j)), v[j]))
}
out[i] = acc
}
return out
}
// Verify checks C == A·B probabilistically for ONE challenge vector r (length n).
// Dims: A is [t x k], B is [k x n], C is [t x n], r is length n. O(tk + kn + tn).
func Verify(a, b, c Mat, r []uint64) bool {
if a.Cols != b.Rows || b.Cols != c.Cols || a.Rows != c.Rows || len(r) != b.Cols {
return false
}
br := matvec(b, r) // length k
abr := matvec(a, br) // length t
cr := matvec(c, r) // length t
return equalU64(abr, cr)
}
// VerifyMulti checks against k independent challenge vectors. Honest C passes ALL; a tampered C
// fails at least one except with probability <= (1/p)^k. Returns true iff every vector passes.
func VerifyMulti(a, b, c Mat, challenges [][]uint64) bool {
if len(challenges) == 0 {
return false
}
for _, r := range challenges {
if !Verify(a, b, c, r) {
return false
}
}
return true
}
// DeriveChallenges derives k challenge vectors of length n over F_p from a seed, using the
// canonical keccak the rest of the bridge uses. Reproducible by prover and verifier; in
// production the seed is keccak(beacon || task || transcriptRoot) so it is unpredictable before
// the prover commits.
func DeriveChallenges(seed []byte, n, k int) [][]uint64 {
out := make([][]uint64, k)
for j := 0; j < k; j++ {
row := make([]uint64, n)
for i := 0; i < n; i++ {
var idx [12]byte
binary.BigEndian.PutUint32(idx[0:4], uint32(j))
binary.BigEndian.PutUint64(idx[4:12], uint64(i))
h := crypto.Keccak256(seed, idx[:])
row[i] = binary.BigEndian.Uint64(h[:8]) % P
}
out[j] = row
}
return out
}
func equalU64(a, b []uint64) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
+114
View File
@@ -0,0 +1,114 @@
package poi
import "testing"
// fixture: A=[[1,2],[3,4]], B=[[5,6],[7,8]], C = A·B = [[19,22],[43,50]].
func fixture() (Mat, Mat, Mat) {
a := NewMat(2, 2, []int64{1, 2, 3, 4})
b := NewMat(2, 2, []int64{5, 6, 7, 8})
c := NewMat(2, 2, []int64{19, 22, 43, 50})
return a, b, c
}
func TestHonestMatmulPasses(t *testing.T) {
a, b, c := fixture()
ch := DeriveChallenges([]byte("beluga"), 2, 4)
if !VerifyMulti(a, b, c, ch) {
t.Fatal("honest C = A·B must pass every challenge")
}
}
func TestTamperedOutputIsCaught(t *testing.T) {
a, b, _ := fixture()
cBad := NewMat(2, 2, []int64{19, 22, 43, 51}) // 50 -> 51: a faker who never multiplied
ch := DeriveChallenges([]byte("beluga"), 2, 4)
if VerifyMulti(a, b, cBad, ch) {
t.Fatal("a single tampered output entry must be CAUGHT (Freivalds soundness)")
}
}
func TestNegativeEntriesFieldReduction(t *testing.T) {
// int8 activations/weights are signed; toField must handle negatives exactly.
a := NewMat(2, 2, []int64{1, -2, 3, 4})
b := NewMat(2, 2, []int64{5, 6, -7, 8})
c := NewMat(2, 2, []int64{19, -10, -13, 50}) // exact A·B
ch := DeriveChallenges([]byte{7}, 2, 4)
if !VerifyMulti(a, b, c, ch) {
t.Fatal("honest matmul with negatives passes")
}
cBad := NewMat(2, 2, []int64{19, -10, -13, 49})
if VerifyMulti(a, b, cBad, ch) {
t.Fatal("tampered negative-entry matmul caught")
}
}
func TestChallengeDerivationDeterministic(t *testing.T) {
if !chEq(DeriveChallenges([]byte("x"), 8, 3), DeriveChallenges([]byte("x"), 8, 3)) {
t.Fatal("prover and verifier must derive identical challenges from the same seed")
}
if chEq(DeriveChallenges([]byte("x"), 8, 3), DeriveChallenges([]byte("y"), 8, 3)) {
t.Fatal("different seeds must give different challenges")
}
}
func TestWrongDimsRejected(t *testing.T) {
a := NewMat(2, 3, []int64{1, 2, 3, 4, 5, 6})
b := NewMat(2, 2, []int64{1, 2, 3, 4}) // b.Rows(2) != a.Cols(3)
c := NewMat(2, 2, []int64{1, 2, 3, 4})
if Verify(a, b, c, []uint64{1, 1}) {
t.Fatal("shape mismatch must fail closed")
}
}
func TestLargerRandomMatmulCatchesSparseCheat(t *testing.T) {
// 16x24 · 24x16: compute C exactly, then flip a single deep entry — the sparse-cheat case.
const tt, kk, nn = 16, 24, 16
var sa, sb uint64 = 0x12345678, 0x9abcdef0
rnd := func(s *uint64) int64 {
*s = (*s)*6364136223846793005 + 1442695040888963407
return int64((*s>>33)%127) - 63
}
aData := make([]int64, tt*kk)
for i := range aData {
aData[i] = rnd(&sa)
}
bData := make([]int64, kk*nn)
for i := range bData {
bData[i] = rnd(&sb)
}
a := NewMat(tt, kk, aData)
b := NewMat(kk, nn, bData)
cData := make([]int64, tt*nn)
for i := 0; i < tt; i++ {
for j := 0; j < nn; j++ {
var acc int64
for x := 0; x < kk; x++ {
acc += aData[i*kk+x] * bData[x*nn+j]
}
cData[i*nn+j] = acc
}
}
c := NewMat(tt, nn, append([]int64(nil), cData...))
ch := DeriveChallenges([]byte("coffee"), nn, 2)
if !VerifyMulti(a, b, c, ch) {
t.Fatal("honest large matmul passes")
}
bad := append([]int64(nil), cData...)
bad[7*nn+11]++ // one entry off by one
cBad := NewMat(tt, nn, bad)
if VerifyMulti(a, b, cBad, ch) {
t.Fatal("off-by-one in one buried entry is caught")
}
}
func chEq(a, b [][]uint64) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !equalU64(a[i], b[i]) {
return false
}
}
return true
}