aivmbridge: native compute-proof precompile (computeattest op) — closes audit G10

verifyComputeProof at 0x0300…0004 selector 0x12000000: decodes a proof-of-inference opening
(crypto/poi wire v1.19.24) and returns (included | freivaldsOK<<1) — Merkle inclusion under the
committed transcript root + Freivalds C==A·B over F_p, native via crypto/poi.CheckOpening. One call
serves both paths (genuine = included&ok, fraud = included&!ok). Gas = base + per-field-op, priced
so a 256-slice ~1M gas and a full layer is unaffordable (forces bounded slices); RequiredGas==Run.
This is what makes a real-model-sized opening verifiable on-chain (Solidity Freivalds is too heavy).
Tests 5/5; full aivmbridge suite green. luxfi/crypto v1.19.22→v1.19.24.
This commit is contained in:
zeekay
2026-06-23 22:40:01 -07:00
parent 126b322a2d
commit 9e6defb3e5
7 changed files with 194 additions and 12 deletions
+9
View File
@@ -35,6 +35,12 @@ const (
// verifyInferenceReceipt(bytes receiptBytes, bytes proofBytes)
// -> (bytes32 intentID, bytes32 canonicalOutputHash, uint8 status)
SelectorVerifyInferenceReceipt uint32 = 0x11000000
// verifyComputeProof(bytes openingWire) -> bytes32 verdict
// the NATIVE proof-of-inference check: decode a beacon-selected matmul opening and return
// (included | freivaldsOK<<1) so the gate can tell a genuine opening from a fraud. See
// computeproof.go. The opening wire is crypto/poi.EncodeOpening.
SelectorVerifyComputeProof uint32 = 0x12000000
)
// MaxFanout bounds N so a single intent cannot request an unbounded provider fan-out
@@ -71,6 +77,9 @@ func runBridge(
return submitIntent(accessibleState, caller, args, suppliedGas, readOnly)
case SelectorVerifyInferenceReceipt:
return verifyReceipt(accessibleState, args, suppliedGas, readOnly)
case SelectorVerifyComputeProof:
// pure verification — no state, so readOnly is fine.
return verifyComputeProof(args, suppliedGas)
default:
return nil, suppliedGas, fmt.Errorf("aivmbridge: unknown selector %#x", selector)
}
+77
View File
@@ -0,0 +1,77 @@
// computeproof.go — the NATIVE proof-of-inference verification (the `computeattest` op).
//
// A prover commits a forward pass as a keccak Merkle root over per-matmul exact-integer operands
// (hanzo-engine emits it; crypto/poi defines the wire). To enforce "no real compute, no mint", a
// challenger opens a beacon-selected matmul and this precompile re-checks it NATIVELY: Merkle
// inclusion under the committed root + Freivalds C == A·B over F_p (p = 2^61-1). Native because a
// real-model-sized opening is far too much arithmetic for gas-bounded EVM bytecode; the Solidity
// ComputeWitnessLib is the byte-identical reference for small slices, this is the production path.
//
// Pure (reads no state, writes none) → safe under a static call. Returns one 32-byte word whose
// low bits carry the verdict, so one native call serves both the attest and the slash paths.
package aivmbridge
import (
"fmt"
"github.com/luxfi/crypto/poi"
)
// computeProofK is the number of Freivalds challenge vectors checked on-chain. k = 2 gives a
// soundness error <= (1/p)^2 ~ 2^-122 — a fabricated matmul survives with negligible probability.
const computeProofK = 2
// Result bits in the returned word's last byte.
const (
resultIncluded = 1 // the operands are committed under the transcript root (Merkle inclusion)
resultFreivaldsOK = 2 // C == A·B (the matmul is genuine)
)
// computeProofGas is the gas for an opening: a base plus the Freivalds work (O(tk+kn+tn) per
// challenge vector). Identical formula in RequiredGas and Run so accounting agrees. The per-op
// price makes a large opening expensive, which is what forces a challenger to open a bounded
// SLICE of a layer rather than a whole 4096-wide matmul.
func computeProofGas(op poi.Opening) uint64 {
return GasVerifyComputeProofBase + poi.FieldOps(op, computeProofK)*GasVerifyComputeProofPerFieldOp
}
// verifyComputeProof decodes a proof-of-inference opening and returns (included, freivaldsOK) in
// the low bits of a 32-byte word. The caller derives genuine = included && freivaldsOK and
// fraud = included && !freivaldsOK. Charges base before any work (bounds griefing on a malformed
// frame), then the Freivalds cost computed from the decoded — and dimension-bounded — operands.
func verifyComputeProof(args []byte, suppliedGas uint64) ([]byte, uint64, error) {
if suppliedGas < GasVerifyComputeProofBase {
return nil, 0, fmt.Errorf("aivmbridge: out of gas: need %d have %d", GasVerifyComputeProofBase, suppliedGas)
}
root, beacon, op, err := poi.DecodeOpening(args)
if err != nil {
// the base was affordable; consume it and surface the decode error.
return nil, suppliedGas - GasVerifyComputeProofBase, err
}
cost := computeProofGas(op)
if suppliedGas < cost {
return nil, 0, fmt.Errorf("aivmbridge: out of gas: compute proof needs %d have %d", cost, suppliedGas)
}
included, ok, err := poi.CheckDecoded(root, beacon, op, computeProofK)
if err != nil {
return nil, suppliedGas - cost, err
}
var out [32]byte
if included {
out[31] |= resultIncluded
}
if ok {
out[31] |= resultFreivaldsOK
}
return out[:], suppliedGas - cost, nil
}
// computeProofRequiredGas is the EVM's pre-charge estimate, matching {computeProofGas}. A
// malformed frame falls back to the base (the call reverts on decode anyway).
func computeProofRequiredGas(args []byte) uint64 {
_, _, op, err := poi.DecodeOpening(args)
if err != nil {
return GasVerifyComputeProofBase
}
return computeProofGas(op)
}
+93
View File
@@ -0,0 +1,93 @@
package aivmbridge
import (
"testing"
"github.com/luxfi/crypto/poi"
)
// buildOpening makes a small proof-of-inference transcript and returns the wire-encoded opening of
// its first matmul. With fabricate=true that matmul's output has one entry flipped (a claimed
// result never produced by A·B).
func buildOpening(fabricate bool) []byte {
s := uint64(0x1234)
rnd := func(rows, cols int) poi.Mat {
d := make([]int64, rows*cols)
for i := range d {
s = s*6364136223846793005 + 1442695040888963407
d[i] = int64((s>>33)%127) - 63
}
return poi.NewMat(rows, cols, d)
}
tr := poi.NewTranscript()
a, b := rnd(4, 16), rnd(16, 8)
if fabricate {
c := poi.ExactMatmul(a, b)
c.Data[3]++ // fabricate one output entry
tr.CommitClaimed(a, b, c)
} else {
tr.Matmul(a, b)
}
tr.Matmul(rnd(2, 4), rnd(4, 3)) // a second matmul so the tree has a real proof path
root := tr.Root()
return poi.EncodeOpening(root, []byte("beacon:precompile"), tr.Open(0))
}
// An honest opening verifies natively: included AND Freivalds-OK → verdict 0b11.
func TestPrecompile_VerifyComputeProof_Honest(t *testing.T) {
ret, rem, err := verifyComputeProof(buildOpening(false), 50_000_000)
if err != nil {
t.Fatalf("verify: %v", err)
}
if ret[31] != resultIncluded|resultFreivaldsOK {
t.Fatalf("honest verdict = %d, want %d (included|ok)", ret[31], resultIncluded|resultFreivaldsOK)
}
if rem >= 50_000_000 {
t.Fatal("gas must be consumed")
}
}
// A fabricated output is caught natively: included but NOT Freivalds-OK → verdict 0b01 (= fraud).
func TestPrecompile_VerifyComputeProof_Fraud(t *testing.T) {
ret, _, err := verifyComputeProof(buildOpening(true), 50_000_000)
if err != nil {
t.Fatalf("verify: %v", err)
}
if ret[31] != resultIncluded {
t.Fatalf("fraud verdict = %d, want %d (included, not ok)", ret[31], resultIncluded)
}
}
// Below the base, the op is out of gas (bounds griefing on a malformed/huge frame).
func TestPrecompile_VerifyComputeProof_OOG(t *testing.T) {
if _, _, err := verifyComputeProof(buildOpening(false), GasVerifyComputeProofBase-1); err == nil {
t.Fatal("must be out of gas below the base")
}
}
// RequiredGas (the EVM's pre-charge) matches what Run actually consumes.
func TestPrecompile_GasAccountingConsistent(t *testing.T) {
in := buildOpening(false)
want := computeProofRequiredGas(in)
if want <= GasVerifyComputeProofBase {
t.Fatal("a real opening must cost more than the base")
}
_, rem, err := verifyComputeProof(in, want)
if err != nil {
t.Fatalf("verify at RequiredGas budget: %v", err)
}
if rem != 0 {
t.Fatalf("Run consumed %d less than RequiredGas estimated (rem=%d) — accounting drift", want-rem, rem)
}
}
// A garbage frame decodes-errs and consumes only the base (no panic, no over-charge).
func TestPrecompile_VerifyComputeProof_Garbage(t *testing.T) {
_, rem, err := verifyComputeProof([]byte{1, 2, 3, 4, 5}, 1_000_000)
if err == nil {
t.Fatal("garbage frame must error")
}
if rem != 1_000_000-GasVerifyComputeProofBase {
t.Fatalf("garbage frame must consume exactly the base, rem=%d", rem)
}
}
+10
View File
@@ -22,6 +22,16 @@ const (
// GasVerifyPerProofNode is charged per merkle path node hashed during verify.
GasVerifyPerProofNode uint64 = 1_000
// GasVerifyComputeProofBase is the base for a native proof-of-inference opening check: the
// wire decode + the Merkle inclusion of the opened matmul. The Freivalds re-execution adds
// the per-field-op price below.
GasVerifyComputeProofBase uint64 = 30_000
// GasVerifyComputeProofPerFieldOp prices each F_p mul-add of the Freivalds check
// (O(tk+kn+tn) per challenge vector). Sized so a 256-wide slice costs ~1M gas and a whole
// 4096-wide layer is unaffordable — which forces a challenger to open a bounded SLICE.
GasVerifyComputeProofPerFieldOp uint64 = 2
)
// verifyGas returns the total gas for a VerifyInferenceReceipt call given the
+2
View File
@@ -126,6 +126,8 @@ func (c *BridgeContract) RequiredGas(input []byte) uint64 {
return GasVerifyInferenceReceiptBase
}
return verifyGas(pathLen)
case SelectorVerifyComputeProof:
return computeProofRequiredGas(input[4:])
default:
return GasVerifyInferenceReceiptBase
}
+1 -2
View File
@@ -13,7 +13,7 @@ require (
github.com/luxfi/ai v0.1.0
github.com/luxfi/chains v1.3.18
github.com/luxfi/corona v0.7.9
github.com/luxfi/crypto v1.19.22
github.com/luxfi/crypto v1.19.24
github.com/luxfi/database v1.20.4
github.com/luxfi/dex v1.5.16
github.com/luxfi/fhe v1.8.2
@@ -78,7 +78,6 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/luxfi/age v1.5.0 // indirect
github.com/luxfi/bft v0.1.5 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/node v1.30.6 // indirect
github.com/luxfi/timer v1.0.2 // indirect
+2 -10
View File
@@ -203,20 +203,14 @@ github.com/luxfi/ai v0.1.0 h1:PwTGob0GJivbdqNpUs82bvwcE8/qxyTokxfY7BZVPoo=
github.com/luxfi/ai v0.1.0/go.mod h1:lTuY32dGjEJUgVheYuanlt1O0jHj0AZ09sUSpRsmH6M=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/bft v0.1.5 h1:5xVLPkog4e5LTgaVlb9pgxA0EWE6tkrKwHPZVRz+RZw=
github.com/luxfi/bft v0.1.5/go.mod h1:5I8Ft8yA69xZlDe3RB0i4MgbqFKLZe65o/sha8JuKvU=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/chains v1.3.14 h1:b/rGNlXiXQ8PoDMr6BWBgdbeQM4uwKp7wgav6QPW/YM=
github.com/luxfi/chains v1.3.14/go.mod h1:lpVNHfHR8bk1y3BswNdK7DFpci0FAHPtO9XuMal8Aoo=
github.com/luxfi/chains v1.3.18 h1:CaKtVZ2LkPgBiaZkDEl+RPsO2iXTXsOfVDwSj2ukhdE=
github.com/luxfi/chains v1.3.18/go.mod h1:MArifjc+fRtZH6M009L47YPJFDMEjjJbHy2BJ/8nxTE=
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/consensus v1.25.18 h1:wegSPk1VyjV9k2J+IZ6cJf6flqiMhzbOuFidaQ31b24=
github.com/luxfi/consensus v1.25.18/go.mod h1:cNhLp215lFwiOPxdj8I+6JBLz3/vDRMV2vqLigulv04=
github.com/luxfi/consensus v1.25.21 h1:izB/XRJ+jWEbXHc4ro1MAA6uuQApfzDq+SSNCJ69mac=
github.com/luxfi/consensus v1.25.21/go.mod h1:8gAYHLYsMvXoYz262gvusIoDRxHBrMqj6Fyj8uSnaYE=
github.com/luxfi/constants v1.5.8 h1:iNP9AWNUcM4Tps7jYnx49CwtCWAC9mYRxJfGou2za0g=
@@ -225,10 +219,8 @@ github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/corona v0.7.9 h1:NQe9V/80CdKLvbaVRE2uepxvxg9KHbWfcGRKWrzLSHc=
github.com/luxfi/corona v0.7.9/go.mod h1:SfS7xo/k4uoteEYwYy+QCMPzTU8EIEbLnbWKx5ENVCw=
github.com/luxfi/crypto v1.19.21 h1:x7s/Yy1BYMv5sbbWsZk+mUU007Z0HX/Ta4/RkrNHiw0=
github.com/luxfi/crypto v1.19.21/go.mod h1:0tfz+EbAjsW1QBWB0cte9kdjB5XhhYFmCr8BkZRux48=
github.com/luxfi/crypto v1.19.22 h1:qVXLyPR+nf6qqLbxA5KTbZmgNNxpqT9E7+3nppdRCq0=
github.com/luxfi/crypto v1.19.22/go.mod h1:0tfz+EbAjsW1QBWB0cte9kdjB5XhhYFmCr8BkZRux48=
github.com/luxfi/crypto v1.19.24 h1:bXhgA4g3x8Ac1RU6jYFQnP6IzUtDpFvyCsgvUbH0YmU=
github.com/luxfi/crypto v1.19.24/go.mod h1:0tfz+EbAjsW1QBWB0cte9kdjB5XhhYFmCr8BkZRux48=
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.20.4 h1:WOt2GIGJxf8AFpg49odMz8DZ8RFSLDrozGhZtmorN70=