Commit 15dd6d56 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into clabby/ct/remove-contracts

parents 6511d9c8 6db7431d
...@@ -1254,11 +1254,6 @@ workflows: ...@@ -1254,11 +1254,6 @@ workflows:
binary_name: indexer binary_name: indexer
working_directory: indexer working_directory: indexer
dependencies: op-bindings dependencies: op-bindings
- go-lint-test-build:
name: bss-core-tests
binary_name: bss-core
working_directory: bss-core
build: false
- go-lint-test-build: - go-lint-test-build:
name: op-heartbeat tests name: op-heartbeat tests
binary_name: op-heartbeat binary_name: op-heartbeat
......
# Legacy codebases # Legacy codebases
/bss-core @ethereum-optimism/legacy-reviewers
/gas-oracle @ethereum-optimism/legacy-reviewers /gas-oracle @ethereum-optimism/legacy-reviewers
/l2geth @ethereum-optimism/legacy-reviewers /l2geth @ethereum-optimism/legacy-reviewers
/packages/actor-tests @ethereum-optimism/legacy-reviewers /packages/actor-tests @ethereum-optimism/legacy-reviewers
......
...@@ -89,7 +89,6 @@ Refer to the Directory Structure section below to understand which packages are ...@@ -89,7 +89,6 @@ Refer to the Directory Structure section below to understand which packages are
│ ├── <a href="./packages/message-relayer">message-relayer</a>: Tool for automatically relaying L1<>L2 messages in development │ ├── <a href="./packages/message-relayer">message-relayer</a>: Tool for automatically relaying L1<>L2 messages in development
│ ├── <a href="./packages/replica-healthcheck">replica-healthcheck</a>: Service for monitoring the health of a replica node │ ├── <a href="./packages/replica-healthcheck">replica-healthcheck</a>: Service for monitoring the health of a replica node
│ └── <a href="./packages/sdk">sdk</a>: provides a set of tools for interacting with Optimism │ └── <a href="./packages/sdk">sdk</a>: provides a set of tools for interacting with Optimism
├── <a href="./bss-core">bss-core</a>: Core batch-submitter logic and utilities
├── <a href="./gas-oracle">gas-oracle</a>: Service for updating L1 gas prices on L2 ├── <a href="./gas-oracle">gas-oracle</a>: Service for updating L1 gas prices on L2
├── <a href="./indexer">indexer</a>: indexes and syncs transactions ├── <a href="./indexer">indexer</a>: indexes and syncs transactions
├── <a href="./infra/op-replica">infra/op-replica</a>: Deployment examples and resources for running an Optimism replica ├── <a href="./infra/op-replica">infra/op-replica</a>: Deployment examples and resources for running an Optimism replica
......
package bsscore
import (
"context"
)
// BatchSubmitter is a service that configures the necessary resources for
// running the TxBatchSubmitter and StateBatchSubmitter sub-services.
type BatchSubmitter struct {
ctx context.Context
services []*Service
cancel func()
}
// NewBatchSubmitter initializes the BatchSubmitter, gathering any resources
// that will be needed by the TxBatchSubmitter and StateBatchSubmitter
// sub-services.
func NewBatchSubmitter(
ctx context.Context,
cancel func(),
services []*Service,
) (*BatchSubmitter, error) {
return &BatchSubmitter{
ctx: ctx,
services: services,
cancel: cancel,
}, nil
}
// Start starts all provided services.
func (b *BatchSubmitter) Start() error {
for _, service := range b.services {
if err := service.Start(); err != nil {
return err
}
}
return nil
}
// Stop stops all provided services and blocks until shutdown.
func (b *BatchSubmitter) Stop() {
b.cancel()
for _, service := range b.services {
_ = service.Stop()
}
}
package bsscore
import (
"crypto/ecdsa"
"errors"
"fmt"
"strings"
"github.com/decred/dcrd/hdkeychain/v3"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/tyler-smith/go-bip39"
)
var (
// ErrCannotGetPrivateKey signals that an both or neither combination of
// mnemonic+hdpath or private key string was used in the configuration.
ErrCannotGetPrivateKey = errors.New("invalid combination of privkey " +
"or mnemonic+hdpath")
)
// ParseAddress parses an ETH address from a hex string. This method will fail if
// the address is not a valid hexadecimal address.
func ParseAddress(address string) (common.Address, error) {
if common.IsHexAddress(address) {
return common.HexToAddress(address), nil
}
return common.Address{}, fmt.Errorf("invalid address: %v", address)
}
// GetConfiguredPrivateKey computes the private key for our configured services.
// The two supported methods are:
// - Derived from BIP39 mnemonic and BIP32 HD derivation path.
// - Directly from a serialized private key.
func GetConfiguredPrivateKey(mnemonic, hdPath, privKeyStr string) (
*ecdsa.PrivateKey, error) {
useMnemonic := mnemonic != "" && hdPath != ""
usePrivKeyStr := privKeyStr != ""
switch {
case useMnemonic && !usePrivKeyStr:
return DerivePrivateKey(mnemonic, hdPath)
case usePrivKeyStr && !useMnemonic:
return ParsePrivateKeyStr(privKeyStr)
default:
return nil, ErrCannotGetPrivateKey
}
}
// fakeNetworkParams implements the hdkeychain.NetworkParams interface. These
// methods are unused in the child derivation, and only needed for serializing
// xpubs/xprivs which we don't rely on.
type fakeNetworkParams struct{}
func (f fakeNetworkParams) HDPrivKeyVersion() [4]byte {
return [4]byte{}
}
func (f fakeNetworkParams) HDPubKeyVersion() [4]byte {
return [4]byte{}
}
// DerivePrivateKey derives the private key from a given mnemonic and BIP32
// deriviation path.
func DerivePrivateKey(mnemonic, hdPath string) (*ecdsa.PrivateKey, error) {
// Parse the seed string into the master BIP32 key.
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, "")
if err != nil {
return nil, err
}
privKey, err := hdkeychain.NewMaster(seed, fakeNetworkParams{})
if err != nil {
return nil, err
}
// Parse the derivation path and derive a child for each level of the
// BIP32 derivation path.
derivationPath, err := accounts.ParseDerivationPath(hdPath)
if err != nil {
return nil, err
}
for _, child := range derivationPath {
privKey, err = privKey.Child(child)
if err != nil {
return nil, err
}
}
rawPrivKey, err := privKey.SerializedPrivKey()
if err != nil {
return nil, err
}
return crypto.ToECDSA(rawPrivKey)
}
// ParsePrivateKeyStr parses a hexadecimal encoded private key, the encoding may
// optionally have an "0x" prefix.
func ParsePrivateKeyStr(privKeyStr string) (*ecdsa.PrivateKey, error) {
hex := strings.TrimPrefix(privKeyStr, "0x")
return crypto.HexToECDSA(hex)
}
// ParseWalletPrivKeyAndContractAddr returns the wallet private key to use for
// sending transactions as well as the contract address to send to for a
// particular sub-service.
func ParseWalletPrivKeyAndContractAddr(
name string,
mnemonic string,
hdPath string,
privKeyStr string,
contractAddrStr string,
) (*ecdsa.PrivateKey, common.Address, error) {
// Parse wallet private key from either privkey string or BIP39 mnemonic
// and BIP32 HD derivation path.
privKey, err := GetConfiguredPrivateKey(mnemonic, hdPath, privKeyStr)
if err != nil {
return nil, common.Address{}, err
}
// Parse the target contract address the wallet will send to.
contractAddress, err := ParseAddress(contractAddrStr)
if err != nil {
return nil, common.Address{}, err
}
// Log wallet address rather than private key...
walletAddress := crypto.PubkeyToAddress(privKey.PublicKey)
log.Info(name+" wallet params parsed successfully", "wallet_address",
walletAddress, "contract_address", contractAddress)
return privKey, contractAddress, nil
}
package bsscore_test
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/tyler-smith/go-bip39"
bsscore "github.com/ethereum-optimism/optimism/bss-core"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
var (
validMnemonic = strings.Join([]string{
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "about",
}, " ")
validHDPath = "m/44'/60'/0'/128"
// validPrivKeyStr is the private key string for the child derived at
// validHDPath from validMnemonic.
validPrivKeyStr = "69d3a0e79bf039ca788924cb98b6b60c5f5aaa5e770aef09b4b15fdb59944d02"
// validPrivKeyBytes is the raw private key bytes for the child derived
// at validHDPath from validMnemonic.
validPrivKeyBytes = []byte{
0x69, 0xd3, 0xa0, 0xe7, 0x9b, 0xf0, 0x39, 0xca,
0x78, 0x89, 0x24, 0xcb, 0x98, 0xb6, 0xb6, 0x0c,
0x5f, 0x5a, 0xaa, 0x5e, 0x77, 0x0a, 0xef, 0x09,
0xb4, 0xb1, 0x5f, 0xdb, 0x59, 0x94, 0x4d, 0x02,
}
// invalidMnemonic has an invalid checksum.
invalidMnemonic = strings.Join([]string{
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
}, " ")
)
// TestParseAddress asserts that ParseAddress correctly parses 40-characater
// hexadecimal strings with optional 0x prefix into valid 20-byte addresses.
func TestParseAddress(t *testing.T) {
tests := []struct {
name string
addr string
expErr error
expAddr common.Address
}{
{
name: "empty address",
addr: "",
expErr: errors.New("invalid address: "),
},
{
name: "only 0x",
addr: "0x",
expErr: errors.New("invalid address: 0x"),
},
{
name: "non hex character",
addr: "0xaaaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
expErr: errors.New("invalid address: 0xaaaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
},
{
name: "valid address",
addr: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
expErr: nil,
expAddr: common.BytesToAddress(bytes.Repeat([]byte{170}, 20)),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
addr, err := bsscore.ParseAddress(test.addr)
require.Equal(t, err, test.expErr)
if test.expErr != nil {
return
}
require.Equal(t, addr, test.expAddr)
})
}
}
// TestDerivePrivateKey asserts that DerivePrivateKey properly parses a BIP39
// mnemonic and BIP32 HD path, and derives the corresponding private key.
func TestDerivePrivateKey(t *testing.T) {
tests := []struct {
name string
mnemonic string
hdPath string
expErr error
expPrivKey []byte
}{
{
name: "invalid mnemonic",
mnemonic: invalidMnemonic,
hdPath: validHDPath,
expErr: bip39.ErrInvalidMnemonic, // the bip39 lib spells mnemonic wrong...
},
{
name: "valid mnemonic invalid hdpath",
mnemonic: validMnemonic,
hdPath: "",
expErr: errors.New("ambiguous path: use 'm/' prefix for absolute " +
"paths, or no leading '/' for relative ones"),
},
{
name: "valid mnemonic invalid hdpath",
mnemonic: validMnemonic,
hdPath: "m/",
expErr: errors.New("invalid component: "),
},
{
name: "valid mnemonic valid hdpath no components",
mnemonic: validMnemonic,
hdPath: "m/0",
expPrivKey: []byte{
0xba, 0xa8, 0x9a, 0x8b, 0xdd, 0x61, 0xc5, 0xe2,
0x2b, 0x9f, 0x10, 0x60, 0x1d, 0x87, 0x91, 0xc9,
0xf8, 0xfc, 0x4b, 0x2f, 0xa6, 0xdf, 0x9d, 0x68,
0xd3, 0x36, 0xf0, 0xeb, 0x03, 0xb0, 0x6e, 0xb6,
},
},
{
name: "valid mnemonic valid hdpath full path",
mnemonic: validMnemonic,
hdPath: validHDPath,
expPrivKey: []byte{
0x69, 0xd3, 0xa0, 0xe7, 0x9b, 0xf0, 0x39, 0xca,
0x78, 0x89, 0x24, 0xcb, 0x98, 0xb6, 0xb6, 0x0c,
0x5f, 0x5a, 0xaa, 0x5e, 0x77, 0x0a, 0xef, 0x09,
0xb4, 0xb1, 0x5f, 0xdb, 0x59, 0x94, 0x4d, 0x02,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
privKey, err := bsscore.DerivePrivateKey(test.mnemonic, test.hdPath)
require.Equal(t, err, test.expErr)
if test.expErr != nil {
return
}
expPrivKey, err := crypto.ToECDSA(test.expPrivKey)
require.Nil(t, err)
require.Equal(t, privKey, expPrivKey)
})
}
}
// TestParsePrivateKeyStr asserts that ParsePrivateKey properly parses
// 64-character hexadecimal strings with optional 0x prefix into valid ECDSA
// private keys.
func TestParsePrivateKeyStr(t *testing.T) {
tests := []struct {
name string
privKeyStr string
expErr error
expPrivKey []byte
}{
{
name: "empty privkey string",
privKeyStr: "",
expErr: errors.New("invalid length, need 256 bits"),
},
{
name: "privkey string only 0x",
privKeyStr: "0x",
expErr: errors.New("invalid length, need 256 bits"),
},
{
name: "non hex privkey string",
privKeyStr: "0xaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
expErr: errors.New("invalid hex character 'z' in private key"),
},
{
name: "valid privkey string",
privKeyStr: validPrivKeyStr,
expPrivKey: validPrivKeyBytes,
},
{
name: "valid privkey string with 0x",
privKeyStr: "0x" + validPrivKeyStr,
expPrivKey: validPrivKeyBytes,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
privKey, err := bsscore.ParsePrivateKeyStr(test.privKeyStr)
require.Equal(t, err, test.expErr)
if test.expErr != nil {
return
}
expPrivKey, err := crypto.ToECDSA(test.expPrivKey)
require.Nil(t, err)
require.Equal(t, privKey, expPrivKey)
})
}
}
// TestGetConfiguredPrivateKey asserts that GetConfiguredPrivateKey either:
// 1. Derives the correct private key assuming the BIP39 mnemonic and BIP32
// derivation path are both present and the private key string is omitted.
// 2. Parses the correct private key assuming only the private key string is
// present, but the BIP39 mnemonic and BIP32 derivation path are omitted.
func TestGetConfiguredPrivateKey(t *testing.T) {
tests := []struct {
name string
mnemonic string
hdPath string
privKeyStr string
expErr error
expPrivKey []byte
}{
{
name: "valid mnemonic+hdpath",
mnemonic: validMnemonic,
hdPath: validHDPath,
privKeyStr: "",
expPrivKey: validPrivKeyBytes,
},
{
name: "valid privkey",
mnemonic: "",
hdPath: "",
privKeyStr: validPrivKeyStr,
expPrivKey: validPrivKeyBytes,
},
{
name: "valid privkey with 0x",
mnemonic: "",
hdPath: "",
privKeyStr: "0x" + validPrivKeyStr,
expPrivKey: validPrivKeyBytes,
},
{
name: "valid menmonic+hdpath and privkey",
mnemonic: validMnemonic,
hdPath: validHDPath,
privKeyStr: validPrivKeyStr,
expErr: bsscore.ErrCannotGetPrivateKey,
},
{
name: "neither menmonic+hdpath or privkey",
mnemonic: "",
hdPath: "",
privKeyStr: "",
expErr: bsscore.ErrCannotGetPrivateKey,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
privKey, err := bsscore.GetConfiguredPrivateKey(
test.mnemonic, test.hdPath, test.privKeyStr,
)
require.Equal(t, err, test.expErr)
if test.expErr != nil {
return
}
expPrivKey, err := crypto.ToECDSA(test.expPrivKey)
require.Nil(t, err)
require.Equal(t, privKey, expPrivKey)
})
}
}
package dial
import "time"
const (
// DefaultTimeout is default duration the service will wait on startup to
// make a connection to either the L1 or L2 backends.
DefaultTimeout = 5 * time.Second
)
package dial
import (
"context"
"crypto/tls"
"net/http"
"strings"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
// L1EthClientWithTimeout attempts to dial the L1 provider using the
// provided URL. If the dial doesn't complete within DefaultTimeout seconds,
// this method will return an error.
func L1EthClientWithTimeout(ctx context.Context, url string, disableHTTP2 bool) (
*ethclient.Client, error) {
ctxt, cancel := context.WithTimeout(ctx, DefaultTimeout)
defer cancel()
if strings.HasPrefix(url, "http") {
httpClient := new(http.Client)
if disableHTTP2 {
log.Info("Disabled HTTP/2 support in L1 eth client")
httpClient.Transport = &http.Transport{
TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
}
}
//nolint:staticcheck // Geth v1.10.23 uses rpc.DialOptions and rpc.WithClient, but we need to update geth first. Lint is flagged because of global go workspace usage.
rpcClient, err := rpc.DialHTTPWithClient(url, httpClient)
if err != nil {
return nil, err
}
return ethclient.NewClient(rpcClient), nil
}
return ethclient.DialContext(ctxt, url)
}
package drivers
import (
"context"
"crypto/ecdsa"
"errors"
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/bss-core/txmgr"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
// ErrClearPendingRetry signals that a transaction from a previous running
// instance confirmed rather than our clearing transaction on startup. In this
// case the caller should retry.
var ErrClearPendingRetry = errors.New("retry clear pending txn")
// ClearPendingTx publishes a NOOP transaction at the wallet's next unused
// nonce. This is used on restarts in order to clear the mempool of any prior
// publications and ensure the batch submitter starts submitting from a clean
// slate.
func ClearPendingTx(
name string,
ctx context.Context,
txMgr txmgr.TxManager,
l1Client L1Client,
walletAddr common.Address,
privKey *ecdsa.PrivateKey,
chainID *big.Int,
) error {
// Query for the submitter's current nonce.
nonce, err := l1Client.NonceAt(ctx, walletAddr, nil)
if err != nil {
log.Error(name+" unable to get current nonce",
"err", err)
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Construct the clearing transaction submission clousure that will attempt
// to send the a clearing transaction transaction at the given nonce and gas
// price.
updateGasPrice := func(
ctx context.Context,
) (*types.Transaction, error) {
log.Info(name+" clearing pending tx", "nonce", nonce)
signedTx, err := SignClearingTx(
name, ctx, walletAddr, nonce, l1Client, privKey, chainID,
)
if err != nil {
log.Error(name+" unable to sign clearing tx", "nonce", nonce,
"err", err)
return nil, err
}
return signedTx, nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
txHash := tx.Hash()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
err := l1Client.SendTransaction(ctx, tx)
switch {
// Clearing transaction successfully confirmed.
case err == nil:
log.Info(name+" submitted clearing tx", "nonce", nonce,
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash)
return nil
// Getting a nonce too low error implies that a previous transaction in
// the mempool has confirmed and we should abort trying to publish at
// this nonce.
case strings.Contains(err.Error(), core.ErrNonceTooLow.Error()):
log.Info(name + " transaction from previous restart confirmed, " +
"aborting mempool clearing")
cancel()
return context.Canceled
// An unexpected error occurred. This also handles the case where the
// clearing transaction has not yet bested the gas price a prior
// transaction in the mempool at this nonce. In such a case we will
// continue until our ratchetting strategy overtakes the old
// transaction, or abort if the old one confirms.
default:
log.Error(name+" unable to submit clearing tx",
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash, "err", err)
return err
}
}
receipt, err := txMgr.Send(ctx, updateGasPrice, sendTx)
switch {
// If the current context is canceled, a prior transaction in the mempool
// confirmed. The caller should retry, which will use the next nonce, before
// proceeding.
case err == context.Canceled:
log.Info(name + " transaction from previous restart confirmed, " +
"proceeding to startup")
return ErrClearPendingRetry
// Otherwise we were unable to confirm our transaction, this method should
// be retried by the caller.
case err != nil:
log.Warn(name+" unable to send clearing tx", "nonce", nonce,
"err", err)
return err
// We succeeded in confirming a clearing transaction. Proceed to startup as
// normal.
default:
log.Info(name+" cleared pending tx", "nonce", nonce,
"txHash", receipt.TxHash)
return nil
}
}
// SignClearingTx creates a signed clearing tranaction which sends 0 ETH back to
// the sender's address. EstimateGas is used to set an appropriate gas limit.
func SignClearingTx(
name string,
ctx context.Context,
walletAddr common.Address,
nonce uint64,
l1Client L1Client,
privKey *ecdsa.PrivateKey,
chainID *big.Int,
) (*types.Transaction, error) {
gasTipCap, err := l1Client.SuggestGasTipCap(ctx)
if err != nil {
if !IsMaxPriorityFeePerGasNotFoundError(err) {
return nil, err
}
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this
// method, so in the event their API is unreachable we can fallback to a
// degraded mode of operation. This also applies to our test
// environments, as hardhat doesn't support the query either.
log.Warn(name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
gasTipCap = FallbackGasTipCap
}
head, err := l1Client.HeaderByNumber(ctx, nil)
if err != nil {
return nil, err
}
gasFeeCap := txmgr.CalcGasFeeCap(head.BaseFee, gasTipCap)
gasLimit, err := l1Client.EstimateGas(ctx, ethereum.CallMsg{
From: walletAddr,
To: &walletAddr,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Value: nil,
Data: nil,
})
if err != nil {
return nil, err
}
tx := CraftClearingTx(walletAddr, nonce, gasFeeCap, gasTipCap, gasLimit)
return types.SignTx(
tx, types.LatestSignerForChainID(chainID), privKey,
)
}
// CraftClearingTx creates an unsigned clearing transaction which sends 0 ETH
// back to the sender's address.
func CraftClearingTx(
walletAddr common.Address,
nonce uint64,
gasFeeCap *big.Int,
gasTipCap *big.Int,
gasLimit uint64,
) *types.Transaction {
return types.NewTx(&types.DynamicFeeTx{
To: &walletAddr,
Nonce: nonce,
Gas: gasLimit,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Value: nil,
Data: nil,
})
}
This diff is collapsed.
package drivers
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// L1Client is an abstraction over an L1 Ethereum client functionality required
// by the batch submitter.
type L1Client interface {
// EstimateGas tries to estimate the gas needed to execute a specific
// transaction based on the current pending state of the backend blockchain.
// There is no guarantee that this is the true gas limit requirement as
// other transactions may be added or removed by miners, but it should
// provide a basis for setting a reasonable default.
EstimateGas(context.Context, ethereum.CallMsg) (uint64, error)
// HeaderByNumber returns a block header from the current canonical chain.
// If number is nil, the latest known header is returned.
HeaderByNumber(context.Context, *big.Int) (*types.Header, error)
// NonceAt returns the account nonce of the given account. The block number
// can be nil, in which case the nonce is taken from the latest known block.
NonceAt(context.Context, common.Address, *big.Int) (uint64, error)
// SendTransaction injects a signed transaction into the pending pool for
// execution.
//
// If the transaction was a contract creation use the TransactionReceipt
// method to get the contract address after the transaction has been mined.
SendTransaction(context.Context, *types.Transaction) error
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
// to allow a timely execution of a transaction.
SuggestGasTipCap(context.Context) (*big.Int, error)
// TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions.
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
}
package drivers
import (
"errors"
"math/big"
"strings"
)
var (
errMaxPriorityFeePerGasNotFound = errors.New(
"Method eth_maxPriorityFeePerGas not found",
)
// FallbackGasTipCap is the default fallback gasTipCap used when we are
// unable to query an L1 backend for a suggested gasTipCap.
FallbackGasTipCap = big.NewInt(1500000000)
)
// IsMaxPriorityFeePerGasNotFoundError returns true if the provided error
// signals that the backend does not support the eth_maxPrirorityFeePerGas
// method. In this case, the caller should fallback to using the constant above.
func IsMaxPriorityFeePerGasNotFoundError(err error) bool {
return strings.Contains(
err.Error(), errMaxPriorityFeePerGasNotFound.Error(),
)
}
module github.com/ethereum-optimism/optimism/bss-core
go 1.18
require (
github.com/decred/dcrd/hdkeychain/v3 v3.0.0
github.com/ethereum/go-ethereum v1.10.26
github.com/getsentry/sentry-go v0.12.0
github.com/prometheus/client_golang v1.11.1
github.com/stretchr/testify v1.7.2
github.com/tyler-smith/go-bip39 v1.1.0
)
require (
github.com/VictoriaMetrics/fastcache v1.9.0 // indirect
github.com/allegro/bigcache v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd v0.22.0-beta // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/base58 v1.0.3 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/fjl/memsize v0.0.1 // indirect
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/go-bexpr v0.1.11 // indirect
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.16.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/tsdb v0.10.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rjeczalik/notify v0.9.2 // indirect
github.com/rs/cors v1.8.2 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/status-im/keycard-go v0.0.0-20211109104530-b0e0482ba91d // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
This diff is collapsed.
package metrics
import "github.com/prometheus/client_golang/prometheus"
type Metrics interface {
// SubsystemName returns the subsystem name for the metrics group.
SubsystemName() string
// BalanceETH tracks the amount of ETH in the submitter's account.
BalanceETH() prometheus.Gauge
// BatchSizeBytes tracks the size of batch submission transactions.
BatchSizeBytes() prometheus.Summary
// NumElementsPerBatch tracks the number of L2 transactions in each batch
// submission.
NumElementsPerBatch() prometheus.Summary
// SubmissionTimestamp tracks the time at which each batch was confirmed.
SubmissionTimestamp() prometheus.Gauge
// SubmissionGasUsedWei tracks the amount of gas used to submit each batch.
SubmissionGasUsedWei() prometheus.Gauge
// BatchsSubmitted tracks the total number of successful batch submissions.
BatchesSubmitted() prometheus.Counter
// FailedSubmissions tracks the total number of failed batch submissions.
FailedSubmissions() prometheus.Counter
// BatchTxBuildTimeMs tracks the duration it takes to construct a batch
// transaction.
BatchTxBuildTimeMs() prometheus.Gauge
// BatchConfirmationTimeMs tracks the duration it takes to confirm a batch
// transaction.
BatchConfirmationTimeMs() prometheus.Gauge
}
package metrics
import (
"fmt"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
type Base struct {
// subsystemName stores the name that will prefix all metrics. This can be
// used by drivers to further extend the core metrics and ensure they use the
// same prefix.
subsystemName string
// balanceETH tracks the amount of ETH in the submitter's account.
balanceETH prometheus.Gauge
// batchSizeBytes tracks the size of batch submission transactions.
batchSizeBytes prometheus.Summary
// numElementsPerBatch tracks the number of L2 transactions in each batch
// submission.
numElementsPerBatch prometheus.Summary
// submissionTimestamp tracks the time at which each batch was confirmed.
submissionTimestamp prometheus.Gauge
// submissionGasUsedWei tracks the amount of gas used to submit each batch.
submissionGasUsedWei prometheus.Gauge
// batchsSubmitted tracks the total number of successful batch submissions.
batchesSubmitted prometheus.Counter
// failedSubmissions tracks the total number of failed batch submissions.
failedSubmissions prometheus.Counter
// batchTxBuildTimeMs tracks the duration it takes to construct a batch
// transaction.
batchTxBuildTimeMs prometheus.Gauge
// batchConfirmationTimeMs tracks the duration it takes to confirm a batch
// transaction.
batchConfirmationTimeMs prometheus.Gauge
}
func NewBase(serviceName, subServiceName string) *Base {
subsystem := MakeSubsystemName(serviceName, subServiceName)
return &Base{
subsystemName: subsystem,
balanceETH: promauto.NewGauge(prometheus.GaugeOpts{
Name: "balance_eth",
Help: "ETH balance of the batch submitter",
Subsystem: subsystem,
}),
batchSizeBytes: promauto.NewSummary(prometheus.SummaryOpts{
Name: "batch_size_bytes",
Help: "Size of batches in bytes",
Subsystem: subsystem,
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
}),
numElementsPerBatch: promauto.NewSummary(prometheus.SummaryOpts{
Name: "num_elements_per_batch",
Help: "Number of elements in each batch",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
Subsystem: subsystem,
}),
submissionTimestamp: promauto.NewGauge(prometheus.GaugeOpts{
Name: "submission_timestamp_ms",
Help: "Timestamp of last batch submitter submission",
Subsystem: subsystem,
}),
submissionGasUsedWei: promauto.NewGauge(prometheus.GaugeOpts{
Name: "submission_gas_used_wei",
Help: "Gas used to submit each batch",
Subsystem: subsystem,
}),
batchesSubmitted: promauto.NewCounter(prometheus.CounterOpts{
Name: "batches_submitted",
Help: "Count of batches submitted",
Subsystem: subsystem,
}),
failedSubmissions: promauto.NewCounter(prometheus.CounterOpts{
Name: "failed_submissions",
Help: "Count of failed batch submissions",
Subsystem: subsystem,
}),
batchTxBuildTimeMs: promauto.NewGauge(prometheus.GaugeOpts{
Name: "batch_tx_build_time_ms",
Help: "Time to construct batch transactions",
Subsystem: subsystem,
}),
batchConfirmationTimeMs: promauto.NewGauge(prometheus.GaugeOpts{
Name: "batch_confirmation_time_ms",
Help: "Time to confirm batch transactions",
Subsystem: subsystem,
}),
}
}
// SubsystemName returns the subsystem name for the metrics group.
func (b *Base) SubsystemName() string {
return b.subsystemName
}
// BalanceETH tracks the amount of ETH in the submitter's account.
func (b *Base) BalanceETH() prometheus.Gauge {
return b.balanceETH
}
// BatchSizeBytes tracks the size of batch submission transactions.
func (b *Base) BatchSizeBytes() prometheus.Summary {
return b.batchSizeBytes
}
// NumElementsPerBatch tracks the number of L2 transactions in each batch
// submission.
func (b *Base) NumElementsPerBatch() prometheus.Summary {
return b.numElementsPerBatch
}
// SubmissionTimestamp tracks the time at which each batch was confirmed.
func (b *Base) SubmissionTimestamp() prometheus.Gauge {
return b.submissionTimestamp
}
// SubmissionGasUsedWei tracks the amount of gas used to submit each batch.
func (b *Base) SubmissionGasUsedWei() prometheus.Gauge {
return b.submissionGasUsedWei
}
// BatchsSubmitted tracks the total number of successful batch submissions.
func (b *Base) BatchesSubmitted() prometheus.Counter {
return b.batchesSubmitted
}
// FailedSubmissions tracks the total number of failed batch submissions.
func (b *Base) FailedSubmissions() prometheus.Counter {
return b.failedSubmissions
}
// BatchTxBuildTimeMs tracks the duration it takes to construct a batch
// transaction.
func (b *Base) BatchTxBuildTimeMs() prometheus.Gauge {
return b.batchTxBuildTimeMs
}
// BatchConfirmationTimeMs tracks the duration it takes to confirm a batch
// transaction.
func (b *Base) BatchConfirmationTimeMs() prometheus.Gauge {
return b.batchConfirmationTimeMs
}
// MakeSubsystemName builds the subsystem name for a group of metrics, which
// prometheus will use to prefix all metrics in the group. If two non-empty
// strings are provided, they are joined with an underscore. If only one
// non-empty string is provided, that name will be used alone. Otherwise an
// empty string is returned after converting the characters to lower case.
//
// NOTE: This method panics if spaces are included in either string.
func MakeSubsystemName(serviceName string, subServiceName string) string {
var subsystem string
switch {
case serviceName != "" && subServiceName != "":
subsystem = fmt.Sprintf("%s_%s", serviceName, subServiceName)
case serviceName != "":
subsystem = serviceName
default:
subsystem = subServiceName
}
if strings.ContainsAny(subsystem, " ") {
panic(fmt.Sprintf("metrics name \"%s\"cannot have spaces", subsystem))
}
return strings.ToLower(subsystem)
}
package metrics
import (
"fmt"
"net/http"
"strconv"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// RunServer spins up a prometheus metrics server at the provided hostname and
// port.
//
// NOTE: This method MUST be run as a goroutine.
func RunServer(hostname string, port uint64) {
metricsPortStr := strconv.FormatUint(port, 10)
metricsAddr := fmt.Sprintf("%s:%s", hostname, metricsPortStr)
http.Handle("/metrics", promhttp.Handler())
_ = http.ListenAndServe(metricsAddr, nil)
}
package mock
import (
"context"
"math/big"
"sync"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// L1ClientConfig houses the internal methods that are executed by the mock
// L1Client. Any members left as nil will panic on execution.
type L1ClientConfig struct {
// BlockNumber returns the most recent block number.
BlockNumber func(context.Context) (uint64, error)
// EstimateGas tries to estimate the gas needed to execute a specific
// transaction based on the current pending state of the backend blockchain.
// There is no guarantee that this is the true gas limit requirement as
// other transactions may be added or removed by miners, but it should
// provide a basis for setting a reasonable default.
EstimateGas func(context.Context, ethereum.CallMsg) (uint64, error)
// HeaderByNumber returns a block header from the current canonical chain.
// If number is nil, the latest known header is returned.
HeaderByNumber func(context.Context, *big.Int) (*types.Header, error)
// NonceAt returns the account nonce of the given account. The block number
// can be nil, in which case the nonce is taken from the latest known block.
NonceAt func(context.Context, common.Address, *big.Int) (uint64, error)
// SendTransaction injects a signed transaction into the pending pool for
// execution.
//
// If the transaction was a contract creation use the TransactionReceipt
// method to get the contract address after the transaction has been mined.
SendTransaction func(context.Context, *types.Transaction) error
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
// to allow a timely execution of a transaction.
SuggestGasTipCap func(context.Context) (*big.Int, error)
// TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions.
TransactionReceipt func(context.Context, common.Hash) (*types.Receipt, error)
}
// L1Client represents a mock L1Client.
type L1Client struct {
cfg L1ClientConfig
mu sync.RWMutex
}
// NewL1Client returns a new L1Client using the mocked methods in the
// L1ClientConfig.
func NewL1Client(cfg L1ClientConfig) *L1Client {
return &L1Client{
cfg: cfg,
}
}
// BlockNumber returns the most recent block number.
func (c *L1Client) BlockNumber(ctx context.Context) (uint64, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.BlockNumber(ctx)
}
// EstimateGas tries to estimate the gas needed to execute a specific
// transaction based on the current pending state of the backend blockchain.
// There is no guarantee that this is the true gas limit requirement as other
// transactions may be added or removed by miners, but it should provide a basis
// for setting a reasonable default.
func (c *L1Client) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.EstimateGas(ctx, msg)
}
// HeaderByNumber returns a block header from the current canonical chain. If
// number is nil, the latest known header is returned.
func (c *L1Client) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*types.Header, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.HeaderByNumber(ctx, blockNumber)
}
// NonceAt executes the mock NonceAt method.
func (c *L1Client) NonceAt(ctx context.Context, addr common.Address, blockNumber *big.Int) (uint64, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.NonceAt(ctx, addr, blockNumber)
}
// SendTransaction executes the mock SendTransaction method.
func (c *L1Client) SendTransaction(ctx context.Context, tx *types.Transaction) error {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.SendTransaction(ctx, tx)
}
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559 to
// allow a timely execution of a transaction.
func (c *L1Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.SuggestGasTipCap(ctx)
}
// TransactionReceipt executes the mock TransactionReceipt method.
func (c *L1Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.TransactionReceipt(ctx, txHash)
}
// SetBlockNumberFunc overwrites the mock BlockNumber method.
func (c *L1Client) SetBlockNumberFunc(
f func(context.Context) (uint64, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.BlockNumber = f
}
// SetEstimateGasFunc overwrites the mock EstimateGas method.
func (c *L1Client) SetEstimateGasFunc(
f func(context.Context, ethereum.CallMsg) (uint64, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.EstimateGas = f
}
// SetHeaderByNumberFunc overwrites the mock HeaderByNumber method.
func (c *L1Client) SetHeaderByNumberFunc(
f func(ctx context.Context, blockNumber *big.Int) (*types.Header, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.HeaderByNumber = f
}
// SetNonceAtFunc overwrites the mock NonceAt method.
func (c *L1Client) SetNonceAtFunc(
f func(context.Context, common.Address, *big.Int) (uint64, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.NonceAt = f
}
// SetSendTransactionFunc overwrites the mock SendTransaction method.
func (c *L1Client) SetSendTransactionFunc(
f func(context.Context, *types.Transaction) error) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.SendTransaction = f
}
// SetSuggestGasTipCapFunc overwrites themock SuggestGasTipCap method.
func (c *L1Client) SetSuggestGasTipCapFunc(
f func(context.Context) (*big.Int, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.SuggestGasTipCap = f
}
// SetTransactionReceiptFunc overwrites the mock TransactionReceipt method.
func (c *L1Client) SetTransactionReceiptFunc(
f func(context.Context, common.Hash) (*types.Receipt, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.TransactionReceipt = f
}
package bsscore
import (
"errors"
"io"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/getsentry/sentry-go"
)
var jsonFmt = log.JSONFormat()
// SentryStreamHandler creates a log.Handler that behaves similarly to
// log.StreamHandler, however it writes any log with severity greater than or
// equal to log.LvlError to Sentry. In that case, the passed log.Record is
// encoded using JSON rather than the default terminal output, so that it can be
// captured for debugging in the Sentry dashboard.
func SentryStreamHandler(wr io.Writer, fmtr log.Format) log.Handler {
h := log.FuncHandler(func(r *log.Record) error {
_, err := wr.Write(fmtr.Format(r))
// If this record's severity is log.LvlError or higher,
// serialize the record using JSON and write it to Sentry. We
// also capture the error message separately so that it's easy
// to parse what the error is in the dashboard.
//
// NOTE: The log.Lvl* constants are defined in reverse order of
// their severity, i.e. zero (log.LvlCrit) is the highest
// severity.
if r.Lvl <= log.LvlError {
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetExtra("context", jsonFmt.Format(r))
sentry.CaptureException(errors.New(r.Msg))
})
}
return err
})
return log.LazyHandler(log.SyncHandler(h))
}
// TraceRateToFloat64 converts a time.Duration into a valid float64 for the
// Sentry client. The client only accepts values between 0.0 and 1.0, so this
// method clamps anything greater than 1 second to 1.0.
func TraceRateToFloat64(rate time.Duration) float64 {
rate64 := float64(rate) / float64(time.Second)
if rate64 > 1.0 {
rate64 = 1.0
}
return rate64
}
package bsscore
import (
"bytes"
"context"
"math/big"
"sync"
"time"
"github.com/ethereum-optimism/optimism/bss-core/metrics"
"github.com/ethereum-optimism/optimism/bss-core/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
var (
// weiToEth is the conversion rate from wei to ether.
weiToEth = new(big.Float).SetFloat64(1e-18)
)
// Driver is an interface for creating and submitting batch transactions for a
// specific contract.
type Driver interface {
// Name is an identifier used to prefix logs for a particular service.
Name() string
// WalletAddr is the wallet address used to pay for batch transaction
// fees.
WalletAddr() common.Address
// Metrics returns the subservice telemetry object.
Metrics() metrics.Metrics
// ClearPendingTx a publishes a transaction at the next available nonce in
// order to clear any transactions in the mempool left over from a prior
// running instance of the batch submitter.
ClearPendingTx(context.Context, txmgr.TxManager, *ethclient.Client) error
// GetBatchBlockRange returns the start and end L2 block heights that
// need to be processed. Note that the end value is *exclusive*,
// therefore if the returned values are identical nothing needs to be
// processed.
GetBatchBlockRange(ctx context.Context) (*big.Int, *big.Int, error)
// CraftBatchTx transforms the L2 blocks between start and end into a batch
// transaction using the given nonce. A dummy gas price is used in the
// resulting transaction to use for size estimation. The driver may return a
// nil value for transaction if there is no action that needs to be
// performed.
//
// NOTE: This method SHOULD NOT publish the resulting transaction.
CraftBatchTx(
ctx context.Context,
start, end, nonce *big.Int,
) (*types.Transaction, error)
// UpdateGasPrice signs an otherwise identical txn to the one provided but
// with updated gas prices sampled from the existing network conditions.
//
// NOTE: Thie method SHOULD NOT publish the resulting transaction.
UpdateGasPrice(
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error)
// SendTransaction injects a signed transaction into the pending pool for
// execution.
SendTransaction(ctx context.Context, tx *types.Transaction) error
}
type ServiceConfig struct {
Context context.Context
Driver Driver
PollInterval time.Duration
ClearPendingTx bool
L1Client *ethclient.Client
TxManagerConfig txmgr.Config
}
type Service struct {
cfg ServiceConfig
ctx context.Context
cancel func()
txMgr txmgr.TxManager
metrics metrics.Metrics
wg sync.WaitGroup
}
func NewService(cfg ServiceConfig) *Service {
ctx, cancel := context.WithCancel(cfg.Context)
txMgr := txmgr.NewSimpleTxManager(
cfg.Driver.Name(), cfg.TxManagerConfig, cfg.L1Client,
)
return &Service{
cfg: cfg,
ctx: ctx,
cancel: cancel,
txMgr: txMgr,
metrics: cfg.Driver.Metrics(),
}
}
func (s *Service) Start() error {
s.wg.Add(1)
go s.eventLoop()
return nil
}
func (s *Service) Stop() error {
s.cancel()
s.wg.Wait()
return nil
}
func (s *Service) eventLoop() {
defer s.wg.Done()
name := s.cfg.Driver.Name()
if s.cfg.ClearPendingTx {
const maxClearRetries = 3
for i := 0; i < maxClearRetries; i++ {
err := s.cfg.Driver.ClearPendingTx(s.ctx, s.txMgr, s.cfg.L1Client)
if err == nil {
break
} else if i < maxClearRetries-1 {
continue
}
log.Crit("Unable to confirm a clearing transaction", "err", err)
}
}
ticker := time.NewTicker(s.cfg.PollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Record the submitter's current ETH balance. This is done first in
// case any of the remaining steps fail, we can at least have an
// accurate view of the submitter's balance.
balance, err := s.cfg.L1Client.BalanceAt(
s.ctx, s.cfg.Driver.WalletAddr(), nil,
)
if err != nil {
log.Error(name+" unable to get current balance", "err", err)
continue
}
s.metrics.BalanceETH().Set(weiToEth64(balance))
// Determine the range of L2 blocks that the batch submitter has not
// processed, and needs to take action on.
log.Info(name + " fetching current block range")
start, end, err := s.cfg.Driver.GetBatchBlockRange(s.ctx)
if err != nil {
log.Error(name+" unable to get block range", "err", err)
continue
}
// No new updates.
if start.Cmp(end) == 0 {
log.Info(name+" no updates", "start", start, "end", end)
continue
}
log.Info(name+" block range", "start", start, "end", end)
// Query for the submitter's current nonce.
nonce64, err := s.cfg.L1Client.NonceAt(
s.ctx, s.cfg.Driver.WalletAddr(), nil,
)
if err != nil {
log.Error(name+" unable to get current nonce",
"err", err)
continue
}
nonce := new(big.Int).SetUint64(nonce64)
batchTxBuildStart := time.Now()
tx, err := s.cfg.Driver.CraftBatchTx(
s.ctx, start, end, nonce,
)
if err != nil {
log.Error(name+" unable to craft batch tx",
"err", err)
continue
} else if tx == nil {
continue
}
batchTxBuildTime := time.Since(batchTxBuildStart) / time.Millisecond
s.metrics.BatchTxBuildTimeMs().Set(float64(batchTxBuildTime))
// Record the size of the batch transaction.
var txBuf bytes.Buffer
if err := tx.EncodeRLP(&txBuf); err != nil {
log.Error(name+" unable to encode batch tx", "err", err)
continue
}
s.metrics.BatchSizeBytes().Observe(float64(len(txBuf.Bytes())))
// Construct the transaction submission clousure that will attempt
// to send the next transaction at the given nonce and gas price.
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
log.Info(name+" updating batch tx gas price", "start", start,
"end", end, "nonce", nonce)
return s.cfg.Driver.UpdateGasPrice(ctx, tx)
}
// Wait until one of our submitted transactions confirms. If no
// receipt is received it's likely our gas price was too low.
batchConfirmationStart := time.Now()
receipt, err := s.txMgr.Send(
s.ctx, updateGasPrice, s.cfg.Driver.SendTransaction,
)
// Record the confirmation time and gas used if we receive a
// receipt, as this indicates the transaction confirmed. We record
// these metrics here as the transaction may have reverted, and will
// abort below.
if receipt != nil {
batchConfirmationTime := time.Since(batchConfirmationStart) /
time.Millisecond
s.metrics.BatchConfirmationTimeMs().Set(float64(batchConfirmationTime))
s.metrics.SubmissionGasUsedWei().Set(float64(receipt.GasUsed))
}
if err != nil {
log.Error(name+" unable to publish batch tx",
"err", err)
s.metrics.FailedSubmissions().Inc()
continue
}
// The transaction was successfully submitted.
log.Info(name+" batch tx successfully published",
"tx_hash", receipt.TxHash)
s.metrics.BatchesSubmitted().Inc()
s.metrics.SubmissionTimestamp().Set(float64(time.Now().UnixNano() / 1e6))
case err := <-s.ctx.Done():
log.Error(name+" service shutting down", "err", err)
return
}
}
}
func weiToEth64(wei *big.Int) float64 {
eth := new(big.Float).SetInt(wei)
eth.Mul(eth, weiToEth)
eth64, _ := eth.Float64()
return eth64
}
package txmgr
import (
"strings"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
)
// SendState tracks information about the publication state of a given txn. In
// this context, a txn may correspond to multiple different txn hashes due to
// varying gas prices, though we treat them all as the same logical txn. This
// struct is primarily used to determine whether or not the txmgr should abort a
// given txn and retry with a higher nonce.
type SendState struct {
minedTxs map[common.Hash]struct{}
nonceTooLowCount uint64
mu sync.RWMutex
safeAbortNonceTooLowCount uint64
}
// NewSendState parameterizes a new SendState from the passed
// safeAbortNonceTooLowCount.
func NewSendState(safeAbortNonceTooLowCount uint64) *SendState {
if safeAbortNonceTooLowCount == 0 {
panic("txmgr: safeAbortNonceTooLowCount cannot be zero")
}
return &SendState{
minedTxs: make(map[common.Hash]struct{}),
nonceTooLowCount: 0,
safeAbortNonceTooLowCount: safeAbortNonceTooLowCount,
}
}
// ProcessSendError should be invoked with the error returned for each
// publication. It is safe to call this method with nil or arbitrary errors.
// Currently it only acts on errors containing the ErrNonceTooLow message.
func (s *SendState) ProcessSendError(err error) {
// Nothing to do.
if err == nil {
return
}
// Only concerned with ErrNonceTooLow.
if !strings.Contains(err.Error(), core.ErrNonceTooLow.Error()) {
return
}
s.mu.Lock()
defer s.mu.Unlock()
// Record this nonce too low observation.
s.nonceTooLowCount++
}
// TxMined records that the txn with txnHash has been mined and is await
// confirmation. It is safe to call this function multiple times.
func (s *SendState) TxMined(txHash common.Hash) {
s.mu.Lock()
defer s.mu.Unlock()
s.minedTxs[txHash] = struct{}{}
}
// TxMined records that the txn with txnHash has not been mined or has been
// reorg'd out. It is safe to call this function multiple times.
func (s *SendState) TxNotMined(txHash common.Hash) {
s.mu.Lock()
defer s.mu.Unlock()
_, wasMined := s.minedTxs[txHash]
delete(s.minedTxs, txHash)
// If the txn got reorged and left us with no mined txns, reset the nonce
// too low count, otherwise we might abort too soon when processing the next
// error. If the nonce too low errors persist, we want to ensure we wait out
// the full safe abort count to enesure we have a sufficient number of
// observations.
if len(s.minedTxs) == 0 && wasMined {
s.nonceTooLowCount = 0
}
}
// ShouldAbortImmediately returns true if the txmgr should give up on trying a
// given txn with the target nonce. For now, this only happens if we see an
// extended period of getting ErrNonceTooLow without having a txn mined.
func (s *SendState) ShouldAbortImmediately() bool {
s.mu.RLock()
defer s.mu.RUnlock()
// Never abort if our latest sample reports having at least one mined txn.
if len(s.minedTxs) > 0 {
return false
}
// Only abort if we've observed enough ErrNonceTooLow to meet our safe abort
// threshold.
return s.nonceTooLowCount >= s.safeAbortNonceTooLowCount
}
// IsWaitingForConfirmation returns true if we have at least one confirmation on
// one of our txs.
func (s *SendState) IsWaitingForConfirmation() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.minedTxs) > 0
}
package txmgr_test
import (
"errors"
"testing"
"github.com/ethereum-optimism/optimism/bss-core/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/stretchr/testify/require"
)
const testSafeAbortNonceTooLowCount = 3
var (
testHash = common.HexToHash("0x01")
)
func newSendState() *txmgr.SendState {
return txmgr.NewSendState(testSafeAbortNonceTooLowCount)
}
func processNSendErrors(sendState *txmgr.SendState, err error, n int) {
for i := 0; i < n; i++ {
sendState.ProcessSendError(err)
}
}
// TestSendStateNoAbortAfterInit asserts that the default SendState won't
// trigger an abort even after the safe abort interval has elapsed.
func TestSendStateNoAbortAfterInit(t *testing.T) {
sendState := newSendState()
require.False(t, sendState.ShouldAbortImmediately())
require.False(t, sendState.IsWaitingForConfirmation())
}
// TestSendStateNoAbortAfterProcessNilError asserts that nil errors are not
// considered for abort status.
func TestSendStateNoAbortAfterProcessNilError(t *testing.T) {
sendState := newSendState()
processNSendErrors(sendState, nil, testSafeAbortNonceTooLowCount)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateNoAbortAfterProcessOtherError asserts that non-nil errors other
// than ErrNonceTooLow are not considered for abort status.
func TestSendStateNoAbortAfterProcessOtherError(t *testing.T) {
sendState := newSendState()
otherError := errors.New("other error")
processNSendErrors(sendState, otherError, testSafeAbortNonceTooLowCount)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateAbortSafelyAfterNonceTooLowButNoTxMined asserts that we will
// abort after the safe abort interval has elapsed if we haven't mined a tx.
func TestSendStateAbortSafelyAfterNonceTooLowButNoTxMined(t *testing.T) {
sendState := newSendState()
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.True(t, sendState.ShouldAbortImmediately())
}
// TestSendStateMiningTxCancelsAbort asserts that a tx getting mined after
// processing ErrNonceTooLow takes precedence and doesn't cause an abort.
func TestSendStateMiningTxCancelsAbort(t *testing.T) {
sendState := newSendState()
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.TxMined(testHash)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateReorgingTxResetsAbort asserts that unmining a tx does not
// consider ErrNonceTooLow's prior to being mined when determine whether to
// abort.
func TestSendStateReorgingTxResetsAbort(t *testing.T) {
sendState := newSendState()
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.TxMined(testHash)
sendState.TxNotMined(testHash)
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateNoAbortEvenIfNonceTooLowAfterTxMined asserts that we will not
// abort if we continue to get ErrNonceTooLow after a tx has been mined.
//
// NOTE: This is the most crucial role of the SendState, as we _expect_ to get
// ErrNonceTooLow failures after one of our txs has been mined, but that
// shouldn't cause us to not continue waiting for confirmations.
func TestSendStateNoAbortEvenIfNonceTooLowAfterTxMined(t *testing.T) {
sendState := newSendState()
sendState.TxMined(testHash)
processNSendErrors(
sendState, core.ErrNonceTooLow, testSafeAbortNonceTooLowCount,
)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateSafeAbortIfNonceTooLowPersistsAfterUnmine asserts that we will
// correctly abort if we continue to get ErrNonceTooLow after a tx is unmined
// but not remined.
func TestSendStateSafeAbortIfNonceTooLowPersistsAfterUnmine(t *testing.T) {
sendState := newSendState()
sendState.TxMined(testHash)
sendState.TxNotMined(testHash)
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.True(t, sendState.ShouldAbortImmediately())
}
// TestSendStateSafeAbortWhileCallingNotMinedOnUnminedTx asserts that we will
// correctly abort if we continue to call TxNotMined on txns that haven't been
// mined.
func TestSendStateSafeAbortWhileCallingNotMinedOnUnminedTx(t *testing.T) {
sendState := newSendState()
processNSendErrors(
sendState, core.ErrNonceTooLow, testSafeAbortNonceTooLowCount,
)
sendState.TxNotMined(testHash)
require.True(t, sendState.ShouldAbortImmediately())
}
// TestSendStateIsWaitingForConfirmationAfterTxMined asserts that we are waiting
// for confirmation after a tx is mined.
func TestSendStateIsWaitingForConfirmationAfterTxMined(t *testing.T) {
sendState := newSendState()
testHash2 := common.HexToHash("0x02")
sendState.TxMined(testHash)
require.True(t, sendState.IsWaitingForConfirmation())
sendState.TxMined(testHash2)
require.True(t, sendState.IsWaitingForConfirmation())
}
// TestSendStateIsNotWaitingForConfirmationAfterTxUnmined asserts that we are
// not waiting for confirmation after a tx is mined then unmined.
func TestSendStateIsNotWaitingForConfirmationAfterTxUnmined(t *testing.T) {
sendState := newSendState()
sendState.TxMined(testHash)
sendState.TxNotMined(testHash)
require.False(t, sendState.IsWaitingForConfirmation())
}
This diff is collapsed.
This diff is collapsed.
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
"workspaces": { "workspaces": {
"packages": [ "packages": [
"packages/*", "packages/*",
"bss-core",
"gas-oracle", "gas-oracle",
"l2geth", "l2geth",
"ops/docker/rpc-proxy", "ops/docker/rpc-proxy",
......
...@@ -27,7 +27,6 @@ Go modules which are not yet versioned: ...@@ -27,7 +27,6 @@ Go modules which are not yet versioned:
```text ```text
./batch-submitter (changesets) ./batch-submitter (changesets)
./bss-core
./gas-oracle (changesets) ./gas-oracle (changesets)
./indexer (changesets) ./indexer (changesets)
./l2geth (changesets) ./l2geth (changesets)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment