Skip to main content

Flashbots SUAVE: Develop Your First Suapp Today!

· 16 min read

suapp-today

This is the third article in Eden's series about Flashbots' SUAVE. It explores the architecture and features of the latest version of SUAVE and reinforces the learning by walking through smart contract (suapp) development. For a full understanding, reading our previous article, TEE Party with SUAVE, is highly recommended.

Introduction

Ever wonder what it'd be like to write a Solidity smart contract that can process information privately, provide cheap performant computation and even access external resources?

You might be thinking, "ah that's only possible with a closed, centralized solution", but SUAVE beg to differ.

SUAVE brings with it all those sweet features through the power of Trusted Execution Environments (TEEs) - specifically, a network of TEEs - and a permissionless chain alongside managing the operation.

This is an ambitious goal, indeed. Yet, with the rollout of SUAVE's first two iterations, Rigil and Sirrah, this vision is quickly becoming a reality. Sirrah begins to explore the potential of TEEs, though it's still in early stages of development. Rigil, on the other hand, doesn't incorporate TEE technology, but is available for experimentation and will be the focus of our discussion.

You have the opportunity to have hands-on experience with SUAVE today, and this article aims to introduce the intricacies of doing this. Let's get started!

SUAVE Rigil

SUAVE is envisioned as a network of nodes within TEEs, called kettles; and SUAVE chain, a permissionless chain managing the kettles and providing public credible storage.

suave-rigil-chart Inspired by diagram from SUAVE docs

The Rigil release is a stepping stone towards this vision and, like the planned version, it also features SUAVE chain and kettles, although the functionalities and implementation of the components might differ from the final version.

  • SUAVE chain can be considered as the public part of SUAVE. It is a proof-of-authority EVM chain, not much different from its EVM counterparts, with sole use of hosting smart contracts for public or confidential execution and providing the ability to store data publicly with stronger storage guarantees.
  • A kettle can be considered the confidential part of SUAVE. Although in this version, a kettle is not hosted in a TEE, and trust in the operator is still required. The core of a kettle is MEVM, which is EVM with additional "plugin" features that extend the abilities of EVM to provide privacy, cheap compute and access to external resources.

With Rigil, both components are run via suave-geth, a modified Ethereum client, and have access to a single settlement chain, which is currently Goerli, but will soon be Holsky or Sepolia.

As one of SUAVE's core features, let's explore how MEVM extends the EVM we know today, and what additional functionality it offers.

MEVM

MEVM is an extension of the classic EVM. It retains the same opcodes as the classic EVM, enhances functionality through the addition of precompiles. This allows developers to reuse existing EVM tools, knowledge, and smart contracts to develop suapps (SUAVE applications).

The precompiles in MEVM bring several powerful capabilities:

  • Private Data Storage and Retrieval: It includes a system for storing sensitive information such as order flow or private keys used during confidential calls. This storage system is known as the confidential store.
  • Affordable Off-Chain Computation: MEVM allows for cost-effective off-chain computations. While these are limited to the services the precompiles offer, those already include useful functionalities such as bundle simulation or even block construction.
  • External Network Access: This feature enables interactions with the external network, ranging from sending blocks to relays to fetching the latest cryptocurrency prices, e.g. DOGE's price from the Binance API.

The most recent additions and updates to these precompiles can be found in the Flashbots documentation, providing a comprehensive view of the capabilities and functionalities MEVM offers.

mevm-chart

While MEVM can access SUAVE chain state with suapps hosted on it, it cannot modify it. Therefore, no storage changes can be made, nor any logs emitted during MEVM execution.

Another difference is that while users can interact with SUAVE chain with regular transactions, similar to Ethereum, engaging with the MEVM requires a more complex interaction process, called a Confidential Compute Request (CCR).

Confidential Compute Requests

A CCR is just another transaction type, although it acts more like a static-call as it doesn't alter the blockchain state. Unlike a traditional transaction or a contract call, CCRs contain fields that will remain private even after the call is completed, thus it acts as a channel between a user and a suapp for passing confidential information.

The execution of a CCR happens in two stages:

  • First, the request is processed in the MEVM environment. Here, the programs can access the precompiles to access external resources, store confidential data or perform intensive computation. Storage cannot be modified, nor logs can be emitted.
  • If the MEVM execution is successful, the output data from the MEVM is then used to create a new transaction, which is sent to the SUAVE chain. The SUAVE transaction is then run in the standard EVM environment, allowing the CCR to indirectly access and potentially modify the state of the SUAVE chain.

ccr-chart Inspired by diagram from SUAVE docs

Now let's tie these concepts together with an example...

Suapp Example

This purpose here is to explore the nuances of SUAVE Rigil through an example suapp that auctions off write access to the extra data field in a block.

Let's start with an Ethereum block, in particular, a field in the block called ExtraData. This field serves no specific function in the block, by default it indicates client version it was build with, but is often overwritten by a builder tag. In reality, any 32 byte message can be placed here, making it a unique advertising space for the builder or anyone willing to pay for it.

Thanks to precompiles that let you build your own block and send it to the Goerli relay, SUAVE can now power this new advertising estate marketplace, and it can even provide the privacy needed for handling payments.

Here is what the flow could look like:

suave-example

Lets dive deeper into such an app! The full implementation is accessible here.

Suapp and deployment

SUAVE chain houses smart contracts used for both public execution as well as confidential one.

It behaves like any other EVM-based chain, so deploying contracts here is no different than anywhere else. Each contract can have methods for confidential and public execution. The difference lies in their capabilities: during confidential execution, specialized precompiles extending EVM can be accessed, but storage modification and log emission are not allowed.

There are no restrictions preventing you from calling confidential methods during EVM execution, but if during execution of this method MEVM precompiles are trying to be accessed, the call will revert as they are not available (effectively they would act as empty addresses). Likewise, nothing would prevent you from calling public methods during confidential execution. But if that method emits a log or modifies any storage, the call will revert as this is not allowed.

For easier distinction between confidential and non-confidential methods and cleaner debugging, one can leverage the IsConfidential precompile that returns whether the execution environment is in-fact confidential.

Now let's explore examples of two methods, a confidential and a public one.

MEVM / Confidential method

For our application we want to allow anyone to submit a bid for a block's extra data. The bid should include the message string bidder wishes to put in the block, for how long they want their bid to be available, and a Goerli transaction that serves as a payment for the bid. To guarantee our payment, we need to send a bundle containing a transaction that will send some value to the coinbase address, either via transfer or priority fees. While the message and duration of the bid could be public, the payment cannot be, as anyone could pay themselves with it.

Therefore the smart contract needs to receive and store the payment transaction privately. However, this is not a feature of vanilla EVM, we need to leverage privacy offered by CCR and MEVM to do this.

Below is a function that facilities bid submission in a private manner using precompiles available in MEVM to do so.

// onlyConfidential checks execution environment is indeed MEVM

function buyAd(uint64 blockLimit, string memory extra) external onlyConfidential returns (bytes memory) {
        // Use precompile ConfidentialInputs to obtain confidential inputs passed with CCR
        bytes memory paymentBundle = this.fetchConfidentialBundleData();
        // Use precompile SimulateBundle to obtain bundle's effective-gas-price for tob
        (,uint64 egp) = simulateBundleSafe(paymentBundle, true);
        crequire(egp > 0, "egp too low");
        // Use precompile ConfidentialStore to store bundle in a private manner for latter use
        Suave.DataId paymentBidId = storePaymentBundle(paymentBundle);
        // Create and return a callback for EVM part
        AdRequest memory request = AdRequest(nextId, extra, blockLimit, paymentBidId);
        return abi.encodeWithSelector(this.buyAdCallback.selector, request, getUnlockPair());
}

Lets walk through this method:

  • Modifier onlyConfidential acts as a guard checking the execution environment is in fact confidential.
  • fetchConfidentialBundleData is an internal method calling the ConfidentialInputs precompile. This precompile returns all confidential inputs that came with the CCR. This data is private and accessible only to the execution environment (later this will be TEE). This is a great channel for a bidder to submit their payment bundle through.
  • (,uint64 egp) = simulateBundleSafe(paymentBundle, true); calls internal method simulateBundleSafe that in turn calls SimulateBundle precompile. This precompile simulates the transactions in the bundle on the latest state of target blockchain and returns the resulting effective gas price, that is ETH paid to the coinbase address per unit of gas transaction used. If the simulation reverts it returns the error associated with revert, so this precompile is not only useful to check how much value is sent to the coinbase, but also if the bundle will execute successfully, and if not what message it reverted with.
  • storePaymentBundle uses ConfidentialStore precompile to store bundle payment for later use while preserving privacy.
  • Lastly, return abi.encodeWithSelector(this.buyAdCallback.selector, request, getUnlockPair()); } returns calldata that kettle will use to construct SUAVE transaction to this contract. In this case SUAVE transaction will call buyAdCallback on the same contract callback originated from.

Let's look deeper into the callback returned from the MEVM method.

EVM / Non-confidential method

In our example the MEVM method returned callback to buyAdCallback method shown below.

function buyAdCallback(AdRequest calldata request, UnlockArgs calldata uArgs) external unlock(uArgs) {
	requests.push(request);
	nextId++;
	emit RequestAdded(request.id, request.extra, request.blockLimit);
}

During an MEVM call, emitting a log or modifying storage is not allowed. At least not directly, instead one can use callbacks to access SUAVE chain and modify storage or emit events.

The above method is meant to be executed via a SUAVE transaction that a kettle submits after successful MEVM execution. However, this doesn't mean it is limited to such execution, SUAVE chain supports the same transaction type as Ethereum and thus such a method can be called from an EOA as well.

Similarly, methods that have nothing to do with MEVM can be executed on SUAVE chain, like the one below.

function getRequests(uint index) external view returns (memory AdRequest) {
	return requests[index];
}

With a better understanding of how smart contracts work in SUAVE Rigil, let's explore how we can interact with them, by sending a bid for extra-data field.

Submitting a bid

CCR

With the contracts deployed, we want to submit a bid for a block message of a certain value. We want the message to say "< this could be your ad >", with a bid of 0.5 ETH that lasts for 100 blocks. As discussed, this needs to be executed in a private manner to prevent anyone from taking advantage of the payment bundle.

Using buyAd, we are sending the request to be executed privately via MEVM, which requires a CCR.

ConfidentialComputeRequest {
    confidentialComputeRecord: ConfidentialComputeRecord, 
    confidentialInputs: string
}

ConfidentialComputeRecord {
    nonce: number,
    to: string,
    gas: number,
    gasPrice: number,
    value: number,
    data: string,
    executionNode: string,
    chainId: BigNumerish,
    confidentialInputsHash: null | string,
    v: null | BigNumberish,
    r: null | BigNumberish,
    s: null | BigNumberish,
}

Let the name not deceive you, only one field in ConfidentialComputeRequest is actually private, that is confidentialInputs, the rest of the fields become public once the MEVM execution completes.

Our payment bundle, as bytes, should be passed into confidentialInputs to be later (during MEVM execution) retrieved by a precompile of the same name and used in the execution. The method call itself, and the arguments accompanying it, are passed in the calldata.

The rest of the fields are the same as on any other Ethereum transaction, except for executionNode and confidentialInputsHash. As the name implies, confidentialInputsHash is a keccak256 hash of confidentialInputs , while executionNode is a public address of the kettle request is submitted to.

One other difference with ConfidentialComputeRequest to other transaction types is its unique RLP encoding, of which one can learn more at: ethers-suave, suave-viem or suave-docs.

To call a method with confidential execution, we need to send a message to the kettle with RLP encoded and signed ConfidentialComputeRequest. The signed request is submitted via the eth_sendRawTransaction method.

Now lets see what happens when our request reaches the kettle.

Lifetime of CCR

Kettles split ConfidentialComputeRequests execution into two synchronous parts - first MEVM (confidential execution) and second EVM.

First buyAd method is executed, fetching the confidential inputs, simulating the bundle, storing it in confidential store, and lastly returning the callback.

If this part of the execution reverts, an error message is returned to the sender and the execution doesn't proceed. Otherwise the kettle collects the callback returned from buyAd, creates a new transaction of SUAVE transaction type, and sends it over SUAVE chain. As the code above shows the callback stores the message in storage, increments the variable nextId and emits the RequestAdded event over SUAVE chain.

Some of you might ask "wait, how does a kettle just create and sign a new transaction without the sender's private key?" Well, it doesn't use sender's account to sign a transaction, instead the kettle, which has its own signer, signs it itself.

With the CCR submitted, you might be wondering what the response to the request looks like and if it substantially differs from the one we are used to.

Responses

The transaction receipt for CCRs is the same as for other Ethereum transactions types, however the transaction response slightly differs by containing two new fields confidentialComputeResult and requestRecord.

Below is an example of TransactionResponse:

{
    "blockHash": "0xf2c4416892ddfc2002f4e587e37a6ae0e3773df179f90e4483493ea5a0ee0d74",
    "blockNumber": 1192323,
    "from": "0x16f2Aa8dF055b6e672b93Ded41FecCCabAB565B0",
    "gas": 2000000,
    "gasPrice": 20000000000,
    "hash": "0x67af32ac0d7f601e5356db20f64874a2c488f757e205b2892395bce376a4ecec",
    "input": "0xee2cc3640000000000000000000000000000000000000000000000000000000000000060620f1eed298d806a99fa785a5ad14aca2ffce410d49cf6fec61c626d3008a6062e671b717c134c3dbedf9cf87ceef3b3698b9fd611890bbc1efbcbc7b6d9c2f00000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000009d73949105b165ab435ca392c21427441f737900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d536f20457874726120f09f94a500000000000000000000000000000000000000",
    "nonce": 1138,
    "r": "0x039a3cb1e4764e72bad1c2eafa09bd285bccff31b7b1c75be9b7c5a125b20954",
    "s": "0x0150048900b993bd84a939e2299f10fdcd6612112f278db66d74b1dcba4fd01b",
    "to": "0x07e60844bCd83B78b1991A3228E749B09AF9E215",
    "transactionIndex": 0,
    "v": 1,
    "value": 0,
    "confidentialComputeResult": "0xee2cc3640000000000000000000000000000000000000000000000000000000000000060620f1eed298d806a99fa785a5ad14aca2ffce410d49cf6fec61c626d3008a6062e671b717c134c3dbedf9cf87ceef3b3698b9fd611890bbc1efbcbc7b6d9c2f00000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000009d73949105b165ab435ca392c21427441f737900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d536f20457874726120f09f94a500000000000000000000000000000000000000",
    "requestRecord": {
        "chainId": "0x1008c45",
        "confidentialInputsHash": "0x09e4743eed40040d4e6c10601550c4aaac9be056fb404db2005229f7bd060854",
        "gas": "0x1e8480",
        "gasPrice": "0x4a817c800",
        "hash": "0x47187b933fe9acfbeb1a266c7b18c880860c7cb60eac716f958a80caadd32f21",
        "input": "0xfd38f21d00000000000000000000000000000000000000000000000000000000009d73940000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000d536f20457874726120f09f94a500000000000000000000000000000000000000",
				"kettleAddress": "0x03493869959c866713c33669ca118e774a30a0e5",
        "maxFeePerGas": null,
        "maxPriorityFeePerGas": null,
        "nonce": "0x472",
        "r": "0xbb008087a177900012da7080a7b49d83f4b72712ad1a55dba0817892b0782f10",
        "s": "0x4a91dfba77b10511d6b5d02af59405a46b74f437f231b4eee347d33aac704507",
        "to": "0x07e60844bcd83b78b1991a3228e749b09af9e215",
        "type": "0x42",
        "v": "0x0",
        "value": "0x0"
    }
}

Great, we submitted the bid via CCR, now let's wrap things up by building the block and sending it to the relay.

Building a block

Imagine there are various users competing to get their message in the extra-data field of the next block. They all submitted their bids, now residing in confidential store, and our job is to find the one that pays the builder the most, then build a block with the associated message, and send it to the relay.

The following method gets the job done by:
  • using SimulateBundle precompile to simulate all bid payments at the top of the block and selecting the highest one;
  • storing the winning bid in the confidential store;
  • modifying extra parameter to reflect the desired message of the winning bid;
  • using precompile BuildEthBlock to build the block and SubmitEthBlockToRelay to send it to the Flashbots Goerli relay;
  • returning the callback.

function buildBlock(
	Suave.BuildBlockArgs memory blockArgs,
	uint64 blockHeight
) public onlyConfidential returns (bytes memory) {
	crequire(requests.length > 0, "No requests");
	// Use precompile SimulateBundle to get effective gas price at the top of the block
  // and select the winning bid
	(Offer memory bestOffer, bytes memory removals) = filterOffers(blockHeight);
	crequire(bestOffer.egp > 0, "No valid offers");
	
	// Store the best offer in "local mempool" - confidential store
	storeBundleInPool(blockHeight, bestOffer);
	// Modify block's extra field so it reflect the one from the winning bid
	blockArgs.extra = bytes(bestOffer.extra);
	// Use precompile BuildEthBlock to build the block and receiving the built block
	// use precompile SubmitEthBlockToRelay to pass it to the relay
	bytes memory externalCallback = builder.buildFromPool(blockArgs, blockHeight);
	
	// Return the callback
	return
		abi.encodeWithSelector(
			this.buildCallback.selector,
			externalCallback,
			abi.encode(bestOffer.id, bestOffer.egp),
			removals,
			getUnlockPair()
		);
}

The interaction with this MEVM method is similar to the one described earlier, so we won't go into too much depth here. Also, we will ignore how the block building parameters are obtained as it's out of the scope for this article.

If the method call succeeds, the block with the extra-data field of the winning bid should be sent to the Flashbots Goerli relay. If the tip is sufficient to win the MEV-Boost auction, the block will land on chain like this:

mev-boost-etherscan

See the block for yourself here

Final thoughts

I hope this article gives you a glimpse into how SUAVE Rigil works, and what is possible to build. Now the only limit is your creativity!

build-on-suave

To learn more, see: