mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
346 lines
12 KiB
Go
346 lines
12 KiB
Go
//go:build crossmodule
|
|
|
|
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// crossmodule_test.go — RED-A cross-module byte-equality proof.
|
|
//
|
|
// Build-tagged `crossmodule`: this is the ONLY file importing
|
|
// github.com/luxfi/chains/aivm (the A-side encoders), resolvable only via the
|
|
// repo go.work workspace. Default `go test ./...` (CI, no workspace) must NOT
|
|
// compile it. Run the parity proof explicitly, with the workspace:
|
|
// GOWORK= go test -tags crossmodule ./aivmbridge/...
|
|
// Production aivmbridge is decoupled from chains/aivm (wire-format agreement only).
|
|
//
|
|
// This is the load-bearing seam: the C side (this package, github.com/luxfi/precompile/aivmbridge)
|
|
// and the A side (the counterparty, github.com/luxfi/chains/aivm) MUST agree, BYTE FOR BYTE, on
|
|
// (1) the intent_id keccak preimage (DeriveIntentID vs aivm.ComputeIntentID)
|
|
// (2) the receipt_hash keccak preimage (ReceiptHash vs aivm.AInferenceReceipt.Hash)
|
|
// (3) the receipt-tree merkle construction (a root+proof aivm EXPORTS must verify under
|
|
// the C-side Pattern-B path verifyInferenceReceipt).
|
|
//
|
|
// If any of these drift, a genuine A receipt can never settle a C intent and the bridge is
|
|
// silently dead. The two modules' own wire tests use DIFFERENT fixtures, so they have never
|
|
// actually compared bytes against each other. This test closes that gap permanently.
|
|
//
|
|
// Both modules are joined by the repo go.work, so we import BOTH here.
|
|
|
|
package aivmbridge
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
aivm "github.com/luxfi/chains/aivm"
|
|
"github.com/luxfi/crypto"
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// sharedInput is ONE identical logical input expressed in both modules' parameter shapes.
|
|
// Every field is a distinct, recognizable pattern so a mis-ordered preimage is obvious.
|
|
type sharedInput struct {
|
|
cChainID [32]byte
|
|
aChainID [32]byte
|
|
cTxHash [32]byte
|
|
callIndex uint32
|
|
caller common.Address
|
|
modelSpecHash [32]byte
|
|
promptHash [32]byte
|
|
n uint16
|
|
threshold uint16
|
|
fee uint64
|
|
}
|
|
|
|
// hh returns common.Hash filled with byte b (addressable, for slicing into A-side fields).
|
|
func hh(b byte) common.Hash {
|
|
x := h32(b)
|
|
return common.BytesToHash(x[:])
|
|
}
|
|
|
|
func crossFixture() sharedInput {
|
|
return sharedInput{
|
|
cChainID: h32(0xC0),
|
|
aChainID: h32(0xA0),
|
|
cTxHash: h32(0x77),
|
|
callIndex: 0x01020304,
|
|
caller: common.HexToAddress("0x1111111111111111111111111111111111111111"),
|
|
modelSpecHash: h32(0x5E),
|
|
promptHash: h32(0x9D),
|
|
n: 3,
|
|
threshold: 2,
|
|
fee: 1_000_000,
|
|
}
|
|
}
|
|
|
|
// cIntent builds the C-side InferenceIntent from the shared input.
|
|
func (s sharedInput) cIntent() InferenceIntent {
|
|
return InferenceIntent{
|
|
CChainID: s.cChainID,
|
|
AChainID: s.aChainID,
|
|
CTxHash: s.cTxHash,
|
|
CallIndex: s.callIndex,
|
|
Caller: s.caller,
|
|
ModelSpecHash: s.modelSpecHash,
|
|
ModelPromptHash: s.promptHash,
|
|
N: s.n,
|
|
Threshold: s.threshold,
|
|
Fee: feeWord(s.fee),
|
|
}
|
|
}
|
|
|
|
// aIntentArgs returns the A-side ComputeIntentID arguments from the shared input.
|
|
func (s sharedInput) aIntentArgs() (cChain, aChain, cTx common.Hash, callIdx uint32, caller common.Address, ms, ph common.Hash, n, threshold uint16, fee *uint256.Int) {
|
|
return common.BytesToHash(s.cChainID[:]),
|
|
common.BytesToHash(s.aChainID[:]),
|
|
common.BytesToHash(s.cTxHash[:]),
|
|
s.callIndex,
|
|
s.caller,
|
|
common.BytesToHash(s.modelSpecHash[:]),
|
|
common.BytesToHash(s.promptHash[:]),
|
|
s.n,
|
|
s.threshold,
|
|
uint256.NewInt(s.fee)
|
|
}
|
|
|
|
// TestCrossModuleIntentIDByteEquality is JOB-1 part (1): one identical logical input MUST
|
|
// derive a byte-identical intent_id on both sides.
|
|
func TestCrossModuleIntentIDByteEquality(t *testing.T) {
|
|
s := crossFixture()
|
|
|
|
cID := DeriveIntentID(s.cIntent())
|
|
aID := aivm.ComputeIntentID(s.aIntentArgs())
|
|
|
|
cHex := hex.EncodeToString(cID[:])
|
|
aHex := hex.EncodeToString(aID.Bytes())
|
|
if cHex != aHex {
|
|
t.Fatalf("INTENT-ID CROSS-MODULE DRIFT:\n C aivmbridge.DeriveIntentID = %s\n A aivm.ComputeIntentID = %s", cHex, aHex)
|
|
}
|
|
t.Logf("SHARED intent_id (C==A) = %s", cHex)
|
|
}
|
|
|
|
// crossReceipt is the fully-populated receipt for the shared input, in both shapes. The
|
|
// IntentID is the shared (C==A) intent id; every other field is a distinct pattern.
|
|
func (s sharedInput) cReceipt(intentID [32]byte) AInferenceReceipt {
|
|
return AInferenceReceipt{
|
|
Version: ReceiptVersion,
|
|
IntentID: intentID,
|
|
TaskID: h32(0x7A),
|
|
CChainID: s.cChainID,
|
|
AChainID: s.aChainID,
|
|
Requester: s.caller,
|
|
ModelSpecHash: s.modelSpecHash,
|
|
PromptHash: s.promptHash,
|
|
CanonicalOutputHash: h32(0x0F),
|
|
Status: StatusCompleted,
|
|
N: s.n,
|
|
Threshold: s.threshold,
|
|
WinnersRoot: h32(0x44),
|
|
OperatorsRoot: h32(0x55),
|
|
FeePaid: feeWord(s.fee),
|
|
SettledAtHeight: 0x0102030405060708,
|
|
}
|
|
}
|
|
|
|
func (s sharedInput) aReceipt(intentID common.Hash) aivm.AInferenceReceipt {
|
|
return aivm.AInferenceReceipt{
|
|
Version: aivm.ReceiptVersion,
|
|
IntentID: intentID,
|
|
TaskID: hh(0x7A),
|
|
CChainID: common.BytesToHash(s.cChainID[:]),
|
|
AChainID: common.BytesToHash(s.aChainID[:]),
|
|
Requester: s.caller,
|
|
ModelSpecHash: common.BytesToHash(s.modelSpecHash[:]),
|
|
PromptHash: common.BytesToHash(s.promptHash[:]),
|
|
CanonicalOutputHash: hh(0x0F),
|
|
Status: aivm.StatusCompleted,
|
|
N: s.n,
|
|
Threshold: s.threshold,
|
|
WinnersRoot: hh(0x44),
|
|
OperatorsRoot: hh(0x55),
|
|
FeePaid: uint256.NewInt(s.fee),
|
|
SettledAtHeight: 0x0102030405060708,
|
|
}
|
|
}
|
|
|
|
// TestCrossModuleReceiptHashByteEquality is JOB-1 part (2): the canonical receipt encoding
|
|
// AND its keccak hash MUST be byte-identical on both sides for an identical receipt.
|
|
func TestCrossModuleReceiptHashByteEquality(t *testing.T) {
|
|
s := crossFixture()
|
|
intentID := DeriveIntentID(s.cIntent())
|
|
intentHash := common.BytesToHash(intentID[:])
|
|
|
|
cR := s.cReceipt(intentID)
|
|
aR := s.aReceipt(intentHash)
|
|
|
|
// (2a) canonical encoding bytes must match.
|
|
cEnc := EncodeReceipt(cR)
|
|
aEnc := aR.Encode()
|
|
if hex.EncodeToString(cEnc) != hex.EncodeToString(aEnc) {
|
|
t.Fatalf("RECEIPT-ENCODING CROSS-MODULE DRIFT:\n C EncodeReceipt = %s\n A .Encode() = %s",
|
|
hex.EncodeToString(cEnc), hex.EncodeToString(aEnc))
|
|
}
|
|
if len(cEnc) != aivm.ReceiptEncodedLen {
|
|
t.Fatalf("encoding length %d != aivm.ReceiptEncodedLen %d", len(cEnc), aivm.ReceiptEncodedLen)
|
|
}
|
|
|
|
// (2b) receipt_hash must match.
|
|
cH := ReceiptHash(cR)
|
|
aH := aR.Hash()
|
|
if hex.EncodeToString(cH[:]) != hex.EncodeToString(aH.Bytes()) {
|
|
t.Fatalf("RECEIPT-HASH CROSS-MODULE DRIFT:\n C ReceiptHash = %s\n A .Hash() = %s",
|
|
hex.EncodeToString(cH[:]), hex.EncodeToString(aH.Bytes()))
|
|
}
|
|
t.Logf("SHARED receipt_hash (C==A) = %s", hex.EncodeToString(cH[:]))
|
|
}
|
|
|
|
// --- JOB-1 part (3): the merkle SEAM ------------------------------------------
|
|
//
|
|
// This is the part single-module testing cannot reach. The A side EXPORTS a (receipt, proof,
|
|
// root) where the tree is built over leafHash(receipt_hash) = keccak(receipt_hash). The C side
|
|
// must verify exactly that exported triple via the full Pattern-B path. We reconstruct the A
|
|
// tree faithfully (and PROVE the reconstruction is faithful by checking aivm.VerifyReceiptProof
|
|
// accepts it), then drive the C verify.
|
|
|
|
// aLeafHash mirrors aivm.quorum_merkle.leafHash (unexported there): keccak(receipt_hash).
|
|
func aLeafHash(receiptHash common.Hash) common.Hash {
|
|
return common.BytesToHash(crypto.Keccak256(receiptHash.Bytes()))
|
|
}
|
|
|
|
// aMerkleNode mirrors aivm.quorum_merkle.merkleNode: keccak(l || r).
|
|
func aMerkleNode(l, r common.Hash) common.Hash {
|
|
return common.BytesToHash(crypto.Keccak256(l.Bytes(), r.Bytes()))
|
|
}
|
|
|
|
// aMerkleRoot mirrors aivm.quorum_merkle.merkleRoot over already-leaf-hashed leaves.
|
|
func aMerkleRoot(leaves []common.Hash) common.Hash {
|
|
if len(leaves) == 0 {
|
|
return common.Hash{}
|
|
}
|
|
level := append([]common.Hash(nil), leaves...)
|
|
for len(level) > 1 {
|
|
next := make([]common.Hash, 0, (len(level)+1)/2)
|
|
for i := 0; i < len(level); i += 2 {
|
|
if i+1 < len(level) {
|
|
next = append(next, aMerkleNode(level[i], level[i+1]))
|
|
} else {
|
|
next = append(next, aMerkleNode(level[i], level[i]))
|
|
}
|
|
}
|
|
level = next
|
|
}
|
|
return level[0]
|
|
}
|
|
|
|
// aMerkleProof mirrors aivm.quorum_merkle.merkleProof over already-leaf-hashed leaves.
|
|
func aMerkleProof(leaves []common.Hash, idx uint32) aivm.MerkleProof {
|
|
proof := aivm.MerkleProof{Index: idx}
|
|
level := append([]common.Hash(nil), leaves...)
|
|
i := int(idx)
|
|
for len(level) > 1 {
|
|
var sib common.Hash
|
|
if i%2 == 0 {
|
|
if i+1 < len(level) {
|
|
sib = level[i+1]
|
|
} else {
|
|
sib = level[i]
|
|
}
|
|
} else {
|
|
sib = level[i-1]
|
|
}
|
|
proof.Siblings = append(proof.Siblings, sib)
|
|
next := make([]common.Hash, 0, (len(level)+1)/2)
|
|
for j := 0; j < len(level); j += 2 {
|
|
if j+1 < len(level) {
|
|
next = append(next, aMerkleNode(level[j], level[j+1]))
|
|
} else {
|
|
next = append(next, aMerkleNode(level[j], level[j]))
|
|
}
|
|
}
|
|
level = next
|
|
i /= 2
|
|
}
|
|
return proof
|
|
}
|
|
|
|
// TestCrossModuleMerkleSeam is JOB-1 part (3): an A-exported receipt_root + proof MUST verify
|
|
// under the C-side Pattern-B path. This is the one that exposes the leaf-hash drift.
|
|
func TestCrossModuleMerkleSeam(t *testing.T) {
|
|
s := crossFixture()
|
|
intentID := DeriveIntentID(s.cIntent())
|
|
intentHash := common.BytesToHash(intentID[:])
|
|
|
|
// A side mints the receipt for the settled intent (same input → byte-identical to C's).
|
|
aR := s.aReceipt(intentHash)
|
|
aReceiptHash := aR.Hash()
|
|
|
|
// A builds a receipt tree. Put our receipt at index 2 in a 5-leaf tree (a realistic
|
|
// settlement order) so the proof actually has a non-trivial path.
|
|
rawLeaves := []common.Hash{
|
|
hh(0x01),
|
|
hh(0x02),
|
|
aReceiptHash, // our receipt
|
|
hh(0x04),
|
|
hh(0x05),
|
|
}
|
|
const ourIdx = 2
|
|
|
|
// A's committed tree is over LEAF-HASHED values (this is what export.go does).
|
|
aLeaves := make([]common.Hash, len(rawLeaves))
|
|
for i, lh := range rawLeaves {
|
|
aLeaves[i] = aLeafHash(lh)
|
|
}
|
|
aRoot := aMerkleRoot(aLeaves)
|
|
aProof := aMerkleProof(aLeaves, ourIdx)
|
|
|
|
// FAITHFULNESS CHECK: prove our reconstruction equals the real A construction by asking
|
|
// the REAL aivm verifier to accept it. If this fails, the test reconstruction is wrong
|
|
// (not the bridge); guard against that so the seam result below is trustworthy.
|
|
if !aivm.VerifyReceiptProof(aReceiptHash, aProof, aRoot) {
|
|
t.Fatalf("test reconstruction of the A tree is not faithful: aivm.VerifyReceiptProof rejected our own root+proof")
|
|
}
|
|
t.Logf("A-side committed receipt_root = %s (aivm.VerifyReceiptProof accepts)", aRoot.Hex())
|
|
|
|
// Now drive the FULL C Pattern-B path against the A-exported (root, receipt, proof).
|
|
// Set up C state: a matching pending intent + the A root committed.
|
|
db := newMemStateDB()
|
|
store := newStateKVStore(db, 100)
|
|
store.PutPendingIntent(intentID, OutboxIntent{
|
|
Status: OutboxPending,
|
|
Caller: s.caller,
|
|
CChainID: s.cChainID,
|
|
AChainID: s.aChainID,
|
|
ModelSpecHash: s.modelSpecHash,
|
|
PromptHash: s.promptHash,
|
|
N: s.n,
|
|
Threshold: s.threshold,
|
|
})
|
|
var rootArr [32]byte
|
|
copy(rootArr[:], aRoot.Bytes())
|
|
CommitReceiptRoot(db, rootArr, 100)
|
|
|
|
// Translate the A proof into the C proof wire shape (same root, same index, same sibling
|
|
// path — only the struct field names differ; the BYTES are identical).
|
|
cProof := AInferenceProof{ReceiptRoot: rootArr, Index: uint64(ourIdx)}
|
|
for _, sib := range aProof.Siblings {
|
|
var n [32]byte
|
|
copy(n[:], sib.Bytes())
|
|
cProof.Path = append(cProof.Path, n)
|
|
}
|
|
|
|
cR := s.cReceipt(intentID)
|
|
|
|
verified, err := verifyInferenceReceipt(store, cR, cProof)
|
|
if err != nil {
|
|
t.Fatalf("C Pattern-B REJECTED a genuine A-exported receipt+proof under the A-committed root: %v\n"+
|
|
" → the bridge cannot settle any real A receipt (merkle leaf-hash drift between A export.go and C verify.go)", err)
|
|
}
|
|
if verified.IntentID != intentID {
|
|
t.Fatalf("verified intent id mismatch: got %x want %x", verified.IntentID, intentID)
|
|
}
|
|
if verified.CanonicalOutputHash != h32(0x0F) {
|
|
t.Fatalf("verified output mismatch")
|
|
}
|
|
t.Logf("SEAM OK: A-exported receipt verifies under the C Pattern-B path")
|
|
}
|