mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
234 lines
8.0 KiB
Go
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))
|
|
})
|
|
}
|
|
}
|