-
Notifications
You must be signed in to change notification settings - Fork 210
/
test_precompile.go
211 lines (181 loc) · 7.22 KB
/
test_precompile.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// (c) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package testutils
import (
"math/big"
"testing"
"time"
"github.com/ava-labs/subnet-evm/commontype"
"github.com/ava-labs/subnet-evm/precompile/contract"
"github.com/ava-labs/subnet-evm/precompile/modules"
"github.com/ava-labs/subnet-evm/precompile/precompileconfig"
"github.com/ava-labs/subnet-evm/utils"
"github.com/ethereum/go-ethereum/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 {
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)
}
}
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()
}
snowContext := utils.TestSnowContext()
accessibleState := contract.NewMockAccessibleState(ctrl)
accessibleState.EXPECT().GetStateDB().Return(state).AnyTimes()
accessibleState.EXPECT().GetBlockContext().Return(blockContext).AnyTimes()
accessibleState.EXPECT().GetSnowContext().Return(snowContext).AnyTimes()
accessibleState.EXPECT().GetChainConfig().Return(chainConfig).AnyTimes()
if test.Config != nil {
err := module.Configure(chainConfig, test.Config, state, blockContext)
require.NoError(t, err)
}
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()
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(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
_, _, _ = module.Contract.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 = module.Contract.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))
})
}
}