driver.go 11.6 KB
Newer Older
1 2 3 4 5 6 7 8 9
package sequencer

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

10 11 12 13
	"github.com/ethereum-optimism/optimism/batch-submitter/bindings/ctc"
	"github.com/ethereum-optimism/optimism/bss-core/drivers"
	"github.com/ethereum-optimism/optimism/bss-core/metrics"
	"github.com/ethereum-optimism/optimism/bss-core/txmgr"
14
	l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
15
	"github.com/ethereum/go-ethereum"
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
	"github.com/ethereum/go-ethereum/accounts/abi"
	"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"
	"github.com/ethereum/go-ethereum/log"
)

const (
	appendSequencerBatchMethodName = "appendSequencerBatch"
)

var bigOne = new(big.Int).SetUint64(1)

type Config struct {
32 33 34 35 36 37 38 39 40 41 42
	Name                  string
	L1Client              *ethclient.Client
	L2Client              *l2ethclient.Client
	BlockOffset           uint64
	MinTxSize             uint64
	MaxTxSize             uint64
	MaxPlaintextBatchSize uint64
	CTCAddr               common.Address
	ChainID               *big.Int
	PrivKey               *ecdsa.PrivateKey
	BatchType             BatchType
43 44 45 46 47 48 49 50
}

type Driver struct {
	cfg            Config
	ctcContract    *ctc.CanonicalTransactionChain
	rawCtcContract *bind.BoundContract
	walletAddr     common.Address
	ctcABI         *abi.ABI
51
	metrics        *Metrics
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
}

func NewDriver(cfg Config) (*Driver, error) {
	ctcContract, err := ctc.NewCanonicalTransactionChain(
		cfg.CTCAddr, cfg.L1Client,
	)
	if err != nil {
		return nil, err
	}

	parsed, err := abi.JSON(strings.NewReader(
		ctc.CanonicalTransactionChainABI,
	))
	if err != nil {
		return nil, err
	}

	ctcABI, err := ctc.CanonicalTransactionChainMetaData.GetAbi()
	if err != nil {
		return nil, err
	}

	rawCtcContract := bind.NewBoundContract(
		cfg.CTCAddr, parsed, cfg.L1Client, cfg.L1Client,
		cfg.L1Client,
	)

	walletAddr := crypto.PubkeyToAddress(cfg.PrivKey.PublicKey)

	return &Driver{
		cfg:            cfg,
		ctcContract:    ctcContract,
		rawCtcContract: rawCtcContract,
		walletAddr:     walletAddr,
		ctcABI:         ctcABI,
87
		metrics:        NewMetrics(cfg.Name),
88 89 90 91 92 93 94 95 96 97 98 99 100
	}, 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
}

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

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
// 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,
	)
}

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
// 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.ctcContract.GetTotalElements(&bind.CallOpts{
		Pending: false,
		Context: ctx,
	})
	if err != nil {
		return nil, nil, err
	}
	start.Add(start, blockOffset)

	latestHeader, err := d.cfg.L2Client.HeaderByNumber(ctx, nil)
	if err != nil {
		return nil, nil, err
	}

	// Add one because end is *exclusive*.
	end := new(big.Int).Add(latestHeader.Number, bigOne)

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

	return start, end, nil
}

154 155
// 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
156 157
// transaction to use for size estimation. A nil transaction is returned if the
// transaction does not meet the minimum size requirements.
158 159 160
//
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) CraftBatchTx(
161
	ctx context.Context,
162 163
	start, end, nonce *big.Int,
) (*types.Transaction, error) {
164 165 166

	name := d.cfg.Name

167
	log.Info(name+" crafting batch tx", "start", start, "end", end,
168
		"nonce", nonce, "type", d.cfg.BatchType.String())
169

170
	var (
171 172 173
		batchElements  []BatchElement
		totalTxSize    uint64
		hasLargeNextTx bool
174
	)
175 176 177 178 179 180
	for i := new(big.Int).Set(start); i.Cmp(end) < 0; i.Add(i, bigOne) {
		block, err := d.cfg.L2Client.BlockByNumber(ctx, i)
		if err != nil {
			return nil, err
		}

181 182 183 184 185 186 187 188 189 190
		// For each sequencer transaction, update our running total with the
		// size of the transaction.
		batchElement := BatchElementFromBlock(block)
		if batchElement.IsSequencerTx() {
			// Abort once the total size estimate is greater than the maximum
			// configured size. This is a conservative estimate, as the total
			// calldata size will be greater when batch contexts are included.
			// Below this set will be further whittled until the raw call data
			// size also adheres to this constraint.
			txLen := batchElement.Tx.Size()
191
			if totalTxSize+uint64(TxLenSize+txLen) > d.cfg.MaxPlaintextBatchSize {
192 193 194 195 196 197
				// Adding this transaction causes the batch to be too large, but
				// we also record if the batch size without the transaction
				// fails to meet our minimum size constraint. This is used below
				// to determine whether or not to ignore the minimum size check,
				// since in this case it can't be avoided.
				hasLargeNextTx = totalTxSize < d.cfg.MinTxSize
198 199 200 201
				break
			}
			totalTxSize += uint64(TxLenSize + txLen)
		}
202

203
		batchElements = append(batchElements, batchElement)
204 205 206
	}

	shouldStartAt := start.Uint64()
207
	var pruneCount int
208 209
	for {
		batchParams, err := GenSequencerBatchParams(
210
			shouldStartAt, d.cfg.BlockOffset, batchElements,
211 212 213 214
		)
		if err != nil {
			return nil, err
		}
215

216 217
		// Encode the batch arguments using the configured encoding type.
		batchArguments, err := batchParams.Serialize(d.cfg.BatchType)
218 219 220
		if err != nil {
			return nil, err
		}
221

222
		appendSequencerBatchID := d.ctcABI.Methods[appendSequencerBatchMethodName].ID
223
		calldata := append(appendSequencerBatchID, batchArguments...)
224

225
		log.Info(name+" testing batch size",
226
			"calldata_size", len(calldata),
227 228 229
			"min_tx_size", d.cfg.MinTxSize,
			"max_tx_size", d.cfg.MaxTxSize)

230 231
		// Continue pruning until plaintext calldata size is less than
		// configured max.
232 233
		calldataSize := uint64(len(calldata))
		if calldataSize > d.cfg.MaxTxSize {
Matthew Slipper's avatar
Matthew Slipper committed
234 235
			oldLen := len(batchElements)
			newBatchElementsLen := (oldLen * 9) / 10
236
			batchElements = batchElements[:newBatchElementsLen]
237 238 239
			log.Info(name+" pruned batch",
				"old_num_txs", oldLen,
				"new_num_txs", newBatchElementsLen)
240
			pruneCount++
241
			continue
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
		}

		// There are two specific cases in which we choose to ignore the minimum
		// L1 tx size. These cases are permitted since they arise from
		// situations where the difference between the configured MinTxSize and
		// MaxTxSize is less than the maximum L2 tx size permitted by the
		// mempool.
		//
		// This configuration is useful when trying to ensure the profitability
		// is sufficient, and we permit batches to be submitted with less than
		// our desired configuration only if it is not possible to construct a
		// batch within the given parameters.
		//
		// The two cases are:
		// 1. When the next elenent is larger than the difference between the
		//    min and the max, causing the batch to be too small without the
		//    element, and too large with it.
		// 2. When pruning a batch that exceeds the mac size below, and then
		//    becomes too small as a result. This is avoided by only applying
		//    the min size check when the pruneCount is zero.
		ignoreMinTxSize := pruneCount > 0 || hasLargeNextTx
263
		if !ignoreMinTxSize && calldataSize < d.cfg.MinTxSize {
264
			log.Info(name+" batch tx size below minimum",
265
				"num_txs", len(batchElements))
266
			return nil, nil
267
		}
268

269
		d.metrics.NumElementsPerBatch().Observe(float64(len(batchElements)))
270
		d.metrics.BatchPruneCount.Set(float64(pruneCount))
271

272 273 274 275
		log.Info(name+" batch constructed",
			"num_txs", len(batchElements),
			"final_size", len(calldata),
			"batch_type", d.cfg.BatchType)
276

277 278 279 280 281 282 283
		opts, err := bind.NewKeyedTransactorWithChainID(
			d.cfg.PrivKey, d.cfg.ChainID,
		)
		if err != nil {
			return nil, err
		}
		opts.Context = ctx
284 285
		opts.Nonce = nonce
		opts.NoSend = true
286

287
		tx, err := d.rawCtcContract.RawTransact(opts, calldata)
288 289 290 291 292 293 294 295 296 297 298 299 300 301
		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
302
			return d.rawCtcContract.RawTransact(opts, calldata)
303 304 305 306

		default:
			return nil, err
		}
307 308
	}
}
309

310 311 312 313 314
// 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(
315 316 317 318
	ctx context.Context,
	tx *types.Transaction,
) (*types.Transaction, error) {

319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
	gasTipCap, err := d.cfg.L1Client.SuggestGasTipCap(ctx)
	if err != 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.
		if !drivers.IsMaxPriorityFeePerGasNotFoundError(err) {
			return nil, err
		}

		log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
			"by current backend, using fallback gasTipCap")
		gasTipCap = drivers.FallbackGasTipCap
	}

	header, err := d.cfg.L1Client.HeaderByNumber(ctx, nil)
	if err != nil {
		return nil, err
	}
	gasFeeCap := txmgr.CalcGasFeeCap(header.BaseFee, gasTipCap)

	// The estimated gas limits performed by RawTransact fail semi-regularly
	// with out of gas exceptions. To remedy this we extract the internal calls
	// to perform gas price/gas limit estimation here and add a buffer to
	// account for any network variability.
	gasLimit, err := d.cfg.L1Client.EstimateGas(ctx, ethereum.CallMsg{
		From:      d.walletAddr,
		To:        &d.cfg.CTCAddr,
		GasPrice:  nil,
		GasTipCap: gasTipCap,
		GasFeeCap: gasFeeCap,
		Value:     nil,
		Data:      tx.Data(),
	})
	if err != nil {
		return nil, err
	}

359 360 361 362 363 364 365 366
	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())
367 368 369
	opts.GasTipCap = gasTipCap
	opts.GasFeeCap = gasFeeCap
	opts.GasLimit = 6 * gasLimit / 5 // add 20% buffer to gas limit
370
	opts.NoSend = true
371

372
	return d.rawCtcContract.RawTransact(opts, tx.Data())
373
}
374 375 376 377 378 379 380 381 382

// 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)
}