mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
295 lines
10 KiB
Go
295 lines
10 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// achain_verify_test.go — Pattern B (VerifyInferenceReceipt) tests. The most
|
|
// security-critical surface. Asserts a valid committed receipt+proof is accepted and
|
|
// records the canonical output, and REJECTS: bad merkle path, uncommitted receipt
|
|
// root, intent_id mismatch, status != Completed, model/prompt mismatch, replay
|
|
// (double-consume), pending/failed receipt (no C credit). It also asserts Pattern B
|
|
// performs ZERO aivm calls (it depends only on C-committed state + the proof).
|
|
|
|
package aivmbridge
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// verifyFixture builds a fully consistent (intent submitted, receipt, committed root,
|
|
// real proof) scenario over one mockState, ready to drive verifyReceipt. The receipt
|
|
// is placed as one leaf among several so the merkle proof is non-trivial.
|
|
type verifyFixture struct {
|
|
st *mockState
|
|
intentID [32]byte
|
|
receipt AInferenceReceipt
|
|
proof AInferenceProof
|
|
caller common.Address
|
|
achainID [32]byte
|
|
}
|
|
|
|
func newVerifyFixture(t *testing.T) *verifyFixture {
|
|
t.Helper()
|
|
achainID := h32(0xA0)
|
|
installNative(t, achainID, nil)
|
|
|
|
st := newMockState(common.HexToHash("0xABCD"))
|
|
st.callIndex = 1
|
|
caller := common.HexToAddress("0x4444444444444444444444444444444444444444")
|
|
modelSpec := h32(0x5E)
|
|
prompt := h32(0x9D)
|
|
|
|
// Submit the intent through the real Pattern-A path so the C outbox is genuinely
|
|
// written and we use the real intent_id.
|
|
in := encodeSubmit(modelSpec, prompt, 3, 2, feeWord(1_000_000), [32]byte{})
|
|
gas := BridgePrecompile.RequiredGas(in)
|
|
ret, _, err := BridgePrecompile.Run(st, caller, ContractAddress, in, gas, false)
|
|
if err != nil {
|
|
t.Fatalf("submit: %v", err)
|
|
}
|
|
intentID := toArr(ret)
|
|
|
|
// Build the committed receipt bound to that intent.
|
|
rcpt := AInferenceReceipt{
|
|
Version: ReceiptVersion,
|
|
IntentID: intentID,
|
|
TaskID: h32(0x7A),
|
|
CChainID: toArr(st.cChainID[:]),
|
|
AChainID: achainID,
|
|
Requester: caller,
|
|
ModelSpecHash: modelSpec,
|
|
PromptHash: prompt,
|
|
CanonicalOutputHash: h32(0x0F),
|
|
Status: StatusCompleted,
|
|
N: 3,
|
|
Threshold: 2,
|
|
WinnersRoot: h32(0x44),
|
|
OperatorsRoot: h32(0x55),
|
|
FeePaid: feeWord(1_000_000),
|
|
SettledAtHeight: 1234,
|
|
}
|
|
|
|
// Build a real merkle tree placing receipt_hash at index 2 of 5 leaves.
|
|
leaves := [][32]byte{h32(0x01), h32(0x02), ReceiptHash(rcpt), h32(0x04), h32(0x05)}
|
|
tree := buildMerkleTree(leaves)
|
|
proof := tree.proof(2)
|
|
|
|
// Commit the receipt root in C state (the A->C seam landing point).
|
|
CommitReceiptRoot(st.db, proof.ReceiptRoot, st.blockNumber)
|
|
|
|
return &verifyFixture{st: st, intentID: intentID, receipt: rcpt, proof: proof, caller: caller, achainID: achainID}
|
|
}
|
|
|
|
func (f *verifyFixture) run(t *testing.T, r AInferenceReceipt, p AInferenceProof) ([]byte, error) {
|
|
t.Helper()
|
|
input := encodeVerify(EncodeReceipt(r), encodeProof(p))
|
|
gas := BridgePrecompile.RequiredGas(input)
|
|
ret, _, err := BridgePrecompile.Run(f.st, f.caller, ContractAddress, input, gas+1, false)
|
|
return ret, err
|
|
}
|
|
|
|
func TestVerify_AcceptsValidCommittedReceipt(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
ret, err := f.run(t, f.receipt, f.proof)
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
if len(ret) != 96 {
|
|
t.Fatalf("ret len = %d, want 96", len(ret))
|
|
}
|
|
if toArr(ret[0:32]) != f.intentID {
|
|
t.Fatalf("returned intentID mismatch")
|
|
}
|
|
if toArr(ret[32:64]) != f.receipt.CanonicalOutputHash {
|
|
t.Fatalf("returned canonical output hash mismatch")
|
|
}
|
|
if ret[95] != byte(StatusCompleted) {
|
|
t.Fatalf("returned status = %d, want Completed", ret[95])
|
|
}
|
|
|
|
// The intent MUST now be marked Consumed.
|
|
store := newStateKVStore(f.st.db, f.st.blockNumber)
|
|
if store.IntentStatus(f.intentID) != OutboxConsumed {
|
|
t.Fatalf("intent not consumed after successful verify")
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsReplayDoubleConsume(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
if _, err := f.run(t, f.receipt, f.proof); err != nil {
|
|
t.Fatalf("first verify: %v", err)
|
|
}
|
|
// Second verify of the SAME receipt must be rejected (double-consume guard).
|
|
if _, err := f.run(t, f.receipt, f.proof); err != ErrIntentConsumed {
|
|
t.Fatalf("second verify err = %v, want ErrIntentConsumed", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsBadMerklePath(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
bad := f.proof
|
|
bad.Path = append([][32]byte(nil), f.proof.Path...)
|
|
bad.Path[0][0] ^= 0xFF // corrupt a sibling
|
|
if _, err := f.run(t, f.receipt, bad); err != ErrMerkleVerify {
|
|
t.Fatalf("err = %v, want ErrMerkleVerify", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsWrongIndex(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
bad := f.proof
|
|
bad.Index = f.proof.Index ^ 1 // wrong left/right turns
|
|
if _, err := f.run(t, f.receipt, bad); err != ErrMerkleVerify {
|
|
t.Fatalf("err = %v, want ErrMerkleVerify", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsUncommittedRoot(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
// A proof under a DIFFERENT (uncommitted) root: rebuild a tree with a tampered
|
|
// leaf so the root differs, then prove the receipt under it (it won't be committed).
|
|
leaves := [][32]byte{h32(0x01), ReceiptHash(f.receipt), h32(0x03)}
|
|
tree := buildMerkleTree(leaves)
|
|
p := tree.proof(1)
|
|
// p.ReceiptRoot is NOT committed in C state.
|
|
if _, err := f.run(t, f.receipt, p); err != ErrReceiptRootNotCommitted {
|
|
t.Fatalf("err = %v, want ErrReceiptRootNotCommitted", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsForgedRootZero(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
bad := f.proof
|
|
bad.ReceiptRoot = [32]byte{} // all-zero root must never be "committed"
|
|
if _, err := f.run(t, f.receipt, bad); err != ErrReceiptRootNotCommitted {
|
|
t.Fatalf("err = %v, want ErrReceiptRootNotCommitted", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsIntentMismatch(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
r := f.receipt
|
|
r.IntentID = h32(0xEE) // no such pending intent
|
|
// Re-merkle for the mutated receipt under a freshly committed root so we get PAST
|
|
// the merkle + root checks and exercise the intent-match check specifically.
|
|
leaves := [][32]byte{ReceiptHash(r), h32(0x02)}
|
|
tree := buildMerkleTree(leaves)
|
|
p := tree.proof(0)
|
|
CommitReceiptRoot(f.st.db, p.ReceiptRoot, f.st.blockNumber)
|
|
if _, err := f.run(t, r, p); err != ErrNoMatchingIntent {
|
|
t.Fatalf("err = %v, want ErrNoMatchingIntent", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsModelPromptMismatch(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
mutate func(r *AInferenceReceipt)
|
|
}{
|
|
{"wrong model", func(r *AInferenceReceipt) { r.ModelSpecHash = h32(0xBB) }},
|
|
{"wrong prompt", func(r *AInferenceReceipt) { r.PromptHash = h32(0xCC) }},
|
|
{"wrong requester", func(r *AInferenceReceipt) { r.Requester = common.Address{0x9} }},
|
|
{"wrong N", func(r *AInferenceReceipt) { r.N = 5 }},
|
|
{"wrong threshold", func(r *AInferenceReceipt) { r.Threshold = 1 }},
|
|
{"wrong cchain", func(r *AInferenceReceipt) { r.CChainID = h32(0xDD) }},
|
|
{"wrong achain", func(r *AInferenceReceipt) { r.AChainID = h32(0xEE) }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := f.receipt
|
|
r.IntentID = f.intentID // still targets the real pending intent
|
|
tc.mutate(&r)
|
|
// Re-merkle + commit so we reach the bind-mismatch check (not merkle/root).
|
|
leaves := [][32]byte{ReceiptHash(r), h32(0x02)}
|
|
tree := buildMerkleTree(leaves)
|
|
p := tree.proof(0)
|
|
CommitReceiptRoot(f.st.db, p.ReceiptRoot, f.st.blockNumber)
|
|
if _, err := f.run(t, r, p); err != ErrReceiptBindMismatch {
|
|
t.Fatalf("err = %v, want ErrReceiptBindMismatch", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsNonCompletedStatus(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
for _, st := range []ReceiptStatus{StatusUnknown, StatusPending, StatusFailed, StatusChallenged} {
|
|
t.Run(statusName(st), func(t *testing.T) {
|
|
r := f.receipt
|
|
r.Status = st
|
|
leaves := [][32]byte{ReceiptHash(r), h32(0x02)}
|
|
tree := buildMerkleTree(leaves)
|
|
p := tree.proof(0)
|
|
CommitReceiptRoot(f.st.db, p.ReceiptRoot, f.st.blockNumber)
|
|
_, err := f.run(t, r, p)
|
|
if err != ErrReceiptNotCompleted {
|
|
t.Fatalf("status %d: err = %v, want ErrReceiptNotCompleted", st, err)
|
|
}
|
|
// And the intent must NOT be consumed (no credit on a non-final receipt).
|
|
store := newStateKVStore(f.st.db, f.st.blockNumber)
|
|
if store.IntentStatus(f.intentID) == OutboxConsumed {
|
|
t.Fatalf("status %d wrongly consumed the intent", st)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsCompletedZeroOutput(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
r := f.receipt
|
|
r.CanonicalOutputHash = [32]byte{} // Completed but empty output is self-contradictory
|
|
leaves := [][32]byte{ReceiptHash(r), h32(0x02)}
|
|
tree := buildMerkleTree(leaves)
|
|
p := tree.proof(0)
|
|
CommitReceiptRoot(f.st.db, p.ReceiptRoot, f.st.blockNumber)
|
|
if _, err := f.run(t, r, p); err != ErrZeroOutput {
|
|
t.Fatalf("err = %v, want ErrZeroOutput", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_PendingFailedNoCredit(t *testing.T) {
|
|
// Explicit "pending/failed receipt -> no C credit, intent stays pending" check.
|
|
f := newVerifyFixture(t)
|
|
for _, st := range []ReceiptStatus{StatusPending, StatusFailed} {
|
|
r := f.receipt
|
|
r.Status = st
|
|
leaves := [][32]byte{ReceiptHash(r), h32(0x02)}
|
|
tree := buildMerkleTree(leaves)
|
|
p := tree.proof(0)
|
|
CommitReceiptRoot(f.st.db, p.ReceiptRoot, f.st.blockNumber)
|
|
if _, err := f.run(t, r, p); err != ErrReceiptNotCompleted {
|
|
t.Fatalf("status %d: %v", st, err)
|
|
}
|
|
store := newStateKVStore(f.st.db, f.st.blockNumber)
|
|
if store.IntentStatus(f.intentID) != OutboxPending {
|
|
t.Fatalf("status %d: intent no longer pending (got credited?)", st)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVerify_ReadOnlyRejected(t *testing.T) {
|
|
f := newVerifyFixture(t)
|
|
f.st.readOnly = true
|
|
input := encodeVerify(EncodeReceipt(f.receipt), encodeProof(f.proof))
|
|
gas := BridgePrecompile.RequiredGas(input)
|
|
if _, _, err := BridgePrecompile.Run(f.st, f.caller, ContractAddress, input, gas, true); err != ErrReadOnly {
|
|
t.Fatalf("err = %v, want ErrReadOnly", err)
|
|
}
|
|
}
|
|
|
|
func statusName(s ReceiptStatus) string {
|
|
switch s {
|
|
case StatusUnknown:
|
|
return "unknown"
|
|
case StatusPending:
|
|
return "pending"
|
|
case StatusFailed:
|
|
return "failed"
|
|
case StatusChallenged:
|
|
return "challenged"
|
|
default:
|
|
return "?"
|
|
}
|
|
}
|