diff --git a/op-service/txmgr/price_bump_test.go b/op-service/txmgr/price_bump_test.go index 5b32b12d0032c..a78641f09a7df 100644 --- a/op-service/txmgr/price_bump_test.go +++ b/op-service/txmgr/price_bump_test.go @@ -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 diff --git a/op-service/txmgr/queue_test.go b/op-service/txmgr/queue_test.go index 043ecb0f859bb..db953fc5173a9 100644 --- a/op-service/txmgr/queue_test.go +++ b/op-service/txmgr/queue_test.go @@ -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) diff --git a/op-service/txmgr/txmgr.go b/op-service/txmgr/txmgr.go index 399d3f76330b1..66f5f06e5e703 100644 --- a/op-service/txmgr/txmgr.go +++ b/op-service/txmgr/txmgr.go @@ -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 +} diff --git a/op-service/txmgr/txmgr_test.go b/op-service/txmgr/txmgr_test.go index cab9f66ca3986..66f6b9affbe9b 100644 --- a/op-service/txmgr/txmgr_test.go +++ b/op-service/txmgr/txmgr_test.go @@ -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 {