-
Notifications
You must be signed in to change notification settings - Fork 241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: precompile Solidity testing in pure Go #1234
Open
ARR4N
wants to merge
20
commits into
master
Choose a base branch
from
arr4n/precompile-go-tests
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
1065aed
feat: `dstest` Go package for parsing `DSTest` Solidity logs
ARR4N 63213be
feat: `evmsim` package
ARR4N 726f095
test(dstest): use `evmsim` to test parsing of `DSTest` logs
ARR4N be80db4
test(allowlist): port some of the precompile's Hardhat test to Go
ARR4N 64e7070
chore: fix linter issue caused by earlier refactoring
ARR4N 114005e
fix: revert deliberate breakage of `ExampleTxAllowListTest`
ARR4N ef5c48b
refactor: move genesis precompiles into `evmsim` constructor config
ARR4N 31459e8
feat: `scripts/abigen` command for cleaner `//go:generate` directives
ARR4N a55b0ff
refactor: remove duplicate `contract Example` and replace with a sing…
ARR4N 30931a4
fix: add regex anchor of double-quote to signal Go import
ARR4N ddc31fb
fix: escape . in regex
ARR4N b01791a
Merge branch 'master' into arr4n/precompile-go-tests
ARR4N 34f98fd
chore: CI check that `abigen` output is up to date
ARR4N 66e4cbd
fix(in theory): run `go generate` directly, without installing `abige…
ARR4N 6253369
chore: bump default `solc` to 0.8.26 to match `apt`
ARR4N 9f7aa8f
chore: run `abigen` update check inside precompile-test job
ARR4N d6e5780
I hate GitHub Actions. I'm not committing anything, just complaining.
ARR4N 06d91ec
fix: install `abigen` in CI
ARR4N dfc0694
fix: use `subnet-evm/cmd/abigen` instead of `geth` version in CI
ARR4N 93b6301
chore: `submodules: true` in `actions/checkout` config for `e2e_preco…
ARR4N File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[submodule "testing/dstest/internal/ds-test"] | ||
path = testing/dstest/internal/ds-test | ||
url = https://github.com/dapphub/ds-test.git |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package contracts | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/ava-labs/subnet-evm/params" | ||
"github.com/ava-labs/subnet-evm/testing/evmsim" | ||
) | ||
|
||
//go:generate sh -c "go run $(git rev-parse --show-toplevel)/scripts/abigen --solc.include-path=./node_modules --abigen.pkg=contracts contracts/**/*.sol > generated_test.go" | ||
|
||
func newEVMSim(tb testing.TB, genesis params.Precompiles) *evmsim.Backend { | ||
tb.Helper() | ||
|
||
cfg := &evmsim.Config{ | ||
GenesisPrecompiles: genesis, | ||
} | ||
return cfg.NewWithHexKeys(tb, keys()) | ||
} | ||
|
||
// keys returns the hex-encoded private keys of the testing accounts; these | ||
// identically match the accounts used in the Hardhat config. | ||
func keys() []string { | ||
return []string{ | ||
"0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027", | ||
"0x7b4198529994b0dc604278c99d153cfd069d594753d471171a1d102a10438e07", | ||
"0x15614556be13730e9e8d6eacc1603143e7b96987429df8726384c2ec4502ef6e", | ||
"0x31b571bf6894a248831ff937bb49f7754509fe93bbd2517c9c73c4144c0e97dc", | ||
"0x6934bef917e01692b789da754a0eae31a8536eb465e7bff752ea291dad88c675", | ||
"0xe700bdbdbc279b808b1ec45f8c2370e4616d3a02c336e68d85d4668e08f53cff", | ||
"0xbbc2865b76ba28016bc2255c7504d000e046ae01934b04c694592a6276988630", | ||
"0xcdbfd34f687ced8c6968854f8a99ae47712c4f4183b78dcc4a903d1bfe8cbf60", | ||
"0x86f78c5416151fe3546dece84fda4b4b1e36089f2dbc48496faf3a950f16157c", | ||
"0x750839e9dbbd2a0910efe40f50b2f3b2f2f59f5580bb4b83bd8c1201cf9a010a", | ||
} | ||
} | ||
|
||
// Convenience labels for using an account by name instead of number. | ||
const ( | ||
admin = iota | ||
_ | ||
_ | ||
_ | ||
_ | ||
_ | ||
_ | ||
_ | ||
_ | ||
allowlistOther | ||
) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package contracts | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/ava-labs/subnet-evm/core/types" | ||
"github.com/ava-labs/subnet-evm/params" | ||
"github.com/ava-labs/subnet-evm/testing/dstest" | ||
"github.com/ava-labs/subnet-evm/testing/evmsim" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" | ||
_ "github.com/ava-labs/subnet-evm/precompile/registry" | ||
) | ||
|
||
func TestAllowList(t *testing.T) { | ||
// This is a demonstration of a Go implementation of the Hardhat tests: | ||
// https://github.com/ava-labs/subnet-evm/blob/dc1d78da/contracts/test/tx_allow_list.ts | ||
|
||
ctx := context.Background() | ||
|
||
sim := newEVMSim(t, params.Precompiles{ | ||
txallowlist.ConfigKey: txallowlist.NewConfig( | ||
new(uint64), | ||
[]common.Address{common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC")}, | ||
nil, nil, | ||
), | ||
}) | ||
|
||
allow := evmsim.Bind(t, sim, NewIAllowList, txallowlist.ContractAddress) | ||
allowSess := &IAllowListSession{ | ||
Contract: allow, | ||
TransactOpts: *sim.From(admin), | ||
} | ||
|
||
sutAddr, sut := evmsim.Deploy(t, sim, admin, DeployExampleTxAllowListTest) | ||
sutSess := &ExampleTxAllowListTestSession{ | ||
Contract: sut, | ||
TransactOpts: *sim.From(admin), | ||
} | ||
parser := dstest.New(sutAddr) | ||
|
||
_, err := allowSess.SetAdmin(sutAddr) | ||
require.NoErrorf(t, err, "%T.SetAdmin(%T address)", allow, sut) | ||
|
||
_, err = sutSess.SetUp() | ||
require.NoErrorf(t, err, "%T.SetUp()", sut) | ||
|
||
// TODO: This table of steps is purely to demonstrate a *direct* | ||
// reimplementation of the Hardhat tests in Go. I (arr4n) believe we should | ||
// refactor the tests before a complete translation, primarily to reduce the | ||
// number of calls that have to happen here. Also note that the original | ||
// tests use a `beforeEach()` whereas the above preamble is equivalent to a | ||
// `beforeAll()`. | ||
for _, step := range []struct { | ||
name string | ||
fn (func() (*types.Transaction, error)) | ||
}{ | ||
{"should add contract deployer as admin", sutSess.StepContractOwnerIsAdmin}, | ||
{"precompile should see admin address has admin role", sutSess.StepPrecompileHasDeployerAsAdmin}, | ||
{"precompile should see test address has no role", sutSess.StepNewAddressHasNoRole}, | ||
} { | ||
t.Run(step.name, func(t *testing.T) { | ||
tx, err := step.fn() | ||
require.NoError(t, err, "running step") | ||
|
||
// TODO(arr4n): DSTest can be used for general logging, so the | ||
// following pattern is only valid when we don't use it as such. | ||
// Failing assertions, however, set a private `failed` boolean, | ||
// which we need to expose. Alternatively, they also hook into HEVM | ||
// cheatcodes if they're implemented; having these would be useful | ||
// for testing in general. | ||
failures := parser.ParseTB(ctx, t, tx, sim) | ||
if len(failures) > 0 { | ||
t.Errorf("Assertion failed:\n%s", failures) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// The abigen command runs `solc | abigen` and writes the generated bindings to | ||
// stdout. | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"flag" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"regexp" | ||
) | ||
|
||
func main() { | ||
var c config | ||
|
||
flag.StringVar(&c.solc.version, "solc.version", "0.8.24", "Version of solc expected; the version used will be sourced from $PATH") | ||
flag.StringVar(&c.solc.evmVersion, "solc.evm-version", "paris", "solc --evm-version flag") | ||
flag.StringVar(&c.solc.basePath, "solc.base-path", "./", "solc --base-path flag") | ||
flag.StringVar(&c.solc.includePath, "solc.include-path", "", "solc --include-path flag; only propagated if not empty") | ||
flag.StringVar(&c.solc.output, "solc.output", "abi,bin", "solc --combined-json flag") | ||
flag.StringVar(&c.abigen.pkg, "abigen.pkg", "", "abigen --pkg flag") | ||
|
||
help := flag.Bool("help", false, "Print usage message") | ||
flag.Parse() | ||
if *help { | ||
flag.Usage() | ||
os.Exit(0) | ||
} | ||
|
||
if err := c.run(context.Background(), os.Stdout, os.Stderr); err != nil { | ||
fmt.Fprint(os.Stderr, err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
type config struct { | ||
solc struct { | ||
version, evmVersion, basePath, includePath, output string | ||
} | ||
abigen struct { | ||
pkg string | ||
} | ||
} | ||
|
||
func (cfg *config) run(ctx context.Context, stdout, stderr io.Writer) error { | ||
solcV := exec.CommandContext(ctx, "solc", "--version") | ||
buf, err := solcV.CombinedOutput() | ||
if err != nil { | ||
return fmt.Errorf("solc --version: %v", err) | ||
} | ||
if !bytes.Contains(buf, []byte(cfg.solc.version)) { | ||
fmt.Fprintf(stderr, "solc --version:\n%s", buf) | ||
return fmt.Errorf("solc version mismatch; not %q", cfg.solc.version) | ||
} | ||
|
||
args := append(nonEmptyArgs(map[string]string{ | ||
"--evm-version": cfg.solc.evmVersion, | ||
"--base-path": cfg.solc.basePath, | ||
"--include-path": cfg.solc.includePath, | ||
"--combined-json": cfg.solc.output, | ||
}), flag.Args()...) | ||
|
||
solc := exec.CommandContext(ctx, "solc", args...) | ||
// Although we could use io.Pipe(), it's much easier to reason about | ||
// non-concurrent processes, and solc doesn't create huge outputs. | ||
var solcOut bytes.Buffer | ||
solc.Stdout = &solcOut | ||
solc.Stderr = stderr | ||
if err := solc.Run(); err != nil { | ||
return fmt.Errorf("solc: %w", err) | ||
} | ||
|
||
abigen := exec.CommandContext(ctx, "abigen", nonEmptyArgs(map[string]string{ | ||
"--combined-json": "-", // stdin | ||
"--pkg": cfg.abigen.pkg, | ||
})...) | ||
abigen.Stdin = &solcOut | ||
var abigenOut bytes.Buffer | ||
abigen.Stdout = &abigenOut | ||
abigen.Stderr = stderr | ||
if err := abigen.Run(); err != nil { | ||
return fmt.Errorf("abigen: %w", err) | ||
} | ||
|
||
re := regexp.MustCompile(`github.com/ethereum/go-ethereum/(accounts|core)/`) | ||
Check failure Code scanning / CodeQL Incomplete regular expression for hostnames High
This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when the regular expression is used.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
_, err = stdout.Write(re.ReplaceAll( | ||
abigenOut.Bytes(), | ||
[]byte(`github.com/ava-labs/subnet-evm/${1}/`)), | ||
) | ||
return err | ||
} | ||
|
||
// nonEmptyArgs returns a slice of arguments suitable for use with exec.Command. | ||
// Any empty values are skipped. | ||
func nonEmptyArgs(from map[string]string) []string { | ||
var args []string | ||
for k, v := range from { | ||
if v != "" { | ||
args = append(args, k, v) | ||
} | ||
} | ||
return args | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.0; | ||
|
||
import {DSTest} from "ds-test/src/test.sol"; | ||
|
||
contract FakeTest is DSTest { | ||
event NotFromDSTest(); | ||
|
||
function logNonDSTest() external { | ||
emit NotFromDSTest(); | ||
} | ||
|
||
function logString(string memory s) external { | ||
emit log(s); | ||
} | ||
|
||
function logNamedAddress(string memory name, address addr) external { | ||
emit log_named_address(name, addr); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Package dstest implements parsing of [DSTest] Solidity-testing errors. | ||
// | ||
// [DSTest]: https://github.com/dapphub/ds-test | ||
package dstest | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/ava-labs/subnet-evm/accounts/abi" | ||
"github.com/ava-labs/subnet-evm/accounts/abi/bind" | ||
"github.com/ava-labs/subnet-evm/core/types" | ||
"github.com/ava-labs/subnet-evm/testing/dstest/internal/dstestbindings" | ||
"github.com/ethereum/go-ethereum/common" | ||
) | ||
|
||
// New returns a new `Parser` with the provided addresses already | ||
// `Register()`ed. | ||
func New(tests ...common.Address) *Parser { | ||
p := &Parser{ | ||
tests: make(map[common.Address]bool), | ||
} | ||
for _, tt := range tests { | ||
p.Register(tt) | ||
} | ||
return p | ||
} | ||
|
||
// A Parser inspects transaction logs of `Register()`ed test addresses, parsing | ||
// those that correspond to [DSTest] error logs. | ||
// | ||
// [DSTest]: https://github.com/dapphub/ds-test | ||
type Parser struct { | ||
tests map[common.Address]bool | ||
} | ||
|
||
// Register marks the provided `Address` as being a test that inherits from the | ||
// [DSTest contract]. | ||
// | ||
// [DSTest contract]: https://github.com/dapphub/ds-test/blob/master/src/test.sol | ||
func (p *Parser) Register(test common.Address) { | ||
p.tests[test] = true | ||
} | ||
|
||
// A Log represents a Solidity event emitted by the `DSTest` contract. Although | ||
// all assertion failures result in an event, not all logged events correspond | ||
// to failures. | ||
type Log struct { | ||
unpacked map[string]any | ||
} | ||
|
||
// String returns `l` as a human-readable string. | ||
// | ||
// The format is not guaranteed to be stable and the returned value SHOULD NOT | ||
// be parsed. | ||
func (l Log) String() string { | ||
switch u := l.unpacked; len(u) { | ||
case 1: | ||
return fmt.Sprintf("%v", u["arg0"]) | ||
case 2: | ||
return fmt.Sprintf("%s = %v", u["key"], u["val"]) | ||
case 3: | ||
return fmt.Sprintf("%s = %v (%v decimals)", u["key"], u["val"], u["decimals"]) | ||
default: | ||
// The above cases are exhaustive at the time of writing; if the default | ||
// is reached then they need to be updated. | ||
return fmt.Sprintf("%+v", u) | ||
} | ||
} | ||
|
||
type Logs []*Log | ||
|
||
// String() returns `ls` as a human-readable string. | ||
func (ls Logs) String() string { | ||
s := make([]string, len(ls)) | ||
for i, l := range ls { | ||
s[i] = l.String() | ||
} | ||
return strings.Join(s, "\n") | ||
} | ||
|
||
// Parse finds all [types.Log]s emitted by test contracts in the provided | ||
// `Transaction`, filters them to keep only those corresponding to `DSTest` | ||
// events, and returns the unpacked data. | ||
func (p *Parser) Parse(ctx context.Context, tx *types.Transaction, b bind.DeployBackend) (Logs, error) { | ||
r, err := b.TransactionReceipt(ctx, tx.Hash()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var logs []*Log | ||
for _, l := range r.Logs { | ||
if !p.tests[l.Address] { | ||
continue | ||
} | ||
ev, err := dstestbindings.EventByID(l.Topics[0]) | ||
if err != nil /* not found */ { | ||
continue | ||
} | ||
|
||
l, err := unpack(ev, l) | ||
if err != nil { | ||
return nil, err | ||
} | ||
logs = append(logs, l) | ||
} | ||
return logs, nil | ||
} | ||
|
||
func unpack(ev *abi.Event, l *types.Log) (*Log, error) { | ||
unpacked := make(map[string]any) | ||
if err := dstestbindings.UnpackLogIntoMap(unpacked, ev.Name, *l); err != nil { | ||
return nil, err | ||
} | ||
return &Log{unpacked}, nil | ||
} | ||
|
||
// ParseTB is identical to [Parse] except that it reports all errors on | ||
// [testing.TB.Fatal]. | ||
func (p *Parser) ParseTB(ctx context.Context, tb testing.TB, tx *types.Transaction, b bind.DeployBackend) Logs { | ||
tb.Helper() | ||
l, err := p.Parse(ctx, tx, b) | ||
if err != nil { | ||
tb.Fatalf("%T.Parse(): %v", p, err) | ||
} | ||
return l | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Missing regular expression anchor High
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed