forked from axieinfinity/ronin
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
core/txpool: make transaction validation reusable across packages (po…
…ols) (#27429) commit ethereum/go-ethereum@950d564. * core/txpool: abstraction prep work for secondary pools (blob pool) * core/txpool: leave subpool concepts to a followup pr * les: fix tests using hard coded errors * core/txpool: use bitmaps instead of maps for tx type filtering
- Loading branch information
Showing
13 changed files
with
484 additions
and
287 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,344 @@ | ||
// Copyright 2023 The go-ethereum Authors | ||
// This file is part of the go-ethereum library. | ||
// | ||
// The go-ethereum library is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Lesser General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// The go-ethereum library is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Lesser General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Lesser General Public License | ||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
package txpool | ||
|
||
import ( | ||
"crypto/sha256" | ||
"fmt" | ||
"math/big" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/core" | ||
"github.com/ethereum/go-ethereum/core/state" | ||
"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" | ||
) | ||
|
||
// ValidationOptions define certain differences between transaction validation | ||
// across the different pools without having to duplicate those checks. | ||
type ValidationOptions struct { | ||
Config *params.ChainConfig // Chain configuration to selectively validate based on current fork rules | ||
|
||
Accept uint8 // Bitmap of transaction types that should be accepted for the calling pool | ||
MaxSize uint64 // Maximum size of a transaction that the caller can meaningfully handle | ||
MinTip *big.Int // Minimum gas tip needed to allow a transaction into the caller pool | ||
MaxGas uint64 // The current max gas limit per block | ||
|
||
// As the Accept bitmap cannot store the sponsored transaction type which is 0x64 (100), | ||
// we need to create a separate bool for this case | ||
AcceptSponsoredTx bool | ||
} | ||
|
||
// ValidateTransaction is a helper method to check whether a transaction is valid | ||
// according to the consensus rules, but does not check state-dependent validation | ||
// (balance, nonce, etc). | ||
// | ||
// This check is public to allow different transaction pools to check the basic | ||
// rules without duplicating code and running the risk of missed updates. | ||
func ValidateTransaction(tx *types.Transaction, blobs []kzg4844.Blob, commits []kzg4844.Commitment, proofs []kzg4844.Proof, head *types.Header, signer types.Signer, opts *ValidationOptions) error { | ||
// Ensure transactions not implemented by the calling pool are rejected | ||
// Check if it's sponsored transaction before using Accept bitmap | ||
if !opts.AcceptSponsoredTx || tx.Type() != types.SponsoredTxType { | ||
if opts.Accept&(1<<tx.Type()) == 0 { | ||
return fmt.Errorf("%w: tx type %v not supported by this pool", core.ErrTxTypeNotSupported, tx.Type()) | ||
} | ||
} | ||
|
||
// Before performing any expensive validations, sanity check that the tx is | ||
// smaller than the maximum limit the pool can meaningfully handle | ||
if uint64(tx.Size()) > opts.MaxSize { | ||
return fmt.Errorf("%w: transaction size %v, limit %v", ErrOversizedData, tx.Size(), opts.MaxSize) | ||
} | ||
// Ensure only transactions that have been enabled are accepted | ||
if !opts.Config.IsBerlin(head.Number) && tx.Type() == types.AccessListTxType { | ||
return fmt.Errorf("%w: type %d rejected, pool not yet in Berlin", core.ErrTxTypeNotSupported, tx.Type()) | ||
} | ||
if !opts.Config.IsLondon(head.Number) && tx.Type() == types.DynamicFeeTxType { | ||
return fmt.Errorf("%w: type %d rejected, pool not yet in London", core.ErrTxTypeNotSupported, tx.Type()) | ||
} | ||
if !opts.Config.IsMiko(head.Number) && tx.Type() == types.SponsoredTxType { | ||
return fmt.Errorf("%w: type %d rejected, pool not yet in London", core.ErrTxTypeNotSupported, tx.Type()) | ||
} | ||
if !opts.Config.IsCancun(head.Number) && tx.Type() == types.BlobTxType { | ||
return fmt.Errorf("%w: type %d rejected, pool not yet in Cancun", core.ErrTxTypeNotSupported, tx.Type()) | ||
} | ||
// Check whether the init code size has been exceeded | ||
if opts.Config.IsShanghai(head.Number) && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize { | ||
return fmt.Errorf("%w: code size %v, limit %v", core.ErrMaxInitCodeSizeExceeded, len(tx.Data()), params.MaxInitCodeSize) | ||
} | ||
// Transactions can't be negative. This may never happen using RLP decoded | ||
// transactions but may occur for transactions created using the RPC. | ||
if tx.Value().Sign() < 0 { | ||
return ErrNegativeValue | ||
} | ||
// Ensure the transaction doesn't exceed the current block limit gas | ||
if opts.MaxGas < tx.Gas() { | ||
return ErrGasLimit | ||
} | ||
// Sanity check for extremely large numbers (supported by RLP or RPC) | ||
if tx.GasFeeCap().BitLen() > 256 { | ||
return core.ErrFeeCapVeryHigh | ||
} | ||
if tx.GasTipCap().BitLen() > 256 { | ||
return core.ErrTipVeryHigh | ||
} | ||
// Ensure gasFeeCap is greater than or equal to gasTipCap | ||
if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 { | ||
return core.ErrTipAboveFeeCap | ||
} | ||
// Make sure the transaction is signed properly | ||
from, err := types.Sender(signer, tx) | ||
if err != nil { | ||
return ErrInvalidSender | ||
} | ||
// Ensure the transaction has more gas than the bare minimum needed to cover | ||
// the transaction metadata | ||
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, opts.Config.IsIstanbul(head.Number), opts.Config.IsShanghai(head.Number)) | ||
if err != nil { | ||
return err | ||
} | ||
if tx.Gas() < intrGas { | ||
return fmt.Errorf("%w: needed %v, allowed %v", core.ErrIntrinsicGas, intrGas, tx.Gas()) | ||
} | ||
// Ensure the gasprice is high enough to cover the requirement of the calling | ||
// pool and/or block producer | ||
if tx.GasTipCapIntCmp(opts.MinTip) < 0 { | ||
return fmt.Errorf("%w: tip needed %v, tip permitted %v", ErrUnderpriced, opts.MinTip, tx.GasTipCap()) | ||
} | ||
// Ensure blob transactions have valid commitments | ||
if tx.Type() == types.BlobTxType { | ||
// Ensure the number of items in the blob transaction and vairous side | ||
// data match up before doing any expensive validations | ||
hashes := tx.BlobHashes() | ||
if len(hashes) == 0 { | ||
return fmt.Errorf("blobless blob transaction") | ||
} | ||
if len(hashes) > params.MaxBlobGasPerBlock/params.BlobTxBlobGasPerBlob { | ||
return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), params.MaxBlobGasPerBlock/params.BlobTxBlobGasPerBlob) | ||
} | ||
if len(blobs) != len(hashes) { | ||
return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(blobs), len(hashes)) | ||
} | ||
if len(commits) != len(hashes) { | ||
return fmt.Errorf("invalid number of %d blob commitments compared to %d blob hashes", len(commits), len(hashes)) | ||
} | ||
if len(proofs) != len(hashes) { | ||
return fmt.Errorf("invalid number of %d blob proofs compared to %d blob hashes", len(proofs), len(hashes)) | ||
} | ||
// Blob quantities match up, validate that the provers match with the | ||
// transaction hash before getting to the cryptography | ||
hasher := sha256.New() | ||
for i, want := range hashes { | ||
hasher.Write(commits[i][:]) | ||
hash := hasher.Sum(nil) | ||
hasher.Reset() | ||
|
||
var vhash common.Hash | ||
vhash[0] = 0x1 | ||
copy(vhash[1:], hash[1:]) | ||
|
||
if vhash != want { | ||
return fmt.Errorf("blob %d: computed hash %#x mismatches transaction one %#x", i, vhash, want) | ||
} | ||
} | ||
// Blob commitments match with the hashes in the transaction, verify the | ||
// blobs themselves via KZG | ||
for i := range blobs { | ||
if err := kzg4844.VerifyBlobProof(&blobs[i], commits[i], proofs[i]); err != nil { | ||
return fmt.Errorf("invalid blob %d: %v", i, err) | ||
} | ||
} | ||
} else if tx.Type() == types.SponsoredTxType { | ||
// Currently, these 2 fields must be the same in sponsored transaction. | ||
// We create 2 separate fields to reserve for the future, in case we | ||
// decide to support dynamic fee transaction. | ||
if tx.GasFeeCap().Cmp(tx.GasTipCap()) != 0 { | ||
return core.ErrDifferentFeeCapTipCap | ||
} | ||
|
||
// Ensure sponsored transaction is not expired | ||
expiredTime := tx.ExpiredTime() | ||
if expiredTime != 0 && expiredTime <= head.Time { | ||
return core.ErrExpiredSponsoredTx | ||
} | ||
|
||
payer, err := types.Payer(signer, tx) | ||
if err != nil { | ||
return ErrInvalidPayer | ||
} | ||
// Ensure payer is different from sender | ||
if payer == from { | ||
return types.ErrSamePayerSenderSponsoredTx | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// ValidationOptionsWithState define certain differences between stateful transaction | ||
// validation across the different pools without having to duplicate those checks. | ||
type ValidationOptionsWithState struct { | ||
Config *params.ChainConfig // Chain configuration to selectively validate based on current fork rules | ||
|
||
Head *types.Header // Current header of blockchain | ||
|
||
State *state.StateDB // State database to check nonces and balances against | ||
|
||
// FirstNonceGap is an optional callback to retrieve the first nonce gap in | ||
// the list of pooled transactions of a specific account. If this method is | ||
// set, nonce gaps will be checked and forbidden. If this method is not set, | ||
// nonce gaps will be ignored and permitted. | ||
FirstNonceGap func(addr common.Address) uint64 | ||
|
||
// ExistingExpenditure is a mandatory callback to retrieve the cummulative | ||
// cost of the already pooled transactions to check for overdrafts. | ||
ExistingExpenditure func(addr common.Address) *big.Int | ||
|
||
// ExistingCost is a mandatory callback to retrieve an already pooled | ||
// transaction's cost with the given nonce to check for overdrafts. | ||
ExistingCost func(addr common.Address, nonce uint64) *big.Int | ||
} | ||
|
||
// ValidateTransactionWithState is a helper method to check whether a transaction | ||
// is valid according to the pool's internal state checks (balance, nonce, gaps). | ||
// | ||
// This check is public to allow different transaction pools to check the stateful | ||
// rules without duplicating code and running the risk of missed updates. | ||
func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, opts *ValidationOptionsWithState) error { | ||
// Ensure the transaction adheres to nonce ordering | ||
from, err := signer.Sender(tx) // already validated (and cached), but cleaner to check | ||
if err != nil { | ||
log.Error("Transaction sender recovery failed", "err", err) | ||
return err | ||
} | ||
next := opts.State.GetNonce(from) | ||
if next > tx.Nonce() { | ||
return fmt.Errorf("%w: next nonce %v, tx nonce %v", core.ErrNonceTooLow, next, tx.Nonce()) | ||
} | ||
// Ensure the transaction doesn't produce a nonce gap in pools that do not | ||
// support arbitrary orderings | ||
if opts.FirstNonceGap != nil { | ||
if gap := opts.FirstNonceGap(from); gap < tx.Nonce() { | ||
return fmt.Errorf("%w: tx nonce %v, gapped nonce %v", core.ErrNonceTooHigh, tx.Nonce(), gap) | ||
} | ||
} | ||
// Ensure the transactor has enough funds to cover the transaction costs | ||
var ( | ||
senderBalance = opts.State.GetBalance(from) | ||
payerBalance *big.Int | ||
gasCost = new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) | ||
senderCost *big.Int | ||
payer common.Address | ||
) | ||
|
||
if tx.Type() == types.SponsoredTxType { | ||
payer, err = signer.Payer(tx) // already validated (and cached), but cleaner to check | ||
if err != nil { | ||
log.Error("Transaction payer recovery failed", "err", err) | ||
return err | ||
} | ||
payerBalance = opts.State.GetBalance(payer) | ||
|
||
if payerBalance.Cmp(gasCost) < 0 { | ||
return fmt.Errorf( | ||
"%w: payer's balance %v, tx gas cost %v, overshot %v", core.ErrInsufficientPayerFunds, | ||
payerBalance, gasCost, new(big.Int).Sub(gasCost, payerBalance), | ||
) | ||
} | ||
senderCost = tx.Value() | ||
if senderBalance.Cmp(senderCost) < 0 { | ||
return fmt.Errorf( | ||
"%w: sender's balance %v, tx value %v, overshot %v", core.ErrInsufficientSenderFunds, | ||
senderBalance, senderCost, new(big.Int).Sub(senderCost, senderBalance), | ||
) | ||
} | ||
} else { | ||
senderCost = tx.Cost() | ||
if senderBalance.Cmp(senderCost) < 0 { | ||
return fmt.Errorf( | ||
"%w: sender's balance %v, tx cost %v, overshot %v", core.ErrInsufficientFunds, | ||
senderBalance, senderCost, new(big.Int).Sub(senderCost, senderBalance), | ||
) | ||
} | ||
} | ||
|
||
// Ensure the transactor has enough funds to cover for replacements or nonce | ||
// expansions without overdrafts | ||
spent := opts.ExistingExpenditure(from) | ||
if prev := opts.ExistingCost(from, tx.Nonce()); prev != nil { | ||
bump := new(big.Int).Sub(senderCost, prev) | ||
need := new(big.Int).Add(spent, bump) | ||
if senderBalance.Cmp(need) < 0 { | ||
return fmt.Errorf( | ||
"%w: sender's balance %v, queued cost %v, tx bumped %v, overshot %v", core.ErrInsufficientFunds, | ||
senderBalance, spent, bump, new(big.Int).Sub(need, senderBalance), | ||
) | ||
} | ||
} else { | ||
need := new(big.Int).Add(spent, senderCost) | ||
if senderBalance.Cmp(need) < 0 { | ||
return fmt.Errorf( | ||
"%w: sender's balance %v, queued cost %v, tx cost %v, overshot %v", core.ErrInsufficientFunds, | ||
senderBalance, spent, senderCost, new(big.Int).Sub(need, senderBalance), | ||
) | ||
} | ||
} | ||
|
||
// Check payer overdraft | ||
// Sponsored transaction does not properly support nonce replacement so | ||
// we don't substract the replaced transaction's cost like above | ||
if tx.Type() == types.SponsoredTxType { | ||
spent := opts.ExistingExpenditure(payer) | ||
need := new(big.Int).Add(spent, gasCost) | ||
if payerBalance.Cmp(need) < 0 { | ||
return fmt.Errorf( | ||
"%w: payer's balance %v, queued cost %v, tx cost %v, overshot %v", core.ErrInsufficientFunds, | ||
payerBalance, spent, gasCost, new(big.Int).Sub(need, payerBalance), | ||
) | ||
} | ||
} | ||
|
||
if tx.To() == nil && opts.Config.Consortium != nil { | ||
var whitelisted bool | ||
if opts.Config.IsAntenna(opts.Head.Number) { | ||
whitelisted = state.IsWhitelistedDeployerV2( | ||
opts.State, | ||
from, | ||
opts.Head.Time, | ||
opts.Config.WhiteListDeployerContractV2Address, | ||
) | ||
} else { | ||
whitelisted = state.IsWhitelistedDeployer(opts.State, from) | ||
} | ||
if !whitelisted { | ||
return ErrUnauthorizedDeployer | ||
} | ||
} | ||
|
||
// Check if sender, payer and recipient are blacklisted | ||
if opts.Config.Consortium != nil && opts.Config.IsOdysseus(opts.Head.Number) { | ||
contractAddr := opts.Config.BlacklistContractAddress | ||
if state.IsAddressBlacklisted(opts.State, contractAddr, &from) || | ||
state.IsAddressBlacklisted(opts.State, contractAddr, tx.To()) || | ||
state.IsAddressBlacklisted(opts.State, contractAddr, &payer) { | ||
return ErrAddressBlacklisted | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.