Flashblocks have been live on Base and I’ve been experimenting to find opportunities. Read the previous two posts if you’re unfamiliar with the concepts and lessons learned during the initial exploration.
The flashblocks-rpc crate built into Base’s Reth fork is rather simplistic. It automates the subscription to the flashblocks feed and provides a view of those flashblocks when certain read-only JSON-RPC calls use “pending” as the block tag. That’s nice, but what we really need to identify opportunities is the ability to perform simulations against the new chain state once that flashblock is applied.
Built-In Reth Support … Soon?
There are some encouraging signs of life on Base’s node-reth repository, including some issues and pull requests for the ability to set some pending block state as new flashblocks appear, which would make simulations against that pending state feasible and performant.
However that’s not ready now, and ten new flashblock opportunities come in every two seconds, so let’s figure out how to attack this with the tools we have. We can always improve it later.
Options
Simulating transactions against an arbitrary state is nothing new. I’ve covered it here using eth_call, eth_simulateV1, debug_traceCall and multi-transaction bundling on a local fork.
The first three methods can be done against a vanilla node. The last method requires a node and a fork.
eth_simulateV1
The highest performance of the call methods is eth_simulateV1 which allows arbitrary bundles of transactions to be applied against a given block state with or without overrides. The call returns the result of each transaction, in order, with the states of previous calls affecting the next. That’s great! Only problem is that eth_simulateV1 is built for transaction visibility. The underlying state of the chain is not included in the output. This means that you can simulate a flashblock to your node and get an accurate view of what it will do (transaction hashes, logs, balance transfers, etc) but the state diff is not included.
To predict how this might scale, assume that at some block n, a flashblock f arrives with a bundle of transactions. The bundle can be simulated, and the returned logs inspected for relevant events. Using the predicted state for any tracked pools, you can perform offchain optimization and identify opportunities.
But the only way to check these for validity is to add the resulting capture transaction to the end of the bundle and simulate it thing again. If you’ve identified three independent opportunities, you could add all three, call it once, and inspect the results for each capture transaction.
But if you identified three mutually exclusive opportunities, you would have to call the bundle three times with each single transaction.
That’s fine and maybe not too much work. But what about the next flashblock? Now you must simulate flashblock f and f+1. That may invalidate results found on the previous flashblock, and it may provide more. Identify opportunities as before and repeat the process, except that each pre-capture bundle must include two flashblocks.
Expand this out and you’ll see how the execution load multiplies with each new flashblock. The lack of persistent block state makes eth_simulateV1 an unattractive option.
debug_traceCall
For the truly devoted, there are ways to make this method work.
Block state can be determined using the prestateTracer built into debug_traceCall. When called in diff mode, a series of state diffs will be returned for modified storage slots. These storage diffs can can be formatted and passed to eth_simulateV1, which would allow you to capture the block state after a full flashblock, updating it with each new one, and performing eth_simulateV1 calls with the “known” state. Pretty good!
But debug_traceCall has some issues — it only accepts transaction dictionaries, it must be called one transaction at a time, and the results must be inspected and reformatted into a unified state override.
I’d rather not manage the complexity, so I’m scrapping the idea.
Anvil To The Rescue
I once again turn to Anvil, my tool of choice. If you don’t know the history of Anvil and the AnvilFork class, which I built to make it dead-simple to use in a Python application, check it out.
Anvil works perfectly for this application. Instead of juggling bespoke state overrides to pass into eth_simulateV1, we can simply launch a fork and execute incoming flashblocks against it.
And since it’s built on the same foundation as Reth, it is fast and supports the same interfaces as our node.
Anvil offers some attractive features. It supports eth_subscribe, so our bot can subscribe to it and receive event logs that are thrown by the transactions within each new flashblock. Mining behavior is adjustable, so we can trigger a new block whenever we want, with transactions ordered as we want, with fees arbitrarily set.
Thus we can just treat the fork like it’s a true node, and operate as normal.