Skip to content

Instantly share code, notes, and snippets.

@karalabe
Last active February 10, 2023 02:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karalabe/e1c4e4c2a226926498cc9816d383cecb to your computer and use it in GitHub Desktop.
Save karalabe/e1c4e4c2a226926498cc9816d383cecb to your computer and use it in GitHub Desktop.
EIP-4844 blob transaction pool

Mandatory reading: EIP-4844.

Design constraints

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 behavioral 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 improvemenets will also mean, however, that we enforce a msignificantly ore agressive 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 8-16 blob transactions, 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 rollups. Rollups 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 network size, propagating a replacement blob transaction to an existing one is should be agressively 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 agressive bump (100%).
  • Cancellation is prohibive. Ejecting an already propagated blob tx is a huge DoS vector. As such, a) replacement (higher-fee) blob txs musn't invalidate already propagated (future) blob txs (cummulative fee); b) nonce-gapped blob txs are disallowed; c) the presence of blob transactions exclude non-blob transactions.
  • 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 transactions retain the 1559 2D gas pricing (and introduce on top a dynamic data 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 constaints don't apply anymore. Even though we could accept blocks contaning 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 finalized.
  • 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 agressive, 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.

Pool design

The blob pool consists of two concepts:

  • Persistent data store:
    • Responsible for storing executable blob transactions on disk.
    • Storage layout is one folder per account with one file per blob transaction in it.
      • Upon processing a new block, evicting an included blob tx is an O(1) operation.
      • Appending a new tx or replacing an existing one is both an O(1) operation.
      • Using up the account balance - outside of pool rules - might result in O(n) drops.
    • There's no state to flush on shutdown, rather data is indexed on startup.
  • Ephemeral indexer:
    • Responsible for tracking and operating on the blob tx metadata.
      • Reads through all the blob txs on startup and generates the index.
      • Validates arriving blob txs and rejects them if pool rules are violated.
@dankrad
Copy link

dankrad commented Jan 24, 2023

Overall this is a good design. I would push for some simplifications along the following lines, in order to make things as simple as possible:

  1. Ensuring that the transaction pool can replay transactions on reorgs should not be a priority. What I mean by this is that we should not make any concession in order to enable this, while I'm fine implementing it where it is trivial to do so. The justification for this is that (a) rollups are professional enough that we can completely expect them to be able to resubmit their blob transactions in this case and (b) where there is economic interest in replaying (tips paid to proposers) we nowadays can be sure that they will be replayed anyway as there are enough professional actors in the MEV ecosystem that will get them included via builder blocks.
  2. This means we should in particular not make any changes to the engine API to accommodate this, so I argue against changing engine_newPayload to pass blobs.
  3. For the same reason, I don't think that restricting to disallow 0 blob transactions at consensus level is necessary. There may be other justifications for this (I can't see any) and this is not an important point, but currently the restriction looks arbitrary and unnecessary.
  4. Restricting at transaction pool level however seems like a very good idea and I would actually suggest allowing only blob transactions with exactly 1 blob in the mempool, as well as only allowing a single blob transaction per account (so in addition to disallowing skipped nonce, I wouldn't even allow a transaction for the next available nonce after a tx that is currently queued). The reason to do this is that we get a nice invariant that cancelling 128 kB of blob transaction in the mempool requires at least one on-chain transaction, which limits DOS potential.

BTW, an interesting idea is to say that instead of creating a separate mempool for blob transactions, what we want to create is a large tx mempool for all large transactions. Large could be as little as 10 kB. All transactions that exceed this limit would go into the large tx mempool and be subject to the rules that we specify here. The advantage of this is that (1) it also adds DOS protection for transactions that are just large because of calldata, which actually have all the same problems as blob transactions and (2) the distinction of 0 blob transactions goes away, as they would just naturally be in the small tx mempool, assuming they don't have large calldata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment