Files
zeekayandGitHub 96da029e9a Thinking Chains: chains (#2)
* hanzo-evm: aiquorum + aimarket precompiles, evmllm (geth AI precompile + e2e), operator/canonical (commit/reveal, int8-quant), aiffi bench. Thinking-chain substrate.

* hanzo-evm/evm: wire AI precompiles (inference 0x0300..03 + modelregistry)

Registers deterministic int8 inference (Tier-1 in-consensus AI) + ModelSpec
registry into the Hanzo EVM. threshold dep aligned to v1.9.4 (go.mod match).
Builds green (55M binary). aivmbridge follows luxfi/precompile release.
2026-06-21 20:53:42 -07:00

236 lines
9.5 KiB
Go

// Copyright (C) 2026, Hanzo AI Inc. All rights reserved.
// RED-2 adversarial tests. Pure Go (no cgo) so they run without the engine:
// GOWORK=off CGO_ENABLED=0 go test -run RED2 .
package operator
import (
"bytes"
"crypto/ecdsa"
"math/big"
"testing"
"github.com/hanzoai/chains/hanzo-evm/operator/canonical"
"github.com/luxfi/crypto"
"github.com/luxfi/geth/common"
)
func mustKey(t *testing.T) *ecdsa.PrivateKey {
t.Helper()
k, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("genkey: %v", err)
}
return k
}
// ---- VECTOR 3: governance preimage injectivity (no false collision) --------
// Exhaustively hash every (vote, bucket) pair under a fixed spec and assert all
// 3*11 = 33 output_hashes are DISTINCT. A collision would be a false-agreement
// bug (two different verdicts counted as the same).
func TestRED2_GovernancePreimageInjective(t *testing.T) {
spec := common.HexToHash("0xdeadbeef")
seen := map[common.Hash]string{}
votes := []canonical.Vote{canonical.VoteYes, canonical.VoteNo, canonical.VoteAbstain}
for _, v := range votes {
for bucket := 0; bucket <= canonical.MaxConfidenceBps; bucket += canonical.ConfidenceGridBps {
d := canonical.Decision{ModelSpecHash: spec, Vote: v, ConfidenceBps: uint16(bucket)}
h := canonical.OutputHashGovernance(d)
key := d.ConsensusKey()
if prev, ok := seen[h]; ok {
t.Fatalf("FALSE COLLISION: %q and %q hash to the same output_hash %s", prev, key, h.Hex())
}
seen[h] = key
}
}
if len(seen) != 33 {
t.Fatalf("expected 33 distinct consensus hashes, got %d", len(seen))
}
}
// ---- VECTOR 6a: signature malleability (high-S must be rejected) -----------
func TestRED2_SignatureMalleability_HighS_Rejected(t *testing.T) {
k := mustKey(t)
op := pubkeyAddress(&k.PublicKey)
r := &Reveal{
JobID: common.HexToHash("0x1"), SpecHash: common.HexToHash("0x2"),
PromptHash: common.HexToHash("0x3"), OutputHash: common.HexToHash("0x4"),
EmbeddingHash: common.HexToHash("0x5"), Operator: op, Nonce: common.HexToHash("0x6"),
}
r.Commit = canonical.Commit(r.JobID, r.SpecHash, r.PromptHash, r.OutputHash, r.EmbeddingHash, r.Operator, r.Nonce)
if err := r.sign(k); err != nil {
t.Fatalf("sign: %v", err)
}
// Honest reveal recovers fine.
if _, err := RecoverReveal(r); err != nil {
t.Fatalf("honest reveal rejected: %v", err)
}
// Malleate: s' = N - s, flip recovery id. This is the classic secp256k1
// malleability; a non-canonical-aware verifier would still accept it.
N := crypto.S256().Params().N
s := new(big.Int).SetBytes(r.Signature[32:64])
sPrime := new(big.Int).Sub(N, s)
mal := make([]byte, 65)
copy(mal[:32], r.Signature[:32])
sp := sPrime.Bytes()
copy(mal[64-len(sp):64], sp)
mal[64] = r.Signature[64] ^ 1 // flip v
bad := *r
bad.Signature = mal
if _, err := RecoverReveal(&bad); err == nil {
t.Fatal("CRITICAL: high-S malleated signature ACCEPTED (malleability => double-reveal / equivocation)")
}
}
// ---- VECTOR 6b: recovery-id confusion (v in {27,28} vs {0,1}) --------------
func TestRED2_RecoveryIDConfusion(t *testing.T) {
k := mustKey(t)
op := pubkeyAddress(&k.PublicKey)
r := &Reveal{
JobID: common.HexToHash("0xa"), SpecHash: common.HexToHash("0xb"),
PromptHash: common.HexToHash("0xc"), OutputHash: common.HexToHash("0xd"),
EmbeddingHash: common.HexToHash("0xe"), Operator: op, Nonce: common.HexToHash("0xf"),
}
r.Commit = canonical.Commit(r.JobID, r.SpecHash, r.PromptHash, r.OutputHash, r.EmbeddingHash, r.Operator, r.Nonce)
if err := r.sign(k); err != nil {
t.Fatalf("sign: %v", err)
}
// Add 27 to v (the "Ethereum" convention). crypto.SigToPub expects 0/1.
bad := *r
mal := make([]byte, 65)
copy(mal, r.Signature)
mal[64] = r.Signature[64] + 27
bad.Signature = mal
if _, err := RecoverReveal(&bad); err == nil {
t.Fatal("recovery-id-confused signature (v+27) ACCEPTED — recovery id not validated")
}
}
// ---- VECTOR 6c: signer != operator ----------------------------------------
func TestRED2_SignerNotOperator(t *testing.T) {
kReal := mustKey(t)
kAttacker := mustKey(t)
victim := pubkeyAddress(&kReal.PublicKey)
// Attacker signs a reveal but claims to be the victim operator.
r := &Reveal{
JobID: common.HexToHash("0x11"), SpecHash: common.HexToHash("0x22"),
PromptHash: common.HexToHash("0x33"), OutputHash: common.HexToHash("0x44"),
EmbeddingHash: common.HexToHash("0x55"), Operator: victim, Nonce: common.HexToHash("0x66"),
}
r.Commit = canonical.Commit(r.JobID, r.SpecHash, r.PromptHash, r.OutputHash, r.EmbeddingHash, r.Operator, r.Nonce)
if err := r.sign(kAttacker); err != nil { // signed by the WRONG key
t.Fatalf("sign: %v", err)
}
if _, err := RecoverReveal(r); err == nil {
t.Fatal("CRITICAL: reveal signed by attacker but claiming victim operator was ACCEPTED")
}
}
// ---- VECTOR 6d: tampered consensus field (commit no longer binds) ----------
func TestRED2_TamperedFieldBreaksCommitBinding(t *testing.T) {
k := mustKey(t)
op := pubkeyAddress(&k.PublicKey)
r := &Reveal{
JobID: common.HexToHash("0x1"), SpecHash: common.HexToHash("0x2"),
PromptHash: common.HexToHash("0x3"), OutputHash: common.HexToHash("0x4"),
EmbeddingHash: common.HexToHash("0x5"), Operator: op, Nonce: common.HexToHash("0x6"),
}
r.Commit = canonical.Commit(r.JobID, r.SpecHash, r.PromptHash, r.OutputHash, r.EmbeddingHash, r.Operator, r.Nonce)
if err := r.sign(k); err != nil {
t.Fatalf("sign: %v", err)
}
// Attacker flips output_hash AFTER commit/sign. revealDigest covers OutputHash,
// so recovery fails on signer mismatch; even if it didn't, commit re-derivation
// must reject. Verify it's rejected.
bad := *r
bad.OutputHash = common.HexToHash("0x99")
if _, err := RecoverReveal(&bad); err == nil {
t.Fatal("CRITICAL: tampered output_hash accepted (commit binding / signature not covering output)")
}
}
// ---- VECTOR 6e: cross-job replay of a signed reveal ------------------------
// A reveal signed for job A must not validate when its JobID is swapped to job B.
func TestRED2_CrossJobReplay(t *testing.T) {
k := mustKey(t)
op := pubkeyAddress(&k.PublicKey)
r := &Reveal{
JobID: common.HexToHash("0xAAAA"), SpecHash: common.HexToHash("0x2"),
PromptHash: common.HexToHash("0x3"), OutputHash: common.HexToHash("0x4"),
EmbeddingHash: common.HexToHash("0x5"), Operator: op, Nonce: common.HexToHash("0x6"),
}
r.Commit = canonical.Commit(r.JobID, r.SpecHash, r.PromptHash, r.OutputHash, r.EmbeddingHash, r.Operator, r.Nonce)
if err := r.sign(k); err != nil {
t.Fatalf("sign: %v", err)
}
replay := *r
replay.JobID = common.HexToHash("0xBBBB") // different job, same signature & commit
if _, err := RecoverReveal(&replay); err == nil {
t.Fatal("CRITICAL: signed reveal replayed across jobs was ACCEPTED")
}
}
// ---- VECTOR 1: ModelSpec length-prefix cross-field ambiguity ---------------
// Try to make two DISTINCT specs collide by shifting bytes between the variable
// ModelID/RuntimeVersion and their neighbors. Fixed-width hashes bracket the
// length-prefixed strings, so this MUST be impossible.
func TestRED2_ModelSpecNoCrossFieldCollision(t *testing.T) {
mk := func(id, rt string) canonical.ModelSpec {
return canonical.ModelSpec{
ModelID: id, ModelHash: common.HexToHash("0x01"), TokenizerHash: common.HexToHash("0x02"),
RuntimeVersion: rt, SamplingHash: common.HexToHash("0x03"),
PromptTemplateHash: common.HexToHash("0x04"), EmbeddingModelHash: common.HexToHash("0x05"),
}
}
// Move one char from ModelID into RuntimeVersion: ("ab","c") vs ("a","bc").
if mk("ab", "c").ModelSpecHash() == mk("a", "bc").ModelSpecHash() {
t.Fatal("cross-field shift collision (ab|c == a|bc)")
}
// Empty vs single-char with compensating length.
if mk("", "abc").ModelSpecHash() == mk("abc", "").ModelSpecHash() {
t.Fatal("empty/nonempty shift collision")
}
// Length-prefix-as-content attack: a ModelID whose bytes spell a length prefix.
a := mk(string([]byte{0, 0, 0, 1}), "") // 4 bytes that look like u32be(1)
b := mk(string([]byte{0, 0, 0, 0, 1}), "") // 5 bytes
if a.ModelSpecHash() == b.ModelSpecHash() {
t.Fatal("length-prefix content confusion")
}
}
// ---- VECTOR 4: JCS audit record cannot affect consensus, and is injection-safe
func TestRED2_AuditRecordCannotAffectConsensus(t *testing.T) {
spec := common.HexToHash("0xab")
base := canonical.Decision{ModelSpecHash: spec, Vote: canonical.VoteYes, ConfidenceBps: 5000}
// Maximally hostile rationale: quotes, braces, JSON, control chars, U+2028/2029,
// HTML metacharacters, a fake "vote" injection attempt.
hostile := "\",\"vote\":\"no\",\"confidence_bps\":0,\"x\":\"" +
"</script>&<>

\x00\x01\x1f{\"nested\":true}"
d1 := base
d1.Rationale = "clean"
d2 := base
d2.Rationale = hostile
d2.Citations = []string{hostile, "ipfs://" + hostile}
// Consensus hash MUST be identical (rationale/citations excluded).
if canonical.OutputHashGovernance(d1) != canonical.OutputHashGovernance(d2) {
t.Fatal("hostile rationale changed the consensus hash")
}
// And the audit JSON must remain well-formed (re-parseable) JSON.
rec := canonical.AuditRecord{
JobID: "0x1", ModelSpecHash: spec.Hex(), Mode: "governance", Operator: "0x2",
Vote: "yes", ConfidenceBps: 5000, BucketBps: 5000, Rationale: hostile,
Citations: []string{hostile}, OutputHash: "0x3", EmbeddingHash: "0x4", Nonce: "0x5", Commit: "0x6",
}
j := rec.CanonicalJSON()
if !bytes.HasPrefix(j, []byte("{")) || !bytes.HasSuffix(j, []byte("}")) {
t.Fatalf("audit JSON malformed: %s", j)
}
// Must be valid JSON (no break-out).
if !jsonValid(j) {
t.Fatalf("hostile rationale broke JCS well-formedness:\n%s", j)
}
}
func jsonValid(b []byte) bool {
return jsonValidImpl(b)
}