driver.go 7.96 KB
Newer Older
1 2 3 4 5 6 7
package proposer

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	"math/big"
8
	"strings"
9

10 11 12 13 14
	"github.com/ethereum-optimism/optimism/batch-submitter/bindings/ctc"
	"github.com/ethereum-optimism/optimism/batch-submitter/bindings/scc"
	"github.com/ethereum-optimism/optimism/bss-core/drivers"
	"github.com/ethereum-optimism/optimism/bss-core/metrics"
	"github.com/ethereum-optimism/optimism/bss-core/txmgr"
15
	l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
16
	"github.com/ethereum-optimism/optimism/l2geth/log"
17
	"github.com/ethereum/go-ethereum/accounts/abi"
18 19 20 21 22 23 24
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
)

25 26 27
// stateRootSize is the size in bytes of a state root.
const stateRootSize = 32

28 29 30
var bigOne = new(big.Int).SetUint64(1) //nolint:unused

type Config struct {
31 32 33 34 35 36 37 38 39 40
	Name                 string
	L1Client             *ethclient.Client
	L2Client             *l2ethclient.Client
	BlockOffset          uint64
	MaxStateRootElements uint64
	MinStateRootElements uint64
	SCCAddr              common.Address
	CTCAddr              common.Address
	ChainID              *big.Int
	PrivKey              *ecdsa.PrivateKey
41 42 43
}

type Driver struct {
44 45 46 47 48
	cfg            Config
	sccContract    *scc.StateCommitmentChain
	rawSccContract *bind.BoundContract
	ctcContract    *ctc.CanonicalTransactionChain
	walletAddr     common.Address
49
	metrics        *metrics.Base
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
}

func NewDriver(cfg Config) (*Driver, error) {
	sccContract, err := scc.NewStateCommitmentChain(
		cfg.SCCAddr, cfg.L1Client,
	)
	if err != nil {
		return nil, err
	}

	ctcContract, err := ctc.NewCanonicalTransactionChain(
		cfg.CTCAddr, cfg.L1Client,
	)
	if err != nil {
		return nil, err
	}

67 68 69 70 71 72 73 74 75 76 77
	parsed, err := abi.JSON(strings.NewReader(
		scc.StateCommitmentChainABI,
	))
	if err != nil {
		return nil, err
	}

	rawSccContract := bind.NewBoundContract(
		cfg.SCCAddr, parsed, cfg.L1Client, cfg.L1Client, cfg.L1Client,
	)

78 79 80
	walletAddr := crypto.PubkeyToAddress(cfg.PrivKey.PublicKey)

	return &Driver{
81 82 83 84 85
		cfg:            cfg,
		sccContract:    sccContract,
		rawSccContract: rawSccContract,
		ctcContract:    ctcContract,
		walletAddr:     walletAddr,
86
		metrics:        metrics.NewBase("batch_submitter", cfg.Name),
87 88 89 90 91 92 93 94 95 96 97 98 99
	}, nil
}

// Name is an identifier used to prefix logs for a particular service.
func (d *Driver) Name() string {
	return d.cfg.Name
}

// WalletAddr is the wallet address used to pay for batch transaction fees.
func (d *Driver) WalletAddr() common.Address {
	return d.walletAddr
}

100
// Metrics returns the subservice telemetry object.
101
func (d *Driver) Metrics() metrics.Metrics {
102 103 104
	return d.metrics
}

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
// 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.
func (d *Driver) ClearPendingTx(
	ctx context.Context,
	txMgr txmgr.TxManager,
	l1Client *ethclient.Client,
) error {

	return drivers.ClearPendingTx(
		d.cfg.Name, ctx, txMgr, l1Client, d.walletAddr, d.cfg.PrivKey,
		d.cfg.ChainID,
	)
}

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
// 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.
func (d *Driver) GetBatchBlockRange(
	ctx context.Context) (*big.Int, *big.Int, error) {

	blockOffset := new(big.Int).SetUint64(d.cfg.BlockOffset)

	start, err := d.sccContract.GetTotalElements(&bind.CallOpts{
		Pending: false,
		Context: ctx,
	})
	if err != nil {
		return nil, nil, err
	}
	start.Add(start, blockOffset)

137
	end, err := d.ctcContract.GetTotalElements(&bind.CallOpts{
138 139 140 141 142 143
		Pending: false,
		Context: ctx,
	})
	if err != nil {
		return nil, nil, err
	}
144
	end.Add(end, blockOffset)
145 146 147 148 149 150 151 152 153

	if start.Cmp(end) > 0 {
		return nil, nil, fmt.Errorf("invalid range, "+
			"end(%v) < start(%v)", end, start)
	}

	return start, end, nil
}

154 155 156 157 158 159
// 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.
//
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) CraftBatchTx(
160
	ctx context.Context,
161 162
	start, end, nonce *big.Int,
) (*types.Transaction, error) {
163

164 165
	name := d.cfg.Name

166 167 168
	log.Info(name+" crafting batch tx", "start", start, "end", end,
		"nonce", nonce)

169
	var stateRoots [][stateRootSize]byte
170
	for i := new(big.Int).Set(start); i.Cmp(end) < 0; i.Add(i, bigOne) {
Matthew Slipper's avatar
Matthew Slipper committed
171
		// Consume state roots until reach our maximum tx size.
172
		if uint64(len(stateRoots)) > d.cfg.MaxStateRootElements {
Matthew Slipper's avatar
Matthew Slipper committed
173 174 175
			break
		}

176 177 178 179 180 181 182 183
		block, err := d.cfg.L2Client.BlockByNumber(ctx, i)
		if err != nil {
			return nil, err
		}

		stateRoots = append(stateRoots, block.Root())
	}

184 185 186 187 188 189 190 191 192
	// Abort if we don't have enough state roots to meet our minimum
	// requirement.
	if uint64(len(stateRoots)) < d.cfg.MinStateRootElements {
		log.Info(name+" number of state roots  below minimum",
			"num_state_roots", len(stateRoots),
			"min_state_roots", d.cfg.MinStateRootElements)
		return nil, nil
	}

193
	d.metrics.NumElementsPerBatch().Observe(float64(len(stateRoots)))
194 195

	log.Info(name+" batch constructed", "num_state_roots", len(stateRoots))
196

197 198 199 200 201 202 203
	opts, err := bind.NewKeyedTransactorWithChainID(
		d.cfg.PrivKey, d.cfg.ChainID,
	)
	if err != nil {
		return nil, err
	}
	opts.Context = ctx
204 205
	opts.Nonce = nonce
	opts.NoSend = true
206 207 208 209

	blockOffset := new(big.Int).SetUint64(d.cfg.BlockOffset)
	offsetStartsAtIndex := new(big.Int).Sub(start, blockOffset)

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
	tx, err := d.sccContract.AppendStateBatch(
		opts, stateRoots, offsetStartsAtIndex,
	)
	switch {
	case err == nil:
		return tx, nil

	// 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.
	case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
		log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
			"by current backend, using fallback gasTipCap")
		opts.GasTipCap = drivers.FallbackGasTipCap
		return d.sccContract.AppendStateBatch(
			opts, stateRoots, offsetStartsAtIndex,
		)

	default:
		return nil, err
	}
234
}
235

236 237 238 239 240
// 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.
func (d *Driver) UpdateGasPrice(
241 242 243 244 245 246 247 248 249 250 251 252
	ctx context.Context,
	tx *types.Transaction,
) (*types.Transaction, error) {

	opts, err := bind.NewKeyedTransactorWithChainID(
		d.cfg.PrivKey, d.cfg.ChainID,
	)
	if err != nil {
		return nil, err
	}
	opts.Context = ctx
	opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
253
	opts.NoSend = true
254

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
	finalTx, err := d.rawSccContract.RawTransact(opts, tx.Data())
	switch {
	case err == nil:
		return finalTx, nil

	// 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.
	case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
		log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
			"by current backend, using fallback gasTipCap")
		opts.GasTipCap = drivers.FallbackGasTipCap
		return d.rawSccContract.RawTransact(opts, tx.Data())

	default:
		return nil, err
	}
275
}
276 277 278 279 280 281 282 283 284

// SendTransaction injects a signed transaction into the pending pool for
// execution.
func (d *Driver) SendTransaction(
	ctx context.Context,
	tx *types.Transaction,
) error {
	return d.cfg.L1Client.SendTransaction(ctx, tx)
}