Files

234 lines
8.0 KiB
Go

// (c) 2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package testutils
import (
"math/big"
"testing"
"time"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/precompile/contract"
"github.com/luxfi/evm/precompile/modules"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/evm/utils"
"github.com/luxfi/geth/common"
"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 contract.StateDB)
// 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 contract.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 contract.StateDB) {
runParams := test.setup(t, module, state)
if runParams.Input != nil {
// Get the contract from the module
contractInterface := module.Contract()
statefulContract, ok := contractInterface.(contract.StatefulPrecompiledContract)
if !ok {
t.Fatal("Module contract does not implement StatefulPrecompiledContract")
return
}
ret, remainingGas, err := statefulContract.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)
}
}
func (test PrecompileTest) setup(t testing.TB, module modules.Module, state contract.StateDB) PrecompileRunparams {
t.Helper()
contractAddress := module.Address()
ctrl := gomock.NewController(t)
if test.BeforeHook != nil {
test.BeforeHook(t, state)
}
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 := utils.TestChainContext()
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 {
// Get the configurator from the module
if configurator := module.Configurator(); configurator != nil {
if conf, ok := configurator.(contract.Configurator); ok {
err := conf.Configure(chainConfig, test.Config, state, blockContext)
require.NoError(t, err)
} else {
t.Fatal("Module configurator does not implement contract.Configurator")
}
}
}
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 (test PrecompileTest) Bench(b *testing.B, module modules.Module, state contract.StateDB) {
runParams := test.setup(b, module, state)
if runParams.Input == nil {
b.Skip("Skipping precompile benchmark due to nil input (used for configuration tests)")
}
stateDB := runParams.AccessibleState.GetStateDB()
snapshot := stateDB.Snapshot()
// Get the contract from the module
contractInterface := module.Contract()
statefulContract, ok := contractInterface.(contract.StatefulPrecompiledContract)
if !ok {
b.Fatal("Module contract does not implement StatefulPrecompiledContract")
return
}
ret, remainingGas, err := statefulContract.Run(runParams.AccessibleState, runParams.Caller, runParams.ContractAddress, runParams.Input, runParams.SuppliedGas, runParams.ReadOnly)
if len(test.ExpectedErr) != 0 {
require.ErrorContains(b, err, test.ExpectedErr)
} else {
require.NoError(b, err)
}
require.Equal(b, uint64(0), remainingGas)
require.Equal(b, test.ExpectedRes, ret)
if test.AfterHook != nil {
test.AfterHook(b, state)
}
b.ReportAllocs()
start := time.Now()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Revert to the previous snapshot and take a new snapshot, so we can reset the state after execution
stateDB.RevertToSnapshot(snapshot)
snapshot = stateDB.Snapshot()
// Ignore return values for benchmark
_, _, _ = statefulContract.Run(runParams.AccessibleState, runParams.Caller, runParams.ContractAddress, runParams.Input, runParams.SuppliedGas, runParams.ReadOnly)
}
b.StopTimer()
elapsed := uint64(time.Since(start))
if elapsed < 1 {
elapsed = 1
}
gasUsed := runParams.SuppliedGas * uint64(b.N)
b.ReportMetric(float64(runParams.SuppliedGas), "gas/op")
// Keep it as uint64, multiply 100 to get two digit float later
mgasps := (100 * 1000 * gasUsed) / elapsed
b.ReportMetric(float64(mgasps)/100, "mgas/s")
// Execute the test one final time to ensure that if our RevertToSnapshot logic breaks such that each run is actually failing or resulting in unexpected behavior
// the benchmark should catch the error here.
stateDB.RevertToSnapshot(snapshot)
ret, remainingGas, err = statefulContract.Run(runParams.AccessibleState, runParams.Caller, runParams.ContractAddress, runParams.Input, runParams.SuppliedGas, runParams.ReadOnly)
if len(test.ExpectedErr) != 0 {
require.ErrorContains(b, err, test.ExpectedErr)
} else {
require.NoError(b, err)
}
require.Equal(b, uint64(0), remainingGas)
require.Equal(b, test.ExpectedRes, ret)
if test.AfterHook != nil {
test.AfterHook(b, state)
}
}
func RunPrecompileTests(t *testing.T, module modules.Module, newStateDB func(t testing.TB) contract.StateDB, contractTests map[string]PrecompileTest) {
t.Helper()
for name, test := range contractTests {
t.Run(name, func(t *testing.T) {
test.Run(t, module, newStateDB(t))
})
}
}