Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
48c863d
build-eth-block: use live gas price
zeroXbrock Apr 9, 2024
6ee19b2
add chain.DeployContractWithArgs to framework
zeroXbrock Apr 10, 2024
bb32462
sending block to relay (in progress)
zeroXbrock Apr 11, 2024
1dd3155
handle ascii-decimal-array error from http calls
zeroXbrock Apr 11, 2024
bdea359
remove unneeded log
zeroXbrock Apr 11, 2024
48038c4
remove unneeded log
zeroXbrock Apr 11, 2024
9de5ad3
(WIP) using beacon chain for block-building data
zeroXbrock Apr 12, 2024
93a0e67
Merge branch 'main' into brock/mainnet-builder
zeroXbrock Apr 12, 2024
6a152c8
(temp) add timers
zeroXbrock Apr 13, 2024
73ca641
nitpick
zeroXbrock Apr 13, 2024
bcf4c57
fix pubkey error. new error: "payload attributes not (yet) found"
zeroXbrock Apr 18, 2024
bb1670f
remove needless nesting & confusing naming
zeroXbrock Apr 19, 2024
5defdad
wait for block time
zeroXbrock Apr 23, 2024
7b7096e
subscribe to payload_attributes, use that to send proper relay reqs
zeroXbrock May 15, 2024
5e4a702
clean up bb demo
zeroXbrock May 17, 2024
1bf0620
remove unnecessary code/comments
zeroXbrock May 17, 2024
1d9d283
flesh out mainnet BB example README
zeroXbrock May 19, 2024
9738ecd
fix log of uninitialized var
zeroXbrock May 19, 2024
a807519
spruce readme
zeroXbrock May 19, 2024
6ad7039
add missing instruction in bb readme
zeroXbrock May 20, 2024
0e366da
add beacon node to top requirements list
zeroXbrock May 20, 2024
870f3ad
add missing step to bb readme, last bit of cleanup
zeroXbrock May 20, 2024
6e0b614
handle unused err in framework
zeroXbrock May 20, 2024
5ce903e
simplify one-shot channel usage
zeroXbrock May 20, 2024
85f4c9e
fix unclosed body & unchecked beacon client
zeroXbrock May 20, 2024
5c21b0d
properly handle beacon service type assertion
zeroXbrock May 20, 2024
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
114 changes: 100 additions & 14 deletions examples/build-eth-block/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,116 @@
# Example Ethereum L1 Block Builder SUAPP

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

## How to use
## Requirements

Start the `suave-geth` development environment
- [suave-geth](https://github.com/flashbots/suave-geth/tree/brock/mainnet-builder)
- [suavex-foundry](https://github.com/flashbots/suavex-foundry)
- [foundry](https://getfoundry.sh/) (system installation to use `cast` and `forge`)
- [Golang](https://go.dev/doc/install) toolchain
- [Rust](https://rustup.rs/) toolchain
- ETH2 beacon RPC with access to `/eth/v1/events`

## Setup

This demo requires *suave-geth* to be configured for mainnet. Currently, it's hard-coded for Holesky testnet.

In another terminal, checkout this branch to configure the node for mainnet and rebuild the binary:

```sh
# in suave-geth/
git checkout brock/mainnet-builder
make suave
```

Run suave-geth devnet with the following flags to ensure we connect to our own Ethereum provider, which we'll set up afterwards.

```sh
# in suave-geth/
./build/bin/suave-geth --suave.dev \
--suave.eth.remote_endpoint=http://localhost:8555 \
--suave.eth.external-whitelist='*'
```
$ make devnet-up # from suave-geth root directory

This demo uses [suavex-anvil](https://github.com/flashbots/suavex-foundry) as the Ethereum provider for suave-geth, to replicate the conditions of building blocks for mainnet by forking a mainnet RPC provider.

In another terminal, set `RPC_URL` in your environment to a real mainnet RPC provider, then run the following to download and run suavex-anvil.

```sh
git clone https://github.com/flashbots/suavex-foundry
cd suavex-foundry
cargo run --bin anvil -- -p 8555 --chain-id 1 -f $RPC_URL
```

The default account which is funded by suave-execution-geth (which we're replacing with suavex-anvil) isn't funded on this fork of anvil by default, so we'll need to send it some ether from one of the default anvil accounts.
We can do this with cast (from Foundry):

```sh
cast send \
-r http://localhost:8555 \
--value 999ether \
--private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 \
0xb5feafbdd752ad52afb7e1bd2e40432a485bbb7f
```

Execute the deployment script:
Now back in this repository, if you haven't already built the contracts, do so now:

```sh
# in suapp-examples/examples/build-eth-block/
forge build
```
$ go run main.go

> For this demo, a mainnet beacon node with access to the `/eth/v1/events` endpoint is required. We use this to listen to the `payload_attributes` event, which gives us data we need to build blocks for mainnet.

Set `L1_BEACON_URL` to your beacon node's RPC in .env (in the project root directory), or in your shell's environment, and run the deployment script:

```sh
# in examples/build-eth-block/
go run main.go
```

Expected output:

```txt
<omitted sensitive trace logs from beacon node>
2024/05/19 12:07:46 Test address 1: 0x85E6919588CF2C82A5489c4606EC9C16Ab960cc9
2024/05/19 12:07:46 funding account 0x85E6919588CF2C82A5489c4606EC9C16Ab960cc9 with 100000000000000000
2024/05/19 12:07:46 funder 0xB5fEAfbDD752ad52Afb7e1bD2E40432A485bBB7F 998799840409509787000
2024/05/19 12:07:47 transaction hash: 0x1421782aadd47d8a033233b6bd8d376d79fe2c983be8e00c5613a3efe94914f3
2024/05/19 12:07:47 deployed contract at 0x19aE73489C3C76f27f110a9Bf51D03bbA99eF38d
2024/05/19 12:07:47 deployed contract at 0x010249e143b3b31286da7aAC26Ad2fCAB3A60a0D
2024/05/19 12:07:47 transaction hash: 0x8d5d492ff7b693d44b7d34972f2220b1bf4446360ff7b88b035fd61d5ef7f9c4
2024/05/19 12:07:47 finished newBundle in 117 ms
2024/05/19 12:07:47 found proposer duty for slot 9110137, {9110137 1229602 {message: {fee_recipient: '0x13F2241aa64bb6DA2B74553fA9E12B713b74F334', gas_limit: 30000000, timestamp: 1708476504, pubkey: '0x8e815d6361afd8475e9ca1388aeadbea8abd1e21a80e7cffae85e3ccb8eaad8704168e96210bdd6c4b778ecd913ce17d'}, signature: '0xa29ede14583f65253d5477b53a171fb5473aa25018c97b544eb43bb0ed02c45f9bf60f48dad6e7f1f07524da72165cc303e36d509791a97a0cb581f6d6d88f5dbeda118e5985247b588d4e7d3c9e81d224b2632616297b2b5a79aff587bb7287'}
}
2024/05/19 12:07:47 transaction hash: 0x1725273667a59c32e7a818516955118f90d5c62feaec08e40e6c9bbcc2ff10df
2024/05/19 12:07:47 finished buildFromPool in 123 ms
2024/05/19 12:07:47 blockBidID: 0x2a20347f1f6a5e749380e856e663e478
2024/05/19 12:07:48 transaction hash: 0xc8f617046e0f0e2acda8eef51a98b1e871ae896cb366227bd00a469d8bf4523f
2024/05/19 12:07:48 finished submitToRelay in 298 ms
2024/05/19 12:07:48 SubmitBlockResponse: @ {"message":{"slot":"9110137","parent_hash":"0x7e3da03170e94cae80e6d40ab8bf144c523f1496c0bb72a24edbd710ed96e13c","block_hash":"0x91f4a5437385cfc026ba229ed7c37d5d22c9a789b97ebe1b11ffc895419000a0","builder_pubkey":"0xaddea0de71ac5a8bc243bec7f7c7d9767aa8b129e54420217603e34faf519be8f57f42850a16539e803a13031dd4cd6b","proposer_pubkey":"0x8e815d6361afd8475e9ca1388aeadbea8abd1e21a80e7cffae85e3ccb8eaad8704168e96210bdd6c4b778ecd913ce17d","proposer_fee_recipient":"0x13F2241aa64bb6DA2B74553fA9E12B713b74F334","gas_limit":"30000000","gas_used":"21000","value":"42035392455000"},"execution_payload":{"parent_hash":"0x7e3da03170e94cae80e6d40ab8bf144c523f1496c0bb72a24edbd710ed96e13c","fee_recipient":"0x13F2241aa64bb6DA2B74553fA9E12B713b74F334","state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prev_randao":"0x15c2cb2e95db2e4c1f62c72c4cd2a83c2cf012595066e89bcb6106df372109ab","block_number":"19905873","gas_limit":"30000000","gas_used":"21000","timestamp":"1716145667","extra_data":"0x","base_fee_per_gas":"2001685355","block_hash":"0x91f4a5437385cfc026ba229ed7c37d5d22c9a789b97ebe1b11ffc895419000a0","transactions":["0xf866808501dcf0076b8252089485e6919588cf2c82a5489c4606ec9c16ab960cc98203e88026a08ee0d6fea35637429d39f477a9c6709116e2287801582d8d9daa4d5f7478da09a06e27aa208b99a05b1b0f3483b14ee278a5b2e011f992a5f78aa66fa0bb2d1852"],"withdrawals":[{"index":"45936075","validator_index":"1083174","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18290423"},{"index":"45936076","validator_index":"1083175","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18432792"},{"index":"45936077","validator_index":"1083176","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18315290"},{"index":"45936078","validator_index":"1083177","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18286639"},{"index":"45936079","validator_index":"1083178","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18340866"},{"index":"45936080","validator_index":"1083179","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18329346"},{"index":"45936081","validator_index":"1083180","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18456127"},{"index":"45936082","validator_index":"1083181","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18361575"},{"index":"45936083","validator_index":"1083182","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18410121"},{"index":"45936084","validator_index":"1083183","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"63239113"},{"index":"45936085","validator_index":"1083184","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18314522"},{"index":"45936086","validator_index":"1083185","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18463203"},{"index":"45936087","validator_index":"1083186","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18370225"},{"index":"45936088","validator_index":"1083187","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18426274"},{"index":"45936089","validator_index":"1083188","address":"0x210b3cb99fa1de0a64085fa80e18c22fe4722a1b","amount":"18347389"},{"index":"45936090","validator_index":"1083189","address":"0x2641c2ded63a0c640629f5edf1189e0f53c06561","amount":"18172407"}],"blob_gas_used":"0","excess_blob_gas":"0"},"blobs_bundle":{"commitments":[],"proofs":[],"blobs":[]},"signature":"0x89d1bd5453693e8ded23c0058fb69cf22e17e44cd1b6404d2245012acb7650fd26ed7aa72ff56775d8ed92f6fe300b1e01355a2c44dd2ce688c1a655470d21bd3910f2bb75c434a5346249e5bd382a490093acfa7a53237aae9c81876126cc77"};{"message":"accepted bid below floor, skipped validation"}
```
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

The block submitted won't be considered for inclusion because the transactions used to build the block in this demo aren't valid on mainnet. However, those transactions could easily be replaced with real transactions in another SUAPP.

⚠️ You may encounter an error `payload attributes not (yet) found`. This is common, and typically results from the beacon node being out of sync. Running the demo again often works. If it doesn't, you may need to check your node.

The `/eth/v1/node/syncing` endpoint is helpful in diagnosing this issue:

```sh
curl $L1_BEACON_URL/eth/v1/node/syncing
```

If your node is healthy, the response should look like this:

```json
{
"data": {
"is_syncing":false,
"is_optimistic":false,
"el_offline":false,
"head_slot":"9110069",
"sync_distance":"0"
}
}
```
29 changes: 17 additions & 12 deletions examples/build-eth-block/builder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
pragma solidity ^0.8.8;

import "suave-std/suavelib/Suave.sol";
import {Suapp} from "suave-std/Suapp.sol";

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

function fetchConfidentialBundleData() public returns (bytes memory) {
Expand Down Expand Up @@ -179,23 +180,27 @@ contract EthBlockContract is AnyBundleContract {
contract EthBlockBidSenderContract is EthBlockContract {
string boostRelayUrl;

event SubmitBlockResponse(bytes, bytes);

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) {
function submitToRelay(Suave.BuildBlockArgs calldata blockArgs, Suave.DataId bidId, string calldata namespace)
public
emitOffchainLogs
returns (bytes memory)
{
require(Suave.isConfidential());

(Suave.DataRecord memory blockDataRecord, bytes memory builderBid) =
this.doBuild(blockArgs, blockHeight, dataRecords, namespace);
Suave.submitEthBlockToRelay(boostRelayUrl, builderBid);
// bytes memory payload = this.unlock(bidId, hex"");
(bytes memory builderBid, bytes memory payload) = Suave.buildEthBlock(blockArgs, bidId, namespace);
bytes memory response = Suave.submitEthBlockToRelay(boostRelayUrl, builderBid);

return bytes.concat(this.emitBlockSubmissionResponse.selector, abi.encode(builderBid, response));
}

emit DataRecordEvent(blockDataRecord.id, blockDataRecord.decryptionCondition, blockDataRecord.allowedPeekers);
return bytes.concat(this.emitDataRecord.selector, abi.encode(blockDataRecord));
function emitBlockSubmissionResponse(bytes memory payload, bytes memory response) public {
emit SubmitBlockResponse(payload, response);
}
}
141 changes: 123 additions & 18 deletions examples/build-eth-block/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import (
"encoding/json"
"log"
"math/big"
"strings"
"time"

eth2 "github.com/attestantio/go-eth2-client"
v1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"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())

func buildBlock(fr *framework.Framework, payloadAttributes *v1.PayloadAttributesEvent) bool {
testAddr1 := framework.GeneratePrivKey()
log.Printf("Test address 1: %s", testAddr1.Address().Hex())

Expand All @@ -43,12 +45,15 @@ func main() {
maybe(err)

bundleContract := fr.Suave.DeployContract("builder.sol/BundleContract.json")
ethBlockContract := fr.Suave.DeployContract("builder.sol/EthBlockContract.json")
ethBlockContract := fr.Suave.DeployContractWithArgs(
"builder.sol/EthBlockBidSenderContract.json",
"https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net",
)

targetBlock := currentBlock(fr).Time()
targetBlock := payloadAttributes.Data.ParentBlockNumber + 1

{ // Send a bundle to the builder
decryptionCondition := targetBlock + 1
decryptionCondition := targetBlock
allowedPeekers := []common.Address{
buildEthBlockAddress,
bundleContract.Raw().Address(),
Expand All @@ -64,27 +69,127 @@ func main() {
confidentialDataBytes, err := bundleContract.Abi.Methods["fetchConfidentialBundleData"].Outputs.Pack(bundleBytes)
maybe(err)

// get current timestamp from system
startTime := time.Now().UnixMilli()
_ = bundleContract.SendConfidentialRequest("newBundle", newBundleArgs, confidentialDataBytes)
duration := time.Now().UnixMilli() - startTime
log.Printf("finished newBundle in %d ms", duration)
}

{ // 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},
var blockBidID [16]byte

validators, err := fr.L1Relay.GetValidators()
maybe(err)
var slotDuty framework.BuilderGetValidatorsResponseEntry
for _, validator := range *validators {
if validator.Slot == uint64(payloadAttributes.Data.ProposalSlot) {
slotDuty = validator
log.Printf("found proposer duty for slot %d, %v", payloadAttributes.Data.ProposalSlot, slotDuty)
break
}
}
if slotDuty.Entry == nil {
log.Printf("no proposer duty found for slot %d", payloadAttributes.Data.ProposalSlot)
return false
}

withdrawals := make([]*types.Withdrawal, len(payloadAttributes.Data.V3.Withdrawals))
for i, withdrawal := range payloadAttributes.Data.V3.Withdrawals {
withdrawals[i] = &types.Withdrawal{
Index: uint64(withdrawal.Index),
Validator: uint64(withdrawal.ValidatorIndex),
Address: common.Address(withdrawal.Address),
Amount: uint64(withdrawal.Amount),
}
}

_ = ethBlockContract.SendConfidentialRequest("buildFromPool", []any{payloadArgsTuple, targetBlock + 1}, nil)
blockArgs := types.BuildBlockArgs{
Slot: uint64(payloadAttributes.Data.ProposalSlot),
Parent: common.Hash(payloadAttributes.Data.ParentBlockHash),
Timestamp: payloadAttributes.Data.V3.Timestamp,
Random: payloadAttributes.Data.V3.PrevRandao,
FeeRecipient: common.Address(slotDuty.Entry.Message.FeeRecipient),
GasLimit: uint64(slotDuty.Entry.Message.GasLimit),
ProposerPubkey: slotDuty.Entry.Message.Pubkey[:],
BeaconRoot: common.Hash(payloadAttributes.Data.ParentBlockRoot),
Withdrawals: withdrawals,
}

{ // Signal to the builder that it's time to build a new block
startTime := time.Now().UnixMilli()
receipt := ethBlockContract.SendConfidentialRequest("buildFromPool", []any{blockArgs, targetBlock}, nil)
maybe(err)

duration := time.Now().UnixMilli() - startTime
log.Printf("finished buildFromPool in %d ms", duration)

for _, receiptLog := range receipt.Logs {
buildEvent := ethBlockContract.Abi.Events["BuilderBoostBidEvent"]
if receiptLog.Topics[0] == buildEvent.ID {
bids, err := buildEvent.Inputs.Unpack(receiptLog.Data)
maybe(err)
blockBidID = bids[0].([16]byte)
log.Printf("blockBidID: %s", hexutil.Encode(blockBidID[:]))
break
}
}
}

{ // Submit block to the relay
startTime := time.Now().UnixMilli()
var receipt *types.Receipt
for {
receipt, err = ethBlockContract.MaybeSendConfidentialRequest("submitToRelay", []any{
blockArgs,
blockBidID,
"",
}, nil)
if err == nil {
break
} else {
if strings.Contains(err.Error(), "payload attributes not (yet) known") {
log.Printf("submitToRelay error: %s. retrying in 3 seconds...", err.Error())
time.Sleep(3 * time.Second)
} else {
panic(err)
}
}
}

duration := time.Now().UnixMilli() - startTime
log.Printf("finished submitToRelay in %d ms", duration)

for _, receiptLog := range receipt.Logs {
if receiptLog.Topics[0] == ethBlockContract.Abi.Events["SubmitBlockResponse"].ID {
log.Printf("SubmitBlockResponse: %s", receiptLog.Data)
}
}
}
return true
}

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))
func main() {
fr := framework.New(framework.WithL1())
eventProvider := eth2.EventsProvider(fr.L1Beacon)
done := make(chan struct{})

// subscribe to the beacon chain event `payload_attributes`
err := eventProvider.Events(context.Background(), []string{"payload_attributes"}, func(e *v1.Event) {
payloadAttributes := e.Data.(*v1.PayloadAttributesEvent)
bbRes := buildBlock(fr, payloadAttributes)
if bbRes {
close(done)
}
})
maybe(err)
return b

// wait for exit conditions
select {
case <-done:
log.Printf("block sent to relay successfully")
case <-time.After(30 * time.Second):
log.Fatalf("timeout")
}
}

func maybe(err error) {
Expand Down
Loading