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

RIP-7755: Empowering Cross-Chain Interactions

Devcon 7 SEASat, Nov 16, 2024, 09:44 AM · 1:32:04

Speaker

Cross-chain interactions are becoming essential as Ethereum Layer 2 solutions multiply. RIP-7755 changes the game by trustlessly bridging the gap between L2 chains, allowing new use cases that rely solely on Ethereum and its rollups. In this workshop, we’ll explore RIP-7755 by building a cross-chain NFT minting app, focusing on nested storage proof implementation details to eliminate trust assumptions.

Transcript

Okay. I think we're good to get started. Hello. Welcome to the workshop for RIP 7755. This is a proposed standard for empowering low-level cross-chain calls with minimal trust assumptions.

My name is Jack Chuma. I'm a senior software engineer working in R&D on the base team at Coinbase. And I'm so excited to share with you all a little bit about this project. So a couple of goals for today. First and foremost, I want to promote a deeper understanding of RIP 7755, what exactly it is and how it works.

Second, considerations for adding new chain support in the future, as I foresee that being one of the main opportunities for open source contribution. Number three, one of the main features of the standard is that it minimizes trust assumptions, and that's done via a mechanism called what we're calling nested storage proofs. So I'd like to do a deep dive there and promote a deeper understanding. And then lastly, integration details. So if you're an app developer and you would like to be able to facilitate some kind of cross-chain call between L2s and the Ethereum ecosystem, how would that work if you're integrating with the standard?

So to give you some context and a high-level purpose on where we're coming from here, I included a screenshot here from L2Beat sometime last week. And what it's showing is activity in Ethereum L2s. And more specifically, how it's surged by over 500% in just the last year alone. And that's being spread out over many different networks. So that's why I chose this screenshot specifically.

It shows a handful of networks. This is just a small subset of how many L2s there are already. And that's only just going to continue expanding. And this has been great for scaling Ethereum. But it has caused fragmentation in the ecosystem, where if you're a user and you want to interact with an app that's deployed to a specific chain that you maybe don't have funds on, there are certain hoops that you need to jump through to get funds to the correct location to be able to interact with that application, and that hurts the user experience.

It's a critical problem that has to be solved, and so to solve that problem, we believe that there should be a standard for communication between chains that checks the following three boxes. Is public and decentralized in the spirit of Web3, relies solely on validation data that is trustlessly available on-chain, so minimal trust assumptions, and has built-in flexibility to support any arbitrary message. These three bullet points were the three North Stars that we kept in mind as we developed the RIP-7755 proposal, and we'll dive in now. So because Ethereum L2s post some sort of state representation to a shared execution environment, they are uniquely positioned to solve this problem with minimal trust assumptions. And this is done via a mechanism called nested storage proofs, as I previously mentioned.

This allows us to prove state about one L2 from another L2, even though they don't have a direct line of communication. To understand storage proofs, I think it makes sense to do a quick refresher on Merkle trees. Of course, this is nothing new, but a required prereq to understand storage proofs and how they work under the hood. So as a quick recap here, a Merkle tree is basically a tree data structure where each node is a hash of its direct descendants. And so if you wanted to represent, like in this diagram here, I don't know if you can see my mouse, yeah.

We have these four data blocks, A, B, C, and D. And if you wanted to convert that into a Merkle tree, each one gets hashed, respectively, to create a leaf node for the tree. You group the nodes into groups of two, concatenate them, hash them together. That creates their parent node. And you do that recursively until you reach the root node of the tree.

and effectively what you've done is generated a unique identifier for the entire data set that is just a single hash. So this has a couple of interesting properties, one being that if any of the data blocks changes in any way whatsoever, the root hash is going to change completely. And then this also, like this property, allows us to efficiently prove inclusion or exclusion of data within the larger data set. So in this example, if we wanted to prove that data block A, it was in this larger data set of A, B, C, and D, we first would need a verifier to have trustless access to the root hash that is represented in this root node up here. If that's in place, all we'd have to supply to the verifier is data block A, and then this node of hash of B, and then this node hash of HCHD.

And that data alone is all the verifier would need to recreate the root hash at the top of the tree. So the verifier would then hash A to create this hash A node, combine hash A with hash B, hash that together to create this hash H hb node and then do that again for this this final level to recreate the root node and if that is equivalent to this the stored root node that the verifier already had then that's a successful proof so how does that apply to ethereum storage basically all of ethereum's state is represented as a modified form of that merkle tree data structure called a merkle Patricia try. And the exact details in terms of the differences between the two data structures are out of scope for this talk, but it's basically a handful of optimizations for Ethereum-specific use cases. All we really need to know or care about for this application is that that Merkle-proof paradigm applies here. So for every block in Ethereum, there's a handful of block headers, one of which being a state root.

This is the root hash for a Merkle-Patricia try for all of Ethereum state, where the values in that try are Ethereum accounts. These accounts could be EOAs, so like any off-chain wallet, like Coinbase Wallet or Metamask, or it could be smart contract accounts. These accounts, as they're represented in the try, are represented by an array of four pieces of metadata about the account, and that's what I have listed out here. So you have the account nonce, balance, storage route, and code hash. So for smart contract accounts, it's highly likely that they're managing some type of state.

And if they are, that state is stored in that contract storage. And that contract storage is also represented as a Merkle Patricia try under the hood. And you guessed it, the root of that try is the storage root that is stored with the account. So if we have access to a state root for a network, we can supply a path down that state try to a specific account within the network. And then using that account's storage root, which can be extracted from the account's metadata, we can supply another path from the storage root to a specific location in that account's storage.

And that's basically the high-level concept of how a storage proof works. So how does that apply to cross-chain messaging, though? Because that's basically the high-level concept of how a storage proof works. So how does that apply to cross-chain messaging, though? Because that's just proving state within a specific network.

So to explain that, I have this diagram here. It's very simplified, obviously. But this is meant to represent two roll-ups in the Ethereum ecosystem that are both sharing state with a shared L1. So this L1 at the bottom would be like Ethereum mainnet, and then chain A and chain B are two L2 networks. What this diagram is depicting is bidirectional communication between the two layer 2 chains and the shared layer 1.

For this downward arrow direction in both chains, an Ethereum L2 chain wouldn't be an Ethereum L2 chain if it wasn't sharing state with L1 in some context. So that's what this is representing here. It's requiring that both chains are sharing some representation of their state with what I'm calling a roll-up contract on layer one. This could be the state root directly, or it could be some other representation of state, the only requirement being that it has to be verifiably linkable to its state root, at least for the way that RIP 7755 is working thus far. And then in the other direction, we need trustless access to a layer one state representation within the L2 chains as well.

So that's what these upward arrows pointing to the beacon roots Oracle contracts are. This is made possible by an improvement proposal that's live today in many networks called EIP-4788, which trustlessly exposes the most recent 8,191 beacon routes for the Ethereum consensus client within the L2 execution environment. It should be noted a beacon route is not the same as execution client state route, but it is verifiably linkable to the L1 execution client state root via a very similar process. So from chain A, if we were trying to prove something about the state of chain B, this diagram represents everything that needs to be true for that to work. So if chain A starts with a trustless access to a beacon root.

It can supply like a Merkle Patricia try based proof to verify the L1 execution client state route. And then if we have a verified L1 execution client state route, that exact storage proof process that I just went through applies. So we could then prove anything about the state of layer one from chain A. In this context, the first step would be to go from the state route to like a path to an account within that state try, and the account here would be chain B rollup contract. And then using chain B rollup contract storage route, we can supply another path to a specific location in that contract.

What's interesting here is if the value that you're verifying inside of Chain B roll-up contract is itself a state representation for Chain B, you can then recursively follow the same steps again to prove something about Chain B's state try. So then starting from Chain B's state route, you can supply a path to a specific account within Chain B, and then again a path from that account's storage route to a specific storage location. And that effectively allows us to prove verifiably a location and storage in an account on chain B from chain A, even though there's no direct line of communication. So that's cool, but how does that help us with cross-chain calls? This diagram is an overall architecture for how RIP-7755 is set up to work, and that should help answer this question here.

So as you can see, we have two chains represented, an origin and a destination, and then we have both on-chain and off-chain components here. Every supported chain is going to have some sort of inbox and outbox contract that we're calling RIP 7755 inbox and outbox, but I'll just stick with inbox and outbox for the rest of the talk. The outbox contract is basically the entry point into the standard. So if a user wants to request a cross-chain call, they submit a request to the outbox contract. If the request settles properly, it'll emit an event that some off-chain actor that we're calling fulfillers should be listening for.

And if there's sufficient incentive to respond to that request, then the fulfiller will. So that brings us back to a way to incentivize fulfillers to respond to the request. Another key piece of logic that happens here is the user will also lock some kind of reward bounty for the Fulfiller to respond to the request. If the reward bounty is sufficiently like Incentivized to the fufiller, then it will respond. So the fufiller then assuming it's a sufficient incentive Will submit the requested call to the destination chain over Here.

That routes through an RIP 7755 inbox contract which will perform a handful of validation steps mainly confirming that the request is arriving to the correct chain and the correct location at the correct chain at that. There's also this custom like optional validation step called a pre-check contract where this could be absolutely anything as long as it adheres to a a pre-check contract, where this could be absolutely anything, as long as it adheres to a specific pre-check contract interface that the standard requires. And all this is meant to do is allow the user to encode any kind of arbitrary fulfillment condition that should be true in order for the fulfillment to work out. But like I said, again, it's totally optional, so if it's not being used, this step will be skipped. If all the validation steps are sufficiently checked out properly, then the requested calls will be routed from there.

So this could be a batch of any low-level arbitrary call that goes to a handful of addresses with encoded call data and any native currency value that may be included. If all of those are successful, the main purpose of this inbox contract is to then store a receipt of successful execution. And this execution receipt gets stored in a deterministic location within this contract storage that is derivable from the origin chain without knowing anything about the state of the destination chain. So that's important, and we'll come back to it in a second. After the call has been successfully submitted, the fulfiller then comes back to the origin chain to say, hey, I did the job.

Now can I have my payment? And what's unique about this standard is that the payment will only be released if the fulfiller can cryptographically prove that they did actually submit the request to the destination chain. And that's done via that nested storage proof concept that we just walked through. So from the origin chain, the origin chain would have to be able to verify a specific storage location in the inbox contract on destination chain. and because of the fact that the outbox contract can derive exactly where that location is supposed to be, then the outbox contract has everything it needs to be able to verify the successful fulfillment of the call.

So only if this nested storage proof checks out will the outbox contract release the reward to the fulfiller. And that would close the loop on the full process for how RIP-7755 is working. Yeah, so for today's workshop, I have an example project for us to go through together. So if anyone's interested in coding along, there is a starter repository on my GitHub that I can show in a second. If you're not interested in following along, I'll be doing it up here as well, so we can go from there.

So I'm going to start by walking through all of the contracts and services that are present in the starter project. This is going to be a self-contained system that is mocking a multi-roll-up ecosystem that will run locally on your machine. So once you clone the repository, you should have everything needed to run the entire app end-to-end locally. So after a brief walkthrough of all the services and how they're working together to make that work, we'll implement and test a nested storage-approved validation contract, as that is where the bulk of the effort is going to have to be applied to add new chain support in the future. I also just think it's really cool.

And then once that is working properly, we'll integrate with an off-chain client application. So for this demo, it's just a simple NFT mint application where the NFT owner wants to be able to support users who don't necessarily have funds on the chain that the NFT contract lives on. So if they don't, it would be an RIP 7755 request to send the cross-chain call to still mint the NFT. So once that is all set up, we should be good to run the app end-to-end. Before we jump in, I'll leave you, well, I guess I'll leave this presentation piece with that.

It's still very early in the research phase that a lot of these details are subject to change, but we have proven the concept on live networks of this nested storage proof and how that can be used to trustlessly verify cross-chain calls. So this seems to hold a lot of potential. It's something I'm very excited about, something the base team is very excited about. We do have an open source proof of concept repository on the base org github that I fully invite anyone and everyone to contribute to if you have interesting ideas. So with that, we should be good to dive into code, but as a quick gut check, are there any questions before doing so?

And there will be time for questions at the end, too, and after this talk. Okay. So I didn't know the best way to share the link, but my GitHub is my name, Jack Chuma, and I have this DevCon 2024 RIP 7755 workshop project. If anyone's interested in coding along, you can clone this repository and follow along with me. I already have it cloned, so we can start with a brief walkthrough here.

So once you have the project, what you'll notice is there's two main directories. We have contracts and services. So this is on-chain and off-chain components from that architecture diagram. We can start by going through the contracts. I'll start with the NFT contract because this is very simple.

But this is what our demo client is going to be using. The main piece here is this mint function. The only reason I'm covering it is because this is going to be needed to set up the integration when we get there. But there's nothing too interesting happening here. Next up we have a rollups directory and this is used to mock the multi rollup system locally.

I didn't want to have to rely on good internet connection for this to work so we got a mock system running locally. This rollup contract is what would be deployed to a mock L1 and this is what will be storing a state representation for the mock L2s. In this context, that state representation is a hash of the L2 block timestamp and the L2 state root. And then on the L2 side of things, we have this beacon Oracle contract, which is meant to mock the EIP-4788 interface to query one of the beacon roots that are being stored in the L2 execution environment. For this example, it's a simplified example, and this is directly storing the L1 execution client state route.

So we are cutting out a step of verification going from beacon route to state route, but I think it still gets the message across. And then we have all of our RIP 7755 contracts. So as a brief walk through for what an actual request looks like, we have this RIP 7755 structs file. And this is exactly what a request would look like. So all the fields are we start with a requester.

So this is the pretty self-explanatory, the address submitting the request. We have a batch of calls where each call is a low level description of the exact address that you'd like to send the call to, encoded call data, and then any native currency value that should be included with that call. And then we have a specified prover contract. The reason this is here is because there's no standard way for L2 chains to post their state representation to L1. And because of that, the exact implementation details for verifying state about a destination chain will vary depending on what that destination chain is.

So right now we have this set up with the proving logic abstracted in two separate contracts. This very likely will change in the near future. If we baked that into the outbox contract, that would require multiple outbox contracts to be deployed to each chain. So it's a trade-off. But for right now, this is set up to be one outbox, one inbox, and then an array of prover contracts deployed to each chain that the user would have to specify which one should be used to verify fulfillment.

And this contract is what we're going to be implementing in a few minutes. Then we have destination chain ID, which is pretty self-explanatory. Inbox contract is the address of the RIP 7755 inbox contract on the destination chain. L2 Oracle address, that is the address of the RIP 7755 inbox contract on the destination chain. L2 Oracle address, that is the address of the roll-up contract that would get deployed to L1 for the destination chain.

So this is the user specifying where the prover contract should be looking for the state representation for the destination chain when it's verifying that the call was submitted. So the user is specifying the address where that should be located submitted. So the user is specifying the address where that should be located as well as the storage key within that address. And then we have a reward address. This could be an ERC-20 address or as specified by ERC-7528, there's a special address value that can be used to depict native currency.

Then there's reward amount, which is the amount of the reward asset that should be locked within the request. It should be noted that reward amount should cover all of the value that's included in these calls, plus whatever the gas cost would be for submitting the call to the destination chain, plus an extra tip for the fulfiller. And that extra tip is what acts as the incentive for the fulfiller to respond to the request. Then we have finality delay seconds. This is the gap after the call is successfully fulfilled on the destination chain.

That has to pass before the fulfiller is allowed to claim the reward. This is basically like destination chain reorg protection for the user. The higher this delay is, the more likely the destination Chain won't be reorged and ensure that the call was Actually submitted and will stay submitted. And we have a nonce value for ensuring that every request is Unique as the way we're identifying these requests is by Hashing this entire structure. An expiry field for when the Request should expire.

This is relevant for the user being able to reclaim the reward if for whatever reason the call was never submitted to the destination chain. There should be a mechanism for the user to recover those funds, and that's what this expiry timestamp is. If the call's not submitted before the expiry timestamp, then that's when the user can reclaim. And then we have these last two fields for the pre-check contract. So a pre-check contract address and an arbitrary bytes array of encoded data for that pre-check.

This is for that optional, like, arbitrary fulfillment condition that should be true. If this is the zero address, that is depicting that we're not going to use it. And we're not going to be doing a pre-check step in today's demo, but I figure it's worth covering that it's there anyways. So the next step, we'll do a quick walkthrough of the inbox and outbox contracts. So the outbox contract being the entry point to the system.

We have one main function that we really care about here, request a cross-chain call. That's where the user would submit the request and where the event would be admitted that the fulfillers are listening for then we have a claim reward function which is where the fulfiller comes to claim their reward after successfully fulfilling the request and that calls into the prover contract here on this line and so this contract is again what we're about to implement it is expected to revert if the proof fails so this contract is again what we're about to implement. It's expected to revert if the proof fails. So there's no return value or anything here. And then lastly, the cancel request function.

So this is like after the expiry timestamp, if the reward has not been claimed yet, then the user gets to reclaim it. On the destination chain side, we have the inbox contract. The main piece of information we care about here is this fulfillment info struct. So this is that execution receipt that should be created in storage, and that will be the target of the nested storage proof validation. The whole point of the storage proof there is to prove that this struct exists in storage for the specific request.

And this is storing the timestamp at which the request was submitted, is to prove that this struct exists in storage for the specific request. And this is storing the timestamp at which the request was submitted, as well as the filler address that should be able to claim the reward back on the source chain. And that gets created during this fulfill function. So we have all our validation steps up here. And then we route the calls here.

And if everything's successful, we're left with the created fulfillment infostruct. So this contract is fairly simple. Something a little bit more interesting, we have this state validator library. And if you in the future are working on setting up a new prover contract for a new destination chain that's not currently supported by the proposal, you likely would be utilizing the state validator library. The whole point of this is to abstract a lot of the complexity involved with storage proofs away from the developer.

No need to reinvent the wheel every time. So the way that this is set up, there are two main functions that we care about. There's validate state, and then there's validate account storage. Validate state is for if you're starting from the beacon route on L1, and you're trying to verify the L1 execution client state route against that beacon route. That's what validate state would do.

And then from there, using the verified state route, it would then prove storage location for an account within that state. Because our example today is not using beacon routes, we don't need to use this function, but I figured I'd briefly cover it. The function we care about is this validate account storage. So this takes in an account, which is a specific account within the network, a state route for the network, and then a handful of these account proof parameters. And using these account proof parameters, which are a specified storage key, an expected storage value, and then an account proof and a storage proof, the account proof can be thought of as the path down the state try from the state root to the specific account we care about, and then the storage proof would be thought of as a path from that account's storage root to the storage location that exists at storage key and should be storing storage value.

So it should be noted that all of the values in the state try are keyed by the hash of the Ethereum address of that account. So that's what we're doing here. We're deriving the account key, and then using that, we can do a merkle try.get to return an encoded account. An encoded account is basically an encoded array of the account metadata that I went through in the slides earlier that have the nonce, the balance, the storage route, et cetera.

So from that, we can extract the storage route, and then using the storage route, we can verify that storage location using the storage proof that exists in the account proof params struct that I just went through. So at a high level, that's how that's working. There's a directory in here called Provers. This is what we're about to implement, so we'll be coming back to that in a second. Just real quick before we dive into the implementation details there, I want to do just a quick summary of what off-chain services are running here.

The demo is the app that is facilitating the NFT mints, so we'll be implementing some details in this directory. But then these other two directories are surrounding services that are needed for the full system to run locally. The sinker is in charge of sharing state representations bidirectionally between the mock L1 and the mock L2s. And then the fulfiller is the off-chain agent that's listening for requests and will validate the request and ensure that the incentive is enough to compensate them for their time. And we'll respond accordingly and submit the request.

And then we'll be generating a full nested storage proof that gets validated against the contract we're about to implement. And if that checks out, we'll be able to see the fulfiller claiming its rewards in real time. So with all of that being said, we have enough here to dive into beginning to implement a prover contract. I have a handful of imports just set up in here already just to save the time from typing them out. You'll notice the first import is an iProver interface.

So we can start here taking a look. And what it defines is a single function. And this is all the approver contract needs because this is the function that the reward claim function from the outbox contract is going to hit. So we can start by literally just copying this entire thing into our prover contract to initiate the implementation of that function. So I will replace this comment with that function declaration and add empty curly braces here.

So, oh, and then we can extend that interface. This is iProver. To take a quick skim through the comments here that are explaining what validate proof should even be doing. It validates storage proofs and verifies fulfillment. Okay, makes sense.

It should revert if the storage proof is invalid. Also makes sense. It should revert if fulfillment info is not found at inbox contract storage key on the specified inbox contract. That is kind of interesting here. It should be noted that the storage key, like I was saying before, is derivable from a network that doesn't actually have context of the destination chain.

And because of that, this is being done in the outbox contract before this function is hit. So this is not coming from the off-chain fulfiller. We can trust this value. Lastly, it should revert if the fulfillment info timestamp is less than the finality delay seconds amount of time from the current destination chain block timestamp. So this is that destination chain reorg protection that I was mentioning earlier.

We need to ensure that the finality delay seconds is not currently still in progress. Then we have a note about that the implementation should vary by destination L2. This is due to the lack of standardization around how L2s post their state representations to L1, like I was saying. And then a quick summary of the input parameters that we have to work with here. So inbox contract storage key, I just kind of mentioned, is the storage location in the inbox contract on the destination chain where we expect the execution receipt to be.

Next up is the fulfillment info struct. So this is that exact execution receipt that should be existing at inbox contract storage key on the destination chain's inbox contract. Then we have the initial request that came from the user. And then we have an arbitrarily encoded proof data bytes array. This is because of the fact that, like I was saying before, the lack of standardization around the state posting, there could be subtle differences in the exact data that is needed for the prover to verify that state.

So there's no enforced structure to this data at the outbox level. This is being implemented here within the prover. So we'll start to set that up in just a moment. Okay, so with all of that gone through, we have enough here to start to set this up, so we can think about what the steps are for validating a nested storage proof. For starters, we'll want to enforce a structure to proof data, so we can decode this into some defined structs that we'll define in storage up here.

So decode proof data. Next with the decoded proof data, we can use some supplied data to trustlessly access the beacon route from L1. So I'll want to query L1 state representation. In a real network, this likely would be, or if the network is supporting EIP 4788, this would be a beacon route. For today's example, it's the state route directly.

So I'll just make a brief comment explaining that. In real network, likely beacon root in two days demo state root. Okay, so then step three, once we have a state representation for L1, using L1 state root, we'll verify storage location on L1, USING L1 STATE ROUTE, WE'LL VERIFY STORAGE LOCATION ON L1 AND THAT STORAGE LOCATION WILL BE THE DESTINATION CHAINED ROLLUP CONTRACT. SO VER. STEP FOUR, WE'LL NEED TO VERIFIABLY LINK A DESTINATION CHAIN STATE ROUTE TO THAT, OH, YEAH, LET'S SEE, USING L1 STATE ROUTE VERIFY STORAGE LOCATION, THIS SHOULD BE DST CHAIN STATE REP.

WE'LL NEED TO USE THAT VERIFIED VALUE TO LINK A STATE ROUTE FOR We will need to use that verified value to link a state root for the destination chain. So we can do that as a step here. Verify. We link to chain state rep. After that step, we should have a verified state route for the destination chain, so then we can essentially repeat step three again.

So this would be using L2 state route. Verify execution receipt in inbox contract on DST chain. And then lastly the only step that we haven't covered is the this revert statement that's saying if finality delay seconds is still in progress this function should be reverting. So we can check that as our final step. Whoops.

Step six. Revert if finality delay seconds in progress. Okay. So if we can successfully set up these six steps, then we should be good to go to verify these nested storage proofs. If we take a look up here, you might have noticed this contract is expecting to be deployed with an address in the constructor.

This address is for the beacon roots Oracle contract. In a live network, you wouldn't have to do this because theIP 4788 specifies a deterministic precompile address that you could just hard code into your contract storage. But for this to work in both tests and deployed to our local network, I have it being deployed with the address specified here. So as our first step, we can store this as an immutable, and call it beacon Roots oracle. Something like that.

Roots. And then assign that within the Constructor. And then if we start to think about how these steps are working, the first step being that we want to decode proof data into some specified structure that we're going to define, we can start by defining a struct, RIP 7755 proof. And the compiler is going to be mad about the struct being empty, but we'll come back to that in a second. We can set up this first step with that in place by decoding the proof data into a local variable that we can call proof that is adhering to this RIP 7755 proof struct.

So if we copy this, paste it here, this will be in memory, is equal to ABI.decode proof data, and then pass in the name of the struct as the second argument here. This will decode the proof data bytes into whatever structure we define at the top of the file here. So for step two, we want to then query the L1 state representation. This is going to come from that beacon Oracle contract from the rollups directory that I covered briefly.

So if we pull that up to take a look at the storage layout here of how exactly we should query that, this has a fallback function in here, and that's to mimic the interface that would be used to query a beacon root from the real beacon root Oracle contract on a live network. And all this takes is an encoded block timestamp. So that's actually the first piece of data that we need to add into our RIP 7755 proof struct. Is we need to know the L1 block timestamp that we're going to be using for the proof. So we can add that in as a Uint256.

Call it L1 timestamp. And then using that, we can set up a static call into the beacon Oracle contract. If you remember, we have the address stored as the immutable variable here. So we can take this, copy that, and under step two, paste that. That'll be, yeah, beacon roots Oracle.

You do dot static call, which is like a low-level call, but kind of like a view function where it's expected to not mutate any state in the destination address that you're calling. And then we're passing in the encoded block timestamp for the L1 chain, which we just added to proof, so this should be available via abi.encode with proof.L1 timestamp passed in. Whoops.

This static call will return a tuple where the first value is a Boolean, so we can call that bool success. And the second value is bytes array in memory. So bytes, memory, data. With any low-level call in an EVM chain, you need to confirm that success comes back as true. Because if something weird happens with this address or the static call fails for some unexpected reason, and success comes back false, we need to ensure that we revert the transaction here.

So for that case, we can add a custom error at the top of the file for if the static call fails. We can call it error. Beacon roots Oracle call failed. Copy that. And then underneath this line, we can do if not success, revert with that custom error.

So if we get past this if statement, we have a returned Data bytes array where the data is representing an encoded Version of the state route for l1. After this if statement, we can decode data into a bytes 32 State route. This would be bytes 32 l1 State root. It's equal to another abi.D code.

With data Passed in and then the data type is bytes 32. Okay. So that Should do it for step two. So at this point we have an L1 state route that we can then use in a storage proof to verify something about the state of L1. And that's exactly what step three is laying out here.

So using L1 state route we want to verify the storage location in the destination chain's roll up contract ON L1. THIS IS GOING TO BE THE STATE REPRESENTATION FOR THE DESTINATION CHAIN. IN ORDER TO GET MORE CONTEXT FOR HOW THAT SHOULD WORK, LET'S TAKE A LOOK AT THE ROLLUP CONTRACT BECAUSE THAT'S WHERE THE DESTINATION CHAIN WILL BE POSTING ITS STATE REPRESENTATION. hosting its state representation. So that will be inside of this rollup contract.

What's kind of interesting here is in a lot of live networks, the exact storage location that the state representation is going to exist is not necessarily known at the time of request. In that case, the request just knows the storage slot, maybe where the data structure is located. So like in this example, the mapping storing output routes exist at storage slot one. So in this context, the request specifying the destination chain's storage location for their roll-up contract on L1 would just be specifying storage slot one, and then we'd have to take that and derive the location of the value for the mapping based off of the destination chain's L2 block timestamp. So that brings us to the next piece of data that we need for this proof.

It's going to be the block timestamp for the destination chain. So we can add that as another Uint256 instead of the proof. And this can be L2 block timestamp. By the way, for anyone following along, try to stick to the exact names I have here for the fulfiller proof to work properly. The logic by itself should work fine either way, but in order for this to be compatible with the surrounding system, we have to use the correct names.

So if we have the L2 block timestamp here, then we have enough to derive the storage location of the output route associated with that block timestamp inside of the roll-up contract. So how do we do that? If we take a look down here, so for step three, we're going to Be using the l1 state route. This is the first example of an Actual storage proof that we're going to use. This is where we would maybe want to take a look at that State validator library again because this has a bunch of Utility functions for facilitating storage proofs Directly.

Nam namely being this account proof parameter struct. This is going to come in handy. And then that second function I mentioned for validating account storage. So we'll actually be using this function here. This takes in an account, a state root, and then proof parameters that should be provided by the fulfiller.

So we can start to set that up now, starting with state validator. And then dot validate account storage. My autocomplete added the names of the variables here. So for the account, what's the account here? This is the roll-up contract for the destination chain.

And if you remember from the structs file that I walked through at the beginning, one of the fields that is specified by the requester is this L2 Oracle address. And this is exactly the account that we care about for this first storage proof. So we can pass that in here. This is going to come from request, which is being passed in. So this will be request.

L2 Oracle for the account. The state route is the L1 state route that we just decoded here. So we can use this for state route. And then the account proof parameters, this is this account proof parameters struct inside of the state validator. This is going to be supplied by the off-chain fulfiller.

So this is the next piece of information we need in our proof struct. So if we copy this, add it as a third argument in the proof struct or third field. Oops. This will be state validator dot account proof parameters. And we're going to call this one DSTL2 state root proof ramps.

So then we can copy that variable name. And this will be the last argument passed into our validate account storage function. So this will be proof dot and then that copied field name. If you take a look at the validate account storage function here, you can see that it returns a Boolean value. So we need to ensure that the Boolean value returned is true.

So we can capture that in a local variable here. We can call it bool is valid L1 state is equal to the state validator. Validate account storage. And then for when it's not a valid L1 state, we can define a custom error up here. This will be error invalid L1 state.

Copy that. And then if not is valid L1 state, we'll revert with that custom error. And there we go. All right. So what we have happening here is after this step, we should have a verified storage location in L1.

To jog your memory on the params that are being passed in by the fulfiller, there is something kind of fishy happening with the way we currently have it set up. These account proof parameters specify an exact storage location and expected value. So if this checks out and returns true, it means we've successfully validated that location. But what if that location's the wrong location? Like what if it's not the state representation for the destination chain?

That would be a problem. So this is where we'd want to override the storage key. We could either derive what it should be and confirm that it's equivalent, or we could override it. For this example, we'll override it, but maybe there's a subtle gas optimization one way over the other. But for starters, we'll want to create a helper function for deriving what the L1 storage key should be.

So we can create that down here as a private helper function. Derive L1 storage key. This will be a private peer returns bytes memory. Because as you see up here the storage key is a byte string not a bytes 32 or anything. So yeah.

So this is the storage key of where the state representation should be in the roll-up contract on L1. So we can take a look at this roll-up contract again to see how that storage layout is set up. We have a mapping located at the first storage slot, which likely is going to be the inside of the structs file. This likely is going to be the L2 Oracle storage key. So I would expect this to be the bytes 32 representation of the number 1.

And then we can take that and hash that with the L2 block timestamp, which under the hood is what Solidity is doing to generate the storage location for the value that should be keyed by the block time stamp. That's exactly what we can recreate here. In order to do that, we need a couple of pieces of information Here. One of them is the l2 oracle Storage key which comes from the request. We can pass in the request here.

Copy this whole thing. Pass that in. And then the other piece of information is the L2 block time stamp, which if you'll remember we added to the proof struct up here. So we can pass that in as well. So if we copy this declaration, we can paste that here as a second input argument.

And then, so what does this return? So this is going to return a derived value for the storage key where the L2's output route should exist. That is going to use, so we'll return an ABI.encode pact. With the block timestamp passed in.

So this is going to be proof.L2 block timestamp. And then the storage key location. So then it's going to be request.L2 Oracle storage key.

So this concatenates them together into a single bytes array. We now need to hash this to generate the storage key. This concatenates them together into a single bytes array. We need to hash this to generate the storage key. Wrap that whole thing in a catch act 256 hash function.

And then one final step here because catch act 256 returns a bytes 32 and we need this to return bytes memory. We have to wrap this in one more abi. Encode. And that should be all that we need to derive the storage Location for the l1 storage key. Now we can close these and use This function to overwrite the storage key that gets passed Into this validation step.

So this will be proof.dst L2 state root proof params dot storage key is equal to derive L1 storage key where we pass in the two input parameters of request and proof. Okay, sweet. So at this point we now have a verified value in the rollup contract for the destination chain on L1. We confirmed that it is in the correct location so we can trust that it's the proper state representation.

And then we can move on from there. So the next step here is to verifiably link the destination chain state route to the destination chain state representation. If the destination chain is directly posting their state route, this is unnecessary. We just need to make sure that we have a verified state route here. So in this example, because we're not directly posting that, it's a hash of block timestamp and state root.

We need to recreate what this output root should be. So we can re-derive that with bytes 32 for a local variable. This will be derived L2 output Root is equal to cat check 256. And what gets passed in here? We're basically just recreating this line over here in the roll up contract.

So this is going to be an ABI.encode pact. And inside of the ABI.encode pact, we want to pass in the block time stamp and a state route. But at this point, we don't have a state route to use.

So that would be the perfect, now is the perfect time to add that as the next field in our RIP 7755 proof struct. We would expect the fulfiller in this case to supply what the destination chain's state root is, and then we can use that to re-derive the state representation that was verified using the first storage proof, and if they're equivalent, then we can trust the passed in L2 state root. So this will be a bytes 32. L2 state root. And we need to pass these Arguments in in the same order they're passed in over here.

So we'd start with the L2 block time stamp. This will be proof.L2 block time stamp. And then we want to pass in the state root. So proof.

L2 state root. Okay. So now at this point we have a re-derived output root for the destination chain. In the case that this doesn't equal the value we just verified, we'll create another custom error for that to revert in that case. So we can call that an error invalid L2 state root.

Copy that. And then we need to compare this to the storage value that we verified in this step. So that'll be if derived L2 output root does not equal proof.dst L2 state root proof params.storage value.

Then we revert with that custom error. And this is yelling at me because the storage value is a bytes string and this is a bytes 32. Because we would be expecting them to be equivalent we can wrap this in a bytes 32 for the type safe properties of solidity. One other thing I'm noticing here, this is kind of just like a personal preference for me, but the state validator library up here, because we're using it on a specific account, for solidity purposes, we can actually bind that library to the account to just improve the legibility of this line, make it a little bit more succinct. So I'm going to do that, but that's totally just a personal preference.

So we would add a line at the top of the prover contract that says using state validator for address. And then what that allows us to do is to copy this request.L2 oracle and remove it from the function call and then paste it here instead of state validator. And then this is doing the same thing, but it's just a little bit shorter. OK.

So at this point, we have a verified destination chain state route. Now we can use that to basically redo step three, but now the account we care about is the inbox contract on the destination chain. So what that's going to look like is a bool is valid L2 state is equal to, and then to jog your memory again on the structure of a request, we actually have the inbox contract being defined by the user when they submit the request. So this address is going to be the address that we're verifying state against. This will be request.

inboxcontract. Verify, what's it called again? Validate account storage. And in here we need to pass in a state root and another instance of the proof params. So the state root here is passed in from the proof struct and at this point we've verified that this can be trusted.

So we'll use that value. So this will be proof.L2 state root. And then we need to add another instance of those proof params This time for the inbox contract on the destination chain. So we can duplicate this line and instead of calling it dst L2 state proof params we can call it dst l2 account proof Params.

And copy that field name down Here for the second storage proof. We can pass that in as the second argument. So this looks like proof dot that destination account proof params field. And then for the case again where is valid L2 state comes back false, we need a custom error to throw as a reversion in that case. So we can define that up here.

Error invalid L2 state. Copy that. And this will be so if not is valid L2 state, revert with that custom error. Cool. And then so if you remember from the first storage proof, we had an issue trusting the storage key that gets passed in from the fulfiller.

You can assume that we have the same issue in the second storage proof, and we do. Luckily, it's a little bit easier to solve on this side of things because, like I said at the beginning of the walkthrough here, the outbox contract rederives where that storage key location should be already, and that gets passed in. So we can just reassign the storage key value with this passed in inbox contract storage key. We'll do that above step five. So this will be proof.

dstl2 account proof params this time dot storage key is equal to inbox contract storage key. And what that's going to do is ensure that we're verifying against the correct storage location where the execution receipt should exist on the inbox contract on the destination chain. So at this point, we have a fully verifiable proof. The last step is we just have to confirm that finality delay seconds is not still in progress. We have the receipt being passed in here, so we can use that for this check.

And for when it is still in progress, we can add a custom error at the top. This will be our last error. So this will be error, finality, delay seconds in progress. Copy that. And then down here at the bottom of this function, if fulfillment info dot timestamp plus request dot finality delay seconds is greater than, if you'll remember for the proof, we defined the L2 block timestamp up here.

This is exactly what we need to use for this timestamp-based protection. So we can do proof.L2 block timestamp. If the timestamp at which it was submitted plus the configured finality delay seconds is greater than the time that we're using for this proof, then we can't accept this proof because finality delay seconds is still in progress. So that's what this line is doing.

So we revert with that new custom error, finality delay seconds in progress. And then cool. That's all of our steps. But there is one final piece of data connection that we're forgetting here. And that is stemming because of, well, we confirmed a specific storage location in the inbox contract on the destination chain.

And then we confirmed a passed in receipt satisfies that finality delay seconds requirement, but we did not confirm that that receipt that we're using for this final check is the same value as the one that we just verified. So we need to make sure that the storage value for the second storage proof is equal to the encoded execution receipt that we're using for this timestamp check in the last step. And so that represents the final piece that we still have to set up to secure this thing. We can set up the encoding of the fulfillment InfoStruct as a separate private helper function. So this will be, this will be, what do we want to call that?

Encode. Fulfillment info. Okay. So for encoding fulfillment info, we need to pass in the struct that we're using for the validation up here. And inside of this, it's a simple ABI.

encode packed. So return ABI.encode packed. With fulfillment info.filler and then fulfill info.

timestamp passed in. This is because of the struct packing rules and solidity storage we have to custom encode this struct here. If I show you the definition of the struct in the inbox contract again, we see that it has two fields, timestamp and filler. The timestamp is a Uint 96 and the filler is an address, so this can get packed into a single Uint256 slot. But the way that it gets packed or the ordering that it gets packed is in the order of the defined fields, and it packs them into the lowest value bits first.

So the timestamp actually ends up on the right side of the storage slot, and then the filler is on the left. So in order to recreate that alignment, we have to use an ABI.encode pact here where we pass in the fields in reverse order instead of just doing ABI.encode and passing in the entire struct. We would have it in reverse order in that case.

So using this function now, we can override the storage value for this final storage proof. So this will be proof.dstl2accountproofparams.storagevalue is equal to encode fulfillment info. And we pass in fulfillment info.

Excellent. So that should be our first pass at a full implementation for validating one of these nested storage proofs. We can now see if this is compiling. So if we CD into the contracts directory, can run a forge, well I like to format it properly first. And then we can do a forge build to check if it's compiling.

It is. So now to check this, I have a test file in here with mock data from a working system. It's commented out just to prevent compiler issues with the initial structure of the project. We can uncomment this and run a forge test to make sure we did implement that properly. It will recompile this test file and then okay, cool, it's passing so this is using a previously generated proof um that adheres to the structure that we just set up in that rip 7755 proof struct and um it uses is being proven against a specific storage route or a specific state route that is being assigned here and this commit beacon route function in the beacon Oracle contract.

So because this is passing, it's a good sign that our implementation is working properly. So that wraps up the on-chain implementation piece. At this point, we should be good to take a step into the off-chain world and look at the integration for this NFT minting site. So I'll close the contracts directory for now, although we'll be back here shortly to reference different function signatures. Inside of the services directory, we care about this demo directory.

So if we take a look in here, a brief walkthrough, it's literally just like a back end server script to run. It's going to just tell you what it's doing. It will display your current NFT balance for the NFT contract that's going to get deployed. We're calling it deployed on a mock arbitrum chain and then the user is going to be minting it from mock base. So it will be displaying your NFT balance on mock arbitrum, your current ETH balance on mock base, what the price is of the NFT, which I think I have hard coded as one ETH in the deployment script.

This is using local anvil nodes so using easy numbers is pretty easy. And then lastly, it'll prompt you for like once you hit enter, it'll trigger minting the NFT, and then we can watch the fulfiller in action generating its proof after the fact, which gets verified against that prover contract we just set up. There's not too much for us to change here. This is just, I just wanted to give you a rundown of what exactly it is that's happening. What we care about is inside of the SRC directory, there's a client file called clients.

service. This is all in TypeScript, if anyone's familiar. There's a function called rip7755mint. That is completely empty. So this is what the developer needs here.

We have to set this up to be an RIP 7755 request to be able to integrate with the cross-chain call and close the loop for this minting process. So we can start here by setting up what this request is going to look like. To do that, it would help to have the structs file up here as a reference. I'll open that up. Because actually before we dive into that, as a refresher in the outbox contract, the function that we care about as the entry point is this request cross-chain call, which accepts one input argument and it's a cross chain request.

So really what we need to build here is a cross chain request with all the correct fields. So that's what I want to show you guys. So to start with this implementation, we can get an outline of what this request is going to look like as well as the low-level call that we need it to do to facilitate. So we can start with const calls. It's a batch, but it's just one call, but it still needs to be set up as an array because it's meant to be able to support a batch of calls.

And then we'll have a const request, which will be an empty object. We can start by just getting the field names in here. I'm just going to assign them all as empty values to start, just to motor through this. And then we can go through each one individually to make sure that we're setting it up properly and explain it all as we go. So we need to add prover contracts.

And again, I'm just copying everything to try to prevent some silly typo mistake. So just bear with me for a second. Pass in the inbox contract. L2 Oracle. L2 Oracle storage key, reward asset, reward amount, initialize at zero, finality delay seconds, initialize at zero, come back through this in a second, nonce is zero, expiry zero, and then the final two pre-check related fields, pre-check contract and pre-checked fields, pre-checked contract and pre-checked data.

And then for the individual call that we want to set up here, this just takes in three parameters. There's two, data and value. So we can take two, empty string,, and then value. Okay, so we have our structure outlined here. Now we need to make sure we set up each of these fields properly, and it helps to have an understanding of exactly what each field is, which we now all should.

In order to help with this, I have inside of this common directory, there's a constants file that has a couple of chain configs in here, one for mock arbitrum and one for mock base. So this is going to be very helpful for where we can pull the addresses from. Assuming everyone deploys the contracts using the deployment scripts that are provided, these addresses are, I mean, they're deterministic, so if they're deployed in the right order, then these addresses should all be correct, which is why they're pre-populated in this file, even though the contracts are not currently deployed on our local network. So, okay, let's start filling these out. For calls, the destination address here, again, what exactly is it that we're trying to do?

We want to mint an NFT. So the to address is going to be the NFT address, which we have up here as mock arbitrum NFT address. So at the top of this client service file, we can import that. So let's see. Import mock arbitrum NFT address from dot dot slash common slash constants.

And then we can copy that into our to field. Data is going to be the encoded call data for the mint function. I'm going to leave that for the last step. We'll come back to that in a second. Value, as you see here, this function is being called with an address.

This is going to be the user address receiving the NFT. And then the mint price, which is the mint price of the NFT denominated in way. So we can just pass in this mint price as the value directly. Yeah, so this will be mint price. And then now as we start to fill out the request, the requester is pretty self-explanatory.

Like I said, the address of the requester is being passed in, so we can use that. Calls is going to be this calls array. Prover contract is what we just implemented. This will be the contract address on the source chain, which in this context is mock base. Of the prover contract that has been implemented specifically to validate request or validate state about the destination chain which in this context is mock arbitrum.

If we look over here, there's only one defined in each but this is set up in a way that we can define multiple prover contracts for each chain. This is pretty straight forward. We'll start with, well, first we have to import the chain configs here. So if we go back up to the top of the file, we can import that from the same file that we just imported the nft address. And then down here in our mint function, we can destructure the mock arbitrum and mock base configs from that outer object that's containing the chain configs.

So this would look like const curly braces with mock arbitrum and mock base is equal to chain configs. And then now we can access all of these config fields directly on mock arbitrum and mock base. So for the prover contract, because this is the base side of the equation here, this will be mock base dot prover contracts dot prover. The destination chain ID is going to be mock arbitrum's chain ID, so we can go mock arbitrum's chain ID. So we can go mock arbitrum.

chain ID. Inbox contract is going to be the address of the RIP 7755 inbox contract on mock arbitrum. So this will be mock arbitrum.contracts.inbox.

L2 Oracle is the roll-up contract on L1 for mock arbitrum here. So this will be mock arbitrum.L2 Oracle is the rollup contract on L1 for mock arbitrum here. So this will be mock arbitrum.L2 Oracle.

Same thing with the storage key or the L2 Oracle storage key. This is for the L2 Oracle contract for mock arbitrum. So we'll grab that from the same place. It will be mock arbitrum.l2 oracle storage key.

And then for a reward asset, for this example, we're just going to use native currency because we're sending native currency for the mint function, but it doesn't necessarily have to be one-to-one like that. We could be passing in some ERC20 and then expect the fulfillers to do the necessary conversions off-chain to make sure that the ERC20 is enough value to account for the native currency that's being passed in here, but it's a little bit cleaner for the demo here to just keep it one-to-one like that. So that's what we're going to do. The ERC that I mentioned earlier that had a specific asset that represents native currency is hard-coded here. So that's what this like OXE address is.

And this is another exported constant from this constants file. So we can copy that and add it to the import statement. And then pass that in for reward address. Reward amount, again, to remind you, is meant to cover all of the value from the calls plus whatever the destination chain gas cost is plus a tip for the fulfiller. The exact mechanism to calculate what that surplus should be can be pretty complicated.

But for the sake of today's demo, doing it in a simplified context, if we add an extra 2% to the request, that should be more than enough for it to be profitable for the fulfiller in our local network. So we'll do it that way. So this will be reward amount is going to be the mint price, which is already in way, to remind you. We'll add a 2% buffer. So we can do that with big ints in TypeScript by multiplying it by something like 102n for a big int and then dividing it by 100.

This results in the mint price having 2% added to it. All right. So then for finality delay seconds, in a live network, this will likely be something on the order of days to a week to ensure protection against reorgs on the destination chain. Because this is a self-contained system that is going to run locally on your machine, we have the flexibility and the freedom to make this really short. And because I want this to be a responsive demo and for us to see everything happening in real time, we'll make it just 10 seconds.

So what this is going to result in is like a 10 to 15 second delay after the request is submitted before we'll see the fulfiller actually generate the proof and claim the reward. NONs doesn't matter because that's going to get overwritten in the outbox contract. It's just like a canonically incrementing NONs for every request. Expiry doesn't really matter too much for this demo. If you see over here in the constants file, we have a one week constant.

We can just add one week to the current block timestamp. We just need like whatever this value is, it has to be greater than the finality delay seconds in the future from like now, plus some extra cancellation buffer period, which is hard-coded as a constant in the outbox contract storage as a full day. So for the demo, just to get a valid value in here, one week should be more than enough. So this will be date.now.

So in JavaScript, if you're not familiar, it has a global date object that if you do date.now, will return the Unix time stamp but in milliseconds and in solidity it's in seconds so we have to divide that by a thousand but solidity doesn't like decimals so then we have to floor that to the nearest integer and then from here we can add the one week constant so we'll add that as another import from this constant file and then use it down here for the expiry time stamp. We'll add one week. Okay. And then the last two fields are this pre-check contract and pre-check data.

Which like I said earlier is an optional like fulfillment condition that the requester wants to be true. Because it's optional we're not going to be using it today. But yeah, so in order to not use it we have to pass in the zero address and there's a helpful constant from the web pre library I'm using theme that is just called the zero address. So we can import that at the top and then use that as the pre-check contract address in here. And then last but not least, pre-check data.

In order for it to pass the off-chain validation that Veeam does, we just have to add an OX. But this can be anything. It just has to be some kind of arbitrary byte string. Because we're not using this step, it doesn't really matter. Okay.

So that just about covers all of the requests. The only thing we haven't done yet is encoded the call data for the mint function on the nft contract. So to reference the nft contract, let me pull that up. It's just a simple mint function that takes one input argument, which is the two addresses that should be receiving the nft. There's another helpful function from Veeam called encode function data.

That we can use for encoding the call data here. We can define this as another local variable above the calls constant. So this will be const encoded call data is equal to encode function data. And this takes in, actually before we define that, we'll just add this in as the value for data, encoded call data. And then now for encode function data, this accepts one input parameter, which is an object with a couple of fields that are necessary here.

The first one being the ABI for this NFT contract, which is actually already being imported as NFT ABI for some of the other helper functions that are defined in this class. So we can just use that. So this will be ABI colon NFT ABI. I don't know why I closed that contract. We still need it.

Next up, we need to define the function name that we're encoding data for. So that's just simply mint. So if we copy that. This field name is function name. With mint passed in.

And then because mint accepts one input parameter, we now have to specify that with a field name called args. So this will be args, which is an array of the input parameters. And in this context, it's just the two address, which is passed in here as address. And that should be it for encoding call data. So at this point, we have a fully set up RIP 7755 request.

Now we need to set up the function call to submit the request. So what does that look like? To give you a little bit more context on how this class is set up, we have something called a wallet client as a local variable here. And if you're unfamiliar with Veeam as a Web3 library, it uses things called public clients and wallet clients. The public client is for reading state from the chain, and wallet client is for writing state to the chain.

This wallet client is already defined in the constructor, so we can just use it as is for the target chain that we are submitting the request to, which is mock base here. So this is going to be an asynchronous call, so we'll start it with a wait. This.walletClient. And the function name that we care about here is writeContract.

So under the hood, this, this creates your, um, your, your, your transaction and signs it and sends it to the network. This will return a transaction hash if it's successful. So we can store that as hash. And then we need to set up the configuration for like what contract are writing to, what function we're writing to, what parameters are needed. So that can be a single object in here where we define for reference I'm going to pull up the constants file again.

We need to define the address that we're sending the transaction to. So address is going to be coming from mock base because we're submitting the request to mock base. The contract we care about is the outbox contract for the standard. So this will be mock base.contracts.

outbox. Next we need to specify the ABI for the outbox. So this can be, we're going to have to import this at the top. So this will be import outbox ABI from dot dot slash ABI slash outbox. We already have that populated in this directory.

So we define that as the ABI here. We then need to define the function name that we're requesting or sending the transaction to. So if we pull up the RIP 7755 outbox contract over here, the entry point, as I said earlier, for the standard is this request cross chain call function. So we can copy that into function name. We then need to define the input args.

So as you see here, it's just one argument, which is the cross chain request. This is going to be the request that we just defined here. And then last but not least, we need to define any value that should be submitted with this transaction. So that's going to be a field called value. And in this context, it's not actually going to be mint price because if you remember, we added a 2% buffer to the mint price as the reward amount for this cross-chain call.

So the value here should be request. setting up the contract send. The last piece here is using a public client from Veeam, we can wait for a transaction receipt to make sure that the request is confirmed or the transaction is confirmed. So we can do that with another await call. In the constants file for the chain configurations, one of the fields for mock base is a public client, so we can use that directly.

So this will be await mock base dot public client dot the function name that we're going to use here is called wait for transaction receipt. And that takes in just one input object, which is the transaction hash. And in JavaScript, if the field name and the value name that you're setting it as are the same name, you can omit the colon and the value. So like this is the same thing as this. So I'll leave it like this for clarity.

And if we get through this wait for transaction receipt call, then the transaction should be successful. So then we'll log something, console.log, transaction success. Okay. So that should be our full request.

It's pretty straightforward. I just wanted to kind of give you a walk through on all the different field names and how they should be assigned and what the values mean and how they're fitting together. This should be enough in place to run the system end to end. So if we take a look in the root of the directory, the root readme here, there's a list of commands for spinning up all of these, all the infra here to deploy the contracts on top of and all of the services that are needed to get everything to work together. So we can start by running these commands.

The first thing we need to do is spin up our local chains. So if we open new terminals that are each at the root of the project, we can open three in split screen side by side. And this is going to be meant to represent the mock L1 and then the two mock L2 chains. So we can start that with, there's a make file defining all these commands. If you want to see what they're actually doing under the hood, we can run make start mock L1 in the first terminal.

So this is like our mock L1 chain. We can then run make start mock base. So we have our mock base chain in the middle. And then lastly, we can do make start mock arbitrum to run our mock arbitrum chain in the terminal on the right here. So now with these three chains running, we have our local chains running, so we can deploy our contracts on top of them.

There's another make file command set up for deploying the contracts in the correct order that is required by the off-chain services here. So we can open a new terminal. The three block chains are still running in the background here. I just have a new terminal running on top of them. We can run make setup contracts, and that's going to compile and deploy all of the contracts to the local network.

So that will just take a second. While that's going, we can copy the make start sinker command. So what this is going to do is start the off chain service for sharing state representations bidirectionally between the mock L1 and the mock L2s. And this one's pretty chatty. So we can now open a new terminal for the final two terminals that are needed here.

It's a ton of terminals, I know. We need to start the off chain fulfiller to listen to in response to requests. So that's makechain fulfiller to listen to in response to requests. So that's make start fulfiller. So now we have the fulfiller listening.

We can do a split screen here to now run the demo. So to run the demo app and mint the NFT, we would just run make demo. So if we type that in. Not too pretty, but logs to the console, what it's doing here, current state. So we have welcome to the demo, mint your NFT, the devcon NFT on mock arbitrum.

We currently have zero in the wallet address that's being used for the demo here. The price is one ETH and the current base balance is almost 10,000 ETH because this is one of the default accounts on the local Anvil nodes. So now if we press enter, this is going to send that mint request to the local network and we can see if everything worked successfully. And it did. So the transaction went through.

We actually, if we run this again, we already have the current NFT on the destination chain because the filler, if you saw over here, picked up on the request almost right away and submitted it because it validated that the incentive was sufficient. And then now in a second, we should see something else happen because the finality delay of 10 seconds has gone past. And what the fulfiller just did is it picked up on the fact that it waited long enough and it's now allowed to claim the reward for that request. And it generated this massive storage proof here and submitted that storage proof to the outbox contract on the mock base chain and everything was successful. So this was validated against the prover contract that we just implemented.

And so if you take a look in the Fulfiller directory here, after that ran, it logged the proof into a JSON file. So you can see exactly what the proof was that it used to verify that the call was submitted. So that's what this file is. And then inside of the SRC directory, there's a database directory that is storing db.json, which this is the representation of the request that it picked up on that we just submitted.

And then it has a rewards file that it's tracking how much ETH that it's claimed in rewards. So we have a locally running, like fully working system end to end. Woo. Woo. Yeah.

And I'm realizing right now that the reward tracking is not accounting for gas cost on the destination chain, but nonetheless, you get the idea. So that just about does it. If you take a look here, yeah, like I said, if we run the demo again, we now see that the current NFT balance is one because the NFT was actually minted on the destination chain as defined by the encoded call data that we set up for the target calls. And then so if we run it again, now it would be just incrementing from there. So that does it for the demo.

Thank you, everyone, for coming. And I'll be hanging around for a little bit if anyone has Questions or if you want to chat the standard a little bit more. Like I said, we have an open source repo for proof of concept Here, so if anyone feels compelled to contribute, we Fully invite you to contribute. So thank you. Thank you.

Automatic transcript — names and jargon may be misspelled.