Skip to content

Commit

Permalink
Merge pull request #4 from 20squares/parallelTxs
Browse files Browse the repository at this point in the history
Added post.
  • Loading branch information
FabrizioRomanoGenovese authored Jan 5, 2024
2 parents 9c2743e + 777a27a commit 54db0af
Show file tree
Hide file tree
Showing 13 changed files with 1,233 additions and 0 deletions.
112 changes: 112 additions & 0 deletions _posts/2024-01-05-correctly-pricing-txs-parallel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
layout: post
title: Correctly pricing blockspace in a causally parallel world
author: Fabrizio Genovese, Daniele Palombi
categories: [parallelization, causality, MEV, EVM, Jito]
excerpt: Where we investigate the thread that ties Jito, the parallel EVM, auction theory and physics together.
image: assetsPosts/2024-01-05-correctly-pricing-txs-parallel/PetriConflict.png
usemathjax: true
thanks:
---

First of all, happy new year! We hope you had a blast as much as we did in 2023, and that you're all set to double down on 2024.

There seems to be a couple of new trends in the Ethereum Twitter echo chamber these days:
- The discovery that there's MEV on Solana, and they ain't fucking around with it;
- The fact that a lot of projects are working on parallelizing the Ethereum Virtual Machine (EVM), and this seems to be 'the thing' in Ethereum now.

Clearly we can't miss the party, and as usual we'll try to wrtite about interesting stuff at the intersection of these topics. For starters, the natural question that we asked ourselves is:

> how do you price blockspace in a parallelized world?

## What is a parallelized world?

Parallel execution, in layman terms, means that you can split a given computation between different threads that happen at the same time. Thus, if you have a multi-core architecture, you can use more than one core at the same time to perform the computation. The advantage is tangible, especially given how in the last decade or so multi-core architectures have become increasingly common.
Clearly, there are some tasks that are highly parallelizable - e.g. bruteforcing a password - and some tasks that aren't - for instance computing Verifiable Delay Functions (VDFs).

## A story of inclusion

Now, our idea is very simple. Parallelization is good because it makes execution faster. As such, one would like to incentivize blocks that are 'as parallel as possible'. Furthermore, we must pair the computation layer with the economic layer, and the way we do it is via *transaction fees*: We put a cost on each transaction that should be proportional to how much (space, computational) resources the transaction consumes, and to how badly the user wants that transaction to be included in a block.

Naïvely, we'd like 'highly parallelizable' transactions to be cheaper as the amount of computation one must do is more efficient. Paradoxically, this is easier said than done, and depends on the fact that fees represent costs for some actors, but revenues for others. If we were to make 'highly parallel transactions' fees cheaper - e.g. by multiplying the transaction gas size by a 'parallelization coefficient' ranging from 0 to 1 - we would incentivize one end of the pipeline (end users, dapp developers) to push for highly parallel blocks. At the same time, we would incentivize the other end of the pipeline (the fee recipients, so validators, builders, etc) to reject highly parallel blocks in favor of sequential ones, just because they pay better.

One consequence of this is that whatever gas pricing mechanism we adopt, it cannot be too naïve. One possible way to align agents across the pipeline would be the following:
- As transaction fees, users specify gas for the 'worst case scenario', when the transaction is not parallelizable at all;
- At block simulation, a certain degree of parallelization is established as a function of the block;
- The users receive a discount on their fee based on this;
- The block builder instead receives the whole value of the user transaction, maybe plus a small bonus;
- The delta between what the user pays and what the builder receives is simply created, as block rewards are created now.

We are not claiming that this is the right way to do it (and indeed it's not, because it may require to sequentially simulate the block to calculate the improvement coefficient, that defies the purpose of parallelization completely). The example above only shows how one must be a bit creative when it comes to incentive design to make sure all actors are aligned. Luckily for us, there's a vast literature on topics such as cloud infrastructure pricing that we can boorrow from, so we don't have to start from scratch. We will probably come up with some follow-up posts about this in the future.

## A story of ordering

In the most ideal setting described above, transaction fees are taken to represent *inclusion preferences*. That is, you pay more depending on how badly you want to be included in a block. Unfortunately, we all know very well how the picture is much more complicated than this, as transaction fees have been (and are) used, especially in Ethereum, to represent *ordering preferences*, that is, how badly you want your transaction to be close to the top of the block it is in.

This observation is what ties together parallel computation and MEV. Imagine that there are two transactions that compete for being top of the block. Assume furthermore that there is no overlap whatsoever between the state read and written by them. Are the transactions 'really' competing? The answer is no, because the way you order them does not matter: By definition, whatever one transaction does won't influence what the other does, and hence it doesn't matter who ends up being on top.

This is precisely what happens in Jito, where 'transactions that do not stomp on each other's feet' are put into different auctions running in parallel. This difference was highlighted in a very approachable way in [Tarun's twitter thread](https://warpcast.com/pinged/0xa6d3fe25).

In a way, this tells us that there are MEV auctions out there that already take parallelization into account. On the surface this shouldn't surprise, as Solana's execution layer is already parallelized. Yet, do not let yourself be fooled by this. Indeed:

> You do not need parallelized execution layers to run parallelized MEV auctions.
## The knot that ties all together

So, on one hand we have parallelized execution layers, and on the other we have 'parallelized economic layers', if you wish. What connects these two concepts is called *causality*, and it is a very, very hot topic in some research areas at the moment (mainly quantum gravity and other complicated stuff, but I won't dwelve into it). The whole spiel of causality is understanding and describing what does it mean for different events to be related. You can see how in a world where the notion of time changes with respect to the observer (a bit like in distributed systems), or where you can instantly teleport things around, saying 'this event came before this one' becomes meaningless. Instead, we want to characterize the notion of 'causation' in a way that is completely independent from the notion of 'time'. If you like physics, a good, technical read about this is [here](https://arxiv.org/abs/2206.08911).

Tarun is definitely on the right track when he points out in his thread that transactions in a Jito auction form a Directed Acyclic Graph (DAG). That's one way to see it, but we have more powerful tools in our toolbox that we can use.

A world where only sequential composition is possible is mathematically called *category*. This is a term you read multiple times on this blog, but in a nutshell it means that you have processes changing state, represented as boxes:

![A graphical depiction of some processes and states.](/assetsPosts/2024-01-05-correctly-pricing-txs-parallel/Boxes.png)

And that processes can be piped together in the obvious way (here $g$ acts on the state changed by $f$):

![A graphical depiction of process composition.](/assetsPosts/2024-01-05-correctly-pricing-txs-parallel/BoxesMerged.png)

From a very high-level perspective, categories are generated by graphs, which are the underlying topologies of *finite state machines*. Indeed, as you will probably know, a finite state machine has just one monolithic state, which you operate on sequentially.
Here causality is really boring, as 'there is not much to do', and the ordering between transactions is fully established. Things become much more interesting when we add another dimension, obtaining what is called a *monoidal category*. Here state can be split in multiple components - represented as different wires - and processes can act on different wires at the same time:

![A graphical depiction of processes and states in a monoidal category.](/assetsPosts/2024-01-05-correctly-pricing-txs-parallel/BoxesMonoidal.png)

One of the main tenets of monoidal categories is the [Eckmann-Hilton argument](https://en.wikipedia.org/wiki/Eckmann%E2%80%93Hilton_argument), which essentially says that 'two processes modifying non overlapping threads have no causal relation'. Graphically, this means we can slide these processes past to each other:

![A graphical depiction of the Eckmann-Hilton argument.](/assetsPosts/2024-01-05-correctly-pricing-txs-parallel/EckmannHilton.png)


Monoidal categories in their most general form are generated by *hypergraphs*, which - [modulo some very deep but boring caveats](https://arxiv.org/abs/2101.04238) - are the underlying topologies of [Petri nets](https://core.ac.uk/download/pdf/82342688.pdf), a very famous model of concurrent systems.

An example of how a parallel computation in a Petri net is expressed in terms of monoidal categories can be seen below from the following picture from [a paper of ours](https://arxiv.org/abs/2101.09100):

![Graphical depiction of how computations in a Petri net correspond to string diagrams in a monoidal category.](/assetsPosts/2024-01-05-correctly-pricing-txs-parallel/PetriNetExecution.png)

As you can see, in the net you can fire $t$ and $u$ independently, and indeed the corresponding $t$ and $u$ processes in the 'string diagram' can be slid one past the other. On the other hand, you can decide if you want $v$ to use the state that was already in $p_2$ or if you want it to use the one generated by $t$ (as we do above). The two choices originate different causal structures (different DAGs in Tarun's language) and generate different diagrams that cannot be homotopically deformed one into the other.

### And so parallel computation and parallel auctions can be independent

Going back to our point, causality is what enables parallelization **both** at the computational and economic layer: Causally independent transactions - as $t$ and $u$ above - can be executed on different cores. Similarly, they do not compete for ordering in a block, and can be put into different auctions.
But you see, these two facts are **not** mutually related: Their are both caused by having causally-independent transactions (yes, pun intended and yes, this is higher-level causality!).

Indeed, in the (currently) non-parallel EVM we could definitely split the block not only horizontally - as in the top of the block/bottom of the block paradigm - but also vertically, allowing for multiple transactions to be top of the block and by running multiple auctions in parallel. Granted, this split only exists at 'MEV time' and the block will be fully sequentialized at 'executon time', but then again this does not matter because of the Eckmann-Hilton argument before: You can sequentialize in any ordering you want!

Obvously, in a world where:
- There are mempools, and
- Execution is parallel

Stitching the (parallel) execution and the (parallel) MEV/auction layer together becomes more important, because obviously you'd like incentives to be aligned. You don't want block builders to cherry pick transactions in a way that reduces the usage of computational parallelism, do you?
...It goes without saying that if you're working on parallelizing the EVM and you never thought about the corresponding MEV/gas pricing mechanisms our phone lines are open!

### Welcome to the Jungle

The causality story runs really deep and there are much more complex forms of causality that we already have the tools to investigate. Interesting, the parallel 'maths - physics - models of computation - economics' keeps holding. Just to give an example, in Physics you have **teleportation**, where processes can go 'from the future to the past', yanking the causal flow of things. The computational analogue, which is probably a bit easier to grasp, is an extension of Petri nets where you can do state borrowing and error correction. In the picture below, again taken from
[a paper of ours](https://arxiv.org/abs/1805.05988), the transition $\nu$ needs a state that hasn't been created yet. To proceed, it creates this state together with a 'debt' (represented by the red token). Now $\nu$ and $\tau$ can be computed in parallel. The consistency of the whole system is restored when $\mu$ is fired, providing the state $\nu$ requested and 'annihilating' the debt (yes, 'annihilation' as in particle-antiparticle annihilation is not a coincidence here).

![Graphical depiction of how extended Petri net executions correspond to string diagrams in a compact closed category.](/assetsPosts/2024-01-05-correctly-pricing-txs-parallel/PetriConflict.png)

By 'yanking' the diagram, one can see that the causal flow of this computation is equivalent to firing $\tau$, then $\mu$, then $\nu$. These advanced forms of causality may be useful in intentland, where maybe we want to solve different intents 'in parallel' and then stitch them together afterwards.

So should you hear about 'self-healing' or 'error correcting' parallel EVM in the future, let it be know that we squatted these terms first! :P

Ok, we hope we weren't too late to the party. Stay tuned for more updates!
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 158 additions & 0 deletions assetsPosts/2024-01-05-correctly-pricing-txs-parallel/tex/Boxes.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@

\documentclass{standalone}
\usepackage{tikz}
\usepackage{amsfonts}

%
\usepackage{tikz} %
\usetikzlibrary{
petri, %
backgrounds, %
arrows, %
positioning, %
decorations.markings, %
calc, %
fit, %
}
\tikzstyle{inarrow}=[->, >=stealth, shorten >=.03cm,line width=0.5]
\tikzstyle{outarrow}=[<-, >=stealth, shorten <=.03cm,line width=1.5]




%
\tikzset{ %
oriented WD/.style={%
every to/.style={
out=0,in=180,draw
},
label/.style={
font=\everymath\expandafter{\the\everymath\scriptstyle},
inner sep=0pt,
node distance=2pt and -2pt
},
semithick,
node distance=1 and 1,
decoration={
markings, mark=at position \stringdecpos with \stringdec
},
ar/.style={
postaction={decorate}
},
execute at begin picture={
\tikzset{
x=\bbx, y=\bby,
every fit/.style={
inner xsep=\bbx, inner ysep=\bby
}
}
}
},
string decoration/.store in=\stringdec,
string decoration={
\arrow{stealth};
},
string decoration pos/.store in=\stringdecpos,
string decoration pos=.7,
bbx/.store in=\bbx,
bbx = 1.5cm,
bby/.store in=\bby,
bby = 1.5ex,
bb port sep/.store in=\bbportsep,
bb port sep=1.5,
bb port length/.store in=\bbportlen,
bb port length=4pt,
bb penetrate/.store in=\bbpenetrate,
bb penetrate=0,
bb min width/.store in=\bbminwidth,
bb min width=1cm,
bb rounded corners/.store in=\bbcorners,
bb rounded corners=2pt,
bb small/.style={
bb port sep=1,
bb port length=2.5pt,
bbx=.4cm, bb min width=.4cm,
bby=.7ex
},
bb medium/.style={
bb port sep=1,
bb port length=2.5pt,
bbx=.4cm,
bb min width=.4cm,
bby=.9ex
},
bb/.code 2 args={%
\pgfmathsetlengthmacro{\bbheight}{\bbportsep * (max(#1,#2)+1) * \bby}
\pgfkeysalso{
draw,
minimum height=\bbheight,
minimum width=\bbminwidth,
outer sep=0pt,
rounded corners=\bbcorners,
thick,
prefix after command={
\pgfextra{\let\fixname\tikzlastnode}
},
append after command={
\pgfextra{
\draw
\ifnum #1=0
{}
\else
foreach \i in {1,...,#1} {
($(\fixname.north west)!{\i/(#1+1)}!(\fixname.south west)$) +(-
\bbportlen,0)
coordinate (\fixname_in\i) -- +(\bbpenetrate,0) coordinate (\fixname_in\i')
}
\fi
%
\ifnum
#2=0{}
\else
foreach \i in {1,...,#2} {
($(\fixname.north east)!{\i/(#2+1)}!(\fixname.south east)$) +(-
\bbpenetrate,0)
coordinate (\fixname_out\i') -- +(\bbportlen,0) coordinate (\fixname_out\i)
}
\fi;
}
}
}
},
bb name/.style={
append after command={
\pgfextra{
\node[anchor=north] at (\fixname.north) {#1}
;}
}
}
}

\begin{document}

\scalebox{3}{
\begin{tikzpicture}[oriented WD, bb port length=10pt]
%
\node[bb={1}{1}, fill={black!20}] (X1) {$f$};
%
%
\draw[label]
node [left=2pt of X1_in1] {$A$}
node [right=2pt of X1_out1] {$B$}
;
%
\begin{scope}[xshift=10em]
\node[bb={1}{1}, fill={black!20}] (X2) {$g$};
%
%
\draw[label]
node [left=2pt of X2_in1] {$B$}
node [right=2pt of X2_out1] {$C$}
;
\end{scope}

\end{tikzpicture}
}{}


\end{document}
Loading

0 comments on commit 54db0af

Please sign in to comment.