mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
poi: transcript + on-chain-parity challenger (gives Verify a real caller)
Mirror of hanzo-engine/src/poi_transcript.rs: ProofTranscript (keccak Merkle over per-matmul leaves), ExactMatmul (whole-K i64), VerifyOpening (Merkle inclusion + Freivalds), ChallengeIndex. VerifyOpening is the production caller the audit found missing — the off-chain watcher imports it and slashes a bond when an opening fails. Tests: transcript round-trip, fabricated-output-caught, swapped-reveal-caught, and a pinned golden DeriveChallenges vector mirrored byte-for-byte in the Rust suite (cross-language parity enforced, not assumed). go test ./poi: ok.
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
// Proof-of-Inference TRANSCRIPT (Go mirror of hanzo-engine/src/poi_transcript.rs).
|
||||
//
|
||||
// This is the wire that makes "no valid compute proof, no mint" real: a prover commits each
|
||||
// proof-bearing matmul as a keccak Merkle leaf over its exact-integer (A,B,C); the Merkle root is
|
||||
// the transcriptRoot posted on-chain. The off-chain watcher (and the computeattest precompile)
|
||||
// challenge a beacon-selected matmul, the prover OPENS it, and VerifyOpening (1) checks the
|
||||
// operands were the committed ones (Merkle inclusion, the same index-fold as
|
||||
// AICoinMiner._verifyMerkle) and (2) Freivalds-verifies C = A·B (Verify, this package). A
|
||||
// fabricated output fails (2); a swapped-in output fails (1). This file is what gives the Verify
|
||||
// primitive a production caller — the watcher imports it and slashes a bond on a failed opening.
|
||||
//
|
||||
// Byte-for-byte identical to the Rust prover: same DOMAIN tag, same canonical Mat serialization,
|
||||
// same keccak fold, same DeriveChallenges. An opening produced by the engine verifies here and
|
||||
// on-chain without re-hashing.
|
||||
package poi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
)
|
||||
|
||||
// DomainMatmulLeaf tags a matmul leaf, disjoint from any other keccak leaf in the bridge.
|
||||
var DomainMatmulLeaf = []byte("hanzo/poi/matmul-leaf/v1")
|
||||
|
||||
// ExactMatmul is the whole-K i64 exact-integer product C = A·B — the proof-bearing GEMM whose
|
||||
// output Freivalds checks with zero false-reject (mirrors poi::exact_matmul).
|
||||
func ExactMatmul(a, b Mat) Mat {
|
||||
if a.Cols != b.Rows {
|
||||
panic("poi: ExactMatmul dim mismatch a.Cols != b.Rows")
|
||||
}
|
||||
t, k, n := a.Rows, a.Cols, b.Cols
|
||||
c := make([]int64, t*n)
|
||||
for i := 0; i < t; i++ {
|
||||
for j := 0; j < n; j++ {
|
||||
var acc int64
|
||||
for x := 0; x < k; x++ {
|
||||
acc += a.Data[i*k+x] * b.Data[x*n+j]
|
||||
}
|
||||
c[i*n+j] = acc
|
||||
}
|
||||
}
|
||||
return NewMat(t, n, c)
|
||||
}
|
||||
|
||||
// matBytes is the canonical serialization: BE32(rows) ‖ BE32(cols) ‖ data[i] as BE64.
|
||||
func matBytes(m Mat) []byte {
|
||||
b := make([]byte, 0, 8+len(m.Data)*8)
|
||||
var u32 [4]byte
|
||||
binary.BigEndian.PutUint32(u32[:], uint32(m.Rows))
|
||||
b = append(b, u32[:]...)
|
||||
binary.BigEndian.PutUint32(u32[:], uint32(m.Cols))
|
||||
b = append(b, u32[:]...)
|
||||
var u64 [8]byte
|
||||
for _, x := range m.Data {
|
||||
binary.BigEndian.PutUint64(u64[:], uint64(x))
|
||||
b = append(b, u64[:]...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// MatmulLeaf binds one matmul: keccak(DOMAIN ‖ mat(A) ‖ mat(B) ‖ mat(C)).
|
||||
func MatmulLeaf(a, b, c Mat) [32]byte {
|
||||
buf := make([]byte, 0, len(DomainMatmulLeaf)+8)
|
||||
buf = append(buf, DomainMatmulLeaf...)
|
||||
buf = append(buf, matBytes(a)...)
|
||||
buf = append(buf, matBytes(b)...)
|
||||
buf = append(buf, matBytes(c)...)
|
||||
var out [32]byte
|
||||
copy(out[:], crypto.Keccak256(buf))
|
||||
return out
|
||||
}
|
||||
|
||||
func hashPair(l, r [32]byte) [32]byte {
|
||||
var buf [64]byte
|
||||
copy(buf[:32], l[:])
|
||||
copy(buf[32:], r[:])
|
||||
var out [32]byte
|
||||
copy(out[:], crypto.Keccak256(buf[:]))
|
||||
return out
|
||||
}
|
||||
|
||||
// MerkleRoot folds leaves into a keccak root; odd levels duplicate the last node; the index fold
|
||||
// matches AICoinMiner._verifyMerkle (even → keccak(node‖sib), odd → keccak(sib‖node)).
|
||||
func MerkleRoot(leaves [][32]byte) [32]byte {
|
||||
if len(leaves) == 0 {
|
||||
panic("poi: MerkleRoot over empty transcript")
|
||||
}
|
||||
level := append([][32]byte(nil), leaves...)
|
||||
for len(level) > 1 {
|
||||
if len(level)%2 == 1 {
|
||||
level = append(level, level[len(level)-1])
|
||||
}
|
||||
next := make([][32]byte, 0, len(level)/2)
|
||||
for i := 0; i < len(level); i += 2 {
|
||||
next = append(next, hashPair(level[i], level[i+1]))
|
||||
}
|
||||
level = next
|
||||
}
|
||||
return level[0]
|
||||
}
|
||||
|
||||
// MerkleProof returns the bottom-up sibling path for the leaf at index.
|
||||
func MerkleProof(leaves [][32]byte, index int) [][32]byte {
|
||||
var proof [][32]byte
|
||||
level := append([][32]byte(nil), leaves...)
|
||||
idx := index
|
||||
for len(level) > 1 {
|
||||
if len(level)%2 == 1 {
|
||||
level = append(level, level[len(level)-1])
|
||||
}
|
||||
var sib int
|
||||
if idx%2 == 0 {
|
||||
sib = idx + 1
|
||||
} else {
|
||||
sib = idx - 1
|
||||
}
|
||||
proof = append(proof, level[sib])
|
||||
next := make([][32]byte, 0, len(level)/2)
|
||||
for i := 0; i < len(level); i += 2 {
|
||||
next = append(next, hashPair(level[i], level[i+1]))
|
||||
}
|
||||
level = next
|
||||
idx /= 2
|
||||
}
|
||||
return proof
|
||||
}
|
||||
|
||||
// MerkleVerify checks a leaf's inclusion under root at index — the exact fold of _verifyMerkle.
|
||||
func MerkleVerify(leaf, root [32]byte, index int, proof [][32]byte) bool {
|
||||
node := leaf
|
||||
idx := index
|
||||
for _, sib := range proof {
|
||||
if idx%2 == 0 {
|
||||
node = hashPair(node, sib)
|
||||
} else {
|
||||
node = hashPair(sib, node)
|
||||
}
|
||||
idx /= 2
|
||||
}
|
||||
return node == root
|
||||
}
|
||||
|
||||
// Opening is what a prover reveals to answer a challenge.
|
||||
type Opening struct {
|
||||
Index int
|
||||
A, B, C Mat
|
||||
Proof [][32]byte
|
||||
}
|
||||
|
||||
// ProofTranscript is an append-only commitment to a forward pass's proof-bearing matmuls.
|
||||
type ProofTranscript struct {
|
||||
leaves [][32]byte
|
||||
ops [][3]Mat
|
||||
}
|
||||
|
||||
func NewTranscript() *ProofTranscript { return &ProofTranscript{} }
|
||||
|
||||
// Matmul runs an exact-integer matmul, commits it, and returns C.
|
||||
func (t *ProofTranscript) Matmul(a, b Mat) Mat {
|
||||
c := ExactMatmul(a, b)
|
||||
t.leaves = append(t.leaves, MatmulLeaf(a, b, c))
|
||||
t.ops = append(t.ops, [3]Mat{a, b, c})
|
||||
return c
|
||||
}
|
||||
|
||||
// CommitClaimed commits a matmul whose C is supplied by the caller (a claimed fast-path output).
|
||||
func (t *ProofTranscript) CommitClaimed(a, b, c Mat) {
|
||||
t.leaves = append(t.leaves, MatmulLeaf(a, b, c))
|
||||
t.ops = append(t.ops, [3]Mat{a, b, c})
|
||||
}
|
||||
|
||||
func (t *ProofTranscript) Len() int { return len(t.leaves) }
|
||||
|
||||
// Root is the transcriptRoot the prover posts on-chain.
|
||||
func (t *ProofTranscript) Root() [32]byte { return MerkleRoot(t.leaves) }
|
||||
|
||||
// Open reveals the matmul at index with its inclusion proof.
|
||||
func (t *ProofTranscript) Open(index int) Opening {
|
||||
o := t.ops[index]
|
||||
return Opening{Index: index, A: o[0], B: o[1], C: o[2], Proof: MerkleProof(t.leaves, index)}
|
||||
}
|
||||
|
||||
// ChallengeIndex selects which matmul to open: keccak(beacon ‖ root) mod len. Unpredictable
|
||||
// before the prover commits root.
|
||||
func ChallengeIndex(beacon []byte, root [32]byte, length int) int {
|
||||
buf := append(append([]byte(nil), beacon...), root[:]...)
|
||||
h := crypto.Keccak256(buf)
|
||||
return int(binary.BigEndian.Uint64(h[:8]) % uint64(length))
|
||||
}
|
||||
|
||||
// OpeningSeed binds the Freivalds challenge to (beacon, root, index): keccak(beacon ‖ root ‖ BE64(index)).
|
||||
func OpeningSeed(beacon []byte, root [32]byte, index int) []byte {
|
||||
var u64 [8]byte
|
||||
binary.BigEndian.PutUint64(u64[:], uint64(index))
|
||||
buf := append(append(append([]byte(nil), beacon...), root[:]...), u64[:]...)
|
||||
return crypto.Keccak256(buf)
|
||||
}
|
||||
|
||||
// VerifyOpening is THE CHALLENGER: (1) the revealed operands are the committed ones (Merkle
|
||||
// inclusion under root), and (2) C = A·B (Freivalds, k beacon-bound vectors). True iff both.
|
||||
// This is the production caller of Verify — the watcher slashes a bond when this returns false.
|
||||
func VerifyOpening(root [32]byte, beacon []byte, op Opening, k int) bool {
|
||||
leaf := MatmulLeaf(op.A, op.B, op.C)
|
||||
if !MerkleVerify(leaf, root, op.Index, op.Proof) {
|
||||
return false
|
||||
}
|
||||
seed := OpeningSeed(beacon, root, op.Index)
|
||||
challenges := DeriveChallenges(seed, op.B.Cols, k)
|
||||
return VerifyMulti(op.A, op.B, op.C, challenges)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package poi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// a tiny deterministic generator so the Go and Rust tests build identical int8-ish matrices.
|
||||
func lcg(s *uint64) int64 {
|
||||
*s = *s*6364136223846793005 + 1442695040888963407
|
||||
return int64((*s>>33)%127) - 63
|
||||
}
|
||||
|
||||
func randMat(rows, cols int, s *uint64) Mat {
|
||||
d := make([]int64, rows*cols)
|
||||
for i := range d {
|
||||
d[i] = lcg(s)
|
||||
}
|
||||
return NewMat(rows, cols, d)
|
||||
}
|
||||
|
||||
func TestExactMatmul(t *testing.T) {
|
||||
a := NewMat(2, 2, []int64{1, 2, 3, 4})
|
||||
b := NewMat(2, 2, []int64{5, 6, 7, 8})
|
||||
c := ExactMatmul(a, b) // [[19,22],[43,50]]
|
||||
want := []int64{19, 22, 43, 50}
|
||||
for i := range want {
|
||||
if c.Data[i] != want[i] {
|
||||
t.Fatalf("ExactMatmul[%d]=%d want %d", i, c.Data[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The headline property: an honest forward pass answers a beacon-selected matmul challenge, and a
|
||||
// fabricated output is CAUGHT — VerifyOpening is the production caller of Verify.
|
||||
func TestHonestOpeningVerifies(t *testing.T) {
|
||||
s := uint64(0xA1CE)
|
||||
tr := NewTranscript()
|
||||
acts := randMat(4, 64, &s)
|
||||
w1 := randMat(64, 48, &s)
|
||||
h := tr.Matmul(acts, w1)
|
||||
tr.Matmul(h, randMat(48, 32, &s))
|
||||
tr.Matmul(randMat(4, 32, &s), randMat(32, 16, &s))
|
||||
|
||||
root := tr.Root()
|
||||
beacon := []byte("beacon:block-9001")
|
||||
idx := ChallengeIndex(beacon, root, tr.Len())
|
||||
op := tr.Open(idx)
|
||||
if !VerifyOpening(root, beacon, op, 2) {
|
||||
t.Fatal("honest forward pass must pass the opened-matmul challenge")
|
||||
}
|
||||
// every matmul, whichever the beacon picks, must verify
|
||||
for i := 0; i < tr.Len(); i++ {
|
||||
if !VerifyOpening(root, beacon, tr.Open(i), 2) {
|
||||
t.Fatalf("honest matmul %d must verify", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFabricatedOutputCaught(t *testing.T) {
|
||||
s := uint64(0xF00D)
|
||||
tr := NewTranscript()
|
||||
a := randMat(4, 64, &s)
|
||||
b := randMat(64, 32, &s)
|
||||
fake := ExactMatmul(a, b)
|
||||
fake.Data[5]++ // a single output entry never produced by A·B
|
||||
tr.CommitClaimed(a, b, fake)
|
||||
if VerifyOpening(tr.Root(), []byte("beacon:catch"), tr.Open(0), 2) {
|
||||
t.Fatal("a fabricated output (C != A*B) must be CAUGHT")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwappedRevealCaught(t *testing.T) {
|
||||
s := uint64(0x5151)
|
||||
tr := NewTranscript()
|
||||
a := randMat(4, 48, &s)
|
||||
b := randMat(48, 24, &s)
|
||||
bad := ExactMatmul(a, b)
|
||||
bad.Data[3] -= 2
|
||||
tr.CommitClaimed(a, b, bad) // committed over a wrong C
|
||||
good := ExactMatmul(a, b)
|
||||
op := Opening{Index: 0, A: a, B: b, C: good, Proof: MerkleProof(tr.leaves, 0)}
|
||||
if VerifyOpening(tr.Root(), []byte("beacon:swap"), op, 2) {
|
||||
t.Fatal("revealing operands never committed must fail Merkle inclusion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerkleInclusionIndexFold(t *testing.T) {
|
||||
leaves := make([][32]byte, 3)
|
||||
for i := range leaves {
|
||||
leaves[i] = MatmulLeaf(NewMat(1, 1, []int64{int64(i)}), NewMat(1, 1, []int64{1}), NewMat(1, 1, []int64{int64(i)}))
|
||||
}
|
||||
root := MerkleRoot(leaves)
|
||||
for i, leaf := range leaves {
|
||||
if !MerkleVerify(leaf, root, i, MerkleProof(leaves, i)) {
|
||||
t.Fatalf("leaf %d must verify", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CROSS-LANGUAGE PARITY: this exact (seed,n,k) and its first values are pinned identically in the
|
||||
// Rust test (hanzo-engine/src/poi.rs::test_derive_challenges_keccak_golden). If either side's
|
||||
// keccak/encoding drifts, one of the two test suites breaks — the parity is enforced, not assumed.
|
||||
func TestDeriveChallengesGolden(t *testing.T) {
|
||||
seed := []byte("poi-parity-seed")
|
||||
ch := DeriveChallenges(seed, 4, 2)
|
||||
if len(ch) != 2 || len(ch[0]) != 4 {
|
||||
t.Fatalf("shape: got %dx%d", len(ch), len(ch[0]))
|
||||
}
|
||||
// emit the golden so it can be pinned in Rust; assert the structural invariants here.
|
||||
for j := range ch {
|
||||
for i := range ch[j] {
|
||||
if ch[j][i] >= P {
|
||||
t.Fatalf("challenge ch[%d][%d]=%d not reduced mod P", j, i, ch[j][i])
|
||||
}
|
||||
}
|
||||
}
|
||||
// pinned values (computed by this canonical Go keccak path; Rust must match byte-for-byte)
|
||||
wantHex := goldenHex(ch)
|
||||
if wantHex != poiGoldenParity {
|
||||
t.Fatalf("golden derive-challenges drifted:\n got %s\n want %s", wantHex, poiGoldenParity)
|
||||
}
|
||||
}
|
||||
|
||||
// goldenHex serializes the challenge matrix as big-endian u64s for a stable cross-language pin.
|
||||
func goldenHex(ch [][]uint64) string {
|
||||
var b bytes.Buffer
|
||||
for _, row := range ch {
|
||||
for _, v := range row {
|
||||
var u [8]byte
|
||||
for k := 0; k < 8; k++ {
|
||||
u[7-k] = byte(v >> (8 * k))
|
||||
}
|
||||
b.Write(u[:])
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(b.Bytes())
|
||||
}
|
||||
|
||||
// poiGoldenParity is the canonical DeriveChallenges([]byte("poi-parity-seed"),4,2) value. It is
|
||||
// recomputed and asserted on every run (TestDeriveChallengesGolden) and mirrored in Rust.
|
||||
const poiGoldenParity = "1b3c3bb2aa818c7e0b75620eafd2e35206f000322286987412a71a33da4219920934404c5bc1635a1b934a274b2eaa4801b82451d55b616d142238c54b6a7d51"
|
||||
Reference in New Issue
Block a user