Skip to content
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
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 Jul 12, 2024
63213be
feat: `evmsim` package
ARR4N Jul 12, 2024
726f095
test(dstest): use `evmsim` to test parsing of `DSTest` logs
ARR4N Jul 12, 2024
be80db4
test(allowlist): port some of the precompile's Hardhat test to Go
ARR4N Jul 12, 2024
64e7070
chore: fix linter issue caused by earlier refactoring
ARR4N Jul 12, 2024
114005e
fix: revert deliberate breakage of `ExampleTxAllowListTest`
ARR4N Jul 12, 2024
ef5c48b
refactor: move genesis precompiles into `evmsim` constructor config
ARR4N Jul 16, 2024
31459e8
feat: `scripts/abigen` command for cleaner `//go:generate` directives
ARR4N Jul 16, 2024
a55b0ff
refactor: remove duplicate `contract Example` and replace with a sing…
ARR4N Jul 16, 2024
30931a4
fix: add regex anchor of double-quote to signal Go import
ARR4N Jul 16, 2024
ddc31fb
fix: escape . in regex
ARR4N Jul 16, 2024
b01791a
Merge branch 'master' into arr4n/precompile-go-tests
ARR4N Jul 16, 2024
34f98fd
chore: CI check that `abigen` output is up to date
ARR4N Jul 16, 2024
66e4cbd
fix(in theory): run `go generate` directly, without installing `abige…
ARR4N Jul 16, 2024
6253369
chore: bump default `solc` to 0.8.26 to match `apt`
ARR4N Jul 16, 2024
9f7aa8f
chore: run `abigen` update check inside precompile-test job
ARR4N Jul 16, 2024
d6e5780
I hate GitHub Actions. I'm not committing anything, just complaining.
ARR4N Jul 16, 2024
06d91ec
fix: install `abigen` in CI
ARR4N Jul 16, 2024
dfc0694
fix: use `subnet-evm/cmd/abigen` instead of `geth` version in CI
ARR4N Jul 17, 2024
93b6301
chore: `submodules: true` in `actions/checkout` config for `e2e_preco…
ARR4N Jul 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Set up Go
uses: actions/setup-go@v5
with:
Expand All @@ -92,6 +93,18 @@ jobs:
- name: Hardhat Compile
run: npx hardhat compile
working-directory: ./contracts
- name: Install solc
shell: bash
run: sudo add-apt-repository ppa:ethereum/ethereum && sudo apt update && sudo apt install -y solc
- name: Install abigen
shell: bash
run: go install github.com/ava-labs/subnet-evm/cmd/abigen
- name: Generate abigen bindings
shell: bash
run: go generate ./contracts/... ./testing/...
- name: Confirm abigen bindings up to date
shell: bash
run: .github/workflows/check-clean-branch.sh
- name: Install AvalancheGo Release
shell: bash
run: BASEDIR=/tmp/e2e-test AVALANCHEGO_BUILD_PATH=/tmp/e2e-test/avalanchego ./scripts/install_avalanchego_release.sh
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
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
5 changes: 5 additions & 0 deletions contracts/contracts/Empty.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @dev Noop contract that can be deployed by example / test contracts.
contract Empty {}
6 changes: 2 additions & 4 deletions contracts/contracts/ExampleDeployerList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/IAllowList.sol";
import {Empty} from "./Empty.sol";
import "./AllowList.sol";

address constant DEPLOYER_LIST = 0x0200000000000000000000000000000000000000;
Expand All @@ -14,9 +15,6 @@ contract ExampleDeployerList is AllowList {
constructor() AllowList(DEPLOYER_LIST) {}

function deployContract() public {
new Example();
new Empty();
}
}

// This is an empty contract that can be used to test contract deployment
contract Example {}
5 changes: 2 additions & 3 deletions contracts/contracts/ExampleTxAllowList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.24;

import "./AllowList.sol";
import {Empty} from "./Empty.sol";
import "./interfaces/IAllowList.sol";

// Precompiled Allow List Contract Address
Expand All @@ -13,8 +14,6 @@ contract ExampleTxAllowList is AllowList {
constructor() AllowList(TX_ALLOW_LIST) {}

function deployContract() public {
new Example();
new Empty();
}
}

contract Example {}
50 changes: 50 additions & 0 deletions contracts/contracts_test.go
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
)
28,701 changes: 28,701 additions & 0 deletions contracts/generated_test.go

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions contracts/txallowlist_test.go
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)
}
})
}
}
106 changes: 106 additions & 0 deletions scripts/abigen/abigen.go
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.26", "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)/`)
_, 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
}
20 changes: 20 additions & 0 deletions testing/dstest/FakeTest.t.sol
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);
}
}
Loading
Loading