New Ethereum talks, every Monday. The week's conference uploads by event, in your inbox.

L1SLOAD in Action: Write L2 Dapps that Read from L1 State

Devcon 7 SEATue, Nov 12, 2024, 08:30 AM · 1:24:10

In this workshop we will explore some interesting new use cases unlocked by the newly proposed L1SLOAD precompile (RIP-7728). We will develop and deploy L2 dapps that read from L1 state using this precompile.

Transcript

Peter Slevin, Project Manager, Scrawl You will need some brain cells to follow along. Although, overall, it's going to be kind of a beginner-friendly workshop. We're going to do kind of a presentation first and then some Solidity coding. If you have some basic experience with Solidity, with smart contracts, or if you're just, you know, comfortable just reading code, then you should be able to follow along. So, yeah, my name is Peter.

I'm a protocol engineer at scroll. And today I won't talk about scroll per se, but I will talk about a new proposal that we proposed called RIP7728 L1S load. So this is a new proposal that scroll kind of implemented in a DevNet, but we hope that this will be adopted by multiple L2s in the future. So the hope is that after this initial 20-minute presentation, you kind of get the idea, like what is L1S load, what can you do with this, why should you care about it. And then for the remainder, myself and my friend Arash will do like a solidity kind of workshop.

So we have two hands-on examples and exercises for you. Okay, so now you can hear me. I haven't said anything important yet. So yeah, let's kick off. Oh, by the way, before we kick off, if you have any questions, feel free to interrupt me.

But also, there is this Q&A function here in the app. So you can just click here. I think most people probably don't know about it. I don't think it's widely used. Feel free to submit any questions here as well.

I will keep an eye on this. All right. So L1Sloads is a new proposal for rollups for L2s. It's a precompile that allows developers to build applications that read L1 state from their L2 contracts. We hope that this is a new functionality.

It's not groundbreaking. This is something, if you're very technical or if you take on the cost, you can already do in your depth. But this proposal makes it much, much easier. And we hope that this will unlock a lot of new applications. So I want to talk about what is L1Sload.

Can I show you the interface, how to use it, what are some of the use cases that we found so far. Then I want to get a bit more philosophical, like why do we want to offer this service on the Proko level? And finally, just some insights into kind of the Proko engineering part and the challenges of designing such a proposal. There's going to be another talk on Thursday where I will jump into much more details about these ProQo level considerations. Alright, so what is L1 S-load?

So L1 S-load, as I mentioned, is a pre-compile. If you're familiar with the EVM, you know that there's the EVM bytecode that you execute, but there's also special contracts called pre-compiles that implant certain functionality. On Ethereum, on layer one, this is usually like heavy, you know, expensive cryptographic operations that will be very expensive to implant in Solidity. But we can also use the same mechanism to expose other functionalities to L2 smart contracts. So L1S load is a pre-compile and you can call it from your contract, just normal solidity code and you can read states directly from layer one which is Ethereum.

This is how it looks. How many of you are familiar with solidity or has written solidity? So I think you should be able to follow along. Basically this is just a very intro example. The way you would interact with any precompile including L1S load is basically you can just issue a call to it like a smart contract call.

Here we use a lower level call called static call and you just encode the payload. And with this proposal, the payload is simply a contract address, an L1 contract address, and one, two, or more storage keys. You can call it from your contract, and then you can decode the result. So this is actually fetching data not from the local blockchain that you're running on, which might be scroll, it might be Optimism, it might be Arbitrum, any L2 that implements this proposal. Instead of that, it's fetching data from Ethereum L1 under the hood.

So this is defined in an RIP. I'm not sure if any of you are familiar with this. So EIP, Ethereum Improvement Proposals, are used to propose new features for the base layer for Ethereum. And there's kind of a new initiative this year called RIP, which is a similar process for rollups. So a rollup improvement proposal.

It's optional, so we can create these proposals. If you guys find it useful, then hopefully more and more rollups will adopt this. Yeah, so just more details here. Why do we care about this? Why is it useful?

This is kind of, as you say, it's kind of low level just looking at the previous slide code example. It's kind of hard to see what it's used for. Luckily, we've already run a couple of hackathons and hackers came up with some great use case ideas in the past few months. And I think we have a similar hackathon just after DevCon. One of my favorite use case examples was a cross-layer Tornado Cache bridge.

So many of you might be familiar with Tornado Cache. It's a shielded pool. Basically, you can deposit your assets and then withdraw to another address, and you cannot correlate the depositor and withdraw address. So it's a privacy-preserving mixing pool. You could only use it on L1, but you could use the same technology to deposit on L1 and redraw on L2, and this could be built using L1 S-load.

Another use case is ENS. ENS is still primarily used on Layer 1, although they are supporting more and more L2s or deploying on L2s. But if you're on a chain that doesn't have ENS, then through L1S loads, you can still resolve these domains just by reading the register contents from L1. There's also cross-layer reputation system incorporating data both from L1 and L2 chains. There's a bunch of DeFi application use cases.

So for instance, you could borrow on L2 without bridging the collateral to L2, just using your collateral on L1. That's an idea that they explored. You could also implement these hooks for Uniswap to make sure that the L2 pool has enough liquidity and not too high slippage compared to the L1 pool. So kind of give more assurances to you when you swap. And you can also use other, you know, multi-sig, A wallet, key store use cases with this.

So these are just some highlights and I linked all of them here. You can check the slides if you're interested. These are very interesting projects that we've seen at hackathons. All right, so now you kind of have a general idea of what is L1S load. So let me jump into kind of why we want to provide this function on the protocol level.

Wanted to give a quick walkthrough of rollups, but I think many of you were here for the previous session by Emily. And I prepared two slides, And Emily did like 50 slides. And she did a great job kind of telling all of you what rollups are. So I'm just going to keep it very short. So a rollup is an Ethereum that's inheriting a lot of security guarantees from L1, which is Ethereum in the case of rollups.

So rollup create an L2 chain, and then package these L2 blocks into blobs, post it on Ethereum, and also post the execution results, which is the L2 state route. And then there's different mechanisms how you can verify the L2 state route towards a canonical bridge. So you can use fraud proofs, you can use ZK proofs. There's different pros and cons. So if you think about information relay between L1 and L2, when you relay information from L1, you need to usually use a deposit message or some kind of other message in a canonical bridge, and this message will be relayed by the sequencer in a verifiable way.

So usually the sequencer would include these deposit messages where you can encode additional information, but the sequencer could also implement other functionality, for instance, relaying the L1 block hashes. The other way around, you usually need to first submit a withdrawal message on the rollup. And then once this is finalized, you need to claim it on the L1 side. So slightly different ways, depending on which direction you want to go. All right, so if you're a dApp developer and you want to read some data from L2 on L1, how do you do that?

Well you're lucky because you already have the L2 state root on L1. That's kind of an inherent property of all rollups that the L2 state root is posted to L1. So your contract can just read the state root and provide a Merkle proof, and then you can verify if I'll be read any state from L2. The other way is basically the same thing, but it's a bit trickier because L2s usually don't have this notion of the L1 state truth. So we would have to implement that first, but there's ways around this to implement it.

And then it's the same thing, you provide the Merkle proof to read from L1. So it is already, without this proposal, it's possible to do it. But it's not very convenient. Like, how do you relay the L1 stage truth? How do you verify it?

How do you provide this Merkle proof? It's a lot of extra complexity and overhead for the developer, as well as cost. So the idea here is all of this process, the second part, how to read L1 state from L2, is something that the sequencer could provide you as a service. So hide all the complexity from the developers, and you just focus on your application logic and just interact with this contract. So I would say it's Merkle-proofed as a sequencer service.

That's one way to think about it. If you think about this, how it's implanted by the sequencer, basically any call to this precompile is translated to an RPC call to an L1 node. So if you're familiar with the JSON RPC of Ethereum, there's one called eth getStorageAt. That's basically what this precompile corresponds to. And then the tricky part is, how do you verify it?

So we also need to make sure either in the fraud proof or in the ZK verification proof that you also verify this, that the sequencer didn't cheat you. The sequencer actually relates the correct message. As an aside, many of you might not know, like, why do we use a precompile? Why don't we use an opcode, for instance? It's mainly because of compatibility.

So we want to maintain compatibility with existing tooling, with the EVM, you know, Solidity Compiler. If you add the new opcode, then none of the tooling knows about it. But if you add the new precompile, then most of the tooling can interact with it as if it were, you know, a normal Solidity contract. So it's a much less invasive way to add functionalities that Ethereum might not have. Just a little bit more details about kind of the implementation, because as myself, protocol developers for rollups and other rollup teams, you need to think very hard about how do you implement it, what are the possible attack factors that this could open up, how do you verify the inclusion, et cetera.

So the interface of the contract that you will also see in more detail in the Solidity example is we try to keep it as simple as possible. So basically, you just need to pass an address, and then one to up to five in the current spec storage key. So you can read up to five storage values from L1 in a single call to this precompile. And then the result is also just standard storage slots. If you're familiar with storage layout, then you know what this means.

If you're not, then Rx will tell you all about it in a couple of minutes. It's just returned to you, and then you can decode it. Implementation-wise, there's two main prerequisites that might be a bit controversial. So first, I briefly mentioned that you need to have this notion of what is the latest L1 block hash. Your L2 needs to have this notion.

So either the sequencer needs to relay this with a special system transaction, or you need to have some other mechanism. But all the L2 nodes must agree on this. Otherwise, your execution would diverge. So I would call this feature trustless L1 block hash relay. Some rollups already have this.

Others have designs for this, but not launched yet. In scroll, the way it would work, and this might be different or similar to other rollups, is the sequencer optimistically relays this information. But any other node can verify it at this point. So if the sequencer cheats, then it's easy to detect. And when we finalize the batch with the zkproof, that's when we verify that the sequencer actually relates the correct block hash.

Because on L1, during finalization, we have access to this information. And the second prerequisite is that for reading L1 states, you need to have access to an L1 node, right? Which is already true for Sequencer. So Sequencer has to process these bridge messages. So they already connect to an L1 node.

But currently, for some L2s, if you run a follower node, you don't need to connect to an L1 node. Now, if you adopt L1S load proposal, then you must always connect to an L1 node. So that might be an additional overhead for some node operators. But I would argue that this is kind of acceptable. So all L2 nodes then must connect to an L1 node so that they can serve these requests about I1 state.

And then just two more notes about implementation. So we have a reference implementation. I have a link to this in the slides if you're interested in our Go Ethereum fork, but basically

Automatic transcript — names and jargon may be misspelled.