core/vm: instruction tests (#16327)

This PR makes it easy to generate and execute testcases for VM arithmetic operations. By enabling and running the testcase TestWriteExpectedValues, a set of json files are created which contain input and output for each arith operation.
The test TestJsonTestcases executes all of those tests.

While meaningless as is, this PR makes it less risky to make changes (optimizations) to the vm operations, since there will be a larger body of testcases.
This commit is contained in:
Martin Holst Swende 2019-04-04 11:19:38 +02:00 committed by GitHub
parent 5164274872
commit 36b78abe61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 158 additions and 86 deletions

View File

@ -18,6 +18,9 @@ package vm
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math/big" "math/big"
"testing" "testing"
@ -26,32 +29,91 @@ import (
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
type twoOperandTest struct { type TwoOperandTestcase struct {
x string X string
y string Y string
expected string Expected string
} }
func testTwoOperandOp(t *testing.T, tests []twoOperandTest, opFn func(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error)) { type twoOperandParams struct {
x string
y string
}
var commonParams []*twoOperandParams
var twoOpMethods map[string]executionFunc
func init() {
// Params is a list of common edgecases that should be used for some common tests
params := []string{
"0000000000000000000000000000000000000000000000000000000000000000", // 0
"0000000000000000000000000000000000000000000000000000000000000001", // +1
"0000000000000000000000000000000000000000000000000000000000000005", // +5
"7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", // + max -1
"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // + max
"8000000000000000000000000000000000000000000000000000000000000000", // - max
"8000000000000000000000000000000000000000000000000000000000000001", // - max+1
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb", // - 5
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // - 1
}
// Params are combined so each param is used on each 'side'
commonParams = make([]*twoOperandParams, len(params)*len(params))
for i, x := range params {
for j, y := range params {
commonParams[i*len(params)+j] = &twoOperandParams{x, y}
}
}
twoOpMethods = map[string]executionFunc{
"add": opAdd,
"sub": opSub,
"mul": opMul,
"div": opDiv,
"sdiv": opSdiv,
"mod": opMod,
"smod": opSmod,
"exp": opExp,
"signext": opSignExtend,
"lt": opLt,
"gt": opGt,
"slt": opSlt,
"sgt": opSgt,
"eq": opEq,
"and": opAnd,
"or": opOr,
"xor": opXor,
"byte": opByte,
"shl": opSHL,
"shr": opSHR,
"sar": opSAR,
}
}
func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFunc, name string) {
var ( var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{}) env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newstack()
pc = uint64(0) pc = uint64(0)
evmInterpreter = NewEVMInterpreter(env, env.vmConfig) evmInterpreter = env.interpreter.(*EVMInterpreter)
) )
// Stuff a couple of nonzero bigints into pool, to ensure that ops do not rely on pooled integers to be zero
env.interpreter = evmInterpreter
evmInterpreter.intPool = poolOfIntPools.get() evmInterpreter.intPool = poolOfIntPools.get()
evmInterpreter.intPool.put(big.NewInt(-1337))
evmInterpreter.intPool.put(big.NewInt(-1337))
evmInterpreter.intPool.put(big.NewInt(-1337))
for i, test := range tests { for i, test := range tests {
x := new(big.Int).SetBytes(common.Hex2Bytes(test.x)) x := new(big.Int).SetBytes(common.Hex2Bytes(test.X))
shift := new(big.Int).SetBytes(common.Hex2Bytes(test.y)) y := new(big.Int).SetBytes(common.Hex2Bytes(test.Y))
expected := new(big.Int).SetBytes(common.Hex2Bytes(test.expected)) expected := new(big.Int).SetBytes(common.Hex2Bytes(test.Expected))
stack.push(x) stack.push(x)
stack.push(shift) stack.push(y)
opFn(&pc, evmInterpreter, nil, nil, stack) opFn(&pc, evmInterpreter, nil, nil, stack)
actual := stack.pop() actual := stack.pop()
if actual.Cmp(expected) != 0 { if actual.Cmp(expected) != 0 {
t.Errorf("Testcase %d, expected %v, got %v", i, expected, actual) t.Errorf("Testcase %v %d, %v(%x, %x): expected %x, got %x", name, i, name, x, y, expected, actual)
} }
// Check pool usage // Check pool usage
// 1.pool is not allowed to contain anything on the stack // 1.pool is not allowed to contain anything on the stack
@ -64,7 +126,7 @@ func testTwoOperandOp(t *testing.T, tests []twoOperandTest, opFn func(pc *uint64
for evmInterpreter.intPool.pool.len() > 0 { for evmInterpreter.intPool.pool.len() > 0 {
key := evmInterpreter.intPool.get() key := evmInterpreter.intPool.get()
if _, exist := poolvals[key]; exist { if _, exist := poolvals[key]; exist {
t.Errorf("Testcase %d, pool contains double-entry", i) t.Errorf("Testcase %v %d, pool contains double-entry", name, i)
} }
poolvals[key] = struct{}{} poolvals[key] = struct{}{}
} }
@ -74,47 +136,22 @@ func testTwoOperandOp(t *testing.T, tests []twoOperandTest, opFn func(pc *uint64
} }
func TestByteOp(t *testing.T) { func TestByteOp(t *testing.T) {
var ( tests := []TwoOperandTestcase{
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{}) {"ABCDEF0908070605040302010000000000000000000000000000000000000000", "00", "AB"},
stack = newstack() {"ABCDEF0908070605040302010000000000000000000000000000000000000000", "01", "CD"},
evmInterpreter = NewEVMInterpreter(env, env.vmConfig) {"00CDEF090807060504030201ffffffffffffffffffffffffffffffffffffffff", "00", "00"},
) {"00CDEF090807060504030201ffffffffffffffffffffffffffffffffffffffff", "01", "CD"},
{"0000000000000000000000000000000000000000000000000000000000102030", "1F", "30"},
env.interpreter = evmInterpreter {"0000000000000000000000000000000000000000000000000000000000102030", "1E", "20"},
evmInterpreter.intPool = poolOfIntPools.get() {"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "20", "00"},
tests := []struct { {"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "FFFFFFFFFFFFFFFF", "00"},
v string
th uint64
expected *big.Int
}{
{"ABCDEF0908070605040302010000000000000000000000000000000000000000", 0, big.NewInt(0xAB)},
{"ABCDEF0908070605040302010000000000000000000000000000000000000000", 1, big.NewInt(0xCD)},
{"00CDEF090807060504030201ffffffffffffffffffffffffffffffffffffffff", 0, big.NewInt(0x00)},
{"00CDEF090807060504030201ffffffffffffffffffffffffffffffffffffffff", 1, big.NewInt(0xCD)},
{"0000000000000000000000000000000000000000000000000000000000102030", 31, big.NewInt(0x30)},
{"0000000000000000000000000000000000000000000000000000000000102030", 30, big.NewInt(0x20)},
{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 32, big.NewInt(0x0)},
{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 0xFFFFFFFFFFFFFFFF, big.NewInt(0x0)},
} }
pc := uint64(0) testTwoOperandOp(t, tests, opByte, "byte")
for _, test := range tests {
val := new(big.Int).SetBytes(common.Hex2Bytes(test.v))
th := new(big.Int).SetUint64(test.th)
stack.push(val)
stack.push(th)
opByte(&pc, evmInterpreter, nil, nil, stack)
actual := stack.pop()
if actual.Cmp(test.expected) != 0 {
t.Fatalf("Expected [%v] %v:th byte to be %v, was %v.", test.v, test.th, test.expected, actual)
}
}
poolOfIntPools.put(evmInterpreter.intPool)
} }
func TestSHL(t *testing.T) { func TestSHL(t *testing.T) {
// Testcases from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-145.md#shl-shift-left // Testcases from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-145.md#shl-shift-left
tests := []twoOperandTest{ tests := []TwoOperandTestcase{
{"0000000000000000000000000000000000000000000000000000000000000001", "00", "0000000000000000000000000000000000000000000000000000000000000001"},
{"0000000000000000000000000000000000000000000000000000000000000001", "01", "0000000000000000000000000000000000000000000000000000000000000002"}, {"0000000000000000000000000000000000000000000000000000000000000001", "01", "0000000000000000000000000000000000000000000000000000000000000002"},
{"0000000000000000000000000000000000000000000000000000000000000001", "ff", "8000000000000000000000000000000000000000000000000000000000000000"}, {"0000000000000000000000000000000000000000000000000000000000000001", "ff", "8000000000000000000000000000000000000000000000000000000000000000"},
{"0000000000000000000000000000000000000000000000000000000000000001", "0100", "0000000000000000000000000000000000000000000000000000000000000000"}, {"0000000000000000000000000000000000000000000000000000000000000001", "0100", "0000000000000000000000000000000000000000000000000000000000000000"},
@ -126,12 +163,12 @@ func TestSHL(t *testing.T) {
{"0000000000000000000000000000000000000000000000000000000000000000", "01", "0000000000000000000000000000000000000000000000000000000000000000"}, {"0000000000000000000000000000000000000000000000000000000000000000", "01", "0000000000000000000000000000000000000000000000000000000000000000"},
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "01", "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"}, {"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "01", "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"},
} }
testTwoOperandOp(t, tests, opSHL) testTwoOperandOp(t, tests, opSHL, "shl")
} }
func TestSHR(t *testing.T) { func TestSHR(t *testing.T) {
// Testcases from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-145.md#shr-logical-shift-right // Testcases from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-145.md#shr-logical-shift-right
tests := []twoOperandTest{ tests := []TwoOperandTestcase{
{"0000000000000000000000000000000000000000000000000000000000000001", "00", "0000000000000000000000000000000000000000000000000000000000000001"}, {"0000000000000000000000000000000000000000000000000000000000000001", "00", "0000000000000000000000000000000000000000000000000000000000000001"},
{"0000000000000000000000000000000000000000000000000000000000000001", "01", "0000000000000000000000000000000000000000000000000000000000000000"}, {"0000000000000000000000000000000000000000000000000000000000000001", "01", "0000000000000000000000000000000000000000000000000000000000000000"},
{"8000000000000000000000000000000000000000000000000000000000000000", "01", "4000000000000000000000000000000000000000000000000000000000000000"}, {"8000000000000000000000000000000000000000000000000000000000000000", "01", "4000000000000000000000000000000000000000000000000000000000000000"},
@ -144,12 +181,12 @@ func TestSHR(t *testing.T) {
{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0100", "0000000000000000000000000000000000000000000000000000000000000000"}, {"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0100", "0000000000000000000000000000000000000000000000000000000000000000"},
{"0000000000000000000000000000000000000000000000000000000000000000", "01", "0000000000000000000000000000000000000000000000000000000000000000"}, {"0000000000000000000000000000000000000000000000000000000000000000", "01", "0000000000000000000000000000000000000000000000000000000000000000"},
} }
testTwoOperandOp(t, tests, opSHR) testTwoOperandOp(t, tests, opSHR, "shr")
} }
func TestSAR(t *testing.T) { func TestSAR(t *testing.T) {
// Testcases from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-145.md#sar-arithmetic-shift-right // Testcases from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-145.md#sar-arithmetic-shift-right
tests := []twoOperandTest{ tests := []TwoOperandTestcase{
{"0000000000000000000000000000000000000000000000000000000000000001", "00", "0000000000000000000000000000000000000000000000000000000000000001"}, {"0000000000000000000000000000000000000000000000000000000000000001", "00", "0000000000000000000000000000000000000000000000000000000000000001"},
{"0000000000000000000000000000000000000000000000000000000000000001", "01", "0000000000000000000000000000000000000000000000000000000000000000"}, {"0000000000000000000000000000000000000000000000000000000000000001", "01", "0000000000000000000000000000000000000000000000000000000000000000"},
{"8000000000000000000000000000000000000000000000000000000000000000", "01", "c000000000000000000000000000000000000000000000000000000000000000"}, {"8000000000000000000000000000000000000000000000000000000000000000", "01", "c000000000000000000000000000000000000000000000000000000000000000"},
@ -168,44 +205,58 @@ func TestSAR(t *testing.T) {
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0100", "0000000000000000000000000000000000000000000000000000000000000000"}, {"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0100", "0000000000000000000000000000000000000000000000000000000000000000"},
} }
testTwoOperandOp(t, tests, opSAR) testTwoOperandOp(t, tests, opSAR, "sar")
} }
func TestSGT(t *testing.T) { // getResult is a convenience function to generate the expected values
tests := []twoOperandTest{ func getResult(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcase {
var (
{"0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000000"}, env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000000"}, stack = newstack()
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000000"}, pc = uint64(0)
{"0000000000000000000000000000000000000000000000000000000000000001", "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000001"}, interpreter = env.interpreter.(*EVMInterpreter)
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000000"}, )
{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000001"}, interpreter.intPool = poolOfIntPools.get()
{"0000000000000000000000000000000000000000000000000000000000000001", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000000"}, result := make([]TwoOperandTestcase, len(args))
{"8000000000000000000000000000000000000000000000000000000000000001", "8000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000000"}, for i, param := range args {
{"8000000000000000000000000000000000000000000000000000000000000001", "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000001"}, x := new(big.Int).SetBytes(common.Hex2Bytes(param.x))
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "8000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000000"}, y := new(big.Int).SetBytes(common.Hex2Bytes(param.y))
{"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb", "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd", "0000000000000000000000000000000000000000000000000000000000000001"}, stack.push(x)
{"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd", "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb", "0000000000000000000000000000000000000000000000000000000000000000"}, stack.push(y)
opFn(&pc, interpreter, nil, nil, stack)
actual := stack.pop()
result[i] = TwoOperandTestcase{param.x, param.y, fmt.Sprintf("%064x", actual)}
} }
testTwoOperandOp(t, tests, opSgt) return result
} }
func TestSLT(t *testing.T) { // utility function to fill the json-file with testcases
tests := []twoOperandTest{ // Enable this test to generate the 'testcases_xx.json' files
{"0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000000"}, func xTestWriteExpectedValues(t *testing.T) {
{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000000"}, for name, method := range twoOpMethods {
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000000"}, data, err := json.Marshal(getResult(commonParams, method))
{"0000000000000000000000000000000000000000000000000000000000000001", "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000000"}, if err != nil {
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000001"}, t.Fatal(err)
{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000000"}, }
{"0000000000000000000000000000000000000000000000000000000000000001", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000001"}, _ = ioutil.WriteFile(fmt.Sprintf("testdata/testcases_%v.json", name), data, 0644)
{"8000000000000000000000000000000000000000000000000000000000000001", "8000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000000"}, if err != nil {
{"8000000000000000000000000000000000000000000000000000000000000001", "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0000000000000000000000000000000000000000000000000000000000000000"}, t.Fatal(err)
{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "8000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000001"}, }
{"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb", "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd", "0000000000000000000000000000000000000000000000000000000000000000"}, }
{"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd", "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb", "0000000000000000000000000000000000000000000000000000000000000001"}, t.Fatal("This test should not be activated")
}
// TestJsonTestcases runs through all the testcases defined as json-files
func TestJsonTestcases(t *testing.T) {
for name := range twoOpMethods {
data, err := ioutil.ReadFile(fmt.Sprintf("testdata/testcases_%v.json", name))
if err != nil {
t.Fatal("Failed to read file", err)
}
var testcases []TwoOperandTestcase
json.Unmarshal(data, &testcases)
testTwoOperandOp(t, testcases, twoOpMethods[name], name)
} }
testTwoOperandOp(t, tests, opSlt)
} }
func opBenchmark(bench *testing.B, op func(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error), args ...string) { func opBenchmark(bench *testing.B, op func(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error), args ...string) {

1
core/vm/testdata/testcases_add.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_and.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_byte.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_div.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_eq.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_exp.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_gt.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_lt.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_mod.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_mul.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_or.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_sar.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_sdiv.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_sgt.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_shl.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_shr.json vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_slt.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_smod.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_sub.json vendored Normal file

File diff suppressed because one or more lines are too long

1
core/vm/testdata/testcases_xor.json vendored Normal file

File diff suppressed because one or more lines are too long