Files
zeekayandHanzo Dev 104bf5f954 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:39:55 -07:00

641 lines
25 KiB
Go

// Copyright (C) 2026, Hanzo AI Inc. All rights reserved.
// RED-2 adversarial exploit battery. NOT part of Blue's suite.
// Each test is an attack attempt; the assertion documents whether it succeeds.
package aimarket
import (
"crypto/ecdsa"
"math/big"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/crypto"
gethsecp "github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// secp256k1 group order n.
var secp256k1N, _ = new(big.Int).SetString("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16)
// malleate flips an ECDSA signature [R||S||V] to its malleable twin
// [R || n-S || V^1]. This is the canonical signature-malleability identity:
// it recovers to the SAME public key over the SAME digest.
func malleate(sig []byte) []byte {
require := sig // keep linter calm; not used
_ = require
out := make([]byte, 65)
copy(out, sig)
s := new(big.Int).SetBytes(sig[32:64])
sPrime := new(big.Int).Sub(secp256k1N, s)
sp := sPrime.Bytes()
// left-pad to 32
for i := range out[32:64] {
out[32+i] = 0
}
copy(out[64-len(sp):64], sp)
out[64] = sig[64] ^ 1 // flip recovery bit
return out
}
// ATTACK 1a: ground-truth — does luxfi/crypto recovery accept a high-s
// (non-canonical) signature at all, and does it recover to the same signer?
func TestRed_Malleability_GroundTruth(t *testing.T) {
k, err := crypto.GenerateKey()
require.NoError(t, err)
signer := crypto.PubkeyToAddress(k.PublicKey)
digest := crypto.Keccak256([]byte("attack-digest-padded-to-32-bytesXX"))[:32]
sig, err := crypto.Sign(digest, k)
require.NoError(t, err)
// The canonical signature from Sign() is low-s. Recover from it.
pub1, err := crypto.SigToPub(digest, sig)
require.NoError(t, err)
require.Equal(t, signer, crypto.PubkeyToAddress(*pub1))
// Malleate it (s -> n-s, v flipped).
bad := malleate(sig)
pub2, err := crypto.SigToPub(digest, bad)
if err != nil {
t.Logf("malleated sig REJECTED by recovery: %v (low-s enforced at recover layer)", err)
return
}
rec := crypto.PubkeyToAddress(*pub2)
t.Logf("malleated sig ACCEPTED by recovery; recovers to %x (signer=%x)", rec[:4], signer[:4])
// The malleability identity: a high-s twin recovers to the SAME signer.
require.Equal(t, signer, rec,
"malleated high-s signature recovered to a DIFFERENT address — that would be an impersonation primitive")
}
// ATTACK 1b: can a malleated-but-valid operator signature settle a record a
// SECOND time (double-settle) after the canonical one already settled it?
func TestRed_Malleability_CannotDoubleSettle(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
opKey, op := redKeypair(t)
fund(db, caller, 100_000_000_000)
require.NoError(t, RegisterOffering(db, op, model, uint256.NewInt(1_000_000_000), 4096))
recordId, _, err := RequestInference(db, lg, caller, op, model, 2000)
require.NoError(t, err)
resultHash := common.HexToHash("0xdeadbeef")
digest := crypto.Keccak256(recordId.Bytes(), resultHash.Bytes())
sig, err := crypto.Sign(digest, opKey)
require.NoError(t, err)
// First settle with canonical sig.
_, err = SettleInference(db, recordId, resultHash, sig, DefaultAttest)
require.NoError(t, err)
// Malleated twin is a DIFFERENT 65-byte string but a valid attestation.
bad := malleate(sig)
require.NotEqual(t, sig, bad, "malleated sig must differ byte-wise")
// Confirm the malleated sig is itself a VALID attestation (recovers to op).
require.True(t, DefaultAttest(op, common.BytesToHash(digest), bad),
"malleated twin should still pass attestation (proves malleability is real)")
// Attempt to settle again with the malleated twin.
_, err = SettleInference(db, recordId, resultHash, bad, DefaultAttest)
require.ErrorIs(t, err, ErrAlreadySettled,
"replay guard is keyed by recordId, so malleability cannot double-settle")
}
// ATTACK 1c: can a malleated sig impersonate a DIFFERENT operator? (i.e. does
// recovery ever land on an attacker-chosen address). This is the only way
// malleability could matter — and it should be impossible.
func TestRed_Malleability_NoImpersonation(t *testing.T) {
victimKey, victim := redKeypair(t)
_, attacker := redKeypair(t)
require.NotEqual(t, victim, attacker)
digest := crypto.Keccak256([]byte("some-32-byte-digest-padding-aaaaaa"))[:32]
sig, err := crypto.Sign(digest, victimKey)
require.NoError(t, err)
bad := malleate(sig)
// Neither the canonical nor the malleated sig attests for the attacker.
require.False(t, DefaultAttest(attacker, common.BytesToHash(digest), sig))
require.False(t, DefaultAttest(attacker, common.BytesToHash(digest), bad))
// Both attest for the victim (that's expected; victim signed).
require.True(t, DefaultAttest(victim, common.BytesToHash(digest), sig))
}
// ATTACK 1d: recovery-id confusion. Does DefaultAttest accept v in {27,28}
// (Ethereum-style) as well as {0,1}? If both are accepted for the SAME (r,s),
// that's a second valid sig string for one signer — another malleability axis.
func TestRed_RecoveryIDConfusion(t *testing.T) {
k, err := crypto.GenerateKey()
require.NoError(t, err)
signer := crypto.PubkeyToAddress(k.PublicKey)
digest := crypto.Keccak256([]byte("recovery-id-digest-padding-bbbbbbb"))[:32]
sig, err := crypto.Sign(digest, k)
require.NoError(t, err)
require.True(t, sig[64] == 0 || sig[64] == 1, "Sign yields v in {0,1}")
// Variant with v += 27.
v27 := make([]byte, 65)
copy(v27, sig)
v27[64] = sig[64] + 27
ok27 := DefaultAttest(common.BytesToAddress(signer[:]), common.BytesToHash(digest), v27)
t.Logf("v+27 accepted by DefaultAttest = %v", ok27)
// Variant with v in {2,3} (extra recids).
v2 := make([]byte, 65)
copy(v2, sig)
v2[64] = 2
ok2 := DefaultAttest(common.BytesToAddress(signer[:]), common.BytesToHash(digest), v2)
t.Logf("v=2 accepted by DefaultAttest = %v (recovers to signer = %v)", ok2, ok2)
// Whatever the answer, it must never let v-confusion attest for someone else.
// (Documented by the ground-truth log above; no money impact regardless.)
}
// ATTACK 2: PREPAID RUG. Operator is credited at Meter time and can Withdraw
// BEFORE attesting. The caller pays for inference that is never served and has
// NO refund path. Demonstrate the full theft-of-service concretely.
func TestRed_PrepaidRug_OperatorWithdrawsWithoutServing(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
opKey, op := redKeypair(t)
_ = opKey // operator NEVER signs an attestation in this attack
const callerStart = 100_000_000_000
fund(db, caller, callerStart)
require.NoError(t, RegisterOffering(db, op, model, uint256.NewInt(1_000_000_000), 4096))
// Victim caller requests (and is charged for) inference.
recordId, cost, err := RequestInference(db, lg, caller, op, model, 4096)
require.NoError(t, err)
// Operator immediately withdraws the prepaid funds — WITHOUT serving or
// attesting anything.
paid, err := Withdraw(db, lg, op)
require.NoError(t, err)
require.Equal(t, cost.Uint64(), paid.Uint64(), "operator pockets the full prepaid cost")
// State of the world after the rug:
require.Equal(t, uint64(callerStart)-cost.Uint64(), db.GetBalance(caller).Uint64(),
"caller was debited")
require.Equal(t, cost.Uint64(), db.GetBalance(op).Uint64(),
"operator holds the caller's money")
require.Zero(t, db.GetBalance(ContractAddress).Uint64(), "escrow drained, nothing to claw back")
// The request is still only Metered (never settled). There is NO refund op.
_, _, status := GetRequest(db, recordId)
require.Equal(t, StatusMetered, status,
"request never settled — caller paid for nothing and cannot recover funds")
// Prove there is no mechanism for the caller to get money back: the only
// state-changing ops are register/meter/withdraw/request/settle. None
// returns funds to a caller. (No Refund selector exists.)
require.NotContains(t, []uint32{
SelectorRegisterOffering, SelectorGetOffering, SelectorMeter,
SelectorWithdraw, SelectorRequestInference, SelectorSettleInference,
SelectorGetCredit, SelectorGetRequest,
}, uint32(0xFF000000), "there is provably no refund selector")
}
// ATTACK 3a: force-credit griefing — can caller A credit an operator they do
// not control, to manipulate the operator's withdrawable balance? (They pay,
// operator gains — so it grieves the CALLER, not the operator. Confirm net.)
func TestRed_ForceCredit_GriefsSelfNotOperator(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
_, op := redKeypair(t)
fund(db, caller, 10_000_000_000)
require.NoError(t, RegisterOffering(db, op, model, uint256.NewInt(1_000_000_000), 4096))
before := GetCredit(db, op).Uint64()
cost, err := Meter(db, lg, caller, op, model, 1000)
require.NoError(t, err)
after := GetCredit(db, op).Uint64()
require.Equal(t, before+cost.Uint64(), after,
"caller can inflate ANY operator's credit — but only by spending their own funds")
// This is not an attack on the operator (operator only gains). It IS a way
// to force an offering to be 'used' without consent; harmless economically.
}
// ATTACK 3b: self-dealing — register your own price-0 offering, meter against
// yourself, withdraw. Can you net-gain anything (mint wei)? Must be net-zero
// minus the base cost held in escrow then returned.
func TestRed_SelfDealing_IsNetNeutral(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
self := common.HexToAddress("0x5e1f000000000000000000000000000000000001")
const start = 5_000_000_000
fund(db, self, start)
require.NoError(t, RegisterOffering(db, self, model, uint256.NewInt(0), 4096))
cost, err := Meter(db, lg, self, self, model, 1000) // caller==operator==self
require.NoError(t, err)
require.Equal(t, BaseCostWei.Uint64(), cost.Uint64(), "price-0 → cost is just base")
paid, err := Withdraw(db, lg, self)
require.NoError(t, err)
require.Equal(t, cost.Uint64(), paid.Uint64())
// Net: self spent `cost` to escrow, then withdrew `cost`. Balance restored.
require.Equal(t, uint64(start), db.GetBalance(self).Uint64(),
"self-dealing mints nothing; perfectly net-neutral")
require.Zero(t, db.GetBalance(ContractAddress).Uint64())
}
// ATTACK 4a: recordId preimage ambiguity. The recordId hashes a VARIABLE-LENGTH
// model name in the MIDDLE of the preimage. Try to find two distinct
// (model, tokens) pairs that collide to the same recordId for the same
// (caller, nonce, operator). If they collide, one settle consumes the other.
func TestRed_RecordIDNoPreimageCollision(t *testing.T) {
c := common.HexToAddress("0x1111111111111111111111111111111111111111")
op := common.HexToAddress("0x2222222222222222222222222222222222222222")
nonce := common.HexToHash("0x07")
// Candidate collision: model "A\x00\x00\x00\x01" + tokens=2 (0x00000002)
// vs model "A" + tokens reinterpret. Because tokens is a FIXED 4-byte
// suffix and model is everything between the 81-byte fixed prefix and that
// suffix, the decode is unambiguous — assert no collision across a sweep.
seen := map[common.Hash][2]string{}
models := [][]byte{
[]byte("A"),
[]byte("A\x00"),
[]byte("A\x00\x00"),
append([]byte("A"), []byte{0, 0, 0, 1}...), // "A"+bytes that look like a token tail
[]byte("AB"),
[]byte("ABC"),
{0x00},
{0x00, 0x00},
}
toks := []uint32{1, 2, 0x41, 0x10000, 0xFFFFFFFF}
for _, m := range models {
for _, tk := range toks {
id := computeRecordID(c, nonce, op, m, tk)
if prev, ok := seen[id]; ok {
t.Fatalf("recordId COLLISION: (%q,%d) and (%s,%s) → %x",
m, tk, prev[0], prev[1], id)
}
seen[id] = [2]string{string(m), string(rune(tk))}
}
}
}
// ATTACK 4b: nonce reuse within a frame. There is no external call in
// RequestInference, so no reentrancy can rewind the nonce. Two sequential
// requests with identical args must produce DISTINCT recordIds (nonce bumped).
func TestRed_NonceMonotonicAcrossIdenticalRequests(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
fund(db, caller, 100_000_000_000)
require.NoError(t, RegisterOffering(db, operator, model, uint256.NewInt(1), 4096))
ids := map[common.Hash]bool{}
for i := range 50 {
id, _, err := RequestInference(db, lg, caller, operator, model, 100)
require.NoError(t, err)
require.False(t, ids[id], "nonce reuse produced a duplicate recordId at i=%d", i)
ids[id] = true
}
}
// ATTACK 5a: dispatcher fuzz — truncated calldata must never panic; it must
// return a clean revert error. Walk every selector with every truncation.
func TestRed_DispatcherTruncationNoPanic(t *testing.T) {
sels := []uint32{
SelectorRegisterOffering, SelectorGetOffering, SelectorMeter,
SelectorWithdraw, SelectorRequestInference, SelectorSettleInference,
SelectorGetCredit, SelectorGetRequest,
}
// Build a maximal well-formed-ish payload then truncate at every length.
for _, sel := range sels {
full := selBytes(sel)
full = append(full, make([]byte, 256)...) // 8 words of zero tail
for n := 0; n <= len(full); n++ {
as := newAS()
fund(as.db, caller, 1_000_000_000_000)
input := full[:n]
require.NotPanics(t, func() {
_, _, _ = Precompile.Run(as, caller, ContractAddress, input, 5_000_000, false)
}, "selector %#x truncated to %d bytes panicked", sel, n)
}
}
}
// ATTACK 5b: dynamic-length bomb. Claim a 2^64-1 byte string. Must be rejected
// cleanly, never allocate or panic.
func TestRed_DynamicLengthBomb(t *testing.T) {
as := newAS()
bad := selBytes(SelectorGetOffering)
bad = append(bad, addrWord(operator)...)
// length word = max uint256
maxWord := make([]byte, 32)
for i := range maxWord {
maxWord[i] = 0xFF
}
bad = append(bad, maxWord...)
bad = append(bad, []byte("x")...)
require.NotPanics(t, func() {
_, _, err := Precompile.Run(as, caller, ContractAddress, bad, 5_000_000, false)
require.Error(t, err)
})
// length word = 2^63 (would be negative when cast to int) must be rejected.
// With the maxDynamicLen cap this is caught at the length check (before any
// int conversion / slice), which is strictly safer than the old buffer-bound
// path that relied on int(uint64) wrap behaviour.
bad2 := selBytes(SelectorGetOffering)
bad2 = append(bad2, addrWord(operator)...)
big63 := make([]byte, 32)
big63[24] = 0x80 // 2^63
bad2 = append(bad2, big63...)
bad2 = append(bad2, []byte("x")...)
require.NotPanics(t, func() {
_, _, err := Precompile.Run(as, caller, ContractAddress, bad2, 5_000_000, false)
require.Error(t, err)
require.Contains(t, err.Error(), "dynamic length too large")
})
}
// ATTACK 5c: dirty high bytes in the uint32 token word must be rejected (not
// silently truncated to a smaller token count that under-charges).
func TestRed_DirtyUint32Rejected(t *testing.T) {
as := newAS()
fund(as.db, caller, 1_000_000_000_000)
_, _, err := Precompile.Run(as, operator, ContractAddress, encReg(model, 1_000_000_000, 4096), 1_000_000, false)
require.NoError(t, err)
// meter(operator, model, tokens) but set a high byte in the tokens word.
in := selBytes(SelectorMeter)
in = append(in, addrWord(operator)...)
dirty := make([]byte, 32)
dirty[0] = 0x01 // dirty high byte (would be ignored by a naive reader)
dirty[28] = 0x00 //
dirty[31] = 0x64 // low byte = 100
in = append(in, dirty...)
in = append(in, padLeft32(uint256.NewInt(uint64(len(model))).Bytes())...)
in = append(in, padRight32(model)...)
_, _, err = Precompile.Run(as, caller, ContractAddress, in, 1_000_000, false)
require.Error(t, err, "dirty high bytes in uint32 token word must be rejected")
}
// REGRESSION for the readBytes cap fix: a dynamic length above maxDynamicLen is
// rejected at the length check (before slicing), even when enough bytes are
// physically present in calldata. Also asserts a name at exactly the cap+1
// boundary is rejected, and that a legitimately-sized field still works.
func TestRed_ReadBytesCapEnforced(t *testing.T) {
// Claim length = maxDynamicLen+1 and actually PROVIDE that many bytes so the
// buffer-bound check would pass — only the explicit cap can stop this.
overCap := maxDynamicLen + 1
bad := selBytes(SelectorGetOffering)
bad = append(bad, addrWord(operator)...)
bad = append(bad, padLeft32(uint256.NewInt(uint64(overCap)).Bytes())...)
bad = append(bad, make([]byte, overCap)...) // enough real bytes to satisfy buffer bound
as := newAS()
_, _, err := Precompile.Run(as, caller, ContractAddress, bad, 50_000_000, false)
require.Error(t, err, "length above maxDynamicLen must be rejected even with bytes present")
require.Contains(t, err.Error(), "dynamic length too large")
// A legitimately-sized model name (well under the cap) still decodes fine.
as2 := newAS()
fund(as2.db, caller, 1_000_000_000_000)
_, _, err = Precompile.Run(as2, operator, ContractAddress, encReg(model, 1, 1), 1_000_000, false)
require.NoError(t, err)
}
// ATTACK 6: adversarial value-conservation. Random-ish interleaving of
// register/meter/withdraw/settle across 3 operators and 3 callers; assert
// total wei constant and escrow == Σ credits at every step.
func TestRed_AdversarialValueConservation(t *testing.T) {
as := newAS()
db := as.db
lg := newLedger(db)
ops := []struct {
key *ecdsa.PrivateKey
ad common.Address
}{}
for range 3 {
k, a := redKeypair(t)
ops = append(ops, struct {
key *ecdsa.PrivateKey
ad common.Address
}{k, a})
}
callers := []common.Address{
common.HexToAddress("0xC0"), common.HexToAddress("0xC1"), common.HexToAddress("0xC2"),
}
const each = 1_000_000_000_000
for _, c := range callers {
fund(db, c, each)
}
models := [][]byte{[]byte("m0"), []byte("m1"), []byte("m2")}
for i, o := range ops {
require.NoError(t, RegisterOffering(db, o.ad, models[i], uint256.NewInt(uint64(1_000_000*(i+1))), 100000))
}
allAddrs := append([]common.Address{ContractAddress}, callers...)
for _, o := range ops {
allAddrs = append(allAddrs, o.ad)
}
total := func() *uint256.Int {
s := new(uint256.Int)
for _, a := range allAddrs {
s.Add(s, db.GetBalance(a))
}
return s
}
credits := func() *uint256.Int {
s := new(uint256.Int)
for _, o := range ops {
s.Add(s, GetCredit(db, o.ad))
}
return s
}
want := total().Clone()
check := func(where string) {
require.Equal(t, want.Hex(), total().Hex(), "wei conservation broken at %s", where)
require.Equal(t, credits().Hex(), db.GetBalance(ContractAddress).Hex(), "escrow != Σcredits at %s", where)
}
type req struct {
id common.Hash
op int
rh common.Hash
}
var pending []req
step := 0
do := func(label string, fn func()) { fn(); step++; check(label) }
for round := range 8 {
ci := round % len(callers)
oi := round % len(ops)
do("meter", func() {
_, _ = Meter(db, lg, callers[ci], ops[oi].ad, models[oi], uint32(1000+round*37))
})
do("request", func() {
id, _, err := RequestInference(db, lg, callers[(ci+1)%3], ops[oi].ad, models[oi], uint32(500+round*11))
if err == nil {
pending = append(pending, req{id, oi, common.BigToHash(big.NewInt(int64(round + 1)))})
}
})
if round%2 == 1 && len(pending) > 0 {
p := pending[0]
pending = pending[1:]
digest := crypto.Keccak256(p.id.Bytes(), p.rh.Bytes())
sig, _ := crypto.Sign(digest, ops[p.op].key)
do("settle", func() {
_, _ = SettleInference(db, p.id, p.rh, sig, DefaultAttest)
})
}
if round%3 == 0 {
do("withdraw", func() { _, _ = Withdraw(db, lg, ops[oi].ad) })
}
}
// Drain everyone.
for _, o := range ops {
do("final-withdraw", func() { _, _ = Withdraw(db, lg, o.ad) })
}
require.Zero(t, db.GetBalance(ContractAddress).Uint64(), "escrow fully drained at end")
}
// ATTACK 7: settle after withdraw — operator withdraws prepaid funds, THEN
// settles. Settlement must not credit a second time (no double-spend).
func TestRed_SettleDoesNotDoubleCreditAfterWithdraw(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
opKey, op := redKeypair(t)
fund(db, caller, 100_000_000_000)
require.NoError(t, RegisterOffering(db, op, model, uint256.NewInt(1_000_000_000), 4096))
recordId, cost, err := RequestInference(db, lg, caller, op, model, 2000)
require.NoError(t, err)
// Operator withdraws BEFORE settling.
paid, err := Withdraw(db, lg, op)
require.NoError(t, err)
require.Equal(t, cost.Uint64(), paid.Uint64())
require.Zero(t, GetCredit(db, op).Uint64())
require.Zero(t, db.GetBalance(ContractAddress).Uint64())
// Now settle. If settle credited again, GetCredit would be > 0 and escrow
// would be negative/underflow on the next withdraw.
resultHash := common.HexToHash("0xfaceb00c")
sig := attestSig(t, opKey, recordId, resultHash)
_, err = SettleInference(db, recordId, resultHash, sig, DefaultAttest)
require.NoError(t, err)
require.Zero(t, GetCredit(db, op).Uint64(), "settle must NOT add a second credit")
// A withdraw now must fail (nothing owed) — proves no phantom credit.
_, err = Withdraw(db, lg, op)
require.ErrorIs(t, err, ErrNoCredit)
}
// ATTACK 10: packing round-trip at exact boundaries. price=2^128-1 and
// maxSize=2^32-1 must survive pack→unpack, and metering at maxSize must compute
// an aggregate cost that exceeds 2^128 while staying within uint256 (no
// overflow, no truncation into the flags/reserved bytes).
func TestRed_PackingBoundaries(t *testing.T) {
maxPrice := new(uint256.Int).Set(uint128Max) // 2^128 - 1
const maxSize = uint32(0xFFFFFFFF) // 2^32 - 1
h, err := packOffering(maxPrice, maxSize)
require.NoError(t, err)
o := unpackOffering(h)
require.True(t, o.Exists)
require.Equal(t, maxPrice.Hex(), o.Price.Hex(), "max price must round-trip")
require.Equal(t, maxSize, o.MaxSizeTokens, "max size must round-trip")
// price = 2^128 exactly must be rejected (one over the cap).
over := new(uint256.Int).Add(uint128Max, uint256.NewInt(1))
_, err = packOffering(over, 1)
require.ErrorIs(t, err, ErrPriceTooLarge)
// Aggregate cost at the boundary: (2^128-1) * (2^32-1) / 1000 + base.
// This is ~2^160, far above 2^128, and must compute without overflow.
cost, err := ComputeCost(maxPrice, maxSize)
require.NoError(t, err)
require.True(t, cost.Gt(uint128Max), "aggregate cost legitimately exceeds 2^128")
// Sanity: recompute independently and compare.
want := new(uint256.Int)
want.Mul(maxPrice, uint256.NewInt(uint64(maxSize)))
want.Div(want, thousand)
want.Add(want, BaseCostWei)
require.Equal(t, want.Hex(), cost.Hex())
// requestRecord packing at max tokens must round-trip the operator + tokens
// without the status byte or reserved tail corrupting them.
rr := requestRecord{Status: StatusSettled, Operator: operator, Tokens: maxSize}
got := unpackRequest(packRequest(rr))
require.Equal(t, rr.Status, got.Status)
require.Equal(t, rr.Operator, got.Operator)
require.Equal(t, rr.Tokens, got.Tokens)
}
// ATTACK 8: EVM value injection. The EVM core runs Context.Transfer(caller,
// precompile, msg.value) BEFORE Run(). So a caller can push native value into
// the escrow account with ANY call. Prove this surplus (a) cannot be stolen by
// an operator over-withdrawing, and (b) is locked (no op pays it out). It is a
// self-burn, not a theft primitive.
func TestRed_ValueInjectionSurplusIsLockedNotStealable(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
_, op := redKeypair(t)
fund(db, caller, 100_000_000_000)
require.NoError(t, RegisterOffering(db, op, model, uint256.NewInt(1_000_000_000), 4096))
cost, err := Meter(db, lg, caller, op, model, 1000)
require.NoError(t, err)
require.Equal(t, cost.Uint64(), db.GetBalance(ContractAddress).Uint64())
// Simulate the EVM transferring msg.value=surplus into escrow BEFORE a call.
surplus := uint256.NewInt(7_777_777_777)
db.AddBalance(ContractAddress, surplus, 0)
// Operator withdraws — must receive EXACTLY its credit (cost), never the
// surplus. Pay only ever pays the recorded credit.
paid, err := Withdraw(db, lg, op)
require.NoError(t, err)
require.Equal(t, cost.Uint64(), paid.Uint64(), "operator cannot drain injected surplus")
// The surplus is now stranded in escrow; no op can pay it out.
require.Equal(t, surplus.Uint64(), db.GetBalance(ContractAddress).Uint64(),
"surplus is locked — a self-burn, not stealable")
// Another operator with zero credit cannot withdraw the surplus either.
_, otherOp := redKeypair(t)
_, err = Withdraw(db, lg, otherOp)
require.ErrorIs(t, err, ErrNoCredit)
require.Equal(t, surplus.Uint64(), db.GetBalance(ContractAddress).Uint64())
}
// ATTACK 9: confirm reverting handlers return a NON-ErrExecutionReverted error.
// The EVM zeroes gas on exactly those errors (evm.go: gas=0 unless
// ErrExecutionReverted), so logic failures consume all gas like a native
// precompile — there is no gas-refund-on-revert grief vector. If any handler
// returned vm.ErrExecutionReverted, the caller would keep gas on a failed
// state-changing attempt; assert none do.
func TestRed_RevertErrorsAreGasConsuming(t *testing.T) {
as := newAS()
fund(as.db, caller, 1_000_000_000_000)
// Meter an unregistered model → ErrOfferingNotFound (a logic revert).
_, _, err := Precompile.Run(as, caller, ContractAddress,
encMeterLike(SelectorMeter, operator, []byte("nope"), 100), 1_000_000, false)
require.Error(t, err)
// It must NOT be the EVM's ErrExecutionReverted sentinel (which would
// preserve gas). It is a package-local error → EVM sets gas=0.
require.NotEqual(t, "execution reverted", err.Error(),
"handler errors must be gas-consuming, not the gas-preserving revert sentinel")
}
// redKeypair mirrors the harness keypair() but is local to the exploit file.
func redKeypair(t *testing.T) (*ecdsa.PrivateKey, common.Address) {
t.Helper()
k, err := crypto.GenerateKey()
require.NoError(t, err)
a := crypto.PubkeyToAddress(k.PublicKey)
return k, common.BytesToAddress(a[:])
}
// touch the secp import so it's available for any low-level probing.
var _ = gethsecp.S256