Skip to content

Commit

Permalink
Add builder example (#35)
Browse files Browse the repository at this point in the history
* Add failing test for builder.

* Deploy contract and call example method stub.

* Pass confidential payload & decryption condition to builder contract.

* WIP. Port TestBlockBuildingContract from suave-geth.

* Use funded account.

* Port framework calls to suapp-examples framework. Remove unneeded calls to ProgressChain.

* Send bundle record to SUAVE builder.

* Fix insufficient gas per block.

* Clean up.

* Remove unused MEVShare method.

* Remove deprecated comment.

* Remove unused contract addresses.

* Add L1Enabled field to config struct.

* Pass framework.WithL1() option.

* Use latest version of suave-geth when testing.

* fix private key config in example framework (#42)

* Upgrade to suave-geth v0.1.3

* Rename example to build-eth-block. Add README.md.

---------

Co-authored-by: brock smedley <[email protected]>
  • Loading branch information
lthibault and zeroXbrock authored Feb 29, 2024
1 parent bf4b505 commit 1316518
Show file tree
Hide file tree
Showing 8 changed files with 860 additions and 45 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ lt: lint test

.PHONY: run-integration
run-integration:
go run examples/build-eth-block/main.go
go run examples/mevm-confidential-store/main.go
go run examples/mevm-is-confidential/main.go
go run examples/onchain-callback/main.go
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3.8"

services:
suave-mevm:
image: flashbots/suave-geth:v0.1.2
image: flashbots/suave-geth:latest
command:
- --suave.dev
- --http.addr=0.0.0.0
Expand Down
30 changes: 30 additions & 0 deletions examples/build-eth-block/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Example Ethereum L1 Block Builder SUAPP

This example demonstrates a simple block building contract that receives bundles and returns an Ethereum L1 block.

## How to use

Start the `suave-geth` development environment

```
$ make devnet-up # from suave-geth root directory
```

Execute the deployment script:

```
$ go run main.go
```

Expected output:

```
2024/02/29 14:59:09 Test address 1: 0x675d92a306187fBC280f8Dd98465770FBAEFf8Ab
2024/02/29 14:59:09 funding account 0x675d92a306187fBC280f8Dd98465770FBAEFf8Ab with 100000000000000000
2024/02/29 14:59:09 funder 0xB5fEAfbDD752ad52Afb7e1bD2E40432A485bBB7F 115792089237316195423570985008687907853269984665640564039457584007913129639927
2024/02/29 14:59:09 transaction hash: 0x29e67f56dfd1a01ab210dcad889eba7a99028ec1bf2206b66d8054efc14e6fda
2024/02/29 14:59:09 deployed contract at 0xd594760B2A36467ec7F0267382564772D7b0b73c
2024/02/29 14:59:09 deployed contract at 0x8f21Fdd6B4f4CacD33151777A46c122797c8BF17
2024/02/29 14:59:09 transaction hash: 0x99a95bc20ea3e8c9d8a2ac21943c1c7a51599b57e4254a48f3773f923d881f2b
2024/02/29 14:59:09 transaction hash: 0xbf9ff92a229c76f59ed7d2be06297763b796c390d725fb1863e199cdb9cff1eb
```
201 changes: 201 additions & 0 deletions examples/build-eth-block/builder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.8;

import "suave-std/suavelib/Suave.sol";

contract AnyBundleContract {
event DataRecordEvent(Suave.DataId dataId, uint64 decryptionCondition, address[] allowedPeekers);

function fetchConfidentialBundleData() public returns (bytes memory) {
require(Suave.isConfidential());

bytes memory confidentialInputs = Suave.confidentialInputs();
return abi.decode(confidentialInputs, (bytes));
}

function emitDataRecord(Suave.DataRecord calldata dataRecord) public {
emit DataRecordEvent(dataRecord.id, dataRecord.decryptionCondition, dataRecord.allowedPeekers);
}
}

contract BundleContract is AnyBundleContract {
function newBundle(
uint64 decryptionCondition,
address[] memory dataAllowedPeekers,
address[] memory dataAllowedStores
) external payable returns (bytes memory) {
require(Suave.isConfidential());

bytes memory bundleData = this.fetchConfidentialBundleData();

uint64 egp = Suave.simulateBundle(bundleData);

Suave.DataRecord memory dataRecord =
Suave.newDataRecord(decryptionCondition, dataAllowedPeekers, dataAllowedStores, "default:v0:ethBundles");

Suave.confidentialStore(dataRecord.id, "default:v0:ethBundles", bundleData);
Suave.confidentialStore(dataRecord.id, "default:v0:ethBundleSimResults", abi.encode(egp));

return emitAndReturn(dataRecord, bundleData);
}

function emitAndReturn(Suave.DataRecord memory dataRecord, bytes memory) internal virtual returns (bytes memory) {
emit DataRecordEvent(dataRecord.id, dataRecord.decryptionCondition, dataRecord.allowedPeekers);
return bytes.concat(this.emitDataRecord.selector, abi.encode(dataRecord));
}
}

contract EthBundleSenderContract is BundleContract {
string[] public builderUrls;

constructor(string[] memory builderUrls_) {
builderUrls = builderUrls_;
}

function emitAndReturn(Suave.DataRecord memory dataRecord, bytes memory bundleData)
internal
virtual
override
returns (bytes memory)
{
for (uint256 i = 0; i < builderUrls.length; i++) {
Suave.submitBundleJsonRPC(builderUrls[i], "eth_sendBundle", bundleData);
}

return BundleContract.emitAndReturn(dataRecord, bundleData);
}
}

struct EgpRecordPair {
uint64 egp; // in wei, beware overflow
Suave.DataId dataId;
}

contract EthBlockContract is AnyBundleContract {
event BuilderBoostBidEvent(Suave.DataId dataId, bytes builderBid);

function idsEqual(Suave.DataId _l, Suave.DataId _r) public pure returns (bool) {
bytes memory l = abi.encodePacked(_l);
bytes memory r = abi.encodePacked(_r);
for (uint256 i = 0; i < l.length; i++) {
if (bytes(l)[i] != r[i]) {
return false;
}
}

return true;
}

function buildFromPool(Suave.BuildBlockArgs memory blockArgs, uint64 blockHeight) public returns (bytes memory) {
require(Suave.isConfidential());

Suave.DataRecord[] memory allRecords = Suave.fetchDataRecords(blockHeight, "default:v0:ethBundles");
if (allRecords.length == 0) {
revert Suave.PeekerReverted(address(this), "no data records");
}

EgpRecordPair[] memory bidsByEGP = new EgpRecordPair[](allRecords.length);
for (uint256 i = 0; i < allRecords.length; i++) {
bytes memory simResults = Suave.confidentialRetrieve(allRecords[i].id, "default:v0:ethBundleSimResults");
uint64 egp = abi.decode(simResults, (uint64));
bidsByEGP[i] = EgpRecordPair(egp, allRecords[i].id);
}

// Bubble sort, cause why not
uint256 n = bidsByEGP.length;
for (uint256 i = 0; i < n - 1; i++) {
for (uint256 j = i + 1; j < n; j++) {
if (bidsByEGP[i].egp < bidsByEGP[j].egp) {
EgpRecordPair memory temp = bidsByEGP[i];
bidsByEGP[i] = bidsByEGP[j];
bidsByEGP[j] = temp;
}
}
}

Suave.DataId[] memory alldataIds = new Suave.DataId[](allRecords.length);
for (uint256 i = 0; i < bidsByEGP.length; i++) {
alldataIds[i] = bidsByEGP[i].dataId;
}

return buildAndEmit(blockArgs, blockHeight, alldataIds, "");
}

function buildAndEmit(
Suave.BuildBlockArgs memory blockArgs,
uint64 blockHeight,
Suave.DataId[] memory records,
string memory namespace
) public virtual returns (bytes memory) {
require(Suave.isConfidential());

(Suave.DataRecord memory blockBid, bytes memory builderBid) =
this.doBuild(blockArgs, blockHeight, records, namespace);

emit BuilderBoostBidEvent(blockBid.id, builderBid);
emit DataRecordEvent(blockBid.id, blockBid.decryptionCondition, blockBid.allowedPeekers);
return bytes.concat(this.emitBuilderBidAndBid.selector, abi.encode(blockBid, builderBid));
}

function doBuild(
Suave.BuildBlockArgs memory blockArgs,
uint64 blockHeight,
Suave.DataId[] memory records,
string memory namespace
) public returns (Suave.DataRecord memory, bytes memory) {
address[] memory allowedPeekers = new address[](2);
allowedPeekers[0] = address(this);
allowedPeekers[1] = Suave.BUILD_ETH_BLOCK;

Suave.DataRecord memory blockBid =
Suave.newDataRecord(blockHeight, allowedPeekers, allowedPeekers, "default:v0:mergedDataRecords");
Suave.confidentialStore(blockBid.id, "default:v0:mergedDataRecords", abi.encode(records));

(bytes memory builderBid, bytes memory payload) = Suave.buildEthBlock(blockArgs, blockBid.id, namespace);
Suave.confidentialStore(blockBid.id, "default:v0:builderPayload", payload); // only through this.unlock

return (blockBid, builderBid);
}

function emitBuilderBidAndBid(Suave.DataRecord memory dataRecord, bytes memory builderBid)
public
returns (Suave.DataRecord memory, bytes memory)
{
emit BuilderBoostBidEvent(dataRecord.id, builderBid);
emit DataRecordEvent(dataRecord.id, dataRecord.decryptionCondition, dataRecord.allowedPeekers);
return (dataRecord, builderBid);
}

function unlock(Suave.DataId dataId, bytes memory signedBlindedHeader) public returns (bytes memory) {
require(Suave.isConfidential());

// TODO: verify the header is correct
// TODO: incorporate protocol name
bytes memory payload = Suave.confidentialRetrieve(dataId, "default:v0:builderPayload");
return payload;
}
}

contract EthBlockBidSenderContract is EthBlockContract {
string boostRelayUrl;

constructor(string memory boostRelayUrl_) {
boostRelayUrl = boostRelayUrl_;
}

function buildAndEmit(
Suave.BuildBlockArgs memory blockArgs,
uint64 blockHeight,
Suave.DataId[] memory dataRecords,
string memory namespace
) public virtual override returns (bytes memory) {
require(Suave.isConfidential());

(Suave.DataRecord memory blockDataRecord, bytes memory builderBid) =
this.doBuild(blockArgs, blockHeight, dataRecords, namespace);
Suave.submitEthBlockToRelay(boostRelayUrl, builderBid);

emit DataRecordEvent(blockDataRecord.id, blockDataRecord.decryptionCondition, blockDataRecord.allowedPeekers);
return bytes.concat(this.emitDataRecord.selector, abi.encode(blockDataRecord));
}
}
89 changes: 89 additions & 0 deletions examples/build-eth-block/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"context"
"encoding/json"
"log"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"

"github.com/flashbots/suapp-examples/framework"
)

var buildEthBlockAddress = common.HexToAddress("0x42100001")

func main() {
fr := framework.New(framework.WithL1())

testAddr1 := framework.GeneratePrivKey()
log.Printf("Test address 1: %s", testAddr1.Address().Hex())

fundBalance := big.NewInt(100000000000000000)
maybe(fr.L1.FundAccount(testAddr1.Address(), fundBalance))

targeAddr := testAddr1.Address()
tx, err := fr.L1.SignTx(testAddr1, &types.LegacyTx{
To: &targeAddr,
Value: big.NewInt(1000),
Gas: 21000,
GasPrice: big.NewInt(6701898710),
})
maybe(err)

bundle := &types.SBundle{
Txs: types.Transactions{tx},
RevertingHashes: []common.Hash{},
}
bundleBytes, err := json.Marshal(bundle)
maybe(err)

bundleContract := fr.Suave.DeployContract("builder.sol/BundleContract.json")
ethBlockContract := fr.Suave.DeployContract("builder.sol/EthBlockContract.json")

targetBlock := currentBlock(fr).Time()

{ // Send a bundle to the builder
decryptionCondition := targetBlock + 1
allowedPeekers := []common.Address{
buildEthBlockAddress,
bundleContract.Address(),
ethBlockContract.Address()}
allowedStores := []common.Address{}
newBundleArgs := []any{
decryptionCondition,
allowedPeekers,
allowedStores}

confidentialDataBytes, err := bundleContract.Abi.Methods["fetchConfidentialBundleData"].Outputs.Pack(bundleBytes)
maybe(err)

_ = bundleContract.SendTransaction("newBundle", newBundleArgs, confidentialDataBytes)
}

{ // Signal to the builder that it's time to build a new block
payloadArgsTuple := types.BuildBlockArgs{
ProposerPubkey: []byte{0x42},
Timestamp: targetBlock + 12, // ethHead + uint64(12),
FeeRecipient: common.Address{0x42},
}

_ = ethBlockContract.SendTransaction("buildFromPool", []any{payloadArgsTuple, targetBlock + 1}, nil)
maybe(err)
}
}

func currentBlock(fr *framework.Framework) *types.Block {
n, err := fr.L1.RPC().BlockNumber(context.TODO())
maybe(err)
b, err := fr.L1.RPC().BlockByNumber(context.TODO(), new(big.Int).SetUint64(n))
maybe(err)
return b
}

func maybe(err error) {
if err != nil {
panic(err)
}
}
13 changes: 7 additions & 6 deletions framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (c *Contract) SendTransaction(method string, args []interface{}, confidenti

type Framework struct {
config *Config
kettleAddress common.Address
KettleAddress common.Address

Suave *Chain
L1 *Chain
Expand All @@ -186,14 +186,15 @@ type Framework struct {
type Config struct {
KettleRPC string `env:"KETTLE_RPC, default=http://localhost:8545"`

// This account is funded in your local L1 devnet
// This account is funded in your local SUAVE devnet
// address: 0xBE69d72ca5f88aCba033a063dF5DBe43a4148De0
FundedAccount *PrivKey `env:"KETTLE_PRIVKEY, default=91ab9a7e53c220e6210460b65a7a3bb2ca181412a8a7b43ff336b3df1737ce12"`

L1RPC string `env:"L1_RPC, default=http://localhost:8555"`

// This account is funded in your local SUAVE devnet
// address: 0xBE69d72ca5f88aCba033a063dF5DBe43a4148De0
FundedAccountL1 *PrivKey `env:"L1_PRIVKEY, default=91ab9a7e53c220e6210460b65a7a3bb2ca181412a8a7b43ff336b3df1737ce12"`
// This account is funded in your local L1 devnet
// address: 0xB5fEAfbDD752ad52Afb7e1bD2E40432A485bBB7F
FundedAccountL1 *PrivKey `env:"L1_PRIVKEY, default=6c45335a22461ccdb978b78ab61b238bad2fae4544fb55c14eb096c875ccfc52"`

// Whether to enable L1 or not
L1Enabled bool
Expand Down Expand Up @@ -230,7 +231,7 @@ func New(opts ...ConfigOption) *Framework {

fr := &Framework{
config: &config,
kettleAddress: accounts[0],
KettleAddress: accounts[0],
Suave: &Chain{rpc: kettleRPC, clt: suaveClt, kettleAddr: accounts[0]},
}

Expand Down
Loading

0 comments on commit 1316518

Please sign in to comment.