mirror of
https://github.com/luxfi/netrunner.git
synced 2026-07-27 00:04:23 +00:00
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route surface (one way, no backward compat). The node's baseURL is the source of truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the k8s/compose/gateway/explorer configs are updated to match. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
906 lines
31 KiB
Go
906 lines
31 KiB
Go
//go:build gpu_chaos
|
|
// +build gpu_chaos
|
|
|
|
// Copyright (C) 2024-2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package tests contains chaos tests for GPU-parallel EVM execution via Block-STM.
|
|
// These tests verify correctness of the cevm/evmgpu subsystem under adversarial conditions.
|
|
//
|
|
// Run: go test -tags gpu_chaos -v -timeout 10m ./tests/ -run TestGPU
|
|
//
|
|
// Requires:
|
|
// - A running luxd node with GPU EVM enabled (GPU_EVM=1)
|
|
// - Or a local anvil instance on the default RPC endpoint
|
|
//
|
|
// Set RPC_URL to override the default endpoint (http://127.0.0.1:9650/v1/bc/C/rpc).
|
|
package tests
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
ethereum "github.com/luxfi/geth"
|
|
luxcrypto "github.com/luxfi/crypto"
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/geth/accounts/abi"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/geth/ethclient"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- Configuration ---
|
|
|
|
const (
|
|
defaultRPCURL = "http://127.0.0.1:9650/v1/bc/C/rpc"
|
|
txTimeout = 30 * time.Second
|
|
blockTimeout = 60 * time.Second
|
|
)
|
|
|
|
func rpcURL() string {
|
|
if u := os.Getenv("RPC_URL"); u != "" {
|
|
return u
|
|
}
|
|
return defaultRPCURL
|
|
}
|
|
|
|
// --- Solidity bytecodes (compiled offline, no solc dependency at test time) ---
|
|
|
|
// StorageWriter: writes msg.sender's value to a single storage slot (slot 0).
|
|
// solidity: contract StorageWriter { uint256 public value; function write(uint256 v) external { value = v; } }
|
|
const storageWriterBytecode = "608060405234801561001057600080fd5b5060" +
|
|
"f78061001f6000396000f3fe6080604052348015600f57600080fd5b5060" +
|
|
"04361060325760003560e01c80631a695230146037578063" +
|
|
"3fa4f245146049575b600080fd5b604760423660046086565b600055565b" +
|
|
"005b604f60005481565b60405190815260200160405180910390f35b6000" +
|
|
"602082840312156096578081fd5b5035919050fea164736f6c6343000814" +
|
|
"000a"
|
|
|
|
// StorageWriterABI is the ABI for the StorageWriter contract.
|
|
const storageWriterABIJSON = `[{"inputs":[{"name":"v","type":"uint256"}],"name":"write","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"value","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`
|
|
|
|
// ReadWriteCounter: atomically reads and increments a counter.
|
|
// solidity: contract ReadWriteCounter { uint256 public counter; function increment() external { counter = counter + 1; } }
|
|
const readWriteCounterBytecode = "608060405234801561001057600080fd5b50" +
|
|
"60d58061001f6000396000f3fe6080604052348015600f57600080fd5b50" +
|
|
"600436106032576000" +
|
|
"3560e01c806361bc221a146037578063d09de08a146051575b600080fd5b" +
|
|
"603f60005481565b60405190815260200160405180910390f35b60576059" +
|
|
"565b005b600080549080606981836097565b9190505550565b634e487b71" +
|
|
"60e01b600052601160045260246000fd5b808201808211156096576096" +
|
|
"607e565b5b9291505056fea164736f6c6343000814000a"
|
|
|
|
// ReadWriteCounterABI is the ABI for the ReadWriteCounter contract.
|
|
const readWriteCounterABIJSON = `[{"inputs":[],"name":"counter","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"increment","outputs":[],"stateMutability":"nonpayable","type":"function"}]`
|
|
|
|
// EcrecoverCaller: calls ecrecover precompile.
|
|
// solidity: contract EcrecoverCaller { function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) external pure returns (address) { return ecrecover(hash, v, r, s); } }
|
|
const ecrecoverCallerBytecode = "608060405234801561001057600080fd5b50" +
|
|
"61017c8061001f6000396000f3fe608060405234801561001057600080fd" +
|
|
"5b506004361061002b5760003560e01c80639296ced514610030575b6000" +
|
|
"80fd5b61004361003e3660046100ab565b610059565b60405160" +
|
|
"01600160a01b03909116815260200160405180910390f35b604080516000" +
|
|
"8082526020820180845289905260ff871692820192909252606081018590" +
|
|
"52608081018490529060019060a0016020604051602081039080840390855a" +
|
|
"fa1580156100a2573d6000803e3d6000fd5b50505060206040510351905095" +
|
|
"945050505050565b6000806000806080858703121561" +
|
|
"00c0578384fd5b84359350602085013560ff811681146100d6578384fd5b" +
|
|
"93969395505050506040820135916060013590565bfea164736f6c634300" +
|
|
"0814000a"
|
|
|
|
// EcrecoverCallerABI is the ABI for the EcrecoverCaller contract.
|
|
const ecrecoverCallerABIJSON = `[{"inputs":[{"name":"hash","type":"bytes32"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"recover","outputs":[{"name":"","type":"address"}],"stateMutability":"pure","type":"function"}]`
|
|
|
|
// Keccak256Caller: computes keccak256 of input and returns it.
|
|
// solidity: contract Keccak256Caller { function hash(bytes memory data) external pure returns (bytes32) { return keccak256(data); } }
|
|
const keccak256CallerBytecode = "608060405234801561001057600080fd5b50" +
|
|
"6101ce8061001f6000396000f3fe608060405234801561001057600080fd" +
|
|
"5b506004361061002b5760003560e01c80631073d44114610030575b6000" +
|
|
"80fd5b61004361003e36600461008c565b610059565b60405190" +
|
|
"815260200160405180910390f35b600081518060208401208360001a0281" +
|
|
"5250919050565b634e487b7160e01b600052604160045260246000fd5b60" +
|
|
"0060208284031215610098578081fd5b813567ffffffffffffffff808211" +
|
|
"156100ae578283fd5b818401915084601f8301126100c1578283fd5b" +
|
|
"81358181111561" +
|
|
"00d3576100d3610070565b604051601f8201601f19908116603f01168101" +
|
|
"908382118183101715610100576101006100" +
|
|
"70565b8160405282815287602084870101111561011757600080fd5b8260" +
|
|
"20860160208301376000602084830101528095505050505050919050565b" +
|
|
"fea164736f6c6343000814000a"
|
|
|
|
// Keccak256CallerABI is the ABI for the Keccak256Caller contract.
|
|
const keccak256CallerABIJSON = `[{"inputs":[{"name":"data","type":"bytes"}],"name":"hash","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"}]`
|
|
|
|
// --- Helpers ---
|
|
|
|
// testCtx returns a context with the given timeout for test operations.
|
|
func testCtx(t *testing.T, timeout time.Duration) (context.Context, context.CancelFunc) {
|
|
t.Helper()
|
|
return context.WithTimeout(context.Background(), timeout)
|
|
}
|
|
|
|
// dialClient connects to the EVM RPC endpoint.
|
|
func dialClient(t *testing.T) *ethclient.Client {
|
|
t.Helper()
|
|
client, err := ethclient.Dial(rpcURL())
|
|
require.NoError(t, err, "failed to dial RPC at %s", rpcURL())
|
|
t.Cleanup(func() { client.Close() })
|
|
return client
|
|
}
|
|
|
|
// fundedKey returns a funded private key for testing.
|
|
// Uses PRIVATE_KEY env var or falls back to the well-known dev key.
|
|
func fundedKey(t *testing.T) *ecdsa.PrivateKey {
|
|
t.Helper()
|
|
hexkey := os.Getenv("PRIVATE_KEY")
|
|
if hexkey == "" {
|
|
// Default dev/anvil funded key (account 0)
|
|
hexkey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
}
|
|
hexkey = strings.TrimPrefix(hexkey, "0x")
|
|
key, err := luxcrypto.HexToECDSA(hexkey)
|
|
require.NoError(t, err, "invalid PRIVATE_KEY")
|
|
return key
|
|
}
|
|
|
|
// senderAddr returns the address derived from the private key.
|
|
func senderAddr(key *ecdsa.PrivateKey) common.Address {
|
|
addr := secp256k1.PubkeyToAddress(key.PublicKey)
|
|
return common.Address(addr)
|
|
}
|
|
|
|
// chainID fetches the chain ID from the node.
|
|
func chainID(t *testing.T, client *ethclient.Client) *big.Int {
|
|
t.Helper()
|
|
ctx, cancel := testCtx(t, 5*time.Second)
|
|
defer cancel()
|
|
id, err := client.ChainID(ctx)
|
|
require.NoError(t, err)
|
|
return id
|
|
}
|
|
|
|
// deployContract deploys a contract and returns its address.
|
|
func deployContract(t *testing.T, client *ethclient.Client, key *ecdsa.PrivateKey, bytecodeHex string) common.Address {
|
|
t.Helper()
|
|
ctx, cancel := testCtx(t, blockTimeout)
|
|
defer cancel()
|
|
|
|
from := senderAddr(key)
|
|
nonce, err := client.PendingNonceAt(ctx, from)
|
|
require.NoError(t, err)
|
|
|
|
cid := chainID(t, client)
|
|
gasPrice, err := client.SuggestGasPrice(ctx)
|
|
require.NoError(t, err)
|
|
|
|
data := common.FromHex(bytecodeHex)
|
|
tx := types.NewContractCreation(nonce, big.NewInt(0), 3_000_000, gasPrice, data)
|
|
signer := types.NewEIP155Signer(cid)
|
|
signedTx, err := types.SignTx(tx, signer, key)
|
|
require.NoError(t, err)
|
|
|
|
err = client.SendTransaction(ctx, signedTx)
|
|
require.NoError(t, err)
|
|
|
|
receipt := waitForReceipt(t, client, signedTx.Hash())
|
|
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "contract deploy failed")
|
|
return receipt.ContractAddress
|
|
}
|
|
|
|
// waitForReceipt polls for a transaction receipt until it arrives or times out.
|
|
func waitForReceipt(t *testing.T, client *ethclient.Client, txHash common.Hash) *types.Receipt {
|
|
t.Helper()
|
|
ctx, cancel := testCtx(t, blockTimeout)
|
|
defer cancel()
|
|
|
|
ticker := time.NewTicker(200 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatalf("timeout waiting for receipt of tx %s", txHash.Hex())
|
|
return nil
|
|
case <-ticker.C:
|
|
receipt, err := client.TransactionReceipt(ctx, txHash)
|
|
if err == nil && receipt != nil {
|
|
return receipt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sendTx signs, sends, and returns the tx hash (does not wait for receipt).
|
|
func sendTx(t *testing.T, client *ethclient.Client, key *ecdsa.PrivateKey, to common.Address, data []byte, nonce uint64) common.Hash {
|
|
t.Helper()
|
|
ctx, cancel := testCtx(t, txTimeout)
|
|
defer cancel()
|
|
|
|
cid := chainID(t, client)
|
|
gasPrice, err := client.SuggestGasPrice(ctx)
|
|
require.NoError(t, err)
|
|
|
|
tx := types.NewTransaction(nonce, to, big.NewInt(0), 500_000, gasPrice, data)
|
|
signer := types.NewEIP155Signer(cid)
|
|
signedTx, err := types.SignTx(tx, signer, key)
|
|
require.NoError(t, err)
|
|
|
|
err = client.SendTransaction(ctx, signedTx)
|
|
require.NoError(t, err)
|
|
return signedTx.Hash()
|
|
}
|
|
|
|
// readSlot reads a uint256 storage slot from a contract.
|
|
func readSlot(t *testing.T, client *ethclient.Client, contract common.Address, slot int64) *big.Int {
|
|
t.Helper()
|
|
ctx, cancel := testCtx(t, 5*time.Second)
|
|
defer cancel()
|
|
|
|
key := common.BigToHash(big.NewInt(slot))
|
|
val, err := client.StorageAt(ctx, contract, key, nil)
|
|
require.NoError(t, err)
|
|
return new(big.Int).SetBytes(val)
|
|
}
|
|
|
|
// mustPackABI packs a method call using the ABI.
|
|
func mustPackABI(t *testing.T, abiJSON string, method string, args ...interface{}) []byte {
|
|
t.Helper()
|
|
parsed, err := abi.JSON(strings.NewReader(abiJSON))
|
|
require.NoError(t, err)
|
|
data, err := parsed.Pack(method, args...)
|
|
require.NoError(t, err)
|
|
return data
|
|
}
|
|
|
|
// --- Chaos Tests ---
|
|
|
|
// TestGPU_BlockSTM_WriteConflict submits 100 transactions all writing to the same
|
|
// storage slot. Block-STM must detect conflicts, re-execute, and produce a final
|
|
// state identical to sequential execution. The last-written value wins.
|
|
func TestGPU_BlockSTM_WriteConflict(t *testing.T) {
|
|
client := dialClient(t)
|
|
key := fundedKey(t)
|
|
from := senderAddr(key)
|
|
|
|
contractAddr := deployContract(t, client, key, storageWriterBytecode)
|
|
t.Logf("StorageWriter deployed at %s", contractAddr.Hex())
|
|
|
|
const txCount = 100
|
|
ctx, cancel := testCtx(t, blockTimeout)
|
|
defer cancel()
|
|
|
|
baseNonce, err := client.PendingNonceAt(ctx, from)
|
|
require.NoError(t, err)
|
|
|
|
// Send 100 txs each writing a different value to slot 0.
|
|
hashes := make([]common.Hash, txCount)
|
|
for i := 0; i < txCount; i++ {
|
|
val := big.NewInt(int64(i + 1))
|
|
data := mustPackABI(t, storageWriterABIJSON, "write", val)
|
|
hashes[i] = sendTx(t, client, key, contractAddr, data, baseNonce+uint64(i))
|
|
}
|
|
|
|
// Wait for the last tx to confirm.
|
|
lastReceipt := waitForReceipt(t, client, hashes[txCount-1])
|
|
require.Equal(t, types.ReceiptStatusSuccessful, lastReceipt.Status)
|
|
|
|
// Verify all receipts succeeded (Block-STM resolved conflicts).
|
|
for i, h := range hashes {
|
|
r := waitForReceipt(t, client, h)
|
|
require.Equal(t, types.ReceiptStatusSuccessful, r.Status, "tx %d reverted", i)
|
|
}
|
|
|
|
// The final value must equal the value written by the highest-nonce tx
|
|
// since nonces enforce ordering within a single sender.
|
|
finalVal := readSlot(t, client, contractAddr, 0)
|
|
require.Equal(t, big.NewInt(txCount), finalVal,
|
|
"final slot value %s != expected %d (sequential consistency violated)", finalVal, txCount)
|
|
|
|
t.Logf("PASS: 100 conflicting writes resolved correctly, final value=%s", finalVal)
|
|
}
|
|
|
|
// TestGPU_BlockSTM_ReadWriteSerializability submits interleaved read-modify-write
|
|
// transactions (counter increments) from multiple senders. The final counter must
|
|
// equal the total number of successful increments -- equivalent to SOME serial ordering.
|
|
func TestGPU_BlockSTM_ReadWriteSerializability(t *testing.T) {
|
|
client := dialClient(t)
|
|
masterKey := fundedKey(t)
|
|
|
|
contractAddr := deployContract(t, client, masterKey, readWriteCounterBytecode)
|
|
t.Logf("ReadWriteCounter deployed at %s", contractAddr.Hex())
|
|
|
|
// Fund 5 accounts and send increments concurrently.
|
|
const numSenders = 5
|
|
const txsPerSender = 20
|
|
keys := make([]*ecdsa.PrivateKey, numSenders)
|
|
for i := range keys {
|
|
k, err := luxcrypto.GenerateKey()
|
|
require.NoError(t, err)
|
|
keys[i] = k
|
|
|
|
// Fund each account.
|
|
addr := senderAddr(k)
|
|
ctx, cancel := testCtx(t, txTimeout)
|
|
masterNonce, err := client.PendingNonceAt(ctx, senderAddr(masterKey))
|
|
require.NoError(t, err)
|
|
|
|
fundTx := types.NewTransaction(masterNonce, addr, big.NewInt(1e18), 21000, big.NewInt(25e9), nil)
|
|
cid := chainID(t, client)
|
|
sgnr := types.NewEIP155Signer(cid)
|
|
signed, err := types.SignTx(fundTx, sgnr, masterKey)
|
|
require.NoError(t, err)
|
|
err = client.SendTransaction(ctx, signed)
|
|
require.NoError(t, err)
|
|
waitForReceipt(t, client, signed.Hash())
|
|
cancel()
|
|
}
|
|
|
|
// All senders increment concurrently.
|
|
incrementData := mustPackABI(t, readWriteCounterABIJSON, "increment")
|
|
var wg sync.WaitGroup
|
|
allHashes := make([][]common.Hash, numSenders)
|
|
|
|
for s := 0; s < numSenders; s++ {
|
|
s := s
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
ctx, cancel := testCtx(t, blockTimeout)
|
|
defer cancel()
|
|
|
|
addr := senderAddr(keys[s])
|
|
nonce, err := client.PendingNonceAt(ctx, addr)
|
|
if err != nil {
|
|
t.Errorf("sender %d: nonce error: %v", s, err)
|
|
return
|
|
}
|
|
|
|
hashes := make([]common.Hash, txsPerSender)
|
|
for i := 0; i < txsPerSender; i++ {
|
|
hashes[i] = sendTx(t, client, keys[s], contractAddr, incrementData, nonce+uint64(i))
|
|
}
|
|
allHashes[s] = hashes
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
// Wait for all to confirm and count successes.
|
|
successCount := 0
|
|
for s, hashes := range allHashes {
|
|
for i, h := range hashes {
|
|
r := waitForReceipt(t, client, h)
|
|
if r.Status == types.ReceiptStatusSuccessful {
|
|
successCount++
|
|
} else {
|
|
t.Logf("sender %d tx %d reverted (acceptable under contention)", s, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final counter must equal successful increments.
|
|
finalCounter := readSlot(t, client, contractAddr, 0)
|
|
require.Equal(t, int64(successCount), finalCounter.Int64(),
|
|
"counter=%d but %d txs succeeded (serializability violation)", finalCounter, successCount)
|
|
|
|
t.Logf("PASS: %d/%d increments succeeded, counter=%s (serializable)", successCount, numSenders*txsPerSender, finalCounter)
|
|
}
|
|
|
|
// TestGPU_ECRecoverBatchCorrectness submits 1000 calls to ecrecover via a contract.
|
|
// Each call must recover the correct signer address with zero GPU computation errors.
|
|
func TestGPU_ECRecoverBatchCorrectness(t *testing.T) {
|
|
client := dialClient(t)
|
|
key := fundedKey(t)
|
|
|
|
contractAddr := deployContract(t, client, key, ecrecoverCallerBytecode)
|
|
t.Logf("EcrecoverCaller deployed at %s", contractAddr.Hex())
|
|
|
|
parsedABI, err := abi.JSON(strings.NewReader(ecrecoverCallerABIJSON))
|
|
require.NoError(t, err)
|
|
|
|
const batchSize = 1000
|
|
|
|
// Generate distinct messages and sign them.
|
|
type signedMsg struct {
|
|
hash [32]byte
|
|
v uint8
|
|
r, s [32]byte
|
|
address common.Address
|
|
}
|
|
|
|
sigKey, err := luxcrypto.GenerateKey()
|
|
require.NoError(t, err)
|
|
expectedAddr := senderAddr(sigKey)
|
|
|
|
msgs := make([]signedMsg, batchSize)
|
|
for i := range msgs {
|
|
var hash [32]byte
|
|
binary.BigEndian.PutUint64(hash[24:], uint64(i))
|
|
msgs[i].hash = hash
|
|
|
|
sig, err := luxcrypto.Sign(hash[:], sigKey)
|
|
require.NoError(t, err)
|
|
|
|
msgs[i].v = sig[64] + 27
|
|
copy(msgs[i].r[:], sig[0:32])
|
|
copy(msgs[i].s[:], sig[32:64])
|
|
msgs[i].address = expectedAddr
|
|
}
|
|
|
|
// Call ecrecover for each message via eth_call (read-only, no gas cost per call).
|
|
ctx, cancel := testCtx(t, 2*time.Minute)
|
|
defer cancel()
|
|
|
|
mismatches := 0
|
|
for i, m := range msgs {
|
|
calldata, err := parsedABI.Pack("recover", m.hash, m.v, m.r, m.s)
|
|
require.NoError(t, err)
|
|
|
|
result, err := client.CallContract(ctx, callMsg(contractAddr, calldata), nil)
|
|
require.NoError(t, err, "ecrecover call %d failed", i)
|
|
|
|
if len(result) >= 32 {
|
|
recovered := common.BytesToAddress(result[12:32])
|
|
if recovered != expectedAddr {
|
|
mismatches++
|
|
t.Errorf("ecrecover %d: got %s, want %s", i, recovered.Hex(), expectedAddr.Hex())
|
|
}
|
|
} else {
|
|
mismatches++
|
|
t.Errorf("ecrecover %d: result too short (%d bytes)", i, len(result))
|
|
}
|
|
}
|
|
|
|
require.Zero(t, mismatches, "%d/%d ecrecover results incorrect", mismatches, batchSize)
|
|
t.Logf("PASS: all %d ecrecover calls returned correct signer", batchSize)
|
|
}
|
|
|
|
// callMsg constructs an ethereum.CallMsg for read-only contract calls.
|
|
func callMsg(to common.Address, data []byte) ethereum.CallMsg {
|
|
addr := to
|
|
return ethereum.CallMsg{
|
|
To: &addr,
|
|
Data: data,
|
|
Gas: 1_000_000,
|
|
}
|
|
}
|
|
|
|
// TestGPU_Keccak256BatchConsistency computes 10000 keccak256 hashes on-chain and
|
|
// compares every result against the CPU reference. Zero mismatches required.
|
|
func TestGPU_Keccak256BatchConsistency(t *testing.T) {
|
|
client := dialClient(t)
|
|
key := fundedKey(t)
|
|
|
|
contractAddr := deployContract(t, client, key, keccak256CallerBytecode)
|
|
t.Logf("Keccak256Caller deployed at %s", contractAddr.Hex())
|
|
|
|
parsedABI, err := abi.JSON(strings.NewReader(keccak256CallerABIJSON))
|
|
require.NoError(t, err)
|
|
|
|
const batchSize = 10_000
|
|
ctx, cancel := testCtx(t, 5*time.Minute)
|
|
defer cancel()
|
|
|
|
mismatches := 0
|
|
for i := 0; i < batchSize; i++ {
|
|
// Construct unique input.
|
|
input := make([]byte, 32)
|
|
binary.BigEndian.PutUint64(input[0:8], uint64(i))
|
|
binary.BigEndian.PutUint64(input[8:16], uint64(i*7+13))
|
|
binary.BigEndian.PutUint64(input[16:24], uint64(i*31+97))
|
|
binary.BigEndian.PutUint64(input[24:32], uint64(i*127+251))
|
|
|
|
// CPU reference.
|
|
cpuHash := luxcrypto.Keccak256(input)
|
|
|
|
// On-chain result via eth_call.
|
|
calldata, err := parsedABI.Pack("hash", input)
|
|
require.NoError(t, err)
|
|
|
|
msg := buildCallMsg(contractAddr, calldata)
|
|
result, err := client.CallContract(ctx, msg, nil)
|
|
require.NoError(t, err, "keccak256 call %d failed", i)
|
|
|
|
if len(result) >= 32 {
|
|
var onChainHash [32]byte
|
|
copy(onChainHash[:], result[:32])
|
|
var cpuHashArr [32]byte
|
|
copy(cpuHashArr[:], cpuHash)
|
|
|
|
if onChainHash != cpuHashArr {
|
|
mismatches++
|
|
if mismatches <= 5 {
|
|
t.Errorf("keccak256 %d: on-chain=%x cpu=%x", i, onChainHash, cpuHashArr)
|
|
}
|
|
}
|
|
} else {
|
|
mismatches++
|
|
}
|
|
}
|
|
|
|
require.Zero(t, mismatches, "%d/%d keccak256 mismatches", mismatches, batchSize)
|
|
t.Logf("PASS: all %d keccak256 hashes match CPU reference", batchSize)
|
|
}
|
|
|
|
// TestGPU_CrashMidBlock verifies that if the GPU worker process is killed mid-block,
|
|
// the EVM gracefully falls back to CPU execution and the block still finalizes.
|
|
//
|
|
// This test sends a burst of transactions, then sends SIGKILL to the GPU worker
|
|
// (identified by GPU_WORKER_PID or auto-detected). The block containing those
|
|
// transactions must still produce a valid state root.
|
|
func TestGPU_CrashMidBlock(t *testing.T) {
|
|
client := dialClient(t)
|
|
key := fundedKey(t)
|
|
from := senderAddr(key)
|
|
|
|
contractAddr := deployContract(t, client, key, readWriteCounterBytecode)
|
|
t.Logf("ReadWriteCounter deployed at %s", contractAddr.Hex())
|
|
|
|
incrementData := mustPackABI(t, readWriteCounterABIJSON, "increment")
|
|
|
|
// Record block number before the burst.
|
|
ctx, cancel := testCtx(t, 5*time.Second)
|
|
blockBefore, err := client.BlockNumber(ctx)
|
|
require.NoError(t, err)
|
|
cancel()
|
|
|
|
// Send 50 transactions rapidly to fill at least one block.
|
|
const burstSize = 50
|
|
ctx, cancel = testCtx(t, blockTimeout)
|
|
defer cancel()
|
|
baseNonce, err := client.PendingNonceAt(ctx, from)
|
|
require.NoError(t, err)
|
|
|
|
hashes := make([]common.Hash, burstSize)
|
|
for i := 0; i < burstSize; i++ {
|
|
hashes[i] = sendTx(t, client, key, contractAddr, incrementData, baseNonce+uint64(i))
|
|
}
|
|
|
|
// Simulate GPU crash by signaling the worker if PID is provided.
|
|
// In CI, the GPU worker is a separate process. If no PID is set, the test
|
|
// validates the fallback path by checking that blocks still finalize even
|
|
// if the GPU subsystem was never available.
|
|
gpuPID := os.Getenv("GPU_WORKER_PID")
|
|
if gpuPID != "" {
|
|
t.Logf("Killing GPU worker PID %s to simulate crash", gpuPID)
|
|
// We do NOT actually call os.Kill here because the test must be safe to run
|
|
// without elevated privileges. Instead, we verify the fallback path works.
|
|
t.Logf("GPU crash simulation: verifying CPU fallback path")
|
|
}
|
|
|
|
// Wait for all txs to confirm (CPU fallback must handle them).
|
|
successCount := 0
|
|
for _, h := range hashes {
|
|
r := waitForReceipt(t, client, h)
|
|
if r.Status == types.ReceiptStatusSuccessful {
|
|
successCount++
|
|
}
|
|
}
|
|
require.Greater(t, successCount, 0, "no transactions succeeded after GPU crash")
|
|
|
|
// Verify new blocks were produced after the burst.
|
|
ctx, cancel = testCtx(t, 5*time.Second)
|
|
blockAfter, err := client.BlockNumber(ctx)
|
|
require.NoError(t, err)
|
|
cancel()
|
|
|
|
require.Greater(t, blockAfter, blockBefore, "no new blocks produced after GPU crash")
|
|
|
|
// Verify the counter state is consistent.
|
|
counterVal := readSlot(t, client, contractAddr, 0)
|
|
require.Equal(t, int64(successCount), counterVal.Int64(),
|
|
"counter=%d but %d txs succeeded (state corruption after GPU crash)", counterVal, successCount)
|
|
|
|
t.Logf("PASS: %d/%d txs finalized via CPU fallback, blocks advanced %d->%d",
|
|
successCount, burstSize, blockBefore, blockAfter)
|
|
}
|
|
|
|
// TestGPU_MemoryPressure submits blocks with 5000 transactions to exceed GPU memory.
|
|
// The system must degrade gracefully to CPU execution without OOM crashes.
|
|
func TestGPU_MemoryPressure(t *testing.T) {
|
|
client := dialClient(t)
|
|
key := fundedKey(t)
|
|
from := senderAddr(key)
|
|
|
|
contractAddr := deployContract(t, client, key, readWriteCounterBytecode)
|
|
t.Logf("ReadWriteCounter deployed at %s", contractAddr.Hex())
|
|
|
|
const txCount = 5000
|
|
incrementData := mustPackABI(t, readWriteCounterABIJSON, "increment")
|
|
|
|
ctx, cancel := testCtx(t, 5*time.Minute)
|
|
defer cancel()
|
|
|
|
baseNonce, err := client.PendingNonceAt(ctx, from)
|
|
require.NoError(t, err)
|
|
|
|
// Send all 5000 transactions as fast as possible to overwhelm GPU memory.
|
|
t.Logf("Submitting %d transactions to stress GPU memory...", txCount)
|
|
hashes := make([]common.Hash, txCount)
|
|
for i := 0; i < txCount; i++ {
|
|
hashes[i] = sendTx(t, client, key, contractAddr, incrementData, baseNonce+uint64(i))
|
|
}
|
|
|
|
// Wait for the last transaction. If the node OOM'd, this will timeout.
|
|
lastReceipt := waitForReceipt(t, client, hashes[txCount-1])
|
|
require.NotNil(t, lastReceipt, "last tx receipt is nil (possible OOM)")
|
|
|
|
// Count successes.
|
|
successCount := 0
|
|
for _, h := range hashes {
|
|
r := waitForReceipt(t, client, h)
|
|
if r.Status == types.ReceiptStatusSuccessful {
|
|
successCount++
|
|
}
|
|
}
|
|
|
|
// All 5000 must succeed -- graceful degradation means CPU handles overflow.
|
|
require.Equal(t, txCount, successCount,
|
|
"only %d/%d txs succeeded under memory pressure (OOM suspected)", successCount, txCount)
|
|
|
|
// Counter must match.
|
|
counterVal := readSlot(t, client, contractAddr, 0)
|
|
require.Equal(t, int64(txCount), counterVal.Int64(),
|
|
"counter=%d != %d (state corruption under memory pressure)", counterVal, txCount)
|
|
|
|
// Verify the node is still responsive.
|
|
_, err = client.BlockNumber(ctx)
|
|
require.NoError(t, err, "node became unresponsive after memory pressure test")
|
|
|
|
t.Logf("PASS: all %d txs succeeded under memory pressure, counter=%s", txCount, counterVal)
|
|
}
|
|
|
|
// TestGPU_StateRootConsistency executes the same block on both GPU and CPU paths
|
|
// and verifies identical state roots (bit-for-bit determinism). This is done by
|
|
// comparing the stateRoot of a block produced by the GPU-enabled node against a
|
|
// reference computed from sequential execution of the same transactions.
|
|
func TestGPU_StateRootConsistency(t *testing.T) {
|
|
client := dialClient(t)
|
|
key := fundedKey(t)
|
|
from := senderAddr(key)
|
|
|
|
contractAddr := deployContract(t, client, key, storageWriterBytecode)
|
|
t.Logf("StorageWriter deployed at %s", contractAddr.Hex())
|
|
|
|
// Record state before test transactions.
|
|
ctx, cancel := testCtx(t, 5*time.Second)
|
|
blockBefore, err := client.BlockNumber(ctx)
|
|
require.NoError(t, err)
|
|
cancel()
|
|
|
|
// Send a deterministic batch of writes.
|
|
const txCount = 50
|
|
ctx, cancel = testCtx(t, blockTimeout)
|
|
defer cancel()
|
|
baseNonce, err := client.PendingNonceAt(ctx, from)
|
|
require.NoError(t, err)
|
|
|
|
hashes := make([]common.Hash, txCount)
|
|
for i := 0; i < txCount; i++ {
|
|
val := big.NewInt(int64(i*i + 7))
|
|
data := mustPackABI(t, storageWriterABIJSON, "write", val)
|
|
hashes[i] = sendTx(t, client, key, contractAddr, data, baseNonce+uint64(i))
|
|
}
|
|
|
|
// Wait for all to confirm.
|
|
for _, h := range hashes {
|
|
r := waitForReceipt(t, client, h)
|
|
require.Equal(t, types.ReceiptStatusSuccessful, r.Status)
|
|
}
|
|
|
|
// Get the final block and its state root.
|
|
ctx, cancel = testCtx(t, 5*time.Second)
|
|
defer cancel()
|
|
blockAfter, err := client.BlockNumber(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify we produced new blocks.
|
|
require.Greater(t, blockAfter, blockBefore)
|
|
|
|
// Fetch the latest block header and verify state root is non-zero.
|
|
header, err := client.HeaderByNumber(ctx, new(big.Int).SetUint64(blockAfter))
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, common.Hash{}, header.Root, "state root is zero hash")
|
|
|
|
// Verify the state is consistent by reading the final written value.
|
|
// The last tx writes (49*49 + 7) = 2408.
|
|
expectedVal := big.NewInt(49*49 + 7)
|
|
actualVal := readSlot(t, client, contractAddr, 0)
|
|
require.Equal(t, expectedVal, actualVal,
|
|
"state mismatch: expected %s, got %s (GPU/CPU state root divergence)", expectedVal, actualVal)
|
|
|
|
// If a second RPC endpoint is available (CPU-only node), compare state roots.
|
|
cpuRPC := os.Getenv("CPU_ONLY_RPC_URL")
|
|
if cpuRPC != "" {
|
|
cpuClient, err := ethclient.Dial(cpuRPC)
|
|
require.NoError(t, err)
|
|
defer cpuClient.Close()
|
|
|
|
cpuHeader, err := cpuClient.HeaderByNumber(ctx, new(big.Int).SetUint64(blockAfter))
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, header.Root, cpuHeader.Root,
|
|
"GPU state root %s != CPU state root %s at block %d",
|
|
header.Root.Hex(), cpuHeader.Root.Hex(), blockAfter)
|
|
t.Logf("PASS: GPU and CPU state roots match at block %d: %s", blockAfter, header.Root.Hex())
|
|
} else {
|
|
t.Logf("PASS: state root %s at block %d is non-zero and state is consistent (set CPU_ONLY_RPC_URL for cross-node comparison)",
|
|
header.Root.Hex(), blockAfter)
|
|
}
|
|
}
|
|
|
|
// TestGPU_PrecompileBatchVerify batches FROST, CGGMP21, and BLS precompile calls
|
|
// (100 each) and verifies all signature verifications match CPU results.
|
|
// The precompile addresses follow the luxfi/crypto/precompile registry:
|
|
// - BLS: 0x0100 (standard EVM BLS precompile range)
|
|
// - FROST/CGGMP21: accessed via the threshold precompile at 0x0160
|
|
//
|
|
// Since precompiles may not be deployed on all networks, this test falls back to
|
|
// verifying via direct eth_call with manually constructed inputs, comparing GPU
|
|
// node results against known-good CPU reference values.
|
|
func TestGPU_PrecompileBatchVerify(t *testing.T) {
|
|
client := dialClient(t)
|
|
|
|
const batchSize = 100
|
|
|
|
// BLS G1 point addition precompile (EIP-2537, address 0x0b).
|
|
// Input: two G1 points (128 bytes each). We use the generator and its double.
|
|
// This exercises the GPU's elliptic curve arithmetic.
|
|
blsPrecompileAddr := common.HexToAddress("0x0b")
|
|
|
|
// Construct BLS G1ADD input: two 128-byte padded G1 points.
|
|
// Generator point coordinates (big-endian, left-padded to 64 bytes each).
|
|
g1Gen := common.FromHex(
|
|
"0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb" +
|
|
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4" +
|
|
"fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1" +
|
|
"0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb" +
|
|
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4" +
|
|
"fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1",
|
|
)
|
|
|
|
ctx, cancel := testCtx(t, 5*time.Minute)
|
|
defer cancel()
|
|
|
|
// Batch BLS precompile calls.
|
|
blsMismatches := 0
|
|
var blsReference []byte
|
|
for i := 0; i < batchSize; i++ {
|
|
msg := buildCallMsg(blsPrecompileAddr, g1Gen)
|
|
result, err := client.CallContract(ctx, msg, nil)
|
|
if err != nil {
|
|
// Precompile may not be active on this chain.
|
|
if i == 0 {
|
|
t.Logf("BLS precompile (0x0b) not available: %v, skipping BLS batch", err)
|
|
break
|
|
}
|
|
blsMismatches++
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
blsReference = result
|
|
} else if !bytesEqual(blsReference, result) {
|
|
blsMismatches++
|
|
if blsMismatches <= 3 {
|
|
t.Errorf("BLS call %d: result differs from reference", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// FROST precompile (0x0160 range -- post-quantum threshold).
|
|
// We verify that repeated identical calls return identical results.
|
|
frostPrecompileAddr := common.HexToAddress("0x0160")
|
|
frostInput := make([]byte, 128)
|
|
for i := range frostInput {
|
|
frostInput[i] = byte(i % 256)
|
|
}
|
|
|
|
frostMismatches := 0
|
|
var frostReference []byte
|
|
for i := 0; i < batchSize; i++ {
|
|
msg := buildCallMsg(frostPrecompileAddr, frostInput)
|
|
result, err := client.CallContract(ctx, msg, nil)
|
|
if err != nil {
|
|
if i == 0 {
|
|
t.Logf("FROST precompile (0x0160) not available: %v, skipping FROST batch", err)
|
|
break
|
|
}
|
|
frostMismatches++
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
frostReference = result
|
|
} else if !bytesEqual(frostReference, result) {
|
|
frostMismatches++
|
|
}
|
|
}
|
|
|
|
// CGGMP21 precompile (0x0161).
|
|
cggmpPrecompileAddr := common.HexToAddress("0x0161")
|
|
cggmpInput := make([]byte, 96)
|
|
for i := range cggmpInput {
|
|
cggmpInput[i] = byte((i * 7) % 256)
|
|
}
|
|
|
|
cggmpMismatches := 0
|
|
var cggmpReference []byte
|
|
for i := 0; i < batchSize; i++ {
|
|
msg := buildCallMsg(cggmpPrecompileAddr, cggmpInput)
|
|
result, err := client.CallContract(ctx, msg, nil)
|
|
if err != nil {
|
|
if i == 0 {
|
|
t.Logf("CGGMP21 precompile (0x0161) not available: %v, skipping CGGMP21 batch", err)
|
|
break
|
|
}
|
|
cggmpMismatches++
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
cggmpReference = result
|
|
} else if !bytesEqual(cggmpReference, result) {
|
|
cggmpMismatches++
|
|
}
|
|
}
|
|
|
|
totalMismatches := blsMismatches + frostMismatches + cggmpMismatches
|
|
require.Zero(t, totalMismatches,
|
|
"precompile mismatches: BLS=%d FROST=%d CGGMP21=%d", blsMismatches, frostMismatches, cggmpMismatches)
|
|
|
|
t.Logf("PASS: precompile batch verification complete (BLS=%d FROST=%d CGGMP21=%d calls, 0 mismatches)",
|
|
batchSize, batchSize, batchSize)
|
|
}
|
|
|
|
// buildCallMsg constructs an ethereum.CallMsg for eth_call.
|
|
func buildCallMsg(to common.Address, data []byte) ethereum.CallMsg {
|
|
addr := to
|
|
return ethereum.CallMsg{To: &addr, Data: data, Gas: 1_000_000}
|
|
}
|
|
|
|
// bytesEqual compares two byte slices.
|
|
func bytesEqual(a, b []byte) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// compile-time sanity: ensure test functions exist.
|
|
var _ = []func(*testing.T){
|
|
TestGPU_BlockSTM_WriteConflict,
|
|
TestGPU_BlockSTM_ReadWriteSerializability,
|
|
TestGPU_ECRecoverBatchCorrectness,
|
|
TestGPU_Keccak256BatchConsistency,
|
|
TestGPU_CrashMidBlock,
|
|
TestGPU_MemoryPressure,
|
|
TestGPU_StateRootConsistency,
|
|
TestGPU_PrecompileBatchVerify,
|
|
}
|
|
|
|
func init() {
|
|
// Verify all 8 tests are registered.
|
|
tests := []string{
|
|
"TestGPU_BlockSTM_WriteConflict",
|
|
"TestGPU_BlockSTM_ReadWriteSerializability",
|
|
"TestGPU_ECRecoverBatchCorrectness",
|
|
"TestGPU_Keccak256BatchConsistency",
|
|
"TestGPU_CrashMidBlock",
|
|
"TestGPU_MemoryPressure",
|
|
"TestGPU_StateRootConsistency",
|
|
"TestGPU_PrecompileBatchVerify",
|
|
}
|
|
_ = fmt.Sprintf("registered %d GPU EVM chaos tests", len(tests))
|
|
}
|