OVM Deep Dive

TL;DR–We built the OVM: a fully-featured, EVM-compliant execution environment designed for Layer 2 systems. This post explains how the OVM can enable rollups that are are identical to the Ethereum main chain.

Ethereum Optimism
May 5, 2020 · 7 min read

Why build the OVM?

Many of our team previously worked to design the first generalized plasma construction with support for contracts–plapps! However, plapps required entirely new developer tooling involving limited “predicate” contracts. We quickly realized that for many this wouldn’t cut it: Ethereum L2 doesn’t just mean using Ethereum to scale: it means scaling Ethereum itself.

This eventually led us to develop Optimistic Rollup, the first L2 construction which promised to bring the full feature-set of Ethereum smart contracts to the scalability landscape. Unipig.exchange first demonstrated this unprecedented functionality: for the first time, Uniswap was on L2. However–Unipig still required that we write a custom smart contract designed for the L2 chain to harness the power of rollup. To preserve the developer experience, we needed better.

What is the OVM?

The OVM is a fully-featured, EVM-compatible execution environment built for use in Layer 2 systems. It allows us to implement a rollup chain which looks, feels, and acts just like the Ethereum main chain–you write contracts in Solidity, and interact with the chain via the Web3 API!

With the OVM, the decision to move a dApp onto L2 is no longer architectural–it’s simply a matter of deployment. Nothing else about building the dApp changes, including tight coupling and composability: new smart contracts can be deployed to an OVM chain at-will. In other words, the money legos still fit like a charm.

So, how does this magic work? And why was the OVM difficult to accomplish? Let’s dive in!

Problem Statement: EVM-in-EVM

The basis for all optimistic L2 schemes is disputes: from plasma to rollups, the key property is “optimistic execution”. Basically, one person (or people) can claim “hey there L1, no need to execute these transactions–the result is X!” If the result is not X, other parties may pay to execute the transactions on the main chain to prove them wrong.

Optimistic execution scales because L2 transactions can be replayed on L1 — but only when necessary!

In the happy case, there is no reason to run transactions on chain–this is why optimistic execution scales overall throughput. However, in the sad case (for example, tx2 above) we still need to be able to play back transactions, or it won’t be secure!

The custom code that was written for Unipig was basically a Uniswap version of execute_L2_tx(). For Unipig, you could even call it execute_uniswap_tx()!

Of course, what we really want is an execute_EVM_tx() function, which would allow us to run any generic L2 Ethereum transaction inside the L1 fraud proof transaction! As it turns out, nesting Ethereum transactions within each other is incredibly tricky–especially when the L2 transaction wasn’t even meant for the L1 chain in the first place!

transactinception.jpg

Why EVM-in-EVM is hard

But before we dive into the unique solution we have built–the OVM–why is this even a problem? Isn’t the EVM the perfect place to run EVM transactions? It is the EVM, after all!

Naive solution: redeploy L2 contracts to L1

At its core, the EVM defines a set of computer instructions, and what each of those instructions should do during a transaction. A big ugly collection of these instructions together is called a smart contract. For example, here is a small sample of the instructions that the Solidity SafeMath.sol library compiles to before being deployed:

A smart contract is just several thousand of these!

If we want to run an L2 transaction on L1, it would make sense that we need to get the code — AKA the smart contract — used by the L2 transaction onto L1. The simplest way to do this would just be to redeploy the contract on L1!

This would be a naive attempt to execute L2 transactions on L1.

Why it doesn’t work: different chains, different results

In some cases, this approach actually would work. For example, the SafeMath library is a simple contract which basically just performs math operations: add(), subtract(), and so on. If we redeployed an L2 SafeMath contract onto L1, it would work just the same as on L2! After all, adding is adding, no matter what chain it occurs on.

However, for other contracts, things quickly fall apart. For example, consider this simple contract which returns the current Ethereum timestamp plus 42:

contract TimeShifter {
function getShiftedTime() returns(uint) {
return block.timestamp + 42;
}
}

Will this contract return the same result when re-deployed on L1 for a fraud proof?

Different chains, different results.

Clearly not! In fact, it doesn’t even return the same result between two different L1 blocks! This is because the re-deployed contract gets the L1 timestamp–but we want it to return the L2 timestamp if we are to correctly execute_l2_tx!

If you think through more examples, you’ll quickly realize that this is an issue for pretty much all smart contracts. For example, imagine an ERC20–when you redeploy the contract on L1, how do you set all the balances to what they were on L2? The list goes on and on.

Solution: The OVM

Historically proposed solutions to the EVM-in-EVM problem have taken two approaches: either fork the VM itself, or just bite the bullet and re-write an entire EVM implementation in Solidity. The OVM is a new approach to the problem which is more performant and flexible–and works on Eth 1 today, no forking needed!

Containerization: The Execution Manager

At its core, the OVM solves this issue is by creating a new smart contract–we call it the “Execution Manager”–which acts as virtual container for OVM contracts. The Execution Manager virtualizes everything which might lead to a difference in execution between L1 and L2, including:

  • Contract storage
  • Transaction “context” such as block number, timestamp, tx.origin, etc.
  • Cross-contract message routing

Basically, for any functionality of the EVM which might exhibit some difference between L1 and L2, the Execution Manager provides a function which makes them consistent between L1 and L2.

As a toy example to demonstrate how it works, we can construct a container solving the timestamp issue above, as follows:

contract TimestampManager {
uint storage ovmTimestamp;

function setOvmTimestamp(number: uint) {
ovmTimestamp = number;
}

function getOvmTimestamp() public returns(uint) {
return ovmTimestamp;
}
}

We can now re-create the smart contract above–but this time, using the container:

contract OvmTimeShifter {
function getShiftedTime() returns(uint) {
return timestampManager.getOvmTimestamp() + 42;
}
}

Now, we can set the container’s “virtual block number” on L1 during a fraud proof, to guarantee the right value is returned!

A new TimeShifter, with TimestampManager serving as its container

This is the essence of how the OVM enables EVM-in-EVM execution: by virtualizing all components of the EVM that might be different between chains! In fact, there are about 15 Ethereum instructions which must be virtualized. You can see the real Execution Manager, which has them all, here!

Safety: The Purity Checker

Note that we did have to modify the contract above slightly, to make a call to the container instead of block.timestamp. This fixed the discrepancy–but only for that particular contract! For L2 to be secure, we need to be sure all L2 contracts use the timestamp container instead of block.timestamp.

To enforce this, the OVM includes a “purity checker”–this is a function which can check to make sure that a given smart contract only accesses the virtualized instructions through the Execution Manager–no block.timestamp allowed! Any contract which does not meet this requirement is blocked from being deployed to the OVM, enforcing that the L2 chain stays safe, even with arbitrary contracts deployed on top!

Developer Experience: The Transpiler

The other challenge with requiring that contracts only call the Execution Manager is developer experience — who wants to go through their contracts and replace every instance of block.timestamp with getOvmTimestamp()? To solve this, we built a transpiler, which takes in normal EVM bytecode, and spits back out OVM bytecode which uses the container as described above. This means that, for developers, the OVM is completely invisible–just plug our solc-transpiler package into your favorite testing suite like Waffle or Truffle, and it’s off to the races!

Onwards

We think the OVM represents a significant step forward for Ethereum L2, because it allows doesn’t just use Ethereum — it is Ethereum. Being able to move Solidity contracts onto a cheaper and faster solution with just a few lines of code has made us the most excited we’ve been about scaling in some time. If you want to try it for yourself, you can check out our most recent OVM battle test — a port of Synthetix’s complex exchange contracts, running in real time on standard Ethereum tools like The Graph and Burner Wallet — here.

If you’re interested in learning more, you can check out our documentation, code, and get in touch our discord!

Ethereum Optimism Blog

Blogs and musings on optimistic rollup and public goods.