Commit 771988b6 authored by Roberto Bayardo's avatar Roberto Bayardo Committed by GitHub

update txmgr to support sending blob txs (#8760)

parent 28c35962
......@@ -19,13 +19,14 @@ type priceBumpTest struct {
newBasefee int64
expectedTip int64
expectedFC int64
isBlobTx bool
}
func (tc *priceBumpTest) run(t *testing.T) {
prevFC := calcGasFeeCap(big.NewInt(tc.prevBasefee), big.NewInt(tc.prevGasTip))
lgr := testlog.Logger(t, log.LvlCrit)
tip, fc := updateFees(big.NewInt(tc.prevGasTip), prevFC, big.NewInt(tc.newGasTip), big.NewInt(tc.newBasefee), lgr)
tip, fc := updateFees(big.NewInt(tc.prevGasTip), prevFC, big.NewInt(tc.newGasTip), big.NewInt(tc.newBasefee), tc.isBlobTx, lgr)
require.Equal(t, tc.expectedTip, tip.Int64(), "tip must be as expected")
require.Equal(t, tc.expectedFC, fc.Int64(), "fee cap must be as expected")
......@@ -39,51 +40,111 @@ func TestUpdateFees(t *testing.T) {
newGasTip: 90, newBasefee: 900,
expectedTip: 110, expectedFC: 2310,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 90, newBasefee: 900,
expectedTip: 200, expectedFC: 4200,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 1000,
expectedTip: 110, expectedFC: 2310,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 1000,
expectedTip: 200, expectedFC: 4200,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 100, newBasefee: 1001,
expectedTip: 110, expectedFC: 2310,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 100, newBasefee: 1001,
expectedTip: 200, expectedFC: 4200,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 900,
expectedTip: 110, expectedFC: 2310,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 900,
expectedTip: 200, expectedFC: 4200,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 90, newBasefee: 1010,
expectedTip: 110, expectedFC: 2310,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 90, newBasefee: 1010,
expectedTip: 200, expectedFC: 4200,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 2000,
expectedTip: 110, expectedFC: 4110,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 3000,
expectedTip: 200, expectedFC: 6200,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 900,
expectedTip: 120, expectedFC: 2310,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 220, newBasefee: 900,
expectedTip: 220, expectedFC: 4200,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1100,
expectedTip: 120, expectedFC: 2320,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 220, newBasefee: 2000,
expectedTip: 220, expectedFC: 4220,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1140,
expectedTip: 120, expectedFC: 2400,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 220, newBasefee: 2040,
expectedTip: 220, expectedFC: 4300,
isBlobTx: true,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1200,
expectedTip: 120, expectedFC: 2520,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 220, newBasefee: 2100,
expectedTip: 220, expectedFC: 4420,
isBlobTx: true,
},
}
for i, test := range tests {
i := i
......
......@@ -193,7 +193,7 @@ func TestQueue_Send(t *testing.T) {
return core.ErrNonceTooLow
}
txHash := tx.Hash()
backend.mine(&txHash, tx.GasFeeCap())
backend.mine(&txHash, tx.GasFeeCap(), nil)
return nil
}
backend.setTxSender(sendTx)
......
......@@ -12,10 +12,14 @@ import (
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/retry"
......@@ -23,15 +27,24 @@ import (
)
const (
// Geth requires a minimum fee bump of 10% for tx resubmission
// geth requires a minimum fee bump of 10% for regular tx resubmission
priceBump int64 = 10
// geth requires a minimum fee bump of 100% for blob tx resubmission
blobPriceBump int64 = 100
)
// new = old * (100 + priceBump) / 100
var (
priceBumpPercent = big.NewInt(100 + priceBump)
oneHundred = big.NewInt(100)
ninetyNine = big.NewInt(99)
priceBumpPercent = big.NewInt(100 + priceBump)
blobPriceBumpPercent = big.NewInt(100 + blobPriceBump)
// geth enforces a 1 gwei minimum for blob tx fee
minBlobTxFee = big.NewInt(params.GWei)
oneHundred = big.NewInt(100)
ninetyNine = big.NewInt(99)
two = big.NewInt(2)
ErrBlobFeeLimit = errors.New("blob fee limit reached")
)
// TxManager is an interface that allows callers to reliably publish txs,
......@@ -149,14 +162,21 @@ func (m *SimpleTxManager) txLogger(tx *types.Transaction, logGas bool) log.Logge
if logGas {
fields = append(fields, "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap(), "gasLimit", tx.Gas())
}
if len(tx.BlobHashes()) != 0 {
// log the number of blobs a tx has only if it's a blob tx
fields = append(fields, "blobs", len(tx.BlobHashes()))
}
return m.l.New(fields...)
}
// TxCandidate is a transaction candidate that can be submitted to ask the
// [TxManager] to construct a transaction with gas price bounds.
type TxCandidate struct {
// TxData is the transaction data to be used in the constructed tx.
// TxData is the transaction calldata to be used in the constructed tx.
TxData []byte
// Blobs to send along in the tx (optional). If len(Blobs) > 0 then a blob tx
// will be sent instead of a DynamicFeeTx.
Blobs []*eth.Blob
// To is the recipient of the constructed tx. Nil means contract creation.
To *common.Address
// GasLimit is the gas limit to be used in the constructed tx.
......@@ -212,44 +232,96 @@ 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) {
gasTipCap, basefee, err := m.suggestGasPriceCaps(ctx)
gasTipCap, basefee, blobBasefee, err := m.suggestGasPriceCaps(ctx)
if err != nil {
m.metr.RPCError()
return nil, fmt.Errorf("failed to get gas price info: %w", err)
}
gasFeeCap := calcGasFeeCap(basefee, gasTipCap)
rawTx := &types.DynamicFeeTx{
ChainID: m.chainID,
To: candidate.To,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: candidate.TxData,
Value: candidate.Value,
}
m.l.Info("Creating tx", "to", rawTx.To, "from", m.cfg.From)
gasLimit := candidate.GasLimit
// If the gas limit is set, we can use that as the gas
if candidate.GasLimit != 0 {
rawTx.Gas = candidate.GasLimit
} else {
if gasLimit == 0 {
// Calculate the intrinsic gas for the transaction
gas, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{
From: m.cfg.From,
To: candidate.To,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: rawTx.Data,
Value: rawTx.Value,
Data: candidate.TxData,
Value: candidate.Value,
})
if err != nil {
return nil, fmt.Errorf("failed to estimate gas: %w", err)
}
rawTx.Gas = gas
gasLimit = gas
}
var sidecar *types.BlobTxSidecar
var blobHashes []common.Hash
if len(candidate.Blobs) > 0 {
if candidate.To == nil {
return nil, errors.New("blob txs cannot deploy contracts")
}
if sidecar, blobHashes, err = makeSidecar(candidate.Blobs); err != nil {
return nil, fmt.Errorf("failed to make sidecar: %w", err)
}
}
return m.signWithNextNonce(ctx, rawTx)
var txMessage types.TxData
if sidecar != nil {
if blobBasefee == nil {
return nil, fmt.Errorf("expected non-nil blobBasefee")
}
blobFeeCap := calcBlobFeeCap(blobBasefee)
message := &types.BlobTx{
To: *candidate.To,
Data: candidate.TxData,
Gas: gasLimit,
BlobHashes: blobHashes,
Sidecar: sidecar,
}
if err := finishBlobTx(message, m.chainID, gasTipCap, gasFeeCap, blobFeeCap, candidate.Value); err != nil {
return nil, fmt.Errorf("failed to create blob transaction: %w", err)
}
txMessage = message
} else {
txMessage = &types.DynamicFeeTx{
ChainID: m.chainID,
To: candidate.To,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Value: candidate.Value,
Data: candidate.TxData,
Gas: gasLimit,
}
}
return m.signWithNextNonce(ctx, txMessage) // signer sets the nonce field of the tx
}
// makeSidecar builds & returns the BlobTxSidecar and corresponding blob hashes from the raw blob
// data.
func makeSidecar(blobs []*eth.Blob) (*types.BlobTxSidecar, []common.Hash, error) {
sidecar := &types.BlobTxSidecar{}
blobHashes := []common.Hash{}
for i, blob := range blobs {
rawBlob := *blob.KZGBlob()
sidecar.Blobs = append(sidecar.Blobs, rawBlob)
commitment, err := kzg4844.BlobToCommitment(rawBlob)
if err != nil {
return nil, nil, fmt.Errorf("cannot compute KZG commitment of blob %d in tx candidate: %w", i, err)
}
sidecar.Commitments = append(sidecar.Commitments, commitment)
proof, err := kzg4844.ComputeBlobProof(rawBlob, commitment)
if err != nil {
return nil, nil, fmt.Errorf("cannot compute KZG proof for fast commitment verification of blob %d in tx candidate: %w", i, err)
}
sidecar.Proofs = append(sidecar.Proofs, proof)
blobHashes = append(blobHashes, eth.KZGToVersionedHash(commitment))
}
return sidecar, blobHashes, nil
}
// signWithNextNonce returns a signed transaction with the next available nonce.
......@@ -257,7 +329,7 @@ func (m *SimpleTxManager) craftTx(ctx context.Context, candidate TxCandidate) (*
// then subsequent calls simply increment this number. If the transaction manager
// is reset, it will query the eth_getTransactionCount nonce again. If signing
// fails, the nonce is not incremented.
func (m *SimpleTxManager) signWithNextNonce(ctx context.Context, rawTx *types.DynamicFeeTx) (*types.Transaction, error) {
func (m *SimpleTxManager) signWithNextNonce(ctx context.Context, txMessage types.TxData) (*types.Transaction, error) {
m.nonceLock.Lock()
defer m.nonceLock.Unlock()
......@@ -275,10 +347,17 @@ func (m *SimpleTxManager) signWithNextNonce(ctx context.Context, rawTx *types.Dy
*m.nonce++
}
rawTx.Nonce = *m.nonce
switch x := txMessage.(type) {
case *types.DynamicFeeTx:
x.Nonce = *m.nonce
case *types.BlobTx:
x.Nonce = *m.nonce
default:
return nil, fmt.Errorf("unrecognized tx type: %T", x)
}
ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout)
defer cancel()
tx, err := m.cfg.Signer(ctx, m.cfg.From, types.NewTx(rawTx))
tx, err := m.cfg.Signer(ctx, m.cfg.From, types.NewTx(txMessage))
if err != nil {
// decrement the nonce, so we can retry signing with the same nonce next time
// signWithNextNonce is called
......@@ -512,42 +591,31 @@ func (m *SimpleTxManager) queryReceipt(ctx context.Context, txHash common.Hash,
return nil
}
// increaseGasPrice takes the previous transaction, clones it, and returns it with fee values that
// are at least `priceBump` percent higher than the previous ones to satisfy Geth's replacement
// rules, and no lower than the values returned by the fee suggestion algorithm to ensure it
// doesn't linger in the mempool. Finally to avoid runaway price increases, fees are capped at a
// `feeLimitMultiplier` multiple of the suggested values.
// increaseGasPrice returns a new transaction that is equivalent to the input transaction but with
// higher fees that should satisfy geth's tx replacement rules. It also computes an updated gas
// limit estimate. To avoid runaway price increases, fees are capped at a `feeLimitMultiplier`
// multiple of the suggested values.
func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
m.txLogger(tx, true).Info("bumping gas price for transaction")
tip, basefee, err := m.suggestGasPriceCaps(ctx)
tip, basefee, blobBasefee, err := m.suggestGasPriceCaps(ctx)
if err != nil {
m.txLogger(tx, false).Warn("failed to get suggested gas tip and basefee", "err", err)
return nil, err
}
bumpedTip, bumpedFee := updateFees(tx.GasTipCap(), tx.GasFeeCap(), tip, basefee, m.l)
bumpedTip, bumpedFee := updateFees(tx.GasTipCap(), tx.GasFeeCap(), tip, basefee, tx.Type() == types.BlobTxType, m.l)
if err := m.checkLimits(tip, basefee, bumpedTip, bumpedFee); err != nil {
return nil, err
}
rawTx := &types.DynamicFeeTx{
ChainID: tx.ChainId(),
Nonce: tx.Nonce(),
GasTipCap: bumpedTip,
GasFeeCap: bumpedFee,
To: tx.To(),
Value: tx.Value(),
Data: tx.Data(),
AccessList: tx.AccessList(),
}
// Re-estimate gaslimit in case things have changed or a previous gaslimit estimate was wrong
gas, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{
From: m.cfg.From,
To: rawTx.To,
To: tx.To(),
GasTipCap: bumpedTip,
GasFeeCap: bumpedFee,
Data: rawTx.Data,
Data: tx.Data(),
Value: tx.Value(),
})
if err != nil {
// If this is a transaction resubmission, we sometimes see this outcome because the
......@@ -562,38 +630,75 @@ func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transa
m.l.Info("re-estimated gas differs", "tx", tx.Hash(), "oldgas", tx.Gas(), "newgas", gas,
"gasFeeCap", bumpedFee, "gasTipCap", bumpedTip)
}
rawTx.Gas = gas
var newTx *types.Transaction
if tx.Type() == types.BlobTxType {
// Blob transactions have an additional blob gas price we must specify, so we must make sure it is
// getting bumped appropriately.
bumpedBlobFee := calcThresholdValue(tx.BlobGasFeeCap(), true)
if bumpedBlobFee.Cmp(blobBasefee) < 0 {
bumpedBlobFee = blobBasefee
}
if err := m.checkBlobFeeLimits(blobBasefee, bumpedBlobFee); err != nil {
return nil, err
}
message := &types.BlobTx{
Nonce: tx.Nonce(),
To: *tx.To(),
Data: tx.Data(),
Gas: gas,
BlobHashes: tx.BlobHashes(),
Sidecar: tx.BlobTxSidecar(),
}
if err := finishBlobTx(message, tx.ChainId(), bumpedTip, bumpedFee, bumpedBlobFee, tx.Value()); err != nil {
return nil, err
}
newTx = types.NewTx(message)
} else {
newTx = types.NewTx(&types.DynamicFeeTx{
ChainID: tx.ChainId(),
Nonce: tx.Nonce(),
To: tx.To(),
GasTipCap: bumpedTip,
GasFeeCap: bumpedFee,
Value: tx.Value(),
Data: tx.Data(),
Gas: gas,
})
}
ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout)
defer cancel()
newTx, err := m.cfg.Signer(ctx, m.cfg.From, types.NewTx(rawTx))
signedTx, err := m.cfg.Signer(ctx, m.cfg.From, newTx)
if err != nil {
m.l.Warn("failed to sign new transaction", "err", err, "tx", tx.Hash())
return tx, nil
}
return newTx, nil
return signedTx, nil
}
// suggestGasPriceCaps suggests what the new tip & new basefee should be based on the current L1 conditions
func (m *SimpleTxManager) suggestGasPriceCaps(ctx context.Context) (*big.Int, *big.Int, error) {
// suggestGasPriceCaps suggests what the new tip, basefee, and blobfee should be based on the
// current L1 conditions. blobfee will be nil if 4844 is not yet active.
func (m *SimpleTxManager) suggestGasPriceCaps(ctx context.Context) (*big.Int, *big.Int, *big.Int, error) {
cCtx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout)
defer cancel()
tip, err := m.backend.SuggestGasTipCap(cCtx)
if err != nil {
m.metr.RPCError()
return nil, nil, fmt.Errorf("failed to fetch the suggested gas tip cap: %w", err)
return nil, nil, nil, fmt.Errorf("failed to fetch the suggested gas tip cap: %w", err)
} else if tip == nil {
return nil, nil, errors.New("the suggested tip was nil")
return nil, nil, nil, errors.New("the suggested tip was nil")
}
cCtx, cancel = context.WithTimeout(ctx, m.cfg.NetworkTimeout)
defer cancel()
head, err := m.backend.HeaderByNumber(cCtx, nil)
if err != nil {
m.metr.RPCError()
return nil, nil, fmt.Errorf("failed to fetch the suggested basefee: %w", err)
return nil, nil, nil, fmt.Errorf("failed to fetch the suggested basefee: %w", err)
} else if head.BaseFee == nil {
return nil, nil, errors.New("txmgr does not support pre-london blocks that do not have a basefee")
return nil, nil, nil, errors.New("txmgr does not support pre-london blocks that do not have a basefee")
}
basefee := head.BaseFee
m.metr.RecordBasefee(basefee)
m.metr.RecordTipCap(tip)
......@@ -608,7 +713,11 @@ func (m *SimpleTxManager) suggestGasPriceCaps(ctx context.Context) (*big.Int, *b
basefee = new(big.Int).Set(m.cfg.MinBasefee)
}
return tip, basefee, nil
var blobFee *big.Int
if head.ExcessBlobGas != nil {
blobFee = eip4844.CalcBlobFee(*head.ExcessBlobGas)
}
return tip, basefee, blobFee, nil
}
func (m *SimpleTxManager) checkLimits(tip, basefee, bumpedTip, bumpedFee *big.Int) error {
......@@ -630,28 +739,46 @@ func (m *SimpleTxManager) checkLimits(tip, basefee, bumpedTip, bumpedFee *big.In
return nil
}
// calcThresholdValue returns ceil(x * priceBumpPercent / 100)
func (m *SimpleTxManager) checkBlobFeeLimits(blobBasefee, bumpedBlobFee *big.Int) error {
// If below threshold, don't apply multiplier limit. Note we use same threshold parameter here
// used for non-blob fee limiting.
if thr := m.cfg.FeeLimitThreshold; thr != nil && thr.Cmp(bumpedBlobFee) == 1 {
return nil
}
maxBlobFee := new(big.Int).Mul(calcBlobFeeCap(blobBasefee), big.NewInt(int64(m.cfg.FeeLimitMultiplier)))
if bumpedBlobFee.Cmp(maxBlobFee) > 0 {
return fmt.Errorf(
"bumped blob fee %v is over %dx multiple of the suggested value: %w",
bumpedBlobFee, m.cfg.FeeLimitMultiplier, ErrBlobFeeLimit)
}
return nil
}
// calcThresholdValue returns ceil(x * priceBumpPercent / 100) for non-blob txs, or
// ceil(x * blobPriceBumpPercent / 100) for blob txs.
// It guarantees that x is increased by at least 1
func calcThresholdValue(x *big.Int) *big.Int {
threshold := new(big.Int).Mul(priceBumpPercent, x)
threshold.Add(threshold, ninetyNine)
threshold.Div(threshold, oneHundred)
return threshold
func calcThresholdValue(x *big.Int, isBlobTx bool) *big.Int {
threshold := new(big.Int)
if isBlobTx {
threshold.Set(blobPriceBumpPercent)
} else {
threshold.Set(priceBumpPercent)
}
return threshold.Mul(threshold, x).Add(threshold, ninetyNine).Div(threshold, oneHundred)
}
// updateFees takes an old transaction's tip & fee cap plus a new tip & basefee, and returns
// a suggested tip and fee cap such that:
//
// (a) each satisfies geth's required tx-replacement fee bumps (we use a 10% increase), and
// (a) each satisfies geth's required tx-replacement fee bumps, and
// (b) gasTipCap is no less than new tip, and
// (c) gasFeeCap is no less than calcGasFee(newBaseFee, newTip)
func updateFees(oldTip, oldFeeCap, newTip, newBaseFee *big.Int, lgr log.Logger) (*big.Int, *big.Int) {
func updateFees(oldTip, oldFeeCap, newTip, newBaseFee *big.Int, isBlobTx bool, lgr log.Logger) (*big.Int, *big.Int) {
newFeeCap := calcGasFeeCap(newBaseFee, newTip)
lgr = lgr.New("old_gasTipCap", oldTip, "old_gasFeeCap", oldFeeCap,
"new_gasTipCap", newTip, "new_gasFeeCap", newFeeCap,
"new_basefee", newBaseFee)
thresholdTip := calcThresholdValue(oldTip)
thresholdFeeCap := calcThresholdValue(oldFeeCap)
"new_gasTipCap", newTip, "new_gasFeeCap", newFeeCap, "new_basefee", newBaseFee)
thresholdTip := calcThresholdValue(oldTip, isBlobTx)
thresholdFeeCap := calcThresholdValue(oldFeeCap, isBlobTx)
if newTip.Cmp(thresholdTip) >= 0 && newFeeCap.Cmp(thresholdFeeCap) >= 0 {
lgr.Debug("Using new tip and feecap")
return newTip, newFeeCap
......@@ -680,10 +807,20 @@ func updateFees(oldTip, oldFeeCap, newTip, newBaseFee *big.Int, lgr log.Logger)
func calcGasFeeCap(baseFee, gasTipCap *big.Int) *big.Int {
return new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(baseFee, big.NewInt(2)),
new(big.Int).Mul(baseFee, two),
)
}
// calcBlobFeeCap computes a suggested blob fee cap that is twice the current header's blob basefee
// value, with a minimum value of minBlobTxFee.
func calcBlobFeeCap(blobBasefee *big.Int) *big.Int {
cap := new(big.Int).Mul(blobBasefee, two)
if cap.Cmp(minBlobTxFee) < 0 {
cap.Set(minBlobTxFee)
}
return cap
}
// errStringMatch returns true if err.Error() is a substring in target.Error() or if both are nil.
// It can accept nil errors without issue.
func errStringMatch(err, target error) bool {
......@@ -694,3 +831,24 @@ func errStringMatch(err, target error) bool {
}
return strings.Contains(err.Error(), target.Error())
}
// finishBlobTx finishes creating a blob tx message by safely converting bigints to uint256
func finishBlobTx(message *types.BlobTx, chainID, tip, fee, blobFee, value *big.Int) error {
var o bool
if message.ChainID, o = uint256.FromBig(chainID); o {
return fmt.Errorf("ChainID overflow")
}
if message.GasTipCap, o = uint256.FromBig(tip); o {
return fmt.Errorf("GasTipCap overflow")
}
if message.GasFeeCap, o = uint256.FromBig(fee); o {
return fmt.Errorf("GasFeeCap overflow")
}
if message.BlobFeeCap, o = uint256.FromBig(blobFee); o {
return fmt.Errorf("BlobFeeCap overflow")
}
if message.Value, o = uint256.FromBig(value); o {
return fmt.Errorf("Value overflow")
}
return nil
}
......@@ -9,17 +9,30 @@ import (
"testing"
"time"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
)
const (
startingNonce = 1 // we pick something other than 0 so we can confirm nonces are getting set properly
)
var (
blobData1 = eth.Data("this is a blob!")
blobData2 = eth.Data("amazing, the txmgr can handle more than one blob in a tx!!")
)
type sendTransactionFunc func(ctx context.Context, tx *types.Transaction) error
......@@ -75,6 +88,21 @@ func (h testHarness) createTxCandidate() TxCandidate {
}
}
// createBlobTxCandidate creates a mock [TxCandidate] that results in a blob tx
func (h testHarness) createBlobTxCandidate() TxCandidate {
inbox := common.HexToAddress("0x42000000000000000000000000000000000000ff")
var b1, b2 eth.Blob
_ = b1.FromData(blobData1)
_ = b2.FromData(blobData2)
return TxCandidate{
To: &inbox,
TxData: []byte{0x00, 0x01, 0x02, 0x03},
GasLimit: uint64(1337),
Blobs: []*eth.Blob{&b1, &b2},
}
}
func configWithNumConfs(numConfirmations uint64) Config {
return Config{
ResubmissionTimeout: time.Second,
......@@ -95,6 +123,7 @@ type gasPricer struct {
mineAtEpoch int64
baseGasTipFee *big.Int
baseBaseFee *big.Int
excessBlobGas uint64
err error
mu sync.Mutex
}
......@@ -104,24 +133,37 @@ func newGasPricer(mineAtEpoch int64) *gasPricer {
mineAtEpoch: mineAtEpoch,
baseGasTipFee: big.NewInt(5),
baseBaseFee: big.NewInt(7),
// Simulate 100 excess blobs, which results in a blobBaseFee of 50 wei. This default means
// blob txs will be subject to the geth minimum blobgas fee of 1 gwei.
excessBlobGas: 100 * (params.BlobTxBlobGasPerBlob),
}
}
func (g *gasPricer) expGasFeeCap() *big.Int {
_, gasFeeCap := g.feesForEpoch(g.mineAtEpoch)
_, gasFeeCap, _ := g.feesForEpoch(g.mineAtEpoch)
return gasFeeCap
}
func (g *gasPricer) expBlobFeeCap() *big.Int {
_, _, excessBlobGas := g.feesForEpoch(g.mineAtEpoch)
return eip4844.CalcBlobFee(excessBlobGas)
}
func (g *gasPricer) shouldMine(gasFeeCap *big.Int) bool {
return g.expGasFeeCap().Cmp(gasFeeCap) == 0
return g.expGasFeeCap().Cmp(gasFeeCap) <= 0
}
func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
epochBaseFee := new(big.Int).Mul(g.baseBaseFee, big.NewInt(epoch))
epochGasTipCap := new(big.Int).Mul(g.baseGasTipFee, big.NewInt(epoch))
epochGasFeeCap := calcGasFeeCap(epochBaseFee, epochGasTipCap)
func (g *gasPricer) shouldMineBlobTx(gasFeeCap, blobFeeCap *big.Int) bool {
return g.shouldMine(gasFeeCap) && g.expBlobFeeCap().Cmp(blobFeeCap) <= 0
}
return epochGasTipCap, epochGasFeeCap
func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int, uint64) {
e := big.NewInt(epoch)
epochBaseFee := new(big.Int).Mul(g.baseBaseFee, e)
epochGasTipCap := new(big.Int).Mul(g.baseGasTipFee, e)
epochGasFeeCap := calcGasFeeCap(epochBaseFee, epochGasTipCap)
epochExcessBlobGas := g.excessBlobGas * uint64(epoch)
return epochGasTipCap, epochGasFeeCap, epochExcessBlobGas
}
func (g *gasPricer) basefee() *big.Int {
......@@ -130,18 +172,25 @@ func (g *gasPricer) basefee() *big.Int {
return new(big.Int).Mul(g.baseBaseFee, big.NewInt(g.epoch))
}
func (g *gasPricer) sample() (*big.Int, *big.Int) {
func (g *gasPricer) excessblobgas() uint64 {
g.mu.Lock()
defer g.mu.Unlock()
return g.excessBlobGas * uint64(g.epoch)
}
func (g *gasPricer) sample() (*big.Int, *big.Int, uint64) {
g.mu.Lock()
defer g.mu.Unlock()
g.epoch++
epochGasTipCap, epochGasFeeCap := g.feesForEpoch(g.epoch)
epochGasTipCap, epochGasFeeCap, epochExcessBlobGas := g.feesForEpoch(g.epoch)
return epochGasTipCap, epochGasFeeCap
return epochGasTipCap, epochGasFeeCap, epochExcessBlobGas
}
type minedTxInfo struct {
gasFeeCap *big.Int
blobFeeCap *big.Int
blockNumber uint64
}
......@@ -176,7 +225,7 @@ func (b *mockBackend) setTxSender(s sendTransactionFunc) {
// mine records a (txHash, gasFeeCap) as confirmed. Subsequent calls to
// TransactionReceipt with a matching txHash will result in a non-nil receipt.
// If a nil txHash is supplied this has the effect of mining an empty block.
func (b *mockBackend) mine(txHash *common.Hash, gasFeeCap *big.Int) {
func (b *mockBackend) mine(txHash *common.Hash, gasFeeCap, blobFeeCap *big.Int) {
b.mu.Lock()
defer b.mu.Unlock()
......@@ -184,6 +233,7 @@ func (b *mockBackend) mine(txHash *common.Hash, gasFeeCap *big.Int) {
if txHash != nil {
b.minedTxs[*txHash] = minedTxInfo{
gasFeeCap: gasFeeCap,
blobFeeCap: blobFeeCap,
blockNumber: b.blockHeight,
}
}
......@@ -210,9 +260,11 @@ func (b *mockBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*typ
if number != nil {
num.Set(number)
}
bg := b.g.excessblobgas()
return &types.Header{
Number: num,
BaseFee: b.g.basefee(),
Number: num,
BaseFee: b.g.basefee(),
ExcessBlobGas: &bg,
}, nil
}
......@@ -227,7 +279,7 @@ func (b *mockBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (ui
}
func (b *mockBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
tip, _ := b.g.sample()
tip, _, _ := b.g.sample()
return tip, nil
}
......@@ -239,21 +291,21 @@ func (b *mockBackend) SendTransaction(ctx context.Context, tx *types.Transaction
}
func (b *mockBackend) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
return 0, nil
return startingNonce, nil
}
func (b *mockBackend) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
return 0, nil
return startingNonce, nil
}
func (*mockBackend) ChainID(ctx context.Context) (*big.Int, error) {
return big.NewInt(1), nil
}
// TransactionReceipt queries the mockBackend for a mined txHash. If none is
// found, nil is returned for both return values. Otherwise, it returns a
// receipt containing the txHash and the gasFeeCap used in the GasUsed to make
// the value accessible from our test framework.
// TransactionReceipt queries the mockBackend for a mined txHash. If none is found, nil is returned
// for both return values. Otherwise, it returns a receipt containing the txHash, the gasFeeCap
// used in GasUsed, and the blobFeeCap in CumuluativeGasUsed to make the values accessible from our
// test framework.
func (b *mockBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
b.mu.RLock()
defer b.mu.RUnlock()
......@@ -265,10 +317,15 @@ func (b *mockBackend) TransactionReceipt(ctx context.Context, txHash common.Hash
// Return the gas fee cap for the transaction in the GasUsed field so that
// we can assert the proper tx confirmed in our tests.
var blobFeeCap uint64
if txInfo.blobFeeCap != nil {
blobFeeCap = txInfo.blobFeeCap.Uint64()
}
return &types.Receipt{
TxHash: txHash,
GasUsed: txInfo.gasFeeCap.Uint64(),
BlockNumber: big.NewInt(int64(txInfo.blockNumber)),
TxHash: txHash,
GasUsed: txInfo.gasFeeCap.Uint64(),
CumulativeGasUsed: blobFeeCap,
BlockNumber: big.NewInt(int64(txInfo.blockNumber)),
}, nil
}
......@@ -284,7 +341,7 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
gasPricer := newGasPricer(1)
gasTipCap, gasFeeCap := gasPricer.sample()
gasTipCap, gasFeeCap, _ := gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
......@@ -293,7 +350,7 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if gasPricer.shouldMine(tx.GasFeeCap()) {
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
h.backend.mine(&txHash, tx.GasFeeCap(), nil)
}
return nil
}
......@@ -315,7 +372,7 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
h := newTestHarness(t)
gasTipCap, gasFeeCap := h.gasPricer.sample()
gasTipCap, gasFeeCap, _ := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
......@@ -341,7 +398,7 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
h := newTestHarness(t)
gasTipCap, gasFeeCap := h.gasPricer.sample()
gasTipCap, gasFeeCap, _ := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
......@@ -349,7 +406,7 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if h.gasPricer.shouldMine(tx.GasFeeCap()) {
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
h.backend.mine(&txHash, tx.GasFeeCap(), nil)
}
return nil
}
......@@ -364,6 +421,43 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// TestTxMgrConfirmsBlobTxAtMaxGasPrice asserts that Send properly returns the max gas price
// receipt if none of the lower gas price txs were mined when attempting to send a blob tx.
func TestTxMgrConfirmsBlobTxAtHigherGasPrice(t *testing.T) {
t.Parallel()
h := newTestHarness(t)
gasTipCap, gasFeeCap, excessBlobGas := h.gasPricer.sample()
blobFeeCap := eip4844.CalcBlobFee(excessBlobGas)
t.Log("Blob fee cap:", blobFeeCap, "gasFeeCap:", gasFeeCap)
tx := types.NewTx(&types.BlobTx{
GasTipCap: uint256.MustFromBig(gasTipCap),
GasFeeCap: uint256.MustFromBig(gasFeeCap),
BlobFeeCap: uint256.MustFromBig(blobFeeCap),
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if h.gasPricer.shouldMineBlobTx(tx.GasFeeCap(), tx.BlobGasFeeCap()) {
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap(), tx.BlobGasFeeCap())
}
return nil
}
h.backend.setTxSender(sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.sendTx(ctx, tx)
require.Nil(t, err)
require.NotNil(t, receipt)
// the fee cap for the blob tx at epoch == 3 should end up higher than the min required gas
// (expFeeCap()) since blob tx fee caps are bumped 100% with each epoch.
require.Less(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
require.Equal(t, h.gasPricer.expBlobFeeCap().Uint64(), receipt.CumulativeGasUsed)
}
// errRpcFailure is a sentinel error used in testing to fail publications.
var errRpcFailure = errors.New("rpc failure")
......@@ -375,7 +469,7 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
h := newTestHarness(t)
gasTipCap, gasFeeCap := h.gasPricer.sample()
gasTipCap, gasFeeCap, _ := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
......@@ -401,22 +495,69 @@ func TestTxMgr_CraftTx(t *testing.T) {
candidate := h.createTxCandidate()
// Craft the transaction.
gasTipCap, gasFeeCap := h.gasPricer.feesForEpoch(h.gasPricer.epoch + 1)
gasTipCap, gasFeeCap, _ := h.gasPricer.feesForEpoch(h.gasPricer.epoch + 1)
tx, err := h.mgr.craftTx(context.Background(), candidate)
require.Nil(t, err)
require.NotNil(t, tx)
require.Equal(t, byte(types.DynamicFeeTxType), tx.Type())
// Validate the gas tip cap and fee cap.
require.Equal(t, gasTipCap, tx.GasTipCap())
require.Equal(t, gasFeeCap, tx.GasFeeCap())
// Validate the nonce was set correctly using the backend.
require.Zero(t, tx.Nonce())
require.Equal(t, uint64(startingNonce), tx.Nonce())
// Check that the gas was set using the gas limit.
require.Equal(t, candidate.GasLimit, tx.Gas())
}
// TestTxMgr_CraftBlobTx ensures that the tx manager will create blob transactions as expected.
func TestTxMgr_CraftBlobTx(t *testing.T) {
t.Parallel()
h := newTestHarness(t)
candidate := h.createBlobTxCandidate()
// Craft the transaction.
gasTipCap, gasFeeCap, _ := h.gasPricer.feesForEpoch(h.gasPricer.epoch + 1)
tx, err := h.mgr.craftTx(context.Background(), candidate)
require.Nil(t, err)
require.NotNil(t, tx)
require.Equal(t, byte(types.BlobTxType), tx.Type())
// Validate the gas tip cap and fee cap.
require.Equal(t, gasTipCap, tx.GasTipCap())
require.Equal(t, gasFeeCap, tx.GasFeeCap())
require.Equal(t, minBlobTxFee, tx.BlobGasFeeCap())
// Validate the nonce was set correctly using the backend.
require.Equal(t, uint64(startingNonce), tx.Nonce())
// Check that the gas was set using the gas limit.
require.Equal(t, candidate.GasLimit, tx.Gas())
// Check the blob fields
require.Equal(t, 2, len(tx.BlobHashes()))
sidecar := tx.BlobTxSidecar()
require.Equal(t, 2, len(sidecar.Blobs))
require.Equal(t, 2, len(sidecar.Commitments))
require.Equal(t, 2, len(sidecar.Proofs))
// verify the blobs
for i := range sidecar.Blobs {
require.NoError(t, kzg4844.VerifyBlobProof(sidecar.Blobs[i], sidecar.Commitments[i], sidecar.Proofs[i]))
}
b1 := eth.Blob(sidecar.Blobs[0])
d1, err := b1.ToData()
require.NoError(t, err)
require.Equal(t, blobData1, d1)
b2 := eth.Blob(sidecar.Blobs[1])
d2, err := b2.ToData()
require.NoError(t, err)
require.Equal(t, blobData2, d2)
}
// TestTxMgr_EstimateGas ensures that the tx manager will estimate
// the gas when candidate gas limit is zero in [CraftTx].
func TestTxMgr_EstimateGas(t *testing.T) {
......@@ -506,7 +647,7 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
h := newTestHarness(t)
gasTipCap, gasFeeCap := h.gasPricer.sample()
gasTipCap, gasFeeCap, _ := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
......@@ -519,7 +660,7 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
}
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
h.backend.mine(&txHash, tx.GasFeeCap(), nil)
return nil
}
h.backend.setTxSender(sendTx)
......@@ -534,14 +675,14 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
}
// TestTxMgrConfirmsMinGasPriceAfterBumping delays the mining of the initial tx
// with the minimum gas price, and asserts that it's receipt is returned even
// with the minimum gas price, and asserts that its receipt is returned even
// though if the gas price has been bumped in other goroutines.
func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
t.Parallel()
h := newTestHarness(t)
gasTipCap, gasFeeCap := h.gasPricer.sample()
gasTipCap, gasFeeCap, _ := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
......@@ -552,7 +693,7 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
if h.gasPricer.shouldMine(tx.GasFeeCap()) {
time.AfterFunc(5*time.Second, func() {
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
h.backend.mine(&txHash, tx.GasFeeCap(), nil)
})
}
return nil
......@@ -573,7 +714,7 @@ func TestTxMgrDoesntAbortNonceTooLowAfterMiningTx(t *testing.T) {
h := newTestHarnessWithConfig(t, configWithNumConfs(2))
gasTipCap, gasFeeCap := h.gasPricer.sample()
gasTipCap, gasFeeCap, _ := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
......@@ -590,9 +731,9 @@ func TestTxMgrDoesntAbortNonceTooLowAfterMiningTx(t *testing.T) {
// Accept and mine the actual txn we expect to confirm.
case h.gasPricer.shouldMine(tx.GasFeeCap()):
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
h.backend.mine(&txHash, tx.GasFeeCap(), nil)
time.AfterFunc(5*time.Second, func() {
h.backend.mine(nil, nil)
h.backend.mine(nil, nil, nil)
})
return nil
......@@ -622,7 +763,7 @@ func TestWaitMinedReturnsReceiptOnFirstSuccess(t *testing.T) {
// Create a tx and mine it immediately using the default backend.
tx := types.NewTx(&types.LegacyTx{})
txHash := tx.Hash()
h.backend.mine(&txHash, new(big.Int))
h.backend.mine(&txHash, new(big.Int), nil)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
......@@ -664,7 +805,7 @@ func TestWaitMinedMultipleConfs(t *testing.T) {
// Create an unimined tx.
tx := types.NewTx(&types.LegacyTx{})
txHash := tx.Hash()
h.backend.mine(&txHash, new(big.Int))
h.backend.mine(&txHash, new(big.Int), nil)
receipt, err := h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour))
require.Equal(t, err, context.DeadlineExceeded)
......@@ -674,7 +815,7 @@ func TestWaitMinedMultipleConfs(t *testing.T) {
defer cancel()
// Mine an empty block, tx should now be confirmed.
h.backend.mine(nil, nil)
h.backend.mine(nil, nil, nil)
receipt, err = h.mgr.waitMined(ctx, tx, NewSendState(10, time.Hour))
require.Nil(t, err)
require.NotNil(t, receipt)
......@@ -707,6 +848,7 @@ type failingBackend struct {
returnSuccessHeader bool
returnSuccessReceipt bool
baseFee, gasTip *big.Int
excessBlobGas *uint64
}
// BlockNumber for the failingBackend returns errRpcFailure on the first
......@@ -743,8 +885,9 @@ func (b *failingBackend) HeaderByNumber(ctx context.Context, _ *big.Int) (*types
}
return &types.Header{
Number: big.NewInt(1),
BaseFee: b.baseFee,
Number: big.NewInt(1),
BaseFee: b.baseFee,
ExcessBlobGas: b.excessBlobGas,
}, nil
}
......@@ -923,15 +1066,17 @@ func TestIncreaseGasPrice(t *testing.T) {
func TestIncreaseGasPriceLimits(t *testing.T) {
t.Run("no-threshold", func(t *testing.T) {
testIncreaseGasPriceLimit(t, gasPriceLimitTest{
expTipCap: 46,
expFeeCap: 354, // just below 5*100
expTipCap: 46,
expFeeCap: 354, // just below 5*100
expBlobFeeCap: 4 * params.GWei,
})
})
t.Run("with-threshold", func(t *testing.T) {
testIncreaseGasPriceLimit(t, gasPriceLimitTest{
thr: big.NewInt(params.GWei),
expTipCap: 131_326_987,
expFeeCap: 933_286_308, // just below 1 gwei
thr: big.NewInt(params.GWei * 10),
expTipCap: 1_293_535_754,
expFeeCap: 9_192_620_686, // just below 10 gwei
expBlobFeeCap: 8 * params.GWei,
})
})
}
......@@ -939,6 +1084,7 @@ func TestIncreaseGasPriceLimits(t *testing.T) {
type gasPriceLimitTest struct {
thr *big.Int
expTipCap, expFeeCap int64
expBlobFeeCap int64
}
// testIncreaseGasPriceLimit runs a gas bumping test that increases the gas price until it hits an error.
......@@ -948,9 +1094,12 @@ func testIncreaseGasPriceLimit(t *testing.T, lt gasPriceLimitTest) {
borkedTip := int64(10)
borkedFee := int64(45)
// simulate 100 excess blobs which yields a 50 wei blob basefee
borkedExcessBlobGas := uint64(100 * params.BlobTxBlobGasPerBlob)
borkedBackend := failingBackend{
gasTip: big.NewInt(borkedTip),
baseFee: big.NewInt(borkedFee),
excessBlobGas: &borkedExcessBlobGas,
returnSuccessHeader: true,
}
......@@ -972,27 +1121,48 @@ func testIncreaseGasPriceLimit(t *testing.T, lt gasPriceLimitTest) {
l: testlog.Logger(t, log.LvlCrit),
metr: &metrics.NoopTxMetrics{},
}
tx := types.NewTx(&types.DynamicFeeTx{
lastGoodTx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(10),
GasFeeCap: big.NewInt(100),
})
// Run IncreaseGasPrice a bunch of times in a row to simulate a very fast resubmit loop.
// Run increaseGasPrice a bunch of times in a row to simulate a very fast resubmit loop to make
// sure it errors out without a runaway fee increase.
ctx := context.Background()
var err error
for {
newTx, err := mgr.increaseGasPrice(ctx, tx)
var tmpTx *types.Transaction
tmpTx, err = mgr.increaseGasPrice(ctx, lastGoodTx)
if err != nil {
break
}
tx = newTx
lastGoodTx = tmpTx
}
require.Error(t, err)
lastTip, lastFee := tx.GasTipCap(), tx.GasFeeCap()
// Confirm that fees only rose until expected threshold
require.Equal(t, lt.expTipCap, lastTip.Int64())
require.Equal(t, lt.expFeeCap, lastFee.Int64())
_, err := mgr.increaseGasPrice(ctx, tx)
require.Error(t, err)
require.Equal(t, lt.expTipCap, lastGoodTx.GasTipCap().Int64())
require.Equal(t, lt.expFeeCap, lastGoodTx.GasFeeCap().Int64())
// Confirm blob txs also don't see runaway fee increase and that blob fee market is also capped
// as expected
blobTx := &types.BlobTx{}
blobTx.GasTipCap = uint256.NewInt(1)
blobTx.GasFeeCap = uint256.NewInt(10)
// set a large initial blobFeeCap to make sure blob fee cap is hit before regular fee cap
blobTx.BlobFeeCap = uint256.NewInt(params.GWei * 2)
lastGoodTx = types.NewTx(blobTx)
for {
var tmpTx *types.Transaction
tmpTx, err = mgr.increaseGasPrice(ctx, lastGoodTx)
if err != nil {
break
}
lastGoodTx = tmpTx
}
require.ErrorIs(t, err, ErrBlobFeeLimit)
// Confirm that fees only rose until expected threshold
require.Equal(t, lt.expBlobFeeCap, lastGoodTx.BlobGasFeeCap().Int64())
}
func TestErrStringMatch(t *testing.T) {
......@@ -1032,7 +1202,7 @@ func TestNonceReset(t *testing.T) {
return core.ErrNonceTooLow
}
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
h.backend.mine(&txHash, tx.GasFeeCap(), nil)
return nil
}
h.backend.setTxSender(sendTx)
......@@ -1050,8 +1220,8 @@ func TestNonceReset(t *testing.T) {
}
}
// internal nonce tracking should be reset every 3rd tx
require.Equal(t, []uint64{0, 0, 1, 2, 0, 1, 2, 0}, nonces)
// internal nonce tracking should be reset to startingNonce value every 3rd tx
require.Equal(t, []uint64{1, 1, 2, 3, 1, 2, 3, 1}, nonces)
}
func TestMinFees(t *testing.T) {
......@@ -1103,7 +1273,7 @@ func TestMinFees(t *testing.T) {
conf.MinTipCap = tt.minTipCap
h := newTestHarnessWithConfig(t, conf)
tip, basefee, err := h.mgr.suggestGasPriceCaps(context.TODO())
tip, basefee, _, err := h.mgr.suggestGasPriceCaps(context.TODO())
require.NoError(err)
if tt.expectMinBasefee {
......
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