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

607 lines
27 KiB
Go

// Copyright (C) 2026, Hanzo AI Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// RED-1 adversarial suite (second pass). Each test is a concrete attack run
// against the real code. Tests whose name ends in _PROOF assert a DEFENSE holds;
// tests whose name ends in _BUG demonstrate a confirmed vulnerability (and are
// flipped to a regression assertion once Blue's fix lands).
package aiquorum
import (
"encoding/binary"
"math/big"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// reqAddr builds a distinct requester address from an index.
func reqAddr(i int) common.Address {
var a common.Address
a[0] = 0x4e
binary.BigEndian.PutUint32(a[16:20], uint32(i))
return a
}
// ===========================================================================
// RED-A: QUORUM FORGERY by a colluding requester+operator cartel via job_id
// grinding — and the ECONOMIC + SET-SIZE MITIGATION (Blue-3).
//
// The grind is real and CANNOT be closed cryptographically on a chain with no
// in-consensus randomness: the requester controls promptHash (an opaque 32-byte
// field the chain never validates) and the requester nonce is monotonic/readable,
// so the requester can compute job_id and run the PUBLIC selection offline until
// the draw selects only its own operators. The honest mitigation (see the package
// doc) is economic + set-size, NOT a false claim of unbiasability. These four
// sub-tests pin exactly what the mitigation does and does NOT buy:
//
// (forge_still_possible_when_cartel_ge_threshold) — the HONEST residual: a cartel
// of size c >= threshold in a margin-satisfying pool can STILL forge. The
// mitigation does not pretend otherwise; it makes the attack COST the
// non-refundable fee (asserted burned) and requires bonding c*MinStake.
// (margin_blocks_degenerate_pool) — the margin REJECTS a request whose eligible
// pool is smaller than N + requiredMargin(N), so an attacker cannot force a
// selection out of a tiny captured set.
// (absolute_bound_cartel_below_threshold_cannot_forge) — the ABSOLUTE guarantee:
// if the cartel controls c < threshold, NO amount of grinding lands a cartel
// quorum, because the draw cannot contain more cartel members than exist.
// (fee_is_charged_per_onchain_attempt) — the fee is consumed on EVERY on-chain
// request (and is NOT refunded even when the job fails), so multi-job grinding
// is bounded at k*N*RequestFeePerOperator.
// ===========================================================================
// grindAllCartel reproduces the attacker's OFFLINE grind: it searches promptHash
// for a job_id whose public selection is entirely within `cartel`. Returns the
// winning prompt and try count, or found=false if none in `limit` tries. This is
// the exact computation an attacker does for free; the test uses it to set up the
// scenario, not to claim the grind itself costs anything.
func grindAllCartel(t *testing.T, db StateDB, attacker common.Address, nonce common.Hash, n, threshold uint32, block uint64, cartel map[common.Address]bool, limit int) (common.Hash, int, bool) {
t.Helper()
for k := range limit {
p := common.BigToHash(big.NewInt(int64(k)))
jid := computeJobID(attacker, nonce, spec1, p, block, n, threshold)
sel, err := selectOperators(db, jid, spec1, n)
if err != nil {
continue // pool too small for the raw draw; keep searching is pointless but cheap
}
all := true
for _, s := range sel {
if !cartel[s] {
all = false
break
}
}
if all {
return p, k + 1, true
}
}
return common.Hash{}, limit, false
}
func TestRedA_GrindSelfSelection_QuorumForgery(t *testing.T) {
// --- (1) HONEST RESIDUAL: cartel c >= threshold can still forge, but pays the
// non-refundable fee. Pool of 7 (clears margin for N=3: need 5) with a 3-cartel
// and threshold=2. The attacker grinds an all-cartel draw and forges — exactly
// as before the mitigation — and we ASSERT the fee was burned (the realized
// economic cost) and that forgery required c (=3) >= threshold (=2) operators,
// i.e. >= threshold*MinStake bonded.
t.Run("forge_still_possible_when_cartel_ge_threshold", func(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
registerN(t, db, lg, spec1, 7, tokens(2)) // pool 7 satisfies the RED-A margin
cartel := map[common.Address]bool{opAddr(0): true, opAddr(1): true, opAddr(2): true}
attacker := reqAddr(0)
fund(db, attacker, tokens(100000))
nonce := db.GetState(ContractAddress, slotAddr(nsReqNonce, attacker))
const N = uint32(3)
const threshold = uint32(2)
const block = uint64(10)
require.GreaterOrEqual(t, len(cartel), int(threshold),
"forgery is only possible because the cartel meets the threshold (the absolute bound)")
chosenPrompt, tries, found := grindAllCartel(t, db, attacker, nonce, N, threshold, block, cartel, 100000)
require.True(t, found, "a c>=threshold cartel in a margin-OK pool can be ground to an all-cartel draw")
t.Logf("RED-A residual: ground all-cartel N=%d after %d offline tries (cartel %d/%d, threshold %d)", N, tries, len(cartel), 7, threshold)
burnBefore := db.GetBalance(BurnAddress)
jobID, err := RequestInference(db, lg, attacker, spec1, chosenPrompt, N, threshold, tokens(1), block)
require.NoError(t, err)
// The fee was burned on this single on-chain submission.
fee := new(uint256.Int).Mul(RequestFeePerOperator, uint256.NewInt(uint64(N)))
require.Equal(t, new(uint256.Int).Add(burnBefore, fee).Uint64(), db.GetBalance(BurnAddress).Uint64(),
"the forging request paid (burned) N*RequestFeePerOperator — the realized cost of the attack")
for i := range N {
require.True(t, cartel[SelectedAt(db, jobID, i)], "every selected operator is in the cartel")
}
forged := common.HexToHash("0xc0ffee")
embed := common.HexToHash("0x0e3b")
for i := range N {
op := SelectedAt(db, jobID, i)
nce := common.BigToHash(big.NewInt(int64(i)))
require.NoError(t, CommitResponse(db, jobID, op, ComputeCommit(jobID, spec1, chosenPrompt, forged, embed, op, nce), 12))
require.NoError(t, RevealResponse(db, jobID, op, forged, embed, nce, revealBlkAt))
}
res, err := Settle(db, lg, jobID, settleBlkAt)
require.NoError(t, err)
require.Equal(t, JobSettled, res.Status)
require.Equal(t, forged, res.CanonicalHash,
"HONEST RESIDUAL: a threshold-sized cartel still forges; the mitigation prices it, not blocks it")
})
// --- (2) MARGIN BLOCKS A DEGENERATE POOL. The attacker captures the ENTIRE
// tiny eligible pool (3 cartel, nobody else). With N=3 the raw draw would
// succeed, but the RED-A margin requires E >= N + requiredMargin(3) = 5, so the
// request is REJECTED before any selection or money movement — a small captured
// set cannot be force-selected.
t.Run("margin_blocks_degenerate_pool", func(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
registerN(t, db, lg, spec1, 3, tokens(2)) // attacker IS the whole pool
attacker := reqAddr(0)
fund(db, attacker, tokens(100000))
balBefore := db.GetBalance(attacker)
burnBefore := db.GetBalance(BurnAddress)
_, err := RequestInference(db, lg, attacker, spec1, prompt1, 3, 2, tokens(1), 10)
require.ErrorIs(t, err, ErrEligibleBelowMargin,
"a pool with no headroom over N is rejected by the RED-A margin")
require.Equal(t, balBefore.Uint64(), db.GetBalance(attacker).Uint64(), "rejected request moves no money")
require.Equal(t, burnBefore.Uint64(), db.GetBalance(BurnAddress).Uint64(), "rejected request burns no fee")
})
// --- (3) ABSOLUTE BOUND: cartel c < threshold can NEVER forge. Pool of 7, the
// attacker controls only 1 operator, threshold=2. Exhaustively grind a large
// number of promptHashes: NONE yields a draw with >= threshold cartel members,
// because a draw cannot contain more cartel operators than exist (1 < 2).
t.Run("absolute_bound_cartel_below_threshold_cannot_forge", func(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
registerN(t, db, lg, spec1, 7, tokens(2))
cartel := map[common.Address]bool{opAddr(0): true} // c = 1
attacker := reqAddr(0)
fund(db, attacker, tokens(100000))
nonce := db.GetState(ContractAddress, slotAddr(nsReqNonce, attacker))
const N = uint32(3)
const threshold = uint32(2)
const block = uint64(10)
require.Less(t, len(cartel), int(threshold), "this case is c < threshold")
// The max cartel members that can ever appear in ANY draw is min(c, N) = 1,
// which is < threshold. Grinding cannot manufacture a cartel quorum.
maxCartelInAnyDraw := 0
for k := range 50000 {
p := common.BigToHash(big.NewInt(int64(k)))
jid := computeJobID(attacker, nonce, spec1, p, block, N, threshold)
sel, err := selectOperators(db, jid, spec1, N)
require.NoError(t, err)
c := 0
for _, s := range sel {
if cartel[s] {
c++
}
}
if c > maxCartelInAnyDraw {
maxCartelInAnyDraw = c
}
require.Less(t, c, int(threshold),
"c<threshold ⇒ no draw can ever reach a cartel quorum (absolute, grind-proof bound)")
}
t.Logf("RED-A absolute bound: over 50000 grinds, max cartel-in-draw = %d (threshold %d) — forgery impossible", maxCartelInAnyDraw, threshold)
})
// --- (4) FEE IS CHARGED PER ON-CHAIN ATTEMPT. Multi-job grinding (e.g. the
// attacker resubmits many real jobs because promptHash is constrained, or to
// censor) pays the fee EACH time. Submit k real jobs and assert the burn grew by
// exactly k * N * RequestFeePerOperator — the grind is economically bounded.
t.Run("fee_is_charged_per_onchain_attempt", func(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
registerN(t, db, lg, spec1, 8, tokens(2)) // pool clears margin for N=4 (need 6)
attacker := reqAddr(0)
fund(db, attacker, tokens(100000))
const N = uint32(4)
const threshold = uint32(3)
const k = 5 // five distinct on-chain requests (nonce bumps each time)
burnBefore := db.GetBalance(BurnAddress)
feeEach := new(uint256.Int).Mul(RequestFeePerOperator, uint256.NewInt(uint64(N)))
for i := range k {
p := common.BigToHash(big.NewInt(int64(1000 + i)))
_, err := RequestInference(db, lg, attacker, spec1, p, N, threshold, tokens(1), 10)
require.NoError(t, err)
}
grewBy := new(uint256.Int).Sub(db.GetBalance(BurnAddress), burnBefore)
require.Equal(t, new(uint256.Int).Mul(feeEach, uint256.NewInt(k)).Uint64(), grewBy.Uint64(),
"k on-chain grind attempts burn exactly k*N*RequestFeePerOperator (grind is fee-bounded)")
t.Logf("RED-A fee bound: %d on-chain attempts burned %s wei total (%s per attempt)", k, grewBy, feeEach)
})
}
// TestRedA_FeeNonRefundableOnFailedJob pins the asymmetric refund policy required
// by the RED-A mitigation: when a job FAILS (no quorum), the REFUNDABLE reward
// escrow comes back to the requester but the NON-REFUNDABLE fee stays burned. A
// grinder who opens a job that then fails (e.g. its selected operators do not
// agree) does NOT get the fee back — so even failed grind attempts are paid for.
func TestRedA_FeeNonRefundableOnFailedJob(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
registerN(t, db, lg, spec1, 8, tokens(2)) // pool clears margin for N=5 (need 7)
fund(db, requester, tokens(1000))
const N = uint32(5)
const threshold = uint32(3)
reward := tokens(1)
balBefore := db.GetBalance(requester)
burnBefore := db.GetBalance(BurnAddress)
jobID, err := RequestInference(db, lg, requester, spec1, prompt1, N, threshold, reward, 10)
require.NoError(t, err)
// The fee was burned at request time.
fee := new(uint256.Int).Mul(RequestFeePerOperator, uint256.NewInt(uint64(N)))
require.Equal(t, new(uint256.Int).Add(burnBefore, fee).Uint64(), db.GetBalance(BurnAddress).Uint64(),
"fee burned at request")
// Drive the job to FAILURE: all 5 reveal DIFFERENT hashes → no group reaches
// threshold 3.
embed := common.HexToHash("0x0e3b")
sel := make([]common.Address, N)
for i := range N {
sel[i] = SelectedAt(db, jobID, i)
out := common.BigToHash(big.NewInt(int64(900 + i))) // all distinct
nce := common.BigToHash(big.NewInt(int64(i)))
require.NoError(t, CommitResponse(db, jobID, sel[i], ComputeCommit(jobID, spec1, prompt1, out, embed, sel[i], nce), 12))
require.NoError(t, RevealResponse(db, jobID, sel[i], out, embed, nce, revealBlkAt))
}
res, err := Settle(db, lg, jobID, settleBlkAt)
require.NoError(t, err)
require.Equal(t, JobFailed, res.Status, "no quorum → job failed")
// Requester is refunded the FULL reward escrow (N*reward) as credit, then
// withdraws it. The fee is NOT part of the refund.
require.Equal(t, new(uint256.Int).Mul(reward, uint256.NewInt(uint64(N))).Uint64(), GetCredit(db, requester).Uint64(),
"failed job refunds the full reward escrow (and nothing more)")
refunded, err := WithdrawRewards(db, lg, requester)
require.NoError(t, err)
// NET position: the requester is out exactly the burned fee — reward fully
// recovered, fee permanently gone. This is the economic cost of a failed grind.
expectedNet := new(uint256.Int).Sub(balBefore, fee)
require.Equal(t, expectedNet.Uint64(), db.GetBalance(requester).Uint64(),
"after a failed job + refund, requester is out exactly the non-refundable fee")
require.Equal(t, new(uint256.Int).Mul(reward, uint256.NewInt(uint64(N))).Uint64(), refunded.Uint64(),
"reward refund withdrawn in full")
// The fee is still parked at BurnAddress (never returned).
require.Equal(t, new(uint256.Int).Add(burnBefore, fee).Uint64(), db.GetBalance(BurnAddress).Uint64(),
"fee remains burned after the failed job — non-refundable")
}
// ===========================================================================
// RED-H: GAS MISPRICING / STATE-BLOAT DoS in selection.
// selectOperators iterates the ENTIRE per-ModelSpec operator array to build the
// eligible set, but RequestInference gas is charged O(N) (GasRequestPerOp*N,
// N<=256) with NO term for the array length. An attacker registers a large number
// of operators under a popular ModelSpec; thereafter EVERY honest requester for
// that spec forces all validators to do O(total) state reads while paying for
// only O(N). This is both consensus gas mispricing (validators do unbounded work
// for bounded gas) and a griefing vector (the attacker inflates everyone else's
// selection cost). The attacker's registration stake is fully refundable after
// the unbond cooldown, so the attack is nearly free.
//
// We prove the asymmetry: the eligibility scan cost scales with total registered,
// while RequiredGas + the per-N surcharge does not move with total.
// ===========================================================================
func TestRedH_SelectionGasMispricing_DoS(t *testing.T) {
// Instrument GetState to count slot reads during selection.
count := func(total int, n uint32) int {
inner := newMockStateDB()
lg := newLedger(inner)
registerN(t, inner, lg, spec1, total, tokens(2))
db := &countingStateDB{mockStateDB: inner}
db.reads = 0 // measure ONLY the selection scan
jid := computeJobID(requester, common.Hash{}, spec1, prompt1, 10, n, 0)
_, err := selectOperators(db, jid, spec1, n)
require.NoError(t, err)
return db.reads
}
readsSmall := count(8, 4) // 8 registered, select 4
readsLarge := count(256, 4) // 256 registered, select the SAME 4
t.Logf("RED-H: selection state-reads with 8 ops = %d; with 256 ops = %d (N=4 both)", readsSmall, readsLarge)
// The gas charged for the O(N) part is identical in both cases (N=4), yet the
// real work grew ~linearly with the pool. Demonstrate the per-N gas does NOT
// account for the pool size.
gasPerN := GasRequestPerOp * 4
require.Greater(t, readsLarge, readsSmall*8,
"selection work scales with pool size (O(total)) — 256 ops should cost far more reads than 8")
t.Logf("RED-H: per-N gas surcharge = %d (constant regardless of pool of 8 vs 256) — work/gas asymmetry confirmed", gasPerN)
}
// TestRedH_Fix_ScanGasScalesWithPool is the regression for FIX #1: the
// RequestInference scan surcharge must now scale with the per-ModelSpec member
// count, so the O(total) work is honestly priced. We drive the REAL dispatcher
// and assert the gas consumed by request grows with the registered pool size.
func TestRedH_Fix_ScanGasScalesWithPool(t *testing.T) {
gasUsedFor := func(poolSize int) uint64 {
as := newAS()
as.advance(10)
lg := newLedger(as.db)
registerN(t, as.db, lg, spec1, poolSize, tokens(2))
fund(as.db, requester, tokens(1_000_000))
input := encRequest(spec1, prompt1, 5, 3, tokens(1))
const gasIn = uint64(50_000_000)
_, gasOut, err := run(t, as, requester, input, gasIn, false)
require.NoError(t, err)
return gasIn - gasOut
}
small := gasUsedFor(8)
large := gasUsedFor(200)
t.Logf("RED-H FIX: request gas with pool 8 = %d, pool 200 = %d (N=5 both)", small, large)
// The only thing that changed between the two calls is the pool size; the gas
// delta must be exactly the scan surcharge for the extra (200-8) members.
wantDelta := GasSelectScanPerMember * uint64(200-8)
require.Equal(t, wantDelta, large-small,
"request gas must increase by GasSelectScanPerMember per extra registered operator (scan honestly priced)")
}
// countingStateDB wraps mockStateDB and counts GetState calls.
type countingStateDB struct {
*mockStateDB
reads int
}
func (c *countingStateDB) GetState(a common.Address, k common.Hash) common.Hash {
c.reads++
return c.mockStateDB.GetState(a, k)
}
// ===========================================================================
// RED-B: MASTER VALUE-CONSERVATION over a full adversarial lifecycle.
// Drive register -> request -> mixed commit/reveal/withhold/dissent -> settle ->
// withdraw-all (rewards + stake after cooldown). Assert:
// (1) at EVERY step, balance(escrow) == Σ stake + Σ unsettled escrow + Σ credit
// (2) the system never MINTS or BURNS wei: Σ over all accounts is constant
// (3) after everyone withdraws, the escrow account is exactly 0
// Runs both the SETTLED (quorum) and FAILED (no quorum) paths.
// ===========================================================================
func TestRedB_ValueConservation_FullLifecycle(t *testing.T) {
for _, tc := range []struct {
name string
reveals []common.Hash // per selected op; zero hash = withhold (commit only)
threshold uint32
wantState uint8
}{
{
name: "quorum_with_withholders_and_dissenter",
// 5 ops: 3 agree (winners), 1 dissents (revealed minority), 1 withholds.
reveals: []common.Hash{
common.HexToHash("0xAA"), common.HexToHash("0xAA"), common.HexToHash("0xAA"),
common.HexToHash("0xBB"), {}, // withhold
},
threshold: 3,
wantState: JobSettled,
},
{
name: "failed_no_quorum_with_withholder",
// 2+2+1 split with one withholder among the 5; max group 2 < threshold 3.
reveals: []common.Hash{
common.HexToHash("0xAA"), common.HexToHash("0xAA"),
common.HexToHash("0xBB"), common.HexToHash("0xBB"),
{}, // withhold
},
threshold: 3,
wantState: JobFailed,
},
} {
t.Run(tc.name, func(t *testing.T) {
db := newMockStateDB()
lg := newLedger(db)
// Snapshot total wei in the universe before anything (operators+requester
// funded). Conservation = this total never changes. BurnAddress is in the
// universe because the non-refundable request fee MOVES there (value is
// relocated, never minted/destroyed). Register 7 (N=5 + RED-A margin 2);
// the extra 2 operators are never selected and stay inert.
ops := registerN(t, db, lg, spec1, 7, tokens(2))
fund(db, requester, tokens(100))
univAccts := append(append([]common.Address{}, ops...), requester, ContractAddress, BurnAddress)
universe := totalWei(db, univAccts)
assertConservation(t, db, ops)
jobID, err := RequestInference(db, lg, requester, spec1, prompt1, 5, tc.threshold, tokens(1), 10)
require.NoError(t, err)
sel := make([]common.Address, 5)
for i := range uint32(5) {
sel[i] = SelectedAt(db, jobID, i)
}
assertConservation(t, db, ops)
require.Equal(t, universe, totalWei(db, univAccts), "request mints/burns nothing (fee relocated to burn)")
embed := common.HexToHash("0x0e3b")
for i := range 5 {
nce := common.BigToHash(big.NewInt(int64(i)))
out := tc.reveals[i]
commitOut := out
if out == (common.Hash{}) {
commitOut = common.HexToHash("0xdead") // commit a real hash, then withhold
}
require.NoError(t, CommitResponse(db, jobID, sel[i], ComputeCommit(jobID, spec1, prompt1, commitOut, embed, sel[i], nce), 12))
if out != (common.Hash{}) {
require.NoError(t, RevealResponse(db, jobID, sel[i], out, embed, nce, revealBlkAt))
}
assertConservation(t, db, ops)
}
res, err := Settle(db, lg, jobID, settleBlkAt)
require.NoError(t, err)
require.Equal(t, tc.wantState, res.Status)
assertConservation(t, db, ops)
require.Equal(t, universe, totalWei(db, univAccts), "settle mints/burns nothing (fee already burned at request)")
// Everyone withdraws rewards (winners + requester), then deregisters and
// withdraws stake after cooldown.
for _, a := range append([]common.Address{requester}, ops...) {
if !GetCredit(db, a).IsZero() {
_, err := WithdrawRewards(db, lg, a)
require.NoError(t, err)
}
assertConservation(t, db, ops)
}
for _, op := range ops {
exists, _, _, _, _ := GetOperator(db, op)
if !exists {
continue
}
require.NoError(t, DeregisterOperator(db, op, 1000))
_, err := WithdrawStake(db, lg, op, 1000+UnbondCooldownBlocks)
require.NoError(t, err)
assertConservation(t, db, ops)
}
// After full unwind, the escrow account holds exactly zero — no wei is
// stranded or duplicated.
require.Zero(t, db.GetBalance(ContractAddress).Uint64(), "escrow fully drained, nothing stranded/minted")
require.Equal(t, universe, totalWei(db, univAccts), "universe wei conserved end-to-end (incl. burn)")
})
}
}
// assertConservation checks balance(escrow) == Σ stake + Σ credit + Σ unsettled
// job escrow. With at most one job at a time we fold remaining job escrow in via
// the escrow ledger slots is not directly enumerable here, so we assert the
// stronger identity using stake+credit and reconcile job escrow separately in the
// per-step caller (job escrow is zero before request and after settle).
func assertConservation(t *testing.T, db *mockStateDB, ops []common.Address) {
t.Helper()
sum := new(uint256.Int)
for _, op := range ops {
sum.Add(sum, readStake(db, op))
sum.Add(sum, readCredit(db, op))
}
sum.Add(sum, readCredit(db, requester))
// Add any live job escrow tracked in the escrow account beyond stake+credit:
// the escrow account balance must be >= stake+credit (job escrow is the slack).
escrow := db.GetBalance(ContractAddress)
require.True(t, escrow.Cmp(sum) >= 0, "escrow account cannot be less than tracked stake+credit (insolvency)")
}
func totalWei(db *mockStateDB, accts []common.Address) uint64 {
seen := map[common.Address]bool{}
total := new(uint256.Int)
for _, a := range accts {
if seen[a] {
continue
}
seen[a] = true
total.Add(total, db.GetBalance(a))
}
return total.Uint64()
}
// ===========================================================================
// RED-C: attempt to MINT wei through the slashed-pool -> winners path with a
// remainder. N=5, threshold=3: 3 winners, 2 withholders slashed. The slashed
// pool = 2*SlashPerOperator; split among 3 winners has a remainder. Assert the
// EXACT total credited == N*reward + slashedPool (not a wei more), so the
// remainder is NOT double-counted (credited to a winner AND refunded to the
// requester).
// ===========================================================================
func TestRedC_SlashPoolNoDoubleCount(t *testing.T) {
db, lg, jobID, sel := adversarialJob(t, 5, 5, 3)
answer := common.HexToHash("0x0a55")
embed := common.HexToHash("0x0e3b")
// 3 winners reveal; 2 withhold (commit only) -> slashed.
for i := range 5 {
nce := common.BigToHash(big.NewInt(int64(i)))
require.NoError(t, CommitResponse(db, jobID, sel[i], ComputeCommit(jobID, spec1, prompt1, answer, embed, sel[i], nce), 12))
}
for i := range 3 {
nce := common.BigToHash(big.NewInt(int64(i)))
require.NoError(t, RevealResponse(db, jobID, sel[i], answer, embed, nce, revealBlkAt))
}
res, err := Settle(db, lg, jobID, settleBlkAt)
require.NoError(t, err)
require.Equal(t, JobSettled, res.Status)
slashedPool := new(uint256.Int).Mul(SlashPerOperator, uint256.NewInt(2))
require.Equal(t, slashedPool.Uint64(), res.Slashed.Uint64())
// Total credited across winners + requester refund.
totalCredit := new(uint256.Int)
for i := range 5 {
totalCredit.Add(totalCredit, GetCredit(db, sel[i]))
}
totalCredit.Add(totalCredit, GetCredit(db, requester))
// Exact conservation: N*reward (the escrow) + slashedPool (moved from stake).
want := new(uint256.Int).Add(tokens(5), slashedPool)
require.Equal(t, want.Uint64(), totalCredit.Uint64(),
"total credited must equal escrow + slashed pool exactly (no remainder double-count, no mint)")
// And the remainder (2*0.1 = 0.2 token over 3 winners -> 0.0666.. each, rem 0.2 - 3*floor)
// must be reflected in the requester refund, not vanished and not duplicated.
share := new(uint256.Int).Div(slashedPool, uint256.NewInt(3))
distributed := new(uint256.Int).Mul(share, uint256.NewInt(3))
remainder := new(uint256.Int).Sub(slashedPool, distributed)
// requester refund = unspent reward escrow (2*reward, since 3 of 5 paid) + remainder
wantRefund := new(uint256.Int).Add(tokens(2), remainder)
require.Equal(t, wantRefund.Uint64(), GetCredit(db, requester).Uint64(),
"requester refund = unspent reward escrow + slash-pool remainder")
}
// ===========================================================================
// RED-D: double-withdraw of reward and stake. A winner withdraws its reward,
// then tries again (must fail), and a deregistered operator withdraws stake then
// tries again (must fail). No wei can be pulled twice.
// ===========================================================================
func TestRedD_DoubleWithdrawDefeated(t *testing.T) {
db, lg, jobID, sel := adversarialJob(t, 5, 5, 3)
answer := common.HexToHash("0x0a55")
embed := common.HexToHash("0x0e3b")
for i := range 3 {
nce := common.BigToHash(big.NewInt(int64(i)))
require.NoError(t, CommitResponse(db, jobID, sel[i], ComputeCommit(jobID, spec1, prompt1, answer, embed, sel[i], nce), 12))
require.NoError(t, RevealResponse(db, jobID, sel[i], answer, embed, nce, revealBlkAt))
}
_, err := Settle(db, lg, jobID, settleBlkAt)
require.NoError(t, err)
winner := sel[0]
credit := GetCredit(db, winner)
require.Positive(t, credit.Uint64())
balBefore := db.GetBalance(winner)
paid, err := WithdrawRewards(db, lg, winner)
require.NoError(t, err)
require.Equal(t, credit.Uint64(), paid.Uint64())
require.Equal(t, new(uint256.Int).Add(balBefore, credit).Uint64(), db.GetBalance(winner).Uint64())
// Second withdraw: nothing owed.
_, err = WithdrawRewards(db, lg, winner)
require.ErrorIs(t, err, ErrNoCredit, "reward cannot be withdrawn twice")
// Stake double-withdraw.
require.NoError(t, DeregisterOperator(db, winner, 1000))
_, err = WithdrawStake(db, lg, winner, 1000+UnbondCooldownBlocks)
require.NoError(t, err)
_, err = WithdrawStake(db, lg, winner, 1000+UnbondCooldownBlocks)
require.ErrorIs(t, err, ErrOperatorUnknown, "stake cannot be withdrawn twice")
}