Skip to content

Commit

Permalink
key handling (#6)
Browse files Browse the repository at this point in the history
* --dev flag replaced with --deploy flag
* relayer executable now requires a key file
* various key and tx reuse issues in tests fixed
  • Loading branch information
dimalinux authored Nov 18, 2022
1 parent 96c85be commit 3dac163
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 131 deletions.
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
bin/
contracts/abi
contracts/bin
/bin/
/contracts/abi
/contracts/bin
eth.key
18 changes: 14 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
GOPATH ?= $(shell go env GOPATH)

.PHONY: build
build:
mkdir -p bin
GOBIN="$(CURDIR)/bin/" go install ./cmd/...

.PHONY: lint
lint:
bash scripts/install-lint.sh
${GOPATH}/bin/golangci-lint run

.PHONY: format
format:
go fmt ./...

.PHONY: test
test:
go test ./...
go test -count=1 ./...

build:
mkdir -p bin
GOBIN="$(CURDIR)/bin/" go install ./cmd/...
.PHONY: clean
clean:
rm -rf bin
43 changes: 24 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,52 @@
[EIP2771-compatible](https://eips.ethereum.org/EIPS/eip-2771) transaction relayer library for Ethereum. The core of the library is bindings to a compatible forwarder contract.

Components:
- `cmd`: example CLI that uses examples/minimal_forwarder
- `examples`: example implementations of forwarder and recipient contracts, as well as Go bindings, required interface implementations, and unit tests
- `examples/gsn_forwarder`: an implementation using OpenGSN's [forwarder contract](https://github.com/opengsn/gsn/tree/master/packages/contracts/src/forwarder).
- `examples/minimal_forwarder`: an implementation using OpenZeppelin's [minimal forwarder](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/MinimalForwarder.sol).
- `examples/mock_recipient`: an implementation of a EIP2771-compatible recipient contract which can receive calls from a forwarder.
- `cmd/relayer`: example CLI that uses impls/minimal_forwarder
- `impls`: example implementations of forwarder and recipient contracts, as well as Go
bindings, required interface implementations, and unit tests
- `impls/gsn_forwarder`: an implementation using OpenGSN's [forwarder contract](https://github.com/opengsn/gsn/tree/master/packages/contracts/src/forwarder).
- `impls/minimal_forwarder`: an implementation using OpenZeppelin's [minimal forwarder](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/MinimalForwarder.sol).
- `examples/mock_recipient`: an implementation of a EIP2771-compatible recipient contract
which can receive calls from a forwarder.
- `relayer`: functionality to call forwarder contracts
- `rpc`: rpc server for an end-user to submit txs to, accepts a `*relayer.Relayer`

## Requirements

- go 1.19+
- abigen: can install with `bash scripts/install-abigen.sh`
- abigen: can install using `./scripts/install-abigen.sh`

## Usage

### As an application

See `cmd/main.go` for an example app using the `examples/minimal_forwarder` package.
See `cmd/relayer/main.go` for an example app using the `impls/gsnforwarder` package.

The app can be built using `make build`.

To run it in dev mode (which auto-deploys a forwarder contract for you):

First, install and run ganache using:
```
NODE_OPTIONS="--max_old_space_size=8192" ganache --deterministic --accounts=50
ganache --deterministic --accounts=50
```

Create a key file using a deterministic ganache key
```bash
echo "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" > eth.key
```

Then:
To run and automatically deploy the relayer contract to the ganache network, you can use:
```bash
$ ./bin/relayer --dev
2022-09-24T07:43:32.499-0400 INFO cmd cmd/main.go:131 starting relayer with ethereum endpoint http://localhost:8545 and chain ID 1337
2022-09-24T07:43:32.541-0400 INFO cmd cmd/main.go:207 deployed MinimalForwarder.sol to 0x8E7a8d3CAeEbbe9A92faC4db19424218aE6791a3
$ ./bin/relayer --deploy
2022-09-24T07:43:32.499-0400 INFO main cmd/main.go:131 starting relayer with ethereum endpoint http://localhost:8545 and chain ID 1337
2022-09-24T07:43:32.541-0400 INFO main cmd/main.go:207 deployed Forwarder.sol to 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
2022-09-24T07:43:32.542-0400 INFO rpc rpc/server.go:62 starting RPC server on http://localhost:7799
```

By default, the relayer server runs on port `7799`.

### Implementing a custom forwarder

See the forwarder examples in `examples/` for a full implementation.
See the forwarder examples in `impls/` for a full implementation.

There are three main components needed:
- Go bindings to the forwarder contract (generated by abigen)
Expand All @@ -54,8 +59,8 @@ There are three main components needed:
// Forwarder must be implemented by a forwarder contract used by a *relayer.Relayer.
// These methods are wrappers around the methods auto-generated by abigen.
//
// See `examples/gsn_forwarder/i_forwarder_wrapped.go` or
// `examples/minimal_forwarder/i_minimal_forwarder_wrapped.go`for examples.
// See `impls/gsn_forwarder/i_forwarder_wrapped.go` or
// `impls/minimal_forwarder/i_minimal_forwarder_wrapped.go`for examples.
type Forwarder interface {
GetNonce(opts *bind.CallOpts, from ethcommon.Address) (*big.Int, error)

Expand All @@ -80,7 +85,7 @@ type Forwarder interface {

// ForwardRequest must be implemented by a request type used by a forwarder contract.
//
// See `examples/gsn_forwarder/request.go` or `examples/minimal_forwarder/request.go`
// See `impls/gsn_forwarder/request.go` or `impls/minimal_forwarder/request.go`
// for examples.
type ForwardRequest interface {
// FromSubmitTransactionRequest set the type underlying the ForwardRequest
Expand Down Expand Up @@ -127,4 +132,4 @@ func (s *RelayerService) SubmitTransaction(
) error
```

See [go-relayer-client](https://github.com/AthanorLabs/go-relayer-client) for client library and example usage.
See [go-relayer-client](https://github.com/AthanorLabs/go-relayer-client) for client library and example usage.
42 changes: 21 additions & 21 deletions cmd/relayer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ const (
flagForwarderAddress = "forwarder-address"
flagKey = "key"
flagRPCPort = "rpc-port"
flagDev = "dev"
flagLog = "log"

defaultGanacheKey = "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
flagDeploy = "deploy"
flagLog = "log-level"
)

var (
Expand All @@ -43,11 +41,6 @@ var (
Value: "http://localhost:8545",
Usage: "Ethereum RPC endpoint",
},
&cli.StringFlag{
Name: flagForwarderAddress,
Value: "",
Usage: "Forwarder contract address",
},
&cli.StringFlag{
Name: flagKey,
Value: "eth.key",
Expand All @@ -59,8 +52,12 @@ var (
Usage: "Relayer RPC server port",
},
&cli.BoolFlag{
Name: flagDev,
Usage: "Use development configuration and deploy forwarder contract",
Name: flagDeploy,
Usage: "Deploy an instance of the forwarder contract",
},
&cli.StringFlag{
Name: flagForwarderAddress,
Usage: "Forwarder contract address",
},
&cli.StringFlag{
Name: flagLog,
Expand Down Expand Up @@ -115,8 +112,6 @@ func run(c *cli.Context) error {
}

port := uint16(c.Uint(flagRPCPort))
dev := c.Bool(flagDev)

endpoint := c.String(flagEndpoint)
ec, err := ethclient.Dial(endpoint)
if err != nil {
Expand All @@ -133,13 +128,23 @@ func run(c *cli.Context) error {

log.Infof("starting relayer with ethereum endpoint %s and chain ID %s", endpoint, chainID)

key, err := getPrivateKey(c.String(flagKey), dev)
key, err := getPrivateKey(c.String(flagKey))
if err != nil {
return err
}

contractAddr := c.String(flagForwarderAddress)
deploy := c.Bool(flagDeploy)
contractSet := contractAddr != ""
if deploy && contractSet {
return fmt.Errorf("flags --%s and --%s are mutually exclusive", flagDeploy, flagForwarderAddress)
}
if !deploy && !contractSet {
return fmt.Errorf("either --%s or --%s is required", flagDeploy, flagForwarderAddress)
}

forwarder, err := deployOrGetForwarder(
c.String(flagForwarderAddress),
contractAddr,
ec,
key,
chainID,
Expand Down Expand Up @@ -215,11 +220,7 @@ func deployOrGetForwarder(
return contracts.NewIForwarder(ethcommon.HexToAddress(addressString), ec)
}

func getPrivateKey(keyFile string, dev bool) (*common.Key, error) {
if dev {
return common.NewKeyFromPrivateKeyString(defaultGanacheKey)
}

func getPrivateKey(keyFile string) (*common.Key, error) {
if keyFile != "" {
fileData, err := os.ReadFile(filepath.Clean(keyFile))
if err != nil {
Expand All @@ -228,6 +229,5 @@ func getPrivateKey(keyFile string, dev bool) (*common.Key, error) {
keyHex := strings.TrimSpace(string(fileData))
return common.NewKeyFromPrivateKeyString(keyHex)
}

return nil, errNoEthereumPrivateKey
}
13 changes: 5 additions & 8 deletions examples/mock_recipient/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/athanorlabs/go-relayer/common"
"github.com/athanorlabs/go-relayer/impls/mforwarder"
"github.com/athanorlabs/go-relayer/tests"
)

// NODE_OPTIONS="--max_old_space_size=8192" ganache --deterministic --accounts=50
Expand Down Expand Up @@ -48,14 +49,12 @@ func TestMock_Execute(t *testing.T) {

address, tx, contract, err := mforwarder.DeployMinimalForwarder(auth, conn)
require.NoError(t, err)
receipt, err := bind.WaitMined(context.Background(), conn, tx)
require.NoError(t, err)
receipt := tests.MineTransaction(t, conn, tx)
t.Logf("gas cost to deploy MinimalForwarder.sol: %d", receipt.GasUsed)

mockAddress, mockTx, _, err := DeployMock(auth, conn, address)
require.NoError(t, err)
receipt, err = bind.WaitMined(context.Background(), conn, mockTx)
require.NoError(t, err)
receipt = tests.MineTransaction(t, conn, mockTx)
t.Logf("gas cost to deploy Mock.sol: %d", receipt.GasUsed)

// transfer to Mock.sol
Expand All @@ -75,8 +74,7 @@ func TestMock_Execute(t *testing.T) {
require.NoError(t, err)
err = conn.SendTransaction(context.Background(), transferTx)
require.NoError(t, err)
_, err = bind.WaitMined(context.Background(), conn, transferTx)
require.NoError(t, err)
tests.MineTransaction(t, conn, transferTx)

// generate ForwardRequest and sign it
key := common.NewKeyFromPrivateKey(pk)
Expand Down Expand Up @@ -125,8 +123,7 @@ func TestMock_Execute(t *testing.T) {
// execute withdraw() via forwarder
tx, err = contract.Execute(auth, *req, sig)
require.NoError(t, err)
receipt, err = bind.WaitMined(context.Background(), conn, tx)
require.NoError(t, err)
receipt = tests.MineTransaction(t, conn, tx)
t.Logf("gas cost to call Mock.withdraw() via MinimalForwarder.execute(): %d", receipt.GasUsed)
require.Equal(t, uint64(1), receipt.Status)
require.Equal(t, 1, len(receipt.Logs))
Expand Down
53 changes: 14 additions & 39 deletions impls/gsnforwarder/forwarder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,36 @@ package gsnforwarder

import (
"context"
"crypto/ecdsa"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"

"github.com/athanorlabs/go-relayer/common"
"github.com/athanorlabs/go-relayer/tests"
)

// NODE_OPTIONS="--max_old_space_size=8192" ganache --deterministic --accounts=50
var ganachePrivateKeys = []string{
"4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d",
"6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1",
}

func setupAuth(t *testing.T) (*bind.TransactOpts, *ethclient.Client, *ecdsa.PrivateKey) {
ec, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
t.Cleanup(func() {
ec.Close()
})
chainID, err := ec.ChainID(context.Background())
require.NoError(t, err)

pk, err := ethcrypto.HexToECDSA(ganachePrivateKeys[0])
require.NoError(t, err)

auth, err := bind.NewKeyedTransactorWithChainID(pk, chainID)
require.NoError(t, err)
return auth, ec, pk
}

func TestForwarder_Verify(t *testing.T) {
auth, conn, pk := setupAuth(t)
chainID, err := conn.ChainID(context.Background())
require.NoError(t, err)
ec, chainID := tests.NewEthClient(t)
pk := tests.GetTestKeyByIndex(t, 0)
auth := tests.NewTXOpts(t, ec, pk)

address, tx, contract, err := DeployForwarder(auth, conn)
address, tx, contract, err := DeployForwarder(auth, ec)
require.NoError(t, err)
require.NotEqual(t, ethcommon.Address{}, address)
require.NotNil(t, tx)
require.NotNil(t, contract)
receipt, err := bind.WaitMined(context.Background(), conn, tx)
require.NoError(t, err)
receipt := tests.MineTransaction(t, ec, tx)
t.Logf("gas cost to deploy MinimalForwarder.sol: %d", receipt.GasUsed)

key := common.NewKeyFromPrivateKey(pk)

auth = tests.NewTXOpts(t, ec, pk)
tx, err = contract.RegisterDomainSeparator(auth, DefaultName, DefaultVersion)
require.NoError(t, err)
receipt, err = bind.WaitMined(context.Background(), conn, tx)
require.NoError(t, err)
receipt = tests.MineTransaction(t, ec, tx)
t.Logf("gas cost to call RegisterDomainSeparator: %d", receipt.GasUsed)

req := &IForwarderForwardRequest{
Expand Down Expand Up @@ -99,13 +73,14 @@ func TestForwarder_Verify(t *testing.T) {
}

func TestForwarder_IsIForwarder(t *testing.T) {
auth, conn, _ := setupAuth(t)
ec, _ := tests.NewEthClient(t)
pk := tests.GetTestKeyByIndex(t, 0)
auth := tests.NewTXOpts(t, ec, pk)

address, tx, _, err := DeployForwarder(auth, conn)
require.NoError(t, err)
_, err = bind.WaitMined(context.Background(), conn, tx)
address, tx, _, err := DeployForwarder(auth, ec)
require.NoError(t, err)
tests.MineTransaction(t, ec, tx)

_, err = NewIForwarder(address, conn)
_, err = NewIForwarder(address, ec)
require.NoError(t, err)
}
Loading

0 comments on commit 3dac163

Please sign in to comment.