Skip to content

Commit e87f2b3

Browse files
authored
Merge pull request #1576 from cfromknecht/bss-resources
feat: batch-submitter peripheral resources
2 parents 4ae9205 + c6ccc9e commit e87f2b3

8 files changed

Lines changed: 823 additions & 22 deletions

File tree

go/batch-submitter/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ LDFLAGSSTRING +=-X main.GitVersion=$(GITVERSION)
88
LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
99

1010
batch-submitter:
11-
env GO111MODULE=on go build $(LDFLAGS) ./cmd/batch-submitter
11+
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/batch-submitter
1212

1313
clean:
1414
rm batch-submitter
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package batchsubmitter
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"fmt"
7+
"net/http"
8+
"os"
9+
"strconv"
10+
"time"
11+
12+
"github.com/ethereum/go-ethereum/common"
13+
"github.com/ethereum/go-ethereum/crypto"
14+
"github.com/ethereum/go-ethereum/ethclient"
15+
"github.com/ethereum/go-ethereum/log"
16+
"github.com/getsentry/sentry-go"
17+
"github.com/prometheus/client_golang/prometheus/promhttp"
18+
"github.com/urfave/cli"
19+
)
20+
21+
const (
22+
// defaultDialTimeout is default duration the service will wait on
23+
// startup to make a connection to either the L1 or L2 backends.
24+
defaultDialTimeout = 5 * time.Second
25+
)
26+
27+
// Main is the entrypoint into the batch submitter service. This method returns
28+
// a closure that executes the service and blocks until the service exits. The
29+
// use of a closure allows the parameters bound to the top-level main package,
30+
// e.g. GitVersion, to be captured and used once the function is executed.
31+
func Main(gitVersion string) func(ctx *cli.Context) error {
32+
return func(ctx *cli.Context) error {
33+
cfg, err := NewConfig(ctx)
34+
if err != nil {
35+
return err
36+
}
37+
38+
// The call to defer is done here so that any errors logged from
39+
// this point on are posted to Sentry before exiting.
40+
if cfg.SentryEnable {
41+
defer sentry.Flush(2 * time.Second)
42+
}
43+
44+
_, err = NewBatchSubmitter(cfg, gitVersion)
45+
if err != nil {
46+
log.Error("Unable to create batch submitter", "error", err)
47+
return err
48+
}
49+
50+
return nil
51+
}
52+
}
53+
54+
// BatchSubmitter is a service that configures the necessary resources for
55+
// running the TxBatchSubmitter and StateBatchSubmitter sub-services.
56+
type BatchSubmitter struct {
57+
ctx context.Context
58+
cfg Config
59+
l1Client *ethclient.Client
60+
l2Client *ethclient.Client
61+
sequencerPrivKey *ecdsa.PrivateKey
62+
proposerPrivKey *ecdsa.PrivateKey
63+
ctcAddress common.Address
64+
sccAddress common.Address
65+
}
66+
67+
// NewBatchSubmitter initializes the BatchSubmitter, gathering any resources
68+
// that will be needed by the TxBatchSubmitter and StateBatchSubmitter
69+
// sub-services.
70+
func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) {
71+
ctx := context.Background()
72+
73+
// Set up our logging. If Sentry is enabled, we will use our custom
74+
// log handler that logs to stdout and forwards any error messages to
75+
// Sentry for collection. Otherwise, logs will only be posted to stdout.
76+
var logHandler log.Handler
77+
if cfg.SentryEnable {
78+
err := sentry.Init(sentry.ClientOptions{
79+
Dsn: cfg.SentryDsn,
80+
Environment: cfg.EthNetworkName,
81+
Release: "batch-submitter@" + gitVersion,
82+
TracesSampleRate: traceRateToFloat64(cfg.SentryTraceRate),
83+
Debug: false,
84+
})
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
logHandler = SentryStreamHandler(os.Stdout, log.TerminalFormat(true))
90+
} else {
91+
logHandler = log.StreamHandler(os.Stdout, log.TerminalFormat(true))
92+
}
93+
94+
logLevel, err := log.LvlFromString(cfg.LogLevel)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
log.Root().SetHandler(log.LvlFilterHandler(logLevel, logHandler))
100+
101+
log.Info("Config", "config", fmt.Sprintf("%#v", cfg))
102+
103+
// Parse sequencer private key and CTC contract address.
104+
sequencerPrivKey, ctcAddress, err := parseWalletPrivKeyAndContractAddr(
105+
"Sequencer", cfg.Mnemonic, cfg.SequencerHDPath,
106+
cfg.SequencerPrivateKey, cfg.CTCAddress,
107+
)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
// Parse proposer private key and SCC contract address.
113+
proposerPrivKey, sccAddress, err := parseWalletPrivKeyAndContractAddr(
114+
"Proposer", cfg.Mnemonic, cfg.ProposerHDPath,
115+
cfg.ProposerPrivateKey, cfg.SCCAddress,
116+
)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
// Connect to L1 and L2 providers. Perform these lastsince they are the
122+
// most expensive.
123+
l1Client, err := dialEthClientWithTimeout(ctx, cfg.L1EthRpc)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
l2Client, err := dialEthClientWithTimeout(ctx, cfg.L2EthRpc)
129+
if err != nil {
130+
return nil, err
131+
}
132+
133+
if cfg.MetricsServerEnable {
134+
go runMetricsServer(cfg.MetricsHostname, cfg.MetricsPort)
135+
}
136+
137+
return &BatchSubmitter{
138+
ctx: ctx,
139+
cfg: cfg,
140+
l1Client: l1Client,
141+
l2Client: l2Client,
142+
sequencerPrivKey: sequencerPrivKey,
143+
proposerPrivKey: proposerPrivKey,
144+
ctcAddress: ctcAddress,
145+
sccAddress: sccAddress,
146+
}, nil
147+
}
148+
149+
// parseWalletPrivKeyAndContractAddr returns the wallet private key to use for
150+
// sending transactions as well as the contract address to send to for a
151+
// particular sub-service.
152+
func parseWalletPrivKeyAndContractAddr(
153+
name string,
154+
mnemonic string,
155+
hdPath string,
156+
privKeyStr string,
157+
contractAddrStr string,
158+
) (*ecdsa.PrivateKey, common.Address, error) {
159+
160+
// Parse wallet private key from either privkey string or BIP39 mnemonic
161+
// and BIP32 HD derivation path.
162+
privKey, err := GetConfiguredPrivateKey(mnemonic, hdPath, privKeyStr)
163+
if err != nil {
164+
return nil, common.Address{}, err
165+
}
166+
167+
// Parse the target contract address the wallet will send to.
168+
contractAddress, err := ParseAddress(contractAddrStr)
169+
if err != nil {
170+
return nil, common.Address{}, err
171+
}
172+
173+
// Log wallet address rather than private key...
174+
walletAddress := crypto.PubkeyToAddress(privKey.PublicKey)
175+
176+
log.Info(name+" wallet params parsed successfully", "wallet_address",
177+
walletAddress, "contract_address", contractAddress)
178+
179+
return privKey, contractAddress, nil
180+
}
181+
182+
// runMetricsServer spins up a prometheus metrics server at the provided
183+
// hostname and port.
184+
//
185+
// NOTE: This method MUST be run as a goroutine.
186+
func runMetricsServer(hostname string, port uint64) {
187+
metricsPortStr := strconv.FormatUint(port, 10)
188+
metricsAddr := fmt.Sprintf("%s: %s", hostname, metricsPortStr)
189+
190+
http.Handle("/metrics", promhttp.Handler())
191+
_ = http.ListenAndServe(metricsAddr, nil)
192+
}
193+
194+
// dialEthClientWithTimeout attempts to dial the L1 or L2 provider using the
195+
// provided URL. If the dial doesn't complete within defaultDialTimeout seconds,
196+
// this method will return an error.
197+
func dialEthClientWithTimeout(ctx context.Context, url string) (
198+
*ethclient.Client, error) {
199+
200+
ctxt, cancel := context.WithTimeout(ctx, defaultDialTimeout)
201+
defer cancel()
202+
203+
return ethclient.DialContext(ctxt, url)
204+
}
205+
206+
// traceRateToFloat64 converts a time.Duration into a valid float64 for the
207+
// Sentry client. The client only accepts values between 0.0 and 1.0, so this
208+
// method clamps anything greater than 1 second to 1.0.
209+
func traceRateToFloat64(rate time.Duration) float64 {
210+
rate64 := float64(rate) / float64(time.Second)
211+
if rate64 > 1.0 {
212+
rate64 = 1.0
213+
}
214+
return rate64
215+
}

go/batch-submitter/cmd/batch-submitter/main.go

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,7 @@ func main() {
3636
app.Description = "Service for generating and submitting batched transactions " +
3737
"that synchronize L2 state to L1 contracts"
3838

39-
app.Action = func(ctx *cli.Context) error {
40-
cfg, err := batchsubmitter.NewConfig(ctx)
41-
if err != nil {
42-
return err
43-
}
44-
45-
logLevel, err := log.LvlFromString(cfg.LogLevel)
46-
if err != nil {
47-
return err
48-
}
49-
50-
log.Root().SetHandler(
51-
log.LvlFilterHandler(
52-
logLevel,
53-
log.StreamHandler(os.Stdout, log.TerminalFormat(true)),
54-
),
55-
)
56-
57-
log.Info("Config", "message", fmt.Sprintf("%#v", cfg))
58-
return nil
59-
}
39+
app.Action = batchsubmitter.Main(GitVersion)
6040
err := app.Run(os.Args)
6141
if err != nil {
6242
log.Crit("Application failed", "message", err)

go/batch-submitter/crypto.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package batchsubmitter
2+
3+
import (
4+
"crypto/ecdsa"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/decred/dcrd/hdkeychain/v3"
10+
"github.com/ethereum/go-ethereum/accounts"
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/ethereum/go-ethereum/crypto"
13+
"github.com/tyler-smith/go-bip39"
14+
)
15+
16+
var (
17+
// ErrCannotGetPrivateKey signals that an both or neither combination of
18+
// mnemonic+hdpath or private key string was used in the configuration.
19+
ErrCannotGetPrivateKey = errors.New("invalid combination of privkey " +
20+
"or mnemonic+hdpath")
21+
)
22+
23+
// ParseAddress parses an ETH addres from a hex string. This method will fail if
24+
// the address is not a valid hexidecimal address.
25+
func ParseAddress(address string) (common.Address, error) {
26+
if common.IsHexAddress(address) {
27+
return common.HexToAddress(address), nil
28+
}
29+
return common.Address{}, fmt.Errorf("invalid address: %v", address)
30+
}
31+
32+
// GetConfiguredPrivateKey computes the private key for our configured services.
33+
// The two supported methods are:
34+
// - Derived from BIP39 mnemonic and BIP32 HD derivation path.
35+
// - Directly from a serialized private key.
36+
func GetConfiguredPrivateKey(mnemonic, hdPath, privKeyStr string) (
37+
*ecdsa.PrivateKey, error) {
38+
39+
useMnemonic := mnemonic != "" && hdPath != ""
40+
usePrivKeyStr := privKeyStr != ""
41+
42+
switch {
43+
case useMnemonic && !usePrivKeyStr:
44+
return DerivePrivateKey(mnemonic, hdPath)
45+
46+
case usePrivKeyStr && !useMnemonic:
47+
return ParsePrivateKeyStr(privKeyStr)
48+
49+
default:
50+
return nil, ErrCannotGetPrivateKey
51+
}
52+
}
53+
54+
// fakeNetworkParams implements the hdkeychain.NetworkParams interface. These
55+
// methods are unused in the child derivation, and only needed for serializing
56+
// xpubs/xprivs which we don't rely on.
57+
type fakeNetworkParams struct{}
58+
59+
func (f fakeNetworkParams) HDPrivKeyVersion() [4]byte {
60+
return [4]byte{}
61+
}
62+
63+
func (f fakeNetworkParams) HDPubKeyVersion() [4]byte {
64+
return [4]byte{}
65+
}
66+
67+
// DerivePrivateKey derives the private key from a given mnemonic and BIP32
68+
// deriviation path.
69+
func DerivePrivateKey(mnemonic, hdPath string) (*ecdsa.PrivateKey, error) {
70+
// Parse the seed string into the master BIP32 key.
71+
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, "")
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
privKey, err := hdkeychain.NewMaster(seed, fakeNetworkParams{})
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
// Parse the derivation path and derive a child for each level of the
82+
// BIP32 derivation path.
83+
derivationPath, err := accounts.ParseDerivationPath(hdPath)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
for _, child := range derivationPath {
89+
privKey, err = privKey.Child(child)
90+
if err != nil {
91+
return nil, err
92+
}
93+
}
94+
95+
rawPrivKey, err := privKey.SerializedPrivKey()
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
return crypto.ToECDSA(rawPrivKey)
101+
}
102+
103+
// ParsePrivateKeyStr parses a hexidecimal encoded private key, the encoding may
104+
// optionally have an "0x" prefix.
105+
func ParsePrivateKeyStr(privKeyStr string) (*ecdsa.PrivateKey, error) {
106+
hex := strings.TrimPrefix(privKeyStr, "0x")
107+
return crypto.HexToECDSA(hex)
108+
}

0 commit comments

Comments
 (0)