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 9 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/builder/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
259 changes: 259 additions & 0 deletions examples/builder/builder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// 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);
}
}

/* Not tested or implemented on the precompile side */
lthibault marked this conversation as resolved.
Show resolved Hide resolved
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 buildMevShare(Suave.BuildBlockArgs memory blockArgs, uint64 blockHeight) public returns (bytes memory) {
lthibault marked this conversation as resolved.
Show resolved Hide resolved
require(Suave.isConfidential());

Suave.DataRecord[] memory allShareMatchDataRecords =
Suave.fetchDataRecords(blockHeight, "mevshare:v0:matchDataRecords");
Suave.DataRecord[] memory allShareUserDataRecords =
Suave.fetchDataRecords(blockHeight, "mevshare:v0:unmatchedBundles");

if (allShareUserDataRecords.length == 0) {
revert Suave.PeekerReverted(address(this), "no data records");
}

Suave.DataRecord[] memory allRecords = new Suave.DataRecord[](allShareUserDataRecords.length);
for (uint256 i = 0; i < allShareUserDataRecords.length; i++) {
// TODO: sort matches by egp first!
Suave.DataRecord memory dataRecordToInsert = allShareUserDataRecords[i]; // will be updated with the best match if any
for (uint256 j = 0; j < allShareMatchDataRecords.length; j++) {
// TODO: should be done once at the start and sorted
Suave.DataId[] memory mergeddataIds = abi.decode(
Suave.confidentialRetrieve(allShareMatchDataRecords[j].id, "mevshare:v0:mergedDataRecords"),
(Suave.DataId[])
);
if (idsEqual(mergeddataIds[0], allShareUserDataRecords[i].id)) {
dataRecordToInsert = allShareMatchDataRecords[j];
break;
}
}
allRecords[i] = dataRecordToInsert;
}

EgpRecordPair[] memory bidsByEGP = new EgpRecordPair[](allRecords.length);
for (uint256 i = 0; i < allRecords.length; i++) {
bytes memory simResults = Suave.confidentialRetrieve(allRecords[i].id, "mevshare: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, "mevshare:v0");
}

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));
}
}
98 changes: 98 additions & 0 deletions examples/builder/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 (
// precompiles
buildEthBlockAddress = common.HexToAddress("0x42100001")

// contracts
newBundleBidAddress = common.HexToAddress("0x642300000")
newBlockBidAddress = common.HexToAddress("0x642310000")
lthibault marked this conversation as resolved.
Show resolved Hide resolved
)

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

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{
newBlockBidAddress,
newBundleBidAddress,
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)
lthibault marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
Loading
Loading