mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
340 lines
12 KiB
Go
340 lines
12 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package compute
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/crypto"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/tracing"
|
|
ethtypes "github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/precompile/contract"
|
|
"github.com/luxfi/precompile/precompileconfig"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
caller1 = common.HexToAddress("0x1111111111111111111111111111111111111111")
|
|
caller2 = common.HexToAddress("0x2222222222222222222222222222222222222222")
|
|
)
|
|
|
|
func TestAddress(t *testing.T) {
|
|
expected := "0x0300000000000000000000000000000000000010"
|
|
require.Equal(t, expected, ContractAddress.Hex())
|
|
}
|
|
|
|
func TestRegisterProvider(t *testing.T) {
|
|
state := newMockState()
|
|
gpuType := common.BytesToHash([]byte("A100-80GB"))
|
|
teeAttest := common.BytesToHash([]byte("attestation-data"))
|
|
|
|
input := make([]byte, 1+64)
|
|
input[0] = OpRegisterProvider
|
|
copy(input[1:33], gpuType.Bytes())
|
|
copy(input[33:65], teeAttest.Bytes())
|
|
|
|
result, gas, err := Precompile.Run(state, caller1, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
require.True(t, gas < 100000)
|
|
require.Len(t, result, 32)
|
|
|
|
// Provider ID should be keccak256(caller, nonce=0)
|
|
expectedID := common.BytesToHash(crypto.Keccak256(caller1.Bytes(), common.Hash{}.Bytes()))
|
|
require.Equal(t, expectedID.Bytes(), result)
|
|
|
|
// Verify stored GPU type
|
|
stored := state.stateDB.GetState(ContractAddress, storageSlot("provider", expectedID.Bytes()))
|
|
require.Equal(t, gpuType, stored)
|
|
}
|
|
|
|
func TestSubmitJob(t *testing.T) {
|
|
state := newMockState()
|
|
modelHash := common.BytesToHash([]byte("model-sha256"))
|
|
inputHash := common.BytesToHash([]byte("input-sha256"))
|
|
maxPrice := common.BytesToHash(padTo32(big.NewInt(1e15).Bytes()))
|
|
|
|
input := make([]byte, 1+96)
|
|
input[0] = OpSubmitJob
|
|
copy(input[1:33], modelHash.Bytes())
|
|
copy(input[33:65], inputHash.Bytes())
|
|
copy(input[65:97], maxPrice.Bytes())
|
|
|
|
result, gas, err := Precompile.Run(state, caller2, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
require.True(t, gas < 100000)
|
|
require.Len(t, result, 32)
|
|
|
|
// Verify job stored
|
|
jobID := common.BytesToHash(result)
|
|
storedModel := state.stateDB.GetState(ContractAddress, storageSlot("job", jobID.Bytes()))
|
|
require.Equal(t, modelHash, storedModel)
|
|
}
|
|
|
|
func TestClaimReward(t *testing.T) {
|
|
state := newMockState()
|
|
|
|
// First register a provider
|
|
regInput := make([]byte, 1+64)
|
|
regInput[0] = OpRegisterProvider
|
|
copy(regInput[1:33], common.BytesToHash([]byte("GPU")).Bytes())
|
|
copy(regInput[33:65], common.BytesToHash([]byte("TEE")).Bytes())
|
|
_, _, err := Precompile.Run(state, caller1, ContractAddress, regInput, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
// Submit a job
|
|
jobInput := make([]byte, 1+96)
|
|
jobInput[0] = OpSubmitJob
|
|
copy(jobInput[1:33], common.BytesToHash([]byte("model")).Bytes())
|
|
copy(jobInput[33:65], common.BytesToHash([]byte("input")).Bytes())
|
|
copy(jobInput[65:97], padTo32(big.NewInt(1000).Bytes()))
|
|
jobResult, _, err := Precompile.Run(state, caller2, ContractAddress, jobInput, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
jobID := common.BytesToHash(jobResult)
|
|
outputHash := common.BytesToHash([]byte("output-result"))
|
|
|
|
// Claim as provider
|
|
claimInput := make([]byte, 1+64)
|
|
claimInput[0] = OpClaimReward
|
|
copy(claimInput[1:33], jobID.Bytes())
|
|
copy(claimInput[33:65], outputHash.Bytes())
|
|
|
|
result, _, err := Precompile.Run(state, caller1, ContractAddress, claimInput, 100000, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, byte(1), result[31], "claim should succeed")
|
|
|
|
// Verify status is claimed
|
|
status := state.stateDB.GetState(ContractAddress, storageSlot("job.status", jobID.Bytes()))
|
|
require.Equal(t, byte(StatusClaimed), status[31])
|
|
}
|
|
|
|
func TestClaimReward_AlreadyClaimed(t *testing.T) {
|
|
state := newMockState()
|
|
|
|
// Register + submit + claim
|
|
regInput := make([]byte, 1+64)
|
|
regInput[0] = OpRegisterProvider
|
|
copy(regInput[1:33], padTo32([]byte("GPU")))
|
|
copy(regInput[33:65], padTo32([]byte("TEE")))
|
|
_, _, _ = Precompile.Run(state, caller1, ContractAddress, regInput, 100000, false)
|
|
|
|
jobInput := make([]byte, 1+96)
|
|
jobInput[0] = OpSubmitJob
|
|
copy(jobInput[1:33], padTo32([]byte("model")))
|
|
copy(jobInput[33:65], padTo32([]byte("input")))
|
|
copy(jobInput[65:97], padTo32(big.NewInt(1000).Bytes()))
|
|
jobResult, _, _ := Precompile.Run(state, caller2, ContractAddress, jobInput, 100000, false)
|
|
|
|
jobID := common.BytesToHash(jobResult)
|
|
claimInput := make([]byte, 1+64)
|
|
claimInput[0] = OpClaimReward
|
|
copy(claimInput[1:33], jobID.Bytes())
|
|
copy(claimInput[33:65], padTo32([]byte("output")))
|
|
_, _, _ = Precompile.Run(state, caller1, ContractAddress, claimInput, 100000, false)
|
|
|
|
// Try to claim again
|
|
_, _, err := Precompile.Run(state, caller1, ContractAddress, claimInput, 100000, false)
|
|
require.ErrorIs(t, err, ErrJobAlreadyClaimed)
|
|
}
|
|
|
|
func TestVerifyCompute(t *testing.T) {
|
|
state := newMockState()
|
|
|
|
// Register + submit + claim
|
|
regInput := make([]byte, 1+64)
|
|
regInput[0] = OpRegisterProvider
|
|
copy(regInput[1:33], padTo32([]byte("GPU")))
|
|
copy(regInput[33:65], padTo32([]byte("TEE")))
|
|
_, _, _ = Precompile.Run(state, caller1, ContractAddress, regInput, 100000, false)
|
|
|
|
jobInput := make([]byte, 1+96)
|
|
jobInput[0] = OpSubmitJob
|
|
copy(jobInput[1:33], padTo32([]byte("model")))
|
|
copy(jobInput[33:65], padTo32([]byte("input")))
|
|
copy(jobInput[65:97], padTo32(big.NewInt(1000).Bytes()))
|
|
jobResult, _, _ := Precompile.Run(state, caller2, ContractAddress, jobInput, 100000, false)
|
|
|
|
jobID := common.BytesToHash(jobResult)
|
|
outputHash := common.BytesToHash(padTo32([]byte("output")))
|
|
|
|
claimInput := make([]byte, 1+64)
|
|
claimInput[0] = OpClaimReward
|
|
copy(claimInput[1:33], jobID.Bytes())
|
|
copy(claimInput[33:65], outputHash.Bytes())
|
|
_, _, _ = Precompile.Run(state, caller1, ContractAddress, claimInput, 100000, false)
|
|
|
|
// Compute correct attestation: keccak256(jobID, outputHash)
|
|
attestation := common.BytesToHash(crypto.Keccak256(jobID.Bytes(), outputHash.Bytes()))
|
|
|
|
verifyInput := make([]byte, 1+64)
|
|
verifyInput[0] = OpVerifyCompute
|
|
copy(verifyInput[1:33], jobID.Bytes())
|
|
copy(verifyInput[33:65], attestation.Bytes())
|
|
|
|
result, _, err := Precompile.Run(state, caller1, ContractAddress, verifyInput, 200000, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, byte(1), result[31], "verify should succeed")
|
|
|
|
// Status should be verified
|
|
status := state.stateDB.GetState(ContractAddress, storageSlot("job.status", jobID.Bytes()))
|
|
require.Equal(t, byte(StatusVerified), status[31])
|
|
}
|
|
|
|
func TestVerifyCompute_BadAttestation(t *testing.T) {
|
|
state := newMockState()
|
|
|
|
// Register + submit + claim
|
|
regInput := make([]byte, 1+64)
|
|
regInput[0] = OpRegisterProvider
|
|
copy(regInput[1:33], padTo32([]byte("GPU")))
|
|
copy(regInput[33:65], padTo32([]byte("TEE")))
|
|
_, _, _ = Precompile.Run(state, caller1, ContractAddress, regInput, 100000, false)
|
|
|
|
jobInput := make([]byte, 1+96)
|
|
jobInput[0] = OpSubmitJob
|
|
copy(jobInput[1:33], padTo32([]byte("model")))
|
|
copy(jobInput[33:65], padTo32([]byte("input")))
|
|
copy(jobInput[65:97], padTo32(big.NewInt(1000).Bytes()))
|
|
jobResult, _, _ := Precompile.Run(state, caller2, ContractAddress, jobInput, 100000, false)
|
|
|
|
jobID := common.BytesToHash(jobResult)
|
|
claimInput := make([]byte, 1+64)
|
|
claimInput[0] = OpClaimReward
|
|
copy(claimInput[1:33], jobID.Bytes())
|
|
copy(claimInput[33:65], padTo32([]byte("output")))
|
|
_, _, _ = Precompile.Run(state, caller1, ContractAddress, claimInput, 100000, false)
|
|
|
|
// Bad attestation
|
|
verifyInput := make([]byte, 1+64)
|
|
verifyInput[0] = OpVerifyCompute
|
|
copy(verifyInput[1:33], jobID.Bytes())
|
|
copy(verifyInput[33:65], padTo32([]byte("wrong-attestation")))
|
|
|
|
result, _, err := Precompile.Run(state, caller1, ContractAddress, verifyInput, 200000, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, byte(0), result[31], "verify should fail with bad attestation")
|
|
}
|
|
|
|
func TestGetPrice(t *testing.T) {
|
|
input := make([]byte, 1+64)
|
|
input[0] = OpGetPrice
|
|
copy(input[1:33], padTo32([]byte("model")))
|
|
copy(input[33:65], padTo32(big.NewInt(1024).Bytes()))
|
|
|
|
result, _, err := Precompile.Run(newMockState(), caller1, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
price := new(big.Int).SetBytes(result)
|
|
// 1024 * 1000 = 1024000
|
|
require.Equal(t, big.NewInt(1024000), price)
|
|
}
|
|
|
|
func TestReadOnlyState(t *testing.T) {
|
|
state := newMockState()
|
|
|
|
// RegisterProvider should fail in readOnly mode
|
|
input := make([]byte, 1+64)
|
|
input[0] = OpRegisterProvider
|
|
_, _, err := Precompile.Run(state, caller1, ContractAddress, input, 100000, true)
|
|
require.ErrorIs(t, err, ErrReadOnlyState)
|
|
|
|
// SubmitJob should fail in readOnly mode
|
|
input2 := make([]byte, 1+96)
|
|
input2[0] = OpSubmitJob
|
|
_, _, err = Precompile.Run(state, caller1, ContractAddress, input2, 100000, true)
|
|
require.ErrorIs(t, err, ErrReadOnlyState)
|
|
}
|
|
|
|
func TestInvalidInput(t *testing.T) {
|
|
state := newMockState()
|
|
_, _, err := Precompile.Run(state, caller1, ContractAddress, nil, 100000, false)
|
|
require.ErrorIs(t, err, ErrInvalidInput)
|
|
|
|
_, _, err = Precompile.Run(state, caller1, ContractAddress, []byte{0xFF}, 100000, false)
|
|
require.ErrorIs(t, err, ErrUnknownOp)
|
|
}
|
|
|
|
// --- Mock infrastructure ---
|
|
|
|
type mockStateDB struct {
|
|
storage map[common.Address]map[common.Hash]common.Hash
|
|
}
|
|
|
|
func newMockStateDB() *mockStateDB {
|
|
return &mockStateDB{storage: make(map[common.Address]map[common.Hash]common.Hash)}
|
|
}
|
|
|
|
func (m *mockStateDB) GetState(addr common.Address, key common.Hash) common.Hash {
|
|
if m.storage[addr] == nil {
|
|
return common.Hash{}
|
|
}
|
|
return m.storage[addr][key]
|
|
}
|
|
|
|
func (m *mockStateDB) SetState(addr common.Address, key, value common.Hash) common.Hash {
|
|
if m.storage[addr] == nil {
|
|
m.storage[addr] = make(map[common.Hash]common.Hash)
|
|
}
|
|
prev := m.storage[addr][key]
|
|
m.storage[addr][key] = value
|
|
return prev
|
|
}
|
|
|
|
func (m *mockStateDB) GetBalance(common.Address) *uint256.Int { return uint256.NewInt(0) }
|
|
func (m *mockStateDB) AddBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int {
|
|
return uint256.Int{}
|
|
}
|
|
func (m *mockStateDB) SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int {
|
|
return uint256.Int{}
|
|
}
|
|
func (m *mockStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { return big.NewInt(0) }
|
|
func (m *mockStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) {}
|
|
func (m *mockStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {}
|
|
func (m *mockStateDB) SetNonce(common.Address, uint64, tracing.NonceChangeReason) {}
|
|
func (m *mockStateDB) GetNonce(common.Address) uint64 { return 0 }
|
|
func (m *mockStateDB) CreateAccount(common.Address) {}
|
|
func (m *mockStateDB) Exist(common.Address) bool { return true }
|
|
func (m *mockStateDB) AddLog(*ethtypes.Log) {}
|
|
func (m *mockStateDB) Logs() []*ethtypes.Log { return nil }
|
|
func (m *mockStateDB) GetPredicateStorageSlots(common.Address, int) ([]byte, bool) { return nil, false }
|
|
func (m *mockStateDB) TxHash() common.Hash { return common.Hash{} }
|
|
func (m *mockStateDB) Snapshot() int { return 0 }
|
|
func (m *mockStateDB) RevertToSnapshot(int) {}
|
|
|
|
type mockBlockContext struct{}
|
|
|
|
func (m *mockBlockContext) Number() *big.Int { return big.NewInt(1) }
|
|
func (m *mockBlockContext) Timestamp() uint64 { return 0 }
|
|
func (m *mockBlockContext) GetPredicateResults(common.Hash, common.Address) []byte { return nil }
|
|
|
|
type mockChainConfig struct{}
|
|
|
|
func (m *mockChainConfig) IsDurango(uint64) bool { return true }
|
|
|
|
type mockPrecompileEnv struct{}
|
|
|
|
func (m *mockPrecompileEnv) ReadOnly() bool { return false }
|
|
|
|
type mockAccessibleState struct {
|
|
stateDB *mockStateDB
|
|
}
|
|
|
|
func newMockState() *mockAccessibleState {
|
|
return &mockAccessibleState{stateDB: newMockStateDB()}
|
|
}
|
|
|
|
func (m *mockAccessibleState) GetStateDB() contract.StateDB { return m.stateDB }
|
|
func (m *mockAccessibleState) GetBlockContext() contract.BlockContext { return &mockBlockContext{} }
|
|
func (m *mockAccessibleState) GetConsensusContext() context.Context { return context.Background() }
|
|
func (m *mockAccessibleState) GetChainConfig() precompileconfig.ChainConfig {
|
|
return &mockChainConfig{}
|
|
}
|
|
func (m *mockAccessibleState) GetPrecompileEnv() contract.PrecompileEnvironment {
|
|
return &mockPrecompileEnv{}
|
|
}
|