Commit 6a3b1ec1 authored by protolambda's avatar protolambda Committed by GitHub

op-signer: update client to support Blob tx signing (#9185)

* op-signer: update client to support Blob tx signing

* op-service: update maxFeePerBlobGas JSON field to match internal geth TransactionArgs
parent 17676bbb
......@@ -219,7 +219,7 @@ require (
rsc.io/tmplfunc v0.0.3 // indirect
)
replace github.com/ethereum/go-ethereum v1.13.5 => github.com/ethereum-optimism/op-geth v1.101305.2-rc.2.0.20240123201117-bfa8ffc685e0
replace github.com/ethereum/go-ethereum v1.13.5 => github.com/ethereum-optimism/op-geth v1.101305.3-rc.1.0.20240124221225-5c6f10d449ab
//replace github.com/ethereum-optimism/superchain-registry/superchain => ../superchain-registry/superchain
//replace github.com/ethereum/go-ethereum v1.13.5 => ../go-ethereum
......@@ -170,8 +170,8 @@ github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/
github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs=
github.com/ethereum-optimism/op-geth v1.101305.2-rc.2.0.20240123201117-bfa8ffc685e0 h1:Cu3e2Pgn0rBTzr7FjLbUBsJRq2RcVWDPf9tTp6dBZtY=
github.com/ethereum-optimism/op-geth v1.101305.2-rc.2.0.20240123201117-bfa8ffc685e0/go.mod h1:/0cPafbmmt3JR0yiuoBJWTRCx8briXUd/mtXr+GJ3Zw=
github.com/ethereum-optimism/op-geth v1.101305.3-rc.1.0.20240124221225-5c6f10d449ab h1:SpoQiEG5mteZnaPCo5Pm5QrberdODUefwLjdAtL9LAE=
github.com/ethereum-optimism/op-geth v1.101305.3-rc.1.0.20240124221225-5c6f10d449ab/go.mod h1:/0cPafbmmt3JR0yiuoBJWTRCx8briXUd/mtXr+GJ3Zw=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240123193359-a5fc767e225a h1:mWIRpGyrAlWHUznUHKgAJUafkNGfO7VmeLjilhVhB80=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240123193359-a5fc767e225a/go.mod h1:/70H/KqrtKcvWvNGVj6S3rAcLC+kUPr3t2aDmYIS+Xk=
github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY=
......
......@@ -389,6 +389,7 @@ func (l *BatchSubmitter) sendTransaction(txdata txData, queue *txmgr.Queue[txDat
}
func (l *BatchSubmitter) blobTxCandidate(data []byte) (*txmgr.TxCandidate, error) {
l.Log.Info("building Blob transaction candidate", "size", len(data))
var b eth.Blob
if err := b.FromData(data); err != nil {
return nil, fmt.Errorf("data could not be converted to blob: %w", err)
......@@ -400,6 +401,7 @@ func (l *BatchSubmitter) blobTxCandidate(data []byte) (*txmgr.TxCandidate, error
}
func (l *BatchSubmitter) calldataTxCandidate(data []byte) *txmgr.TxCandidate {
l.Log.Info("building Calldata transaction candidate", "size", len(data))
return &txmgr.TxCandidate{
To: &l.RollupConfig.BatchInboxAddress,
TxData: data,
......
......@@ -16,6 +16,7 @@ import (
"github.com/ethereum-optimism/optimism/op-batcher/flags"
"github.com/ethereum-optimism/optimism/op-batcher/metrics"
"github.com/ethereum-optimism/optimism/op-batcher/rpc"
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/ethereum-optimism/optimism/op-service/dial"
......@@ -172,6 +173,7 @@ func (bs *BatcherService) initRollupConfig(ctx context.Context) error {
if err := bs.RollupConfig.Check(); err != nil {
return fmt.Errorf("invalid rollup config: %w", err)
}
bs.RollupConfig.LogDescription(bs.Log, chaincfg.L2ChainIDToNetworkDisplayName)
return nil
}
......@@ -197,9 +199,23 @@ func (bs *BatcherService) initChannelConfig(cfg *CLIConfig) error {
}
bs.ChannelConfig.MaxFrameSize-- // subtract 1 byte for version
if bs.UseBlobs && !bs.RollupConfig.IsEcotone(uint64(time.Now().Unix())) {
bs.Log.Error("Cannot use Blob data before Ecotone!") // log only, the batcher may not be actively running.
}
if !bs.UseBlobs && bs.RollupConfig.IsEcotone(uint64(time.Now().Unix())) {
bs.Log.Warn("Ecotone upgrade is active, but batcher is not configured to use Blobs!")
}
if err := bs.ChannelConfig.Check(); err != nil {
return fmt.Errorf("invalid channel configuration: %w", err)
}
bs.Log.Info("Initialized channel-config",
"use_blobs", bs.UseBlobs,
"max_frame_size", bs.ChannelConfig.MaxFrameSize,
"max_channel_duration", bs.ChannelConfig.MaxChannelDuration,
"channel_timeout", bs.ChannelConfig.ChannelTimeout,
"batch_type", bs.ChannelConfig.BatchType,
"sub_safety_margin", bs.ChannelConfig.SubSafetyMargin)
return nil
}
......
......@@ -93,17 +93,23 @@ func (s *SignerClient) pingVersion() (string, error) {
}
func (s *SignerClient) SignTransaction(ctx context.Context, chainId *big.Int, from common.Address, tx *types.Transaction) (*types.Transaction, error) {
args := NewTransactionArgsFromTransaction(chainId, from, tx)
sidecar := tx.BlobTxSidecar()
args := NewTransactionArgsFromTransaction(chainId, &from, tx.WithoutBlobTxSidecar())
var result hexutil.Bytes
if err := s.client.CallContext(ctx, &result, "eth_signTransaction", args); err != nil {
return nil, fmt.Errorf("eth_signTransaction failed: %w", err)
}
signed := &types.Transaction{}
var signed types.Transaction
if err := signed.UnmarshalBinary(result); err != nil {
return nil, err
}
if sidecar != nil {
if err := signed.SetBlobTxSidecar(sidecar); err != nil {
return nil, fmt.Errorf("failed to attach sidecar to signed blob tx: %w", err)
}
}
return signed, nil
return &signed, nil
}
package signer
import (
"bytes"
"errors"
"fmt"
"math/big"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
......@@ -10,6 +15,8 @@ import (
// TransactionArgs represents the arguments to construct a new transaction
// or a message call.
// Geth has an internal version of this, but this is not exported, and only supported in v1.13.11 and forward.
// This signing API format is based on the legacy personal-account signing RPC of ethereum.
type TransactionArgs struct {
From *common.Address `json:"from"`
To *common.Address `json:"to"`
......@@ -28,16 +35,20 @@ type TransactionArgs struct {
AccessList *types.AccessList `json:"accessList,omitempty"`
ChainID *hexutil.Big `json:"chainId,omitempty"`
// Custom extension for EIP-4844 support
BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
BlobFeeCap *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
}
// NewTransactionArgsFromTransaction creates a TransactionArgs struct from an EIP-1559 transaction
func NewTransactionArgsFromTransaction(chainId *big.Int, from common.Address, tx *types.Transaction) *TransactionArgs {
// NewTransactionArgsFromTransaction creates a TransactionArgs struct from an EIP-1559 or EIP-4844 transaction
func NewTransactionArgsFromTransaction(chainId *big.Int, from *common.Address, tx *types.Transaction) *TransactionArgs {
data := hexutil.Bytes(tx.Data())
nonce := hexutil.Uint64(tx.Nonce())
gas := hexutil.Uint64(tx.Gas())
accesses := tx.AccessList()
args := &TransactionArgs{
From: &from,
From: from,
Input: &data,
Nonce: &nonce,
Value: (*hexutil.Big)(tx.Value()),
......@@ -47,6 +58,8 @@ func NewTransactionArgsFromTransaction(chainId *big.Int, from common.Address, tx
MaxFeePerGas: (*hexutil.Big)(tx.GasFeeCap()),
MaxPriorityFeePerGas: (*hexutil.Big)(tx.GasTipCap()),
AccessList: &accesses,
BlobVersionedHashes: tx.BlobHashes(),
BlobFeeCap: (*hexutil.Big)(tx.BlobGasFeeCap()),
}
return args
}
......@@ -62,23 +75,106 @@ func (args *TransactionArgs) data() []byte {
return nil
}
// ToTransaction converts the arguments to a transaction.
func (args *TransactionArgs) ToTransaction() *types.Transaction {
func (args *TransactionArgs) Check() error {
if args.Gas == nil {
return errors.New("gas not specified")
}
if args.GasPrice != nil {
return errors.New("only accepts maxFeePerGas/maxPriorityFeePerGas params")
}
if args.MaxFeePerGas == nil || args.MaxPriorityFeePerGas == nil {
return errors.New("missing maxFeePerGas or maxPriorityFeePerGas")
}
// Both EIP-1559 fee parameters are now set; sanity check them.
if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 {
return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas)
}
if args.Nonce == nil {
return errors.New("nonce not specified")
}
if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
return errors.New(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`)
}
if args.To == nil && len(args.data()) == 0 {
return errors.New("contract creation without any data provided")
}
if args.ChainID == nil {
return errors.New("chain id not specified")
}
if args.Value == nil {
args.Value = new(hexutil.Big)
}
if args.AccessList == nil {
args.AccessList = &types.AccessList{}
}
if args.BlobVersionedHashes != nil {
if len(args.BlobVersionedHashes) == 0 {
return errors.New("non-null blob versioned hashes should not be empty")
}
if args.BlobFeeCap == nil {
return errors.New("when including blobs a blob-fee-cap is required")
}
} else {
if args.BlobFeeCap != nil {
return errors.New("unexpected blob-fee-cap, transaction does not include blobs")
}
}
return nil
}
// ToTransactionData converts the arguments to transaction content-data. Warning: this excludes blob data.
func (args *TransactionArgs) ToTransactionData() (types.TxData, error) {
var data types.TxData
al := types.AccessList{}
if args.AccessList != nil {
al = *args.AccessList
}
data = &types.DynamicFeeTx{
To: args.To,
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(*args.Nonce),
Gas: uint64(*args.Gas),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
Value: (*big.Int)(args.Value),
Data: args.data(),
AccessList: al,
}
return types.NewTx(data)
if len(args.BlobVersionedHashes) > 0 {
chainID, overflow := uint256.FromBig((*big.Int)(args.ChainID))
if overflow {
return nil, fmt.Errorf("chainID %s too large for blob tx", args.ChainID)
}
maxFeePerGas, overflow := uint256.FromBig((*big.Int)(args.MaxFeePerGas))
if overflow {
return nil, fmt.Errorf("maxFeePerGas %s too large for blob tx", args.MaxFeePerGas)
}
maxPriorityFeePerGas, overflow := uint256.FromBig((*big.Int)(args.MaxPriorityFeePerGas))
if overflow {
return nil, fmt.Errorf("maxPriorityFeePerGas %s too large for blob tx", args.MaxPriorityFeePerGas)
}
value, overflow := uint256.FromBig((*big.Int)(args.Value))
if overflow {
return nil, fmt.Errorf("value %s too large for blob tx", args.Value)
}
blobFeeCap, overflow := uint256.FromBig((*big.Int)(args.BlobFeeCap))
if overflow {
return nil, fmt.Errorf("blobFeeCap %s too large for blob tx", args.BlobFeeCap)
}
data = &types.BlobTx{
ChainID: chainID,
Nonce: uint64(*args.Nonce),
GasTipCap: maxPriorityFeePerGas,
GasFeeCap: maxFeePerGas,
Gas: uint64(*args.Gas),
To: *args.To,
Value: value,
Data: args.data(),
AccessList: al,
BlobFeeCap: blobFeeCap,
BlobHashes: args.BlobVersionedHashes,
}
} else {
data = &types.DynamicFeeTx{
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(*args.Nonce),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
Gas: uint64(*args.Gas),
To: args.To,
Value: (*big.Int)(args.Value),
Data: args.data(),
AccessList: al,
}
}
return data, nil
}
......@@ -245,6 +245,7 @@ func (m *SimpleTxManager) send(ctx context.Context, candidate TxCandidate) (*typ
// NOTE: If the [TxCandidate.GasLimit] is non-zero, it will be used as the transaction's gas.
// NOTE: Otherwise, the [SimpleTxManager] will query the specified backend for an estimate.
func (m *SimpleTxManager) craftTx(ctx context.Context, candidate TxCandidate) (*types.Transaction, error) {
m.l.Debug("crafting Transaction", "blobs", len(candidate.Blobs), "calldata_size", len(candidate.TxData))
gasTipCap, baseFee, blobBaseFee, err := m.suggestGasPriceCaps(ctx)
if err != nil {
m.metr.RPCError()
......
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