erigon-pulse/core/vm/evm_test.go
Evgeny Danilenko 4cd72c8328
Keep readonly value while changing interpreters back and forth (#2508)
* restore TEVM

* fuzzing and property based

* comment

* lint

* stack callback into defer ater checking an error

* sequential tests
2021-08-10 09:48:56 +07:00

447 lines
12 KiB
Go

package vm
import (
"fmt"
"testing"
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/params"
"github.com/holiman/uint256"
"pgregory.net/rapid"
)
func TestInterpreterReadonly(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
env := NewEVM(BlockContext{
ContractHasTEVM: func(common.Hash) (bool, error) { return false, nil },
}, TxContext{}, &dummyStatedb{}, params.TestChainConfig, Config{EnableTEMV: true})
isEVMSliceTest := rapid.SliceOfN(rapid.Bool(), 1, -1).Draw(t, "tevm").([]bool)
readOnlySliceTest := rapid.SliceOfN(rapid.Bool(), len(isEVMSliceTest), len(isEVMSliceTest)).Draw(t, "readonly").([]bool)
isEVMCalled := make([]bool, len(isEVMSliceTest))
readOnlies := make([]*readOnlyState, len(readOnlySliceTest))
currentIdx := new(int)
*currentIdx = -1
evmInterpreter := &testVM{
readonlyGetSetter: env.interpreters[EVMType].(*EVMInterpreter),
isTEMV: false,
recordedReadOnlies: &readOnlies,
recordedIsEVMCalled: &isEVMCalled,
env: env,
isEVMSliceTest: isEVMSliceTest,
readOnlySliceTest: readOnlySliceTest,
currentIdx: currentIdx,
}
tevmInterpreter := &testVM{
readonlyGetSetter: env.interpreters[TEVMType].(*TEVMInterpreter),
isTEMV: true,
recordedReadOnlies: &readOnlies,
recordedIsEVMCalled: &isEVMCalled,
env: env,
isEVMSliceTest: isEVMSliceTest,
readOnlySliceTest: readOnlySliceTest,
currentIdx: currentIdx,
}
env.interpreters[EVMType] = evmInterpreter
env.interpreters[TEVMType] = tevmInterpreter
dummyContract := NewContract(
&dummyContractRef{},
&dummyContractRef{},
new(uint256.Int),
0,
false,
false,
)
newTestSequential(env, currentIdx, readOnlySliceTest, isEVMSliceTest).Run(dummyContract, nil, false)
var gotReadonly bool
var firstReadOnly int
// properties-invariants
if len(readOnlies) != len(readOnlySliceTest) {
t.Fatalf("expected static calls the same stack length as generated. got %d, expected %d", len(readOnlies), len(readOnlySliceTest))
}
if len(isEVMCalled) != len(isEVMSliceTest) {
t.Fatalf("expected VM calls the same stack length as generated. got %d, expected %d", len(isEVMCalled), len(isEVMSliceTest))
}
if *currentIdx != len(readOnlies) {
t.Fatalf("expected VM calls the same amount of calls as generated calls. got %d, expected %d", *currentIdx, len(readOnlies))
}
for i, readOnly := range readOnlies {
if isEVMCalled[i] != isEVMSliceTest[i] {
t.Fatalf("wrong VM was called in %d index, got EVM %t, expected EVM %t",
i, isEVMCalled[i], isEVMSliceTest[i])
}
if readOnly.outer != readOnlySliceTest[i] {
t.Fatalf("outer readOnly appeared in %d index, got readOnly %t, expected %t",
i, readOnly.outer, readOnlySliceTest[i])
}
if i > 0 {
if readOnly.before != readOnlies[i-1].in {
t.Fatalf("before readOnly appeared in %d index, got readOnly %t, expected %t",
i, readOnly.before, readOnlies[i-1].in)
}
}
if readOnly.in && !gotReadonly {
gotReadonly = true
firstReadOnly = i
}
if gotReadonly {
if !readOnly.in {
t.Fatalf("readOnly appeared in %d index, got non-readOnly in %d: %v",
firstReadOnly, i, trace(isEVMCalled, readOnlies))
}
switch {
case i < firstReadOnly:
if readOnly.after != false {
t.Fatalf("after readOnly appeared in %d index(first readonly %d, case <firstReadOnly), got readOnly %t, expected %t",
i, firstReadOnly, readOnly.after, false)
}
case i == firstReadOnly:
if readOnly.after != false {
t.Fatalf("after readOnly appeared in %d index(first readonly %d, case ==firstReadOnly), got readOnly %t, expected %t",
i, firstReadOnly, readOnly.after, false)
}
case i > firstReadOnly:
if readOnly.after != true {
t.Fatalf("after readOnly appeared in %d index(first readonly %d, case >firstReadOnly), got readOnly %t, expected %t",
i, firstReadOnly, readOnly.after, true)
}
}
} else {
if readOnly.after != false {
t.Fatalf("after readOnly didn't appear. %d index, got readOnly %t, expected %t",
i, readOnly.after, false)
}
}
}
})
}
func TestReadonlyBasicCases(t *testing.T) {
cases := []struct {
testName string
readonlySliceTest []bool
expectedReadonlySlice []readOnlyState
}{
{
"simple non-readonly",
[]bool{false},
[]readOnlyState{
{
false,
false,
false,
false,
},
},
},
{
"simple readonly",
[]bool{true},
[]readOnlyState{
{
true,
true,
true,
false,
},
},
},
{
"2 calls non-readonly",
[]bool{false, false},
[]readOnlyState{
{
false,
false,
false,
false,
},
{
false,
false,
false,
false,
},
},
},
{
"2 calls true,false",
[]bool{true, false},
[]readOnlyState{
{
true,
true,
true,
false,
},
{
true,
true,
true,
true,
},
},
},
{
"2 calls false,true",
[]bool{false, true},
[]readOnlyState{
{
false,
false,
false,
false,
},
{
true,
true,
true,
false,
},
},
},
{
"2 calls readonly",
[]bool{true, true},
[]readOnlyState{
{
true,
true,
true,
true,
},
{
true,
true,
true,
true,
},
},
},
}
type evmsParamsTest struct {
emvs []bool
suffix string
}
evmsTest := make([]evmsParamsTest, len(cases))
// fill all possible isEVM combinations
for i, testCase := range cases {
isEVMSliceTest := make([]bool, len(testCase.readonlySliceTest))
copy(isEVMSliceTest, testCase.readonlySliceTest)
evmsTest[i].emvs = isEVMSliceTest
suffix := "-isEVMSliceTest"
for _, evmParam := range testCase.readonlySliceTest {
if evmParam {
suffix += "-true"
} else {
suffix += "-false"
}
}
evmsTest[i].suffix = suffix
}
for _, testCase := range cases {
for _, evmsParams := range evmsTest {
testcase := testCase
evmsTestcase := evmsParams
if len(testcase.readonlySliceTest) != len(evmsTestcase.emvs) {
continue
}
t.Run(testcase.testName+evmsTestcase.suffix, func(t *testing.T) {
readonlySliceTest := testcase.readonlySliceTest
env := NewEVM(BlockContext{
ContractHasTEVM: func(common.Hash) (bool, error) { return false, nil },
}, TxContext{}, &dummyStatedb{}, params.TestChainConfig, Config{EnableTEMV: true})
readonliesGot := make([]*readOnlyState, len(testcase.readonlySliceTest))
isEVMGot := make([]bool, len(evmsTestcase.emvs))
currentIdx := new(int)
*currentIdx = -1
evmInterpreter := &testVM{
readonlyGetSetter: env.interpreters[EVMType].(*EVMInterpreter),
isTEMV: false,
recordedReadOnlies: &readonliesGot,
recordedIsEVMCalled: &isEVMGot,
env: env,
isEVMSliceTest: evmsTestcase.emvs,
readOnlySliceTest: testcase.readonlySliceTest,
currentIdx: currentIdx,
}
tevmInterpreter := &testVM{
readonlyGetSetter: env.interpreters[TEVMType].(*TEVMInterpreter),
isTEMV: true,
recordedReadOnlies: &readonliesGot,
recordedIsEVMCalled: &isEVMGot,
env: env,
isEVMSliceTest: evmsTestcase.emvs,
readOnlySliceTest: testcase.readonlySliceTest,
currentIdx: currentIdx,
}
env.interpreters[EVMType] = evmInterpreter
env.interpreters[TEVMType] = tevmInterpreter
dummyContract := NewContract(
&dummyContractRef{},
&dummyContractRef{},
new(uint256.Int),
0,
false,
false,
)
newTestSequential(env, currentIdx, readonlySliceTest, evmsTestcase.emvs).Run(dummyContract, nil, false)
if len(readonliesGot) != len(readonlySliceTest) {
t.Fatalf("expected static calls the same stack length as generated. got %d, expected %d - %v", len(readonliesGot), len(readonlySliceTest), readonlySliceTest)
}
if len(isEVMGot) != len(evmsTestcase.emvs) {
t.Fatalf("expected VM calls the same stack length as generated. got %d, expected %d - %v, readonly %v", len(isEVMGot), len(evmsTestcase.emvs), evmsTestcase.emvs, readonlySliceTest)
}
if *currentIdx != len(readonlySliceTest) {
t.Fatalf("expected VM calls the same amount of calls as generated calls. got %d, expected %d", *currentIdx, len(readonlySliceTest))
}
var gotReadonly bool
var firstReadOnly int
for callIndex, readOnly := range readonliesGot {
if isEVMGot[callIndex] != evmsTestcase.emvs[callIndex] {
t.Fatalf("wrong VM was called in %d index, got EVM %t, expected EVM %t. Test EVMs %v; test readonly %v",
callIndex, isEVMGot[callIndex], evmsTestcase.emvs[callIndex], evmsTestcase.emvs, readonlySliceTest)
}
if readOnly.outer != readonlySliceTest[callIndex] {
t.Fatalf("outer readOnly appeared in %d index, got readOnly %t, expected %t. Test EVMs %v; test readonly %v",
callIndex, readOnly.outer, readonlySliceTest[callIndex], evmsTestcase.emvs, readonlySliceTest)
}
if callIndex > 0 {
if readOnly.before != readonliesGot[callIndex-1].in {
t.Fatalf("before readOnly appeared in %d index, got readOnly %t, expected %t. Test EVMs %v; test readonly %v",
callIndex, readOnly.before, readonliesGot[callIndex-1].in, evmsTestcase.emvs, readonlySliceTest)
}
}
if readOnly.in && !gotReadonly {
gotReadonly = true
firstReadOnly = callIndex
}
if gotReadonly {
if !readOnly.in {
t.Fatalf("readOnly appeared in %d index, got non-readOnly in %d: %v. Test EVMs %v; test readonly %v",
firstReadOnly, callIndex, trace(isEVMGot, readonliesGot), evmsTestcase.emvs, readonlySliceTest)
}
switch {
case callIndex < firstReadOnly:
if readOnly.after != false {
t.Fatalf("after readOnly appeared in %d index(first readonly %d, case <firstReadOnly), got readOnly %t, expected %t. Test EVMs %v; test readonly %v",
callIndex, firstReadOnly, readOnly.after, false, evmsTestcase.emvs, readonlySliceTest)
}
case callIndex == firstReadOnly:
if readOnly.after != false {
t.Fatalf("after readOnly appeared in %d index(first readonly %d, case ==firstReadOnly), got readOnly %t, expected %t. Test EVMs %v; test readonly %v",
callIndex, firstReadOnly, readOnly.after, false, evmsTestcase.emvs, readonlySliceTest)
}
case callIndex > firstReadOnly:
if readOnly.after != true {
t.Fatalf("after readOnly appeared in %d index(first readonly %d, case >firstReadOnly), got readOnly %t, expected %t. Test EVMs %v; test readonly %v",
callIndex, firstReadOnly, readOnly.after, true, evmsTestcase.emvs, readonlySliceTest)
}
}
} else {
if readOnly.after != false {
t.Fatalf("after readOnly didn't appear. %d index, got readOnly %t, expected %t. Test EVMs %v; test readonly %v",
callIndex, readOnly.after, false, evmsTestcase.emvs, readonlySliceTest)
}
}
}
})
}
}
}
type testSequential struct {
env *EVM
currentIdx *int
readOnlys []bool
isEVMCalled []bool
}
func newTestSequential(env *EVM, currentIdx *int, readonlies []bool, isEVMCalled []bool) *testSequential {
return &testSequential{env, currentIdx, readonlies, isEVMCalled}
}
func (st *testSequential) Run(_ *Contract, _ []byte, _ bool) ([]byte, error) {
*st.currentIdx++
nextContract := NewContract(
&dummyContractRef{},
&dummyContractRef{},
new(uint256.Int),
0,
false,
!st.isEVMCalled[*st.currentIdx],
)
return run(st.env, nextContract, nil, st.readOnlys[*st.currentIdx])
}
func trace(isEVMSlice []bool, readOnlySlice []*readOnlyState) string {
res := "trace:\n"
for i := 0; i < len(isEVMSlice); i++ {
res += fmt.Sprintf("%d: EVM %t, readonly %t\n", i, isEVMSlice[i], readOnlySlice[i].in)
}
return res
}