blobpool

package
v0.0.0-...-283b1fa Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2024 License: GPL-3.0, LGPL-3.0 Imports: 26 Imported by: 0

Documentation

Overview

Package blobpool implements the EIP-4844 blob transaction pool.

Index

Constants

This section is empty.

Variables

View Source
var DefaultConfig = Config{
	Datadir:   "blobpool",
	Datacap:   10 * 1024 * 1024 * 1024,
	PriceBump: 100,
}

DefaultConfig contains the default configurations for the transaction pool.

Functions

This section is empty.

Types

type BlobPool

type BlobPool struct {
	// contains filtered or unexported fields
}

BlobPool is the transaction pool dedicated to EIP-4844 blob transactions.

Blob transactions are special snowflakes that are designed for a very specific purpose (rollups) and are expected to adhere to that specific use case. These behavioural expectations allow us to design a transaction pool that is more robust (i.e. resending issues) and more resilient to DoS attacks (e.g. replace-flush attacks) than the generic tx pool. These improvements will also mean, however, that we enforce a significantly more aggressive strategy on entering and exiting the pool:

  • Blob transactions are large. With the initial design aiming for 128KB blobs, we must ensure that these only traverse the network the absolute minimum number of times. Broadcasting to sqrt(peers) is out of the question, rather these should only ever be announced and the remote side should request it if it wants to.

  • Block blob-space is limited. With blocks being capped to a few blob txs, we can make use of the very low expected churn rate within the pool. Notably, we should be able to use a persistent disk backend for the pool, solving the tx resend issue that plagues the generic tx pool, as long as there's no artificial churn (i.e. pool wars).

  • Purpose of blobs are layer-2s. Layer-2s are meant to use blob transactions to commit to their own current state, which is independent of Ethereum mainnet (state, txs). This means that there's no reason for blob tx cancellation or replacement, apart from a potential basefee / miner tip adjustment.

  • Replacements are expensive. Given their size, propagating a replacement blob transaction to an existing one should be aggressively discouraged. Whilst generic transactions can start at 1 Wei gas cost and require a 10% fee bump to replace, we suggest requiring a higher min cost (e.g. 1 gwei) and a more aggressive bump (100%).

  • Cancellation is prohibitive. Evicting an already propagated blob tx is a huge DoS vector. As such, a) replacement (higher-fee) blob txs mustn't invalidate already propagated (future) blob txs (cumulative fee); b) nonce-gapped blob txs are disallowed; c) the presence of blob transactions exclude non-blob transactions.

  • Malicious cancellations are possible. Although the pool might prevent txs that cancel blobs, blocks might contain such transaction (malicious miner or flashbotter). The pool should cap the total number of blob transactions per account as to prevent propagating too much data before cancelling it via a normal transaction. It should nonetheless be high enough to support resurrecting reorged transactions. Perhaps 4-16.

  • Local txs are meaningless. Mining pools historically used local transactions for payouts or for backdoor deals. With 1559 in place, the basefee usually dominates the final price, so 0 or non-0 tip doesn't change much. Blob txs retain the 1559 2D gas pricing (and introduce on top a dynamic blob gas fee), so locality is moot. With a disk backed blob pool avoiding the resend issue, there's also no need to save own transactions for later.

  • No-blob blob-txs are bad. Theoretically there's no strong reason to disallow blob txs containing 0 blobs. In practice, admitting such txs into the pool breaks the low-churn invariant as blob constraints don't apply anymore. Even though we could accept blocks containing such txs, a reorg would require moving them back into the blob pool, which can break invariants.

  • Dropping blobs needs delay. When normal transactions are included, they are immediately evicted from the pool since they are contained in the including block. Blobs however are not included in the execution chain, so a mini reorg cannot re-pool "lost" blob transactions. To support reorgs, blobs are retained on disk until they are finalised.

  • Blobs can arrive via flashbots. Blocks might contain blob transactions we have never seen on the network. Since we cannot recover them from blocks either, the engine_newPayload needs to give them to us, and we cache them until finality to support reorgs without tx losses.

Whilst some constraints above might sound overly aggressive, the general idea is that the blob pool should work robustly for its intended use case and whilst anyone is free to use blob transactions for arbitrary non-rollup use cases, they should not be allowed to run amok the network.

Implementation wise there are a few interesting design choices:

  • Adding a transaction to the pool blocks until persisted to disk. This is viable because TPS is low (2-4 blobs per block initially, maybe 8-16 at peak), so natural churn is a couple MB per block. Replacements doing O(n) updates are forbidden and transaction propagation is pull based (i.e. no pileup of pending data).

  • When transactions are chosen for inclusion, the primary criteria is the signer tip (and having a basefee/data fee high enough of course). However, same-tip transactions will be split by their basefee/datafee, preferring those that are closer to the current network limits. The idea being that very relaxed ones can be included even if the fees go up, when the closer ones could already be invalid.

When the pool eventually reaches saturation, some old transactions - that may never execute - will need to be evicted in favor of newer ones. The eviction strategy is quite complex:

  • Exceeding capacity evicts the highest-nonce of the account with the lowest paying blob transaction anywhere in the pooled nonce-sequence, as that tx would be executed the furthest in the future and is thus blocking anything after it. The smallest is deliberately not evicted to avoid a nonce-gap.

  • Analogously, if the pool is full, the consideration price of a new tx for evicting an old one is the smallest price in the entire nonce-sequence of the account. This avoids malicious users DoSing the pool with seemingly high paying transactions hidden behind a low-paying blocked one.

  • Since blob transactions have 3 price parameters: execution tip, execution fee cap and data fee cap, there's no singular parameter to create a total price ordering on. What's more, since the base fee and blob fee can move independently of one another, there's no pre-defined way to combine them into a stable order either. This leads to a multi-dimensional problem to solve after every block.

  • The first observation is that comparing 1559 base fees or 4844 blob fees needs to happen in the context of their dynamism. Since these fees jump up or down in ~1.125 multipliers (at max) across blocks, comparing fees in two transactions should be based on log1.125(fee) to eliminate noise.

  • The second observation is that the basefee and blobfee move independently, so there's no way to split mixed txs on their own (A has higher base fee, B has higher blob fee). Rather than look at the absolute fees, the useful metric is the max time it can take to exceed the transaction's fee caps. Specifically, we're interested in the number of jumps needed to go from the current fee to the transaction's cap:

    jumps = log1.125(txfee) - log1.125(basefee)

  • The third observation is that the base fee tends to hover around rather than swing wildly. The number of jumps needed from the current fee starts to get less relevant the higher it is. To remove the noise here too, the pool will use log(jumps) as the delta for comparing transactions.

    delta = sign(jumps) * log(abs(jumps))

  • To establish a total order, we need to reduce the dimensionality of the two base fees (log jumps) to a single value. The interesting aspect from the pool's perspective is how fast will a tx get executable (fees going down, crossing the smaller negative jump counter) or non-executable (fees going up, crossing the smaller positive jump counter). As such, the pool cares only about the min of the two delta values for eviction priority.

    priority = min(delta-basefee, delta-blobfee)

  • The above very aggressive dimensionality and noise reduction should result in transaction being grouped into a small number of buckets, the further the fees the larger the buckets. This is good because it allows us to use the miner tip meaningfully as a splitter.

  • For the scenario where the pool does not contain non-executable blob txs anymore, it does not make sense to grant a later eviction priority to txs with high fee caps since it could enable pool wars. As such, any positive priority will be grouped together.

    priority = min(delta-basefee, delta-blobfee, 0)

Optimisation tradeoffs:

  • Eviction relies on 3 fee minimums per account (exec tip, exec cap and blob cap). Maintaining these values across all transactions from the account is problematic as each transaction replacement or inclusion would require a rescan of all other transactions to recalculate the minimum. Instead, the pool maintains a rolling minimum across the nonce range. Updating all the minimums will need to be done only starting at the swapped in/out nonce and leading up to the first no-change.

func New

func New(config Config, chain BlockChain) *BlobPool

New creates a new blob transaction pool to gather, sort and filter inbound blob transactions from the network.

func (*BlobPool) Add

func (p *BlobPool) Add(txs []*types.Transaction, local bool, sync bool) []error

Add inserts a set of blob transactions into the pool if they pass validation (both consensus validity and pool restictions).

func (*BlobPool) Close

func (p *BlobPool) Close() error

Close closes down the underlying persistent store.

func (*BlobPool) Content

func (p *BlobPool) Content() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction)

Content retrieves the data content of the transaction pool, returning all the pending as well as queued transactions, grouped by account and sorted by nonce.

For the blob pool, this method will return nothing for now. TODO(karalabe): Abstract out the returned metadata.

func (*BlobPool) ContentFrom

func (p *BlobPool) ContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction)

ContentFrom retrieves the data content of the transaction pool, returning the pending as well as queued transactions of this address, grouped by nonce.

For the blob pool, this method will return nothing for now. TODO(karalabe): Abstract out the returned metadata.

func (*BlobPool) Filter

func (p *BlobPool) Filter(tx *types.Transaction) bool

Filter returns whether the given transaction can be consumed by the blob pool.

func (*BlobPool) Get

func (p *BlobPool) Get(hash common.Hash) *types.Transaction

Get returns a transaction if it is contained in the pool, or nil otherwise.

func (*BlobPool) Has

func (p *BlobPool) Has(hash common.Hash) bool

Has returns an indicator whether subpool has a transaction cached with the given hash.

func (*BlobPool) HasLocal

func (p *BlobPool) HasLocal(hash common.Hash) bool

func (*BlobPool) Init

func (p *BlobPool) Init(gasTip *big.Int, head *types.Header, reserve txpool.AddressReserver) error

Init sets the gas price needed to keep a transaction in the pool and the chain head to allow balance / nonce checks. The transaction journal will be loaded from disk and filtered based on the provided starting settings.

func (*BlobPool) IteratePending

func (pool *BlobPool) IteratePending(f func(tx *types.Transaction) bool) bool

IteratePending iterates over [pool.pending] until [f] returns false. The caller must not modify [tx]. Returns false if iteration was interrupted.

func (*BlobPool) Locals

func (p *BlobPool) Locals() []common.Address

Locals retrieves the accounts currently considered local by the pool.

There is no notion of local accounts in the blob pool.

func (*BlobPool) Nonce

func (p *BlobPool) Nonce(addr common.Address) uint64

Nonce returns the next nonce of an account, with all transactions executable by the pool already applied on top.

func (*BlobPool) Pending

func (p *BlobPool) Pending(enforceTips bool) map[common.Address][]*txpool.LazyTransaction

Pending retrieves all currently processable transactions, grouped by origin account and sorted by nonce.

func (*BlobPool) PendingFrom

func (p *BlobPool) PendingFrom(addrs []common.Address, enforceTips bool) map[common.Address][]*txpool.LazyTransaction

PendingFrom returns the same set of transactions that would be returned from Pending restricted to only transactions from [addrs].

func (*BlobPool) PendingWithBaseFee

func (p *BlobPool) PendingWithBaseFee(enforceTips bool, baseFee *big.Int) map[common.Address][]*txpool.LazyTransaction

func (*BlobPool) Reset

func (p *BlobPool) Reset(oldHead, newHead *types.Header)

Reset implements txpool.SubPool, allowing the blob pool's internal state to be kept in sync with the main transacion pool's internal state.

func (*BlobPool) SetGasTip

func (p *BlobPool) SetGasTip(tip *big.Int)

SetGasTip implements txpool.SubPool, allowing the blob pool's gas requirements to be kept in sync with the main transacion pool's gas requirements.

func (*BlobPool) SetMinFee

func (p *BlobPool) SetMinFee(minFee *big.Int)

func (*BlobPool) Stats

func (p *BlobPool) Stats() (int, int)

Stats retrieves the current pool stats, namely the number of pending and the number of queued (non-executable) transactions.

func (*BlobPool) Status

func (p *BlobPool) Status(hash common.Hash) txpool.TxStatus

Status returns the known status (unknown/pending/queued) of a transaction identified by their hashes.

func (*BlobPool) SubscribeTransactions

func (p *BlobPool) SubscribeTransactions(ch chan<- core.NewTxsEvent) event.Subscription

SubscribeTransactions registers a subscription of NewTxsEvent and starts sending event to the given channel.

type BlockChain

type BlockChain interface {
	// Config retrieves the chain's fork configuration.
	Config() *params.ChainConfig

	// CurrentBlock returns the current head of the chain.
	CurrentBlock() *types.Header

	// CurrentFinalBlock returns the current block below which blobs should not
	// be maintained anymore for reorg purposes.
	CurrentFinalBlock() *types.Header

	// GetBlock retrieves a specific block, used during pool resets.
	GetBlock(hash common.Hash, number uint64) *types.Block

	// StateAt returns a state database for a given root hash (generally the head).
	StateAt(root common.Hash) (*state.StateDB, error)
}

BlockChain defines the minimal set of methods needed to back a blob pool with a chain. Exists to allow mocking the live chain out of tests.

type Config

type Config struct {
	Datadir   string // Data directory containing the currently executable blobs
	Datacap   uint64 // Soft-cap of database storage (hard cap is larger due to overhead)
	PriceBump uint64 // Minimum price bump percentage to replace an already existing nonce
}

Config are the configuration parameters of the blob transaction pool.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL