From d5c0a153a1c4c3a348c6d880fcf78c93ca5ff75f Mon Sep 17 00:00:00 2001 From: Samuel Laferriere Date: Fri, 29 Mar 2024 11:45:27 -0700 Subject: [PATCH] Move estimateGasAndNonce from wallet to txMgr, and add gasLimit 1.1x bump by default (#167) * moved estimateGas from wallet to txMgr, and bumped gasLimit by 1.1 (default value) * lower fallbackGasTipCap from 15 to 5 gwei * update our gasFeeCap formula to 2*basefee+gastip * bump gasLimitMultiplier to 1.20 --- chainio/clients/wallet/privatekey_wallet.go | 58 ----------- chainio/txmgr/txmgr.go | 105 ++++++++++++++++++-- 2 files changed, 96 insertions(+), 67 deletions(-) diff --git a/chainio/clients/wallet/privatekey_wallet.go b/chainio/clients/wallet/privatekey_wallet.go index ecc14409..b37947f7 100644 --- a/chainio/clients/wallet/privatekey_wallet.go +++ b/chainio/clients/wallet/privatekey_wallet.go @@ -9,17 +9,12 @@ import ( "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/signerv2" sdktypes "github.com/Layr-Labs/eigensdk-go/types" - "github.com/ethereum/go-ethereum" "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" ) -var ( - FallbackGasTipCap = big.NewInt(15_000_000_000) -) - var _ Wallet = (*privateKeyWallet)(nil) type privateKeyWallet struct { @@ -43,14 +38,6 @@ func NewPrivateKeyWallet(ethClient eth.Client, signer signerv2.SignerFn, signerA } func (t *privateKeyWallet) SendTransaction(ctx context.Context, tx *types.Transaction) (TxID, error) { - // Estimate gas and nonce - // can't print tx hash in logs because the tx changes below when we complete and sign it - // so the txHash is meaningless at this point - t.logger.Debug("Estimating gas and nonce") - tx, err := t.estimateGasAndNonce(ctx, tx) - if err != nil { - return "", err - } t.logger.Debug("Getting signer for tx") signer, err := t.signerFn(ctx, t.address) @@ -95,48 +82,3 @@ func (t *privateKeyWallet) GetTransactionReceipt(ctx context.Context, txID TxID) func (t *privateKeyWallet) SenderAddress(ctx context.Context) (common.Address, error) { return t.address, nil } - -// estimateGasAndNonce we are explicitly implementing this because -// * We want to support legacy transactions (i.e. not dynamic fee) -// * We want to support gas management, i.e. add buffer to gas limit -func (t *privateKeyWallet) estimateGasAndNonce(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) { - gasTipCap, err := t.ethClient.SuggestGasTipCap(ctx) - if err != nil { - // If the transaction failed because the backend does not support - // eth_maxPriorityFeePerGas, fallback to using the default constant. - t.logger.Info("eth_maxPriorityFeePerGas is unsupported by current backend, using fallback gasTipCap") - gasTipCap = FallbackGasTipCap - } - - header, err := t.ethClient.HeaderByNumber(ctx, nil) - if err != nil { - return nil, err - } - - gasFeeCap := new(big.Int).Add(header.BaseFee, gasTipCap) - - gasLimit, err := t.ethClient.EstimateGas(ctx, ethereum.CallMsg{ - From: t.address, - To: tx.To(), - GasTipCap: gasTipCap, - GasFeeCap: gasFeeCap, - Value: tx.Value(), - Data: tx.Data(), - }) - if err != nil { - return nil, err - } - - rawTx := &types.DynamicFeeTx{ - ChainID: tx.ChainId(), - To: tx.To(), - GasTipCap: gasTipCap, - GasFeeCap: gasFeeCap, - Data: tx.Data(), - Value: tx.Value(), - Gas: gasLimit, // TODO(add buffer) - Nonce: tx.Nonce(), // We are not doing any nonce management for now but we probably should later for more robustness - } - - return types.NewTx(rawTx), nil -} diff --git a/chainio/txmgr/txmgr.go b/chainio/txmgr/txmgr.go index a202e5da..44e8e0b6 100644 --- a/chainio/txmgr/txmgr.go +++ b/chainio/txmgr/txmgr.go @@ -3,6 +3,7 @@ package txmgr import ( "context" "errors" + "math/big" "time" "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" @@ -15,6 +16,13 @@ import ( "github.com/ethereum/go-ethereum/log" ) +var ( + // 5 gwei in case the backend does not support eth_maxPriorityFeePerGas (no idea if/when this ever happens..) + FallbackGasTipCap = big.NewInt(5_000_000_000) + // 1.20x gas limit multiplier. This is arbitrary but should be safe for most cases + FallbackGasLimitMultiplier = 1.20 +) + // We are taking inspiration from the optimism TxManager interface // https://github.com/ethereum-optimism/optimism/blob/develop/op-service/txmgr/txmgr.go @@ -30,10 +38,11 @@ type TxManager interface { } type SimpleTxManager struct { - wallet wallet.Wallet - client eth.Client - log logging.Logger - sender common.Address + wallet wallet.Wallet + client eth.Client + log logging.Logger + sender common.Address + gasLimitMultiplier float64 } var _ TxManager = (*SimpleTxManager)(nil) @@ -47,13 +56,19 @@ func NewSimpleTxManager( sender common.Address, ) *SimpleTxManager { return &SimpleTxManager{ - wallet: wallet, - client: client, - log: log, - sender: sender, + wallet: wallet, + client: client, + log: log, + sender: sender, + gasLimitMultiplier: FallbackGasLimitMultiplier, } } +func (m *SimpleTxManager) WithGasLimitMultiplier(multiplier float64) *SimpleTxManager { + m.gasLimitMultiplier = multiplier + return m +} + // Send is used to send a transaction to the Ethereum node. It takes an unsigned/signed transaction // and then sends it to the Ethereum node. // It also takes care of gas estimation and adds a buffer to the gas limit @@ -61,7 +76,24 @@ func NewSimpleTxManager( // and resign the transaction after adding the nonce and gas limit. // To check out the whole flow on how this works, check out the README.md in this folder func (m *SimpleTxManager) Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) { - txID, err := m.wallet.SendTransaction(ctx, tx) + // Estimate gas and nonce + // can't print tx hash in logs because the tx changes below when we complete and sign it + // so the txHash is meaningless at this point + m.log.Debug("Estimating gas and nonce") + tx, err := m.estimateGasAndNonce(ctx, tx) + if err != nil { + return nil, err + } + bumpedGasTx := &types.DynamicFeeTx{ + To: tx.To(), + Nonce: tx.Nonce(), + GasFeeCap: tx.GasFeeCap(), + GasTipCap: tx.GasTipCap(), + Gas: uint64(float64(tx.Gas()) * m.gasLimitMultiplier), + Value: tx.Value(), + Data: tx.Data(), + } + txID, err := m.wallet.SendTransaction(ctx, types.NewTx(bumpedGasTx)) if err != nil { return nil, errors.Join(errors.New("send: failed to estimate gas and nonce"), err) } @@ -120,3 +152,58 @@ func (m *SimpleTxManager) queryReceipt(ctx context.Context, txID wallet.TxID) *t return receipt } + +// estimateGasAndNonce we are explicitly implementing this because +// * We want to support legacy transactions (i.e. not dynamic fee) +// * We want to support gas management, i.e. add buffer to gas limit +func (m *SimpleTxManager) estimateGasAndNonce(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) { + gasTipCap, err := m.client.SuggestGasTipCap(ctx) + if err != nil { + // If the transaction failed because the backend does not support + // eth_maxPriorityFeePerGas, fallback to using the default constant. + m.log.Info("eth_maxPriorityFeePerGas is unsupported by current backend, using fallback gasTipCap") + gasTipCap = FallbackGasTipCap + } + + header, err := m.client.HeaderByNumber(ctx, nil) + if err != nil { + return nil, err + } + + // 2*baseFee + gasTipCap makes sure that the tx remains includeable for 6 consecutive 100% full blocks. + // see https://www.blocknative.com/blog/eip-1559-fees + gasFeeCap := new(big.Int).Add(header.BaseFee.Mul(header.BaseFee, big.NewInt(2)), gasTipCap) + + gasLimit := tx.Gas() + // we only estimate if gasLimit is not already set + if gasLimit == 0 { + from, err := m.wallet.SenderAddress(ctx) + if err != nil { + return nil, errors.Join(errors.New("send: failed to get sender address"), err) + } + gasLimit, err = m.client.EstimateGas(ctx, ethereum.CallMsg{ + From: from, + To: tx.To(), + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Value: tx.Value(), + Data: tx.Data(), + }) + if err != nil { + return nil, errors.Join(errors.New("send: failed to estimate gas"), err) + } + } + + rawTx := &types.DynamicFeeTx{ + ChainID: tx.ChainId(), + To: tx.To(), + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Data: tx.Data(), + Value: tx.Value(), + Gas: uint64(float64(gasLimit) * m.gasLimitMultiplier), + Nonce: tx.Nonce(), // We are not doing any nonce management for now but we probably should later for more robustness + } + + return types.NewTx(rawTx), nil +}