Skip to content

Commit

Permalink
core/txpool: make transaction validation reusable across packages (po…
Browse files Browse the repository at this point in the history
…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
karalabe authored and minh-bq committed Jul 18, 2024
1 parent ef47c7c commit 46be7dc
Show file tree
Hide file tree
Showing 13 changed files with 484 additions and 287 deletions.
2 changes: 1 addition & 1 deletion cmd/ronin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend) {
}
// Set the gas price to the limits from the CLI and start mining
gasprice := flags.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
ethBackend.TxPool().SetGasPrice(gasprice)
ethBackend.TxPool().SetGasTip(gasprice)
// start mining
threads := ctx.Int(utils.MinerThreadsFlag.Name)
if err := ethBackend.StartMining(threads); err != nil {
Expand Down
259 changes: 54 additions & 205 deletions core/txpool/txpool.go

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions core/txpool/txpool2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ func TestTransactionFutureAttack(t *testing.T) {

// Create the pool to test the limit enforcement with
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
blockchain := &testBlockChain{1000000, statedb, new(event.Feed), 0}
config := testTxPoolConfig
config.GlobalQueue = 100
config.GlobalSlots = 100
pool := NewTxPool(config, eip1559Config, blockchain)
pool := New(config, eip1559Config, blockchain)
defer pool.Stop()
fillPool(t, pool)
pending, _ := pool.Stats()
Expand Down Expand Up @@ -115,8 +115,8 @@ func TestTransactionFuture1559(t *testing.T) {
t.Parallel()
// Create the pool to test the pricing enforcement with
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain)
blockchain := &testBlockChain{1000000, statedb, new(event.Feed), 0}
pool := New(testTxPoolConfig, eip1559Config, blockchain)
defer pool.Stop()

// Create a number of test accounts, fund them and make transactions
Expand Down Expand Up @@ -147,8 +147,8 @@ func TestTransactionZAttack(t *testing.T) {
t.Parallel()
// Create the pool to test the pricing enforcement with
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain)
blockchain := &testBlockChain{1000000, statedb, new(event.Feed), 0}
pool := New(testTxPoolConfig, eip1559Config, blockchain)
defer pool.Stop()
mikoSigner := types.NewMikoSigner(common.Big1)
// Create a number of test accounts, fund them and make transactions
Expand Down
132 changes: 67 additions & 65 deletions core/txpool/txpool_test.go

Large diffs are not rendered by default.

344 changes: 344 additions & 0 deletions core/txpool/validation.go
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
}
2 changes: 1 addition & 1 deletion eth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (api *PrivateMinerAPI) SetGasPrice(gasPrice hexutil.Big) bool {
api.e.gasPrice = (*big.Int)(&gasPrice)
api.e.lock.Unlock()

api.e.txPool.SetGasPrice((*big.Int)(&gasPrice))
api.e.txPool.SetGasTip((*big.Int)(&gasPrice))
return true
}

Expand Down
Loading

0 comments on commit 46be7dc

Please sign in to comment.