Skip to content

Commit

Permalink
Generate precompile tests (#565)
Browse files Browse the repository at this point in the history
* add verify allow list tests

* use new verify allow list suite in precompiles

* add equal test suite

* nits

* add cnfig test template

* add contract unit test generation

* Remove abis

* nits

* fix testing input in template

* Update accounts/abi/bind/precompilebind/precompile_contract_test_template.go

Co-authored-by: Darioush Jalali <darioush.jalali@avalabs.org>

* Update accounts/abi/bind/precompilebind/precompile_contract_test_template.go

Co-authored-by: Darioush Jalali <darioush.jalali@avalabs.org>

* Update accounts/abi/bind/precompilebind/precompile_contract_test_template.go

Co-authored-by: Darioush Jalali <darioush.jalali@avalabs.org>

* fix non-allowlist tests
  • Loading branch information
ceyonur authored May 1, 2023
1 parent aeaf1db commit 19e90a9
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 54 deletions.
5 changes: 5 additions & 0 deletions accounts/abi/bind/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ func BindHelper(types []string, abis []string, bytecodes []string, fsigs []map[s
"capitalise": capitalise,
"decapitalise": decapitalise,
"convertToNil": convertToNil,
"mkList": mkList,
}

// render the template
Expand Down Expand Up @@ -700,3 +701,7 @@ func hasStruct(t abi.Type) bool {
return false
}
}

func mkList(args ...interface{}) []interface{} {
return args
}
42 changes: 37 additions & 5 deletions accounts/abi/bind/precompilebind/precompile_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,58 @@ const (
readAllowListFuncKey = "readAllowList"
)

// BindedFiles contains the generated binding file contents.
// This is used to return the contents in a expandable way.
type BindedFiles struct {
Contract string
Config string
Module string
ConfigTest string
ContractTest string
}

// PrecompileBind generates a Go binding for a precompiled contract. It returns config binding and contract binding.
func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang bind.Lang, libs map[string]string, aliases map[string]string, abifilename string) (string, string, string, error) {
func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang bind.Lang, libs map[string]string, aliases map[string]string, abifilename string, generateTests bool) (BindedFiles, error) {
// create hooks
configHook := createPrecompileHook(abifilename, tmplSourcePrecompileConfigGo)
contractHook := createPrecompileHook(abifilename, tmplSourcePrecompileContractGo)
moduleHook := createPrecompileHook(abifilename, tmplSourcePrecompileModuleGo)
configTestHook := createPrecompileHook(abifilename, tmplSourcePrecompileConfigTestGo)
contractTestHook := createPrecompileHook(abifilename, tmplSourcePrecompileContractTestGo)

configBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, configHook)
if err != nil {
return "", "", "", fmt.Errorf("failed to generate config binding: %w", err)
return BindedFiles{}, fmt.Errorf("failed to generate config binding: %w", err)
}
contractBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, contractHook)
if err != nil {
return "", "", "", fmt.Errorf("failed to generate contract binding: %w", err)
return BindedFiles{}, fmt.Errorf("failed to generate contract binding: %w", err)
}
moduleBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, moduleHook)
if err != nil {
return "", "", "", fmt.Errorf("failed to generate module binding: %w", err)
return BindedFiles{}, fmt.Errorf("failed to generate module binding: %w", err)
}
bindedFiles := BindedFiles{
Contract: contractBind,
Config: configBind,
Module: moduleBind,
}
return configBind, contractBind, moduleBind, nil

if generateTests {
configTestBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, configTestHook)
if err != nil {
return BindedFiles{}, fmt.Errorf("failed to generate config test binding: %w", err)
}
bindedFiles.ConfigTest = configTestBind

contractTestBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, contractTestHook)
if err != nil {
return BindedFiles{}, fmt.Errorf("failed to generate contract test binding: %w", err)
}
bindedFiles.ContractTest = contractTestBind
}

return bindedFiles, nil
}

// createPrecompileHook creates a bind hook for precompiled contracts.
Expand Down
2 changes: 1 addition & 1 deletion accounts/abi/bind/precompilebind/precompile_bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func golangBindingsFailure(t *testing.T) {
for i, tt := range bindFailedTests {
t.Run(tt.name, func(t *testing.T) {
// Generate the binding
_, _, _, err := PrecompileBind([]string{tt.name}, tt.abi, tt.bytecode, tt.fsigs, "bindtest", bind.LangGo, tt.libs, tt.aliases, "")
_, err := PrecompileBind([]string{tt.name}, tt.abi, tt.bytecode, tt.fsigs, "bindtest", bind.LangGo, tt.libs, tt.aliases, "", true)
if err == nil {
t.Fatalf("test %d: no error occurred but was expected", i)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// (c) 2019-2022, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package precompilebind

// tmplSourcePrecompileConfigGo is the Go precompiled config source template.
const tmplSourcePrecompileConfigTestGo = `
// Code generated
// This file is a generated precompile config test with the skeleton of test functions.
// The file is generated by a template. Please inspect every code and comment in this file before use.
package {{.Package}}
import (
"math/big"
"testing"
"github.com/ava-labs/subnet-evm/precompile/precompileconfig"
"github.com/ava-labs/subnet-evm/precompile/testutils"
{{- if .Contract.AllowList}}
"github.com/ava-labs/subnet-evm/precompile/allowlist"
"github.com/ethereum/go-ethereum/common"
{{- end}}
)
// TestVerify tests the verification of Config.
func TestVerify(t *testing.T) {
{{- if .Contract.AllowList}}
admins := []common.Address{allowlist.TestAdminAddr}
enableds := []common.Address{allowlist.TestEnabledAddr}
{{- end}}
tests := map[string]testutils.ConfigVerifyTest{
"valid config": {
Config: NewConfig(big.NewInt(3){{- if .Contract.AllowList}}, admins, enableds{{- end}}),
ExpectedError: "",
},
// CUSTOM CODE STARTS HERE
// Add your own Verify tests here, e.g.:
// "your custom test name": {
// Config: NewConfig(big.NewInt(3), {{- if .Contract.AllowList}} admins, enableds{{- end}}),
// ExpectedError: ErrYourCustomError.Error(),
// },
}
{{- if .Contract.AllowList}}
// Verify the precompile with the allowlist.
// This adds allowlist verify tests to your custom tests
// and runs them all together.
// Even if you don't add any custom tests, keep this. This will still
// run the default allowlist verify tests.
allowlist.VerifyPrecompileWithAllowListTests(t, Module, tests)
{{- else}}
// Run verify tests.
testutils.RunVerifyTests(t, tests)
{{- end}}
}
// TestEqual tests the equality of Config with other precompile configs.
func TestEqual(t *testing.T) {
{{- if .Contract.AllowList}}
admins := []common.Address{allowlist.TestAdminAddr}
enableds := []common.Address{allowlist.TestEnabledAddr}
{{- end}}
tests := map[string]testutils.ConfigEqualTest{
"non-nil config and nil other": {
Config: NewConfig(big.NewInt(3){{- if .Contract.AllowList}}, admins, enableds{{- end}}),
Other: nil,
Expected: false,
},
"different type": {
Config: NewConfig(big.NewInt(3){{- if .Contract.AllowList}}, admins, enableds{{- end}}),
Other: precompileconfig.NewNoopStatefulPrecompileConfig(),
Expected: false,
},
"different timestamp": {
Config: NewConfig(big.NewInt(3){{- if .Contract.AllowList}}, admins, enableds{{- end}}),
Other: NewConfig(big.NewInt(4){{- if .Contract.AllowList}}, admins, enableds{{- end}}),
Expected: false,
},
"same config": {
Config: NewConfig(big.NewInt(3){{- if .Contract.AllowList}}, admins, enableds{{- end}}),
Other: NewConfig(big.NewInt(3){{- if .Contract.AllowList}}, admins, enableds{{- end}}),
Expected: true,
},
// CUSTOM CODE STARTS HERE
// Add your own Equal tests here
}
{{- if .Contract.AllowList}}
// Run allow list equal tests.
// This adds allowlist equal tests to your custom tests
// and runs them all together.
// Even if you don't add any custom tests, keep this. This will still
// run the default allowlist equal tests.
allowlist.EqualPrecompileWithAllowListTests(t, Module, tests)
{{- else}}
// Run equal tests.
testutils.RunEqualTests(t, tests)
{{- end}}
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import (
)
{{$contract := .Contract}}
const (
// Gas costs for each function. These are set to 0 by default.
// Gas costs for each function. These are set to 1 by default.
// You should set a gas cost for each function in your contract.
// Generally, you should not set gas costs very low as this may cause your network to be vulnerable to DoS attacks.
// There are some predefined gas costs in contract/utils.go that you can use.
Expand All @@ -54,10 +54,10 @@ const (
// You should also increase gas costs of functions that read from AllowList storage.
{{- end}}
{{- range .Contract.Funcs}}
{{.Normalized.Name}}GasCost uint64 = 0 {{if not .Original.IsConstant | and $contract.AllowList}} + allowlist.ReadAllowListGasCost {{end}} // SET A GAS COST HERE
{{.Normalized.Name}}GasCost uint64 = 1 /* SET A GAS COST HERE */ {{if not .Original.IsConstant | and $contract.AllowList}} + allowlist.ReadAllowListGasCost {{end}}
{{- end}}
{{- if .Contract.Fallback}}
{{.Contract.Type}}FallbackGasCost uint64 = 0 // SET A GAS COST LESS THAN 2300 HERE
{{.Contract.Type}}FallbackGasCost uint64 = 1 // SET A GAS COST LESS THAN 2300 HERE
{{- end}}
)
Expand Down
142 changes: 142 additions & 0 deletions accounts/abi/bind/precompilebind/precompile_contract_test_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// (c) 2019-2022, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package precompilebind

// tmplSourcePrecompileConfigGo is the Go precompiled config source template.
const tmplSourcePrecompileContractTestGo = `
// Code generated
// This file is a generated precompile contract test with the skeleton of test functions.
// The file is generated by a template. Please inspect every code and comment in this file before use.
package {{.Package}}
import (
"testing"
"github.com/ava-labs/subnet-evm/core/state"
{{- if .Contract.AllowList}}
"github.com/ava-labs/subnet-evm/precompile/allowlist"
{{- end}}
"github.com/ava-labs/subnet-evm/precompile/testutils"
"github.com/ava-labs/subnet-evm/vmerrs"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
// TestRun tests the Run function of the precompile contract.
// These tests are run against the precompile contract directly with
// the given input and expected output. They're just a guide to
// help you write your own tests. These tests are for general cases like
// allowlist, readOnly behaviour, and gas cost. You should write your own
// tests for specific cases.
func TestRun(t *testing.T) {
tests := map[string]testutils.PrecompileTest{
{{- $contract := .Contract}}
{{- $structs := .Structs}}
{{- range .Contract.Funcs}}
{{- $func := .}}
{{- if $contract.AllowList}}
{{- $roles := mkList "NoRole" "Enabled" "Admin"}}
{{- range $role := $roles}}
{{- $fail := and (not $func.Original.IsConstant) (eq $role "NoRole")}}
"calling {{decapitalise $func.Normalized.Name}} from {{$role}} should {{- if $fail}} fail {{- else}} succeed{{- end}}": {
Caller: allowlist.Test{{$role}}Addr,
BeforeHook: allowlist.SetDefaultRoles(Module.Address),
InputFn: func(t testing.TB) []byte {
{{- if len $func.Normalized.Inputs | lt 1}}
// CUSTOM CODE STARTS HERE
// populate test input here
testInput := {{capitalise $func.Normalized.Name}}Input{}
input, err := Pack{{$func.Normalized.Name}}(testInput)
{{- else if len $func.Normalized.Inputs | eq 1 }}
{{- $input := index $func.Normalized.Inputs 0}}
// CUSTOM CODE STARTS HERE
// set test input to a value here
var testInput {{bindtype $input.Type $structs}}
input, err := Pack{{$func.Normalized.Name}}(testInput)
{{- else}}
input, err := Pack{{$func.Normalized.Name}}()
{{- end}}
require.NoError(t, err)
return input
},
{{- if not $fail}}
// This test is for a successful call. You can set the expected output here.
// CUSTOM CODE STARTS HERE
ExpectedRes: []byte{},
{{- end}}
SuppliedGas: {{$func.Normalized.Name}}GasCost,
ReadOnly: false,
ExpectedErr: {{if $fail}} ErrCannot{{$func.Normalized.Name}}.Error() {{- else}} "" {{- end}},
},
{{- end}}
{{- end}}
{{- if not $func.Original.IsConstant}}
"readOnly {{decapitalise $func.Normalized.Name}} should fail": {
Caller: common.Address{1},
InputFn: func(t testing.TB) []byte {
{{- if len $func.Normalized.Inputs | lt 1}}
// CUSTOM CODE STARTS HERE
// populate test input here
testInput := {{capitalise $func.Normalized.Name}}Input{}
input, err := Pack{{$func.Normalized.Name}}(testInput)
{{- else if len $func.Normalized.Inputs | eq 1 }}
{{- $input := index $func.Normalized.Inputs 0}}
// CUSTOM CODE STARTS HERE
// set test input to a value here
var testInput {{bindtype $input.Type $structs}}
input, err := Pack{{$func.Normalized.Name}}(testInput)
{{- else}}
input, err := Pack{{$func.Normalized.Name}}()
{{- end}}
require.NoError(t, err)
return input
},
SuppliedGas: {{$func.Normalized.Name}}GasCost,
ReadOnly: true,
ExpectedErr: vmerrs.ErrWriteProtection.Error(),
},
{{- end}}
"insufficient gas for {{decapitalise $func.Normalized.Name}} should fail": {
Caller: common.Address{1},
InputFn: func(t testing.TB) []byte {
{{- if len $func.Normalized.Inputs | lt 1}}
// CUSTOM CODE STARTS HERE
// populate test input here
testInput := {{capitalise $func.Normalized.Name}}Input{}
input, err := Pack{{$func.Normalized.Name}}(testInput)
{{- else if len $func.Normalized.Inputs | eq 1 }}
{{- $input := index $func.Normalized.Inputs 0}}
// CUSTOM CODE STARTS HERE
// set test input to a value here
var testInput {{bindtype $input.Type $structs}}
input, err := Pack{{$func.Normalized.Name}}(testInput)
{{- else}}
input, err := Pack{{$func.Normalized.Name}}()
{{- end}}
require.NoError(t, err)
return input
},
SuppliedGas: {{$func.Normalized.Name}}GasCost - 1,
ReadOnly: false,
ExpectedErr: vmerrs.ErrOutOfGas.Error(),
},
{{- end}}
}
{{- if .Contract.AllowList}}
// Run tests with allowlist tests.
// This adds allowlist run tests to your custom tests
// and runs them all together.
// Even if you don't add any custom tests, keep this. This will still
// run the default allowlist tests.
allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, tests)
{{- else}}
// Run tests.
for name, test := range tests {
t.Run(name, func(t *testing.T) {
test.Run(t, Module, state.NewTestStateDB(t))
})
}
{{- end}}
}
`
Loading

0 comments on commit 19e90a9

Please sign in to comment.