Skip to content

Commit

Permalink
Merge pull request #1999 from OffchainLabs/external_signer
Browse files Browse the repository at this point in the history
Add e2e test for external signer, fix access list for external signer, don't require account for sequencer if batchposter with external signer is enabled
  • Loading branch information
anodar authored Dec 6, 2023
2 parents d01eb16 + 0160fe6 commit 6da8981
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 13 deletions.
22 changes: 12 additions & 10 deletions arbnode/batch_poster.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,6 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e
bridgeAddr: opts.DeployInfo.Bridge,
daWriter: opts.DAWriter,
redisLock: redisLock,
accessList: func(SequencerInboxAccs, AfterDelayedMessagesRead int) types.AccessList {
return AccessList(&AccessListOpts{
SequencerInboxAddr: opts.DeployInfo.SequencerInbox,
DataPosterAddr: opts.TransactOpts.From,
BridgeAddr: opts.DeployInfo.Bridge,
GasRefunderAddr: opts.Config().gasRefunder,
SequencerInboxAccs: SequencerInboxAccs,
AfterDelayedMessagesRead: AfterDelayedMessagesRead,
})
},
}
dataPosterConfigFetcher := func() *dataposter.DataPosterConfig {
return &(opts.Config().DataPoster)
Expand All @@ -298,6 +288,18 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e
if err != nil {
return nil, err
}
// Dataposter sender may be external signer address, so we should initialize
// access list after initializing dataposter.
b.accessList = func(SequencerInboxAccs, AfterDelayedMessagesRead int) types.AccessList {
return AccessList(&AccessListOpts{
SequencerInboxAddr: opts.DeployInfo.SequencerInbox,
DataPosterAddr: b.dataPoster.Sender(),
BridgeAddr: opts.DeployInfo.Bridge,
GasRefunderAddr: opts.Config().gasRefunder,
SequencerInboxAccs: SequencerInboxAccs,
AfterDelayedMessagesRead: AfterDelayedMessagesRead,
})
}
return b, nil
}

Expand Down
5 changes: 4 additions & 1 deletion cmd/nitro/nitro.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,10 @@ func mainImpl() int {
var dataSigner signature.DataSignerFunc
var l1TransactionOptsValidator *bind.TransactOpts
var l1TransactionOptsBatchPoster *bind.TransactOpts
sequencerNeedsKey := (nodeConfig.Node.Sequencer && !nodeConfig.Node.Feed.Output.DisableSigning) || nodeConfig.Node.BatchPoster.Enable
// If sequencer and signing is enabled or batchposter is enabled without
// external signing sequencer will need a key.
sequencerNeedsKey := (nodeConfig.Node.Sequencer && !nodeConfig.Node.Feed.Output.DisableSigning) ||
(nodeConfig.Node.BatchPoster.Enable && nodeConfig.Node.BatchPoster.DataPoster.ExternalSigner.URL == "")
validatorNeedsKey := nodeConfig.Node.Staker.OnlyCreateWalletContract || nodeConfig.Node.Staker.Enable && !strings.EqualFold(nodeConfig.Node.Staker.Strategy, "watchtower")

l1Wallet.ResolveDirectoryNames(nodeConfig.Persistent.Chain)
Expand Down
55 changes: 53 additions & 2 deletions system_tests/batch_poster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ import (
"crypto/rand"
"fmt"
"math/big"
"net/http"
"strings"
"testing"
"time"

"github.com/andybalholm/brotli"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"

"github.com/offchainlabs/nitro/arbnode"
"github.com/offchainlabs/nitro/solgen/go/bridgegen"
"github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen"
"github.com/offchainlabs/nitro/util/redisutil"
)

Expand All @@ -27,10 +33,50 @@ func TestRedisBatchPosterParallel(t *testing.T) {
testBatchPosterParallel(t, true)
}

func addNewBatchPoster(ctx context.Context, t *testing.T, builder *NodeBuilder, address common.Address) {
t.Helper()
upgradeExecutor, err := upgrade_executorgen.NewUpgradeExecutor(builder.L2.ConsensusNode.DeployInfo.UpgradeExecutor, builder.L1.Client)
if err != nil {
t.Fatal("Failed to get new upgrade executor", err)
}
sequencerInboxABI, err := abi.JSON(strings.NewReader(bridgegen.SequencerInboxABI))
if err != nil {
t.Fatal("Failed to parse sequencer inbox abi", err)
}
setIsBatchPoster, err := sequencerInboxABI.Pack("setIsBatchPoster", address, true)
if err != nil {
t.Fatal("Failed to pack setIsBatchPoster", err)
}
ownerOpts := builder.L1Info.GetDefaultTransactOpts("RollupOwner", ctx)
tx, err := upgradeExecutor.ExecuteCall(
&ownerOpts,
builder.L1Info.GetAddress("SequencerInbox"),
setIsBatchPoster)
if err != nil {
t.Fatalf("Error creating transaction to set batch poster: %v", err)
}
if _, err := builder.L1.EnsureTxSucceeded(tx); err != nil {
t.Fatalf("Error setting batch poster: %v", err)
}
}

func testBatchPosterParallel(t *testing.T, useRedis bool) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
httpSrv, srv := newServer(ctx, t)
t.Cleanup(func() {
if err := httpSrv.Shutdown(ctx); err != nil {
t.Fatalf("Error shutting down http server: %v", err)
}
})
go func() {
log.Debug("Server is listening on port 1234...")
if err := httpSrv.ListenAndServeTLS(signerServerCert, signerServerKey); err != nil && err != http.ErrServerClosed {
log.Debug("ListenAndServeTLS() failed", "error", err)
return
}
}()

var redisUrl string
if useRedis {
Expand All @@ -48,14 +94,19 @@ func testBatchPosterParallel(t *testing.T, useRedis bool) {
builder := NewNodeBuilder(ctx).DefaultConfig(t, true)
builder.nodeConfig.BatchPoster.Enable = false
builder.nodeConfig.BatchPoster.RedisUrl = redisUrl
builder.nodeConfig.BatchPoster.DataPoster.ExternalSigner = *externalSignerTestCfg(srv.address)

cleanup := builder.Build(t)
defer cleanup()

testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{})
defer cleanupB()

builder.L2Info.GenerateAccount("User2")

addNewBatchPoster(ctx, t, builder, srv.address)

builder.L1.SendWaitTestTransactions(t, []*types.Transaction{
builder.L1Info.PrepareTxTo("Faucet", &srv.address, 30000, big.NewInt(1e18), nil)})

var txs []*types.Transaction

for i := 0; i < 100; i++ {
Expand Down
188 changes: 188 additions & 0 deletions system_tests/external_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package arbtest

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"os"
"testing"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/offchainlabs/nitro/arbnode/dataposter"
)

var (
signerPort = 1234
signerURL = fmt.Sprintf("https://localhost:%v", signerPort)
signerMethod = "test_signTransaction"
signerServerCert = "../arbnode/dataposter/testdata/localhost.crt"
signerServerKey = "../arbnode/dataposter/testdata/localhost.key"
signerClientCert = "../arbnode/dataposter/testdata/client.crt"
signerClientPrivateKey = "../arbnode/dataposter/testdata/client.key"
)

func externalSignerTestCfg(addr common.Address) *dataposter.ExternalSignerCfg {
return &dataposter.ExternalSignerCfg{
Address: common.Bytes2Hex(addr.Bytes()),
URL: signerURL,
Method: signerMethod,
RootCA: signerServerCert,
ClientCert: signerClientCert,
ClientPrivateKey: signerClientPrivateKey,
}
}

type server struct {
handlers map[string]func(*json.RawMessage) (string, error)
signerFn bind.SignerFn
address common.Address
}

type request struct {
ID *json.RawMessage `json:"id"`
Method string `json:"method"`
Params *json.RawMessage `json:"params"`
}

type response struct {
ID *json.RawMessage `json:"id"`
Result string `json:"result,omitempty"`
}

// newServer returns http server and server struct that implements RPC methods.
// It sets up an account in temporary directory and cleans up after test is
// done.
func newServer(ctx context.Context, t *testing.T) (*http.Server, *server) {
t.Helper()
signer, address, err := setupAccount("/tmp/keystore")
if err != nil {
t.Fatalf("Error setting up account: %v", err)
}
t.Cleanup(func() { os.RemoveAll("/tmp/keystore") })

s := &server{signerFn: signer, address: address}
s.handlers = map[string]func(*json.RawMessage) (string, error){
signerMethod: s.signTransaction,
}
m := http.NewServeMux()

clientCert, err := os.ReadFile(signerClientCert)
if err != nil {
t.Fatalf("Error reading client certificate: %v", err)
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(clientCert)

httpSrv := &http.Server{
Addr: fmt.Sprintf(":%v", signerPort),
Handler: m,
ReadTimeout: 5 * time.Second,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: pool,
},
}
m.HandleFunc("/", s.mux)
return httpSrv, s
}

// setupAccount creates a new account in a given directory, unlocks it, creates
// signer with that account and returns it along with account address.
func setupAccount(dir string) (bind.SignerFn, common.Address, error) {
ks := keystore.NewKeyStore(
dir,
keystore.StandardScryptN,
keystore.StandardScryptP,
)
a, err := ks.NewAccount("password")
if err != nil {
return nil, common.Address{}, fmt.Errorf("creating account account: %w", err)
}
if err := ks.Unlock(a, "password"); err != nil {
return nil, common.Address{}, fmt.Errorf("unlocking account: %w", err)
}
txOpts, err := bind.NewKeyStoreTransactorWithChainID(ks, a, big.NewInt(1337))
if err != nil {
return nil, common.Address{}, fmt.Errorf("creating transactor: %w", err)
}
return txOpts.Signer, a.Address, nil
}

// UnmarshallFirst unmarshalls slice of params and returns the first one.
// Parameters in Go ethereum RPC calls are marashalled as slices. E.g.
// eth_sendRawTransaction or eth_signTransaction, marshall transaction as a
// slice of transactions in a message:
// https://github.com/ethereum/go-ethereum/blob/0004c6b229b787281760b14fb9460ffd9c2496f1/rpc/client.go#L548
func unmarshallFirst(params []byte) (*types.Transaction, error) {
var arr []apitypes.SendTxArgs
if err := json.Unmarshal(params, &arr); err != nil {
return nil, fmt.Errorf("unmarshaling first param: %w", err)
}
if len(arr) != 1 {
return nil, fmt.Errorf("argument should be a single transaction, but got: %d", len(arr))
}
return arr[0].ToTransaction(), nil
}

func (s *server) signTransaction(params *json.RawMessage) (string, error) {
tx, err := unmarshallFirst(*params)
if err != nil {
return "", err
}
signedTx, err := s.signerFn(s.address, tx)
if err != nil {
return "", fmt.Errorf("signing transaction: %w", err)
}
data, err := rlp.EncodeToBytes(signedTx)
if err != nil {
return "", fmt.Errorf("rlp encoding transaction: %w", err)
}
return hexutil.Encode(data), nil
}

func (s *server) mux(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "can't read body", http.StatusBadRequest)
return
}
var req request
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, "can't unmarshal JSON request", http.StatusBadRequest)
return
}
method, ok := s.handlers[req.Method]
if !ok {
http.Error(w, "method not found", http.StatusNotFound)
return
}
result, err := method(req.Params)
if err != nil {
fmt.Printf("error calling method: %v\n", err)
http.Error(w, "error calling method", http.StatusInternalServerError)
return
}
resp := response{ID: req.ID, Result: result}
respBytes, err := json.Marshal(resp)
if err != nil {
http.Error(w, fmt.Sprintf("error encoding response: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(respBytes); err != nil {
fmt.Printf("error writing response: %v\n", err)
}
}

0 comments on commit 6da8981

Please sign in to comment.