Files

185 lines
6.3 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package precompiletest
import (
"math/big"
"testing"
"time"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/core/extstate"
"github.com/luxfi/evm/core/state"
"github.com/luxfi/evm/precompile/contract"
"github.com/luxfi/evm/precompile/modules"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/evm/utils/utilstest"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
// PrecompileTest is a test case for a precompile
type PrecompileTest struct {
// Caller is the address of the precompile caller
Caller common.Address
// Input the raw input bytes to the precompile
Input []byte
// InputFn is a function that returns the raw input bytes to the precompile
// If specified, Input will be ignored.
InputFn func(t testing.TB) []byte
// SuppliedGas is the amount of gas supplied to the precompile
SuppliedGas uint64
// ReadOnly is whether the precompile should be called in read only
// mode. If true, the precompile should not modify the state.
ReadOnly bool
// Config is the config to use for the precompile
// It should be the same precompile config that is used in the
// precompile's configurator.
// If nil, Configure will not be called.
Config precompileconfig.Config
// BeforeHook is called before the precompile is called.
BeforeHook func(t testing.TB, state *state.StateDB)
// Predicates that the precompile should have access to.
Predicates [][]byte
// SetupBlockContext sets the expected calls on MockBlockContext for the test execution.
SetupBlockContext func(*contract.MockBlockContext)
// AfterHook is called after the precompile is called.
AfterHook func(t testing.TB, state *state.StateDB)
// ExpectedRes is the expected raw byte result returned by the precompile
ExpectedRes []byte
// ExpectedErr is the expected error returned by the precompile
ExpectedErr string
// ChainConfigFn returns the chain config to use for the precompile's block context
// If nil, the default chain config will be used.
ChainConfigFn func(*gomock.Controller) precompileconfig.ChainConfig
}
type PrecompileRunparams struct {
AccessibleState contract.AccessibleState
Caller common.Address
ContractAddress common.Address
Input []byte
SuppliedGas uint64
ReadOnly bool
}
func (test PrecompileTest) Run(t *testing.T, module modules.Module) {
state := newTestStateDB(t, map[common.Address][][]byte{
module.Address: test.Predicates,
})
runParams := test.setup(t, module, state)
if runParams.Input != nil {
ret, remainingGas, err := module.Contract.Run(runParams.AccessibleState, runParams.Caller, runParams.ContractAddress, runParams.Input, runParams.SuppliedGas, runParams.ReadOnly)
if len(test.ExpectedErr) != 0 {
require.ErrorContains(t, err, test.ExpectedErr)
} else {
require.NoError(t, err)
}
require.Equal(t, uint64(0), remainingGas)
require.Equal(t, test.ExpectedRes, ret)
}
if test.AfterHook != nil {
test.AfterHook(t, state.underlyingStateDB)
}
}
func (test PrecompileTest) setup(t testing.TB, module modules.Module, state *testStateDB) PrecompileRunparams {
t.Helper()
contractAddress := module.Address
ctrl := gomock.NewController(t)
if test.BeforeHook != nil {
test.BeforeHook(t, state.underlyingStateDB)
}
if test.ChainConfigFn == nil {
test.ChainConfigFn = func(ctrl *gomock.Controller) precompileconfig.ChainConfig {
mockChainConfig := precompileconfig.NewMockChainConfig(ctrl)
mockChainConfig.EXPECT().GetFeeConfig().AnyTimes().Return(commontype.ValidTestFeeConfig)
mockChainConfig.EXPECT().AllowedFeeRecipients().AnyTimes().Return(false)
mockChainConfig.EXPECT().IsDurango(gomock.Any()).AnyTimes().Return(true)
return mockChainConfig
}
}
chainConfig := test.ChainConfigFn(ctrl)
blockContext := contract.NewMockBlockContext(ctrl)
if test.SetupBlockContext != nil {
test.SetupBlockContext(blockContext)
} else {
blockContext.EXPECT().Number().Return(big.NewInt(0)).AnyTimes()
blockContext.EXPECT().Timestamp().Return(uint64(time.Now().Unix())).AnyTimes()
}
consensusContext := utilstest.NewTestConsensusContext(t)
accessibleState := contract.NewMockAccessibleState(ctrl)
accessibleState.EXPECT().GetStateDB().Return(state).AnyTimes()
accessibleState.EXPECT().GetBlockContext().Return(blockContext).AnyTimes()
accessibleState.EXPECT().GetConsensusContext().Return(consensusContext).AnyTimes()
accessibleState.EXPECT().GetChainConfig().Return(chainConfig).AnyTimes()
if test.Config != nil {
require.NoError(t, module.Configure(chainConfig, test.Config, state, blockContext))
}
input := test.Input
if test.InputFn != nil {
input = test.InputFn(t)
}
return PrecompileRunparams{
AccessibleState: accessibleState,
Caller: test.Caller,
ContractAddress: contractAddress,
Input: input,
SuppliedGas: test.SuppliedGas,
ReadOnly: test.ReadOnly,
}
}
func RunPrecompileTests(t *testing.T, module modules.Module, contractTests map[string]PrecompileTest) {
t.Helper()
for name, test := range contractTests {
t.Run(name, func(t *testing.T) {
test.Run(t, module)
})
}
}
// testStateDB allows for mocking the predicate storage slots without calling
// Prepare on the statedb.
type testStateDB struct {
contract.StateDB
underlyingStateDB *state.StateDB // Keep reference to underlying StateDB for tests
predicateStorageSlots map[common.Address][][]byte
}
// GetPredicateStorageSlots returns the storage slots associated with the address, index pair.
// This method overrides the embedded StateDB's method to use the test predicates.
func (s *testStateDB) GetPredicateStorageSlots(address common.Address, index int) ([]byte, bool) {
predicates, exists := s.predicateStorageSlots[address]
if !exists || index >= len(predicates) {
return nil, false
}
return predicates[index], true
}
func newTestStateDB(t testing.TB, predicateStorageSlots map[common.Address][][]byte) *testStateDB {
db := rawdb.NewMemoryDatabase()
statedb, err := state.New(common.Hash{}, state.NewDatabase(db), nil)
require.NoError(t, err)
extState := extstate.New(statedb)
return &testStateDB{
StateDB: extstate.NewPrecompileAdapter(extState),
underlyingStateDB: statedb,
predicateStorageSlots: predicateStorageSlots,
}
}