Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add builder example #35

Merged
merged 19 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
lthibault marked this conversation as resolved.
Show resolved Hide resolved
// 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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did u mean to change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This addresses the account funding issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this account should be set to the devnet account that gets funded? but i guess if it passes tests its fine!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll need to look into this a bit more and figure out exactly what's going on. I'm thinking we can ship this PR first, then add extra assertions to the test per your other comment, then investigate what's up with this account funding.


// 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
Loading