This repository contains the Command Line Interface (CLI) for interacting with a Suave Geth node for live testing and demo'ing purposes. The Suave CLI allows users to deploy contracts, send bids, and perform various actions related to MEV-Share and Block Building.
As this repository is under development these commands may not always be up to date but they should be a good starting point for interacting with a SUAVE node.
Before running the Suave CLI, ensure you have the following:
- Golang (Go) installed
- Ethereum account with private key (for testing)
- Suave RPC endpoint (local or remote)
- Goerli RPC endpoint (local or remote)
- Goerli Beacon RPC endpoint (local or remote)
- Boost Relay URL
To get started with the Suave CLI, follow these steps:
-
Clone this repository to your local machine.
-
Install the necessary dependencies by running
go mod tidy
. -
Build the CLI using
go build -o suavecli ./suave-geth/suave/cmd/suavecli
. -
Run the Suave CLI with the desired command and subcommand.
For example:
./suavecli deployBlockSenderContract
-
deployBlockSenderContract
: Deploys the BlockSender contract to the Suave network. This contract is used to send constructed blocks for execution via the Boost Relay. -
deployMevShareContract
: Deploys the MevShare contract to the Suave network. This contract is used for sharing Maximum Extractable Value (MEV) profits with the MevExtractor contract.
-
sendBundle
: Sends a bundle of transactions to specified MEVM contract. -
sendMevShareBundle
: Sends a MEVShare bundle to specified MEVM contract. -
sendMevShareMatch
: Sends a MEV share match transaction to the Suave network via the Boost Relay for matching MEV share recipients with their corresponding transactions. -
sendBuildShareBlock
: Sends a transaction to build a Goerli block using MEV-Share orderflow and sends to specified Goerli relay.
-
startHintListener
: Starts a hint listener for demo purposes. This command listens for hints emmited from MEV-Share on the Suave Chain. -
subscribeBeaconAndBoost
: Subscribes to events from the Beacon Chain and Boost for demo purposes. -
startRelayListener
: Starts a relay listener for demo purposes. This command listens for block submisisons and deliveries from the Boost Relay.
-
testDeployAndShare
: Performs an end-to-end test scenario that includes contract deployment and block sharing. -
buildGoerliBlocks
: Performs an end-to-end test scenario for building and sharing blocks on the Goerli network.
To get a better understanding of how the MEVM works, let's delve into the deployment of a simple version of [mev-share](https://github.com/flashbots/mev-share)
, a protocol for orderflow auctions, defined via smart contract on SUAVE. Our journey below will guide you through the steps of deploying simple mev-share and block builder contracts, interacting with them, and ultimately seeing a block land onchain.
In this walkthrough we will go through a script located inside the suave cli tool, so don't worry about running the below code, it’s mainly for conceptual purposes. To follow along and use the tool you will need:
- SUAVE chain node and SUAVE execution node setup.
- Basic knowledge of the Ethereum's Golang libraries and the Ethereum RPC methods.
Ensure these details for the command line tool are on hand:
suave_rpc
: address of suave rpcgoerli_rpc
: address of goerli execution node rpcgoerli_beacon_rpc
: address of goerli beacon rpckettleAddress
: wallet address of execution nodeprivKeyHex
: private key as hex (for testing)relay_url
: address of boost relay that the contract will send blocks to
Our first step is to deploy the compiled byte code from our mev-share
contract. As you will see, deploying on SUAVE feels just like deploying on any other EVM chain. First we gather our transaction details, nounce and gas price, sign the transaction, and then send using the normal eth_sendRawTransaction
using your suaveClient
mevShareAddrPtr, txHash, err := sendMevShareCreationTx(suaveClient, suaveSigner, privKey)
if err != nil {
panic(err.Error())
}
waitForTransactionToBeConfirmed(suaveClient, txHash)
mevShareAddr := *mevShareAddrPtr
Now we take a look under the hood of sendMevShareCreationTx
.
func sendMevShareCreationTx(suaveClient *rpc.Client, suaveSigner types.Signer, privKey *ecdsa.PrivateKey) (*common.Address, *common.Hash, error) {
var suaveAccNonceBytes hexutil.Uint64
err := suaveClient.Call(
&suaveAccNonceBytes,
"eth_getTransactionCount",
crypto.PubkeyToAddress(privKey.PublicKey),
"latest"
)
suaveAccNonce := uint64(suaveAccNonceBytes)
var suaveGp hexutil.Big
err = suaveClient.Call(&suaveGp, "eth_gasPrice")
calldata := hexutil.MustDecode(mevshareContractBytecode)
mevshareContractBytecode)
ccTxData := &types.LegacyTx{
Nonce: suaveAccNonce,
To: nil, // contract creation
Value: big.NewInt(0),
Gas: 10000000,
GasPrice: (*big.Int)(&suaveGp),
Data: calldata,
}
tx, err := types.SignTx(types.NewTx(ccTxData), suaveSigner, privKey)
from, _ := types.Sender(suaveSigner, tx)
mevshareAddr := crypto.CreateAddress(from, tx.Nonce())
log.Info("contract address will be", "addr", mevshareAddr)
txBytes, err := tx.MarshalBinary()
var txHash common.Hash
err = suaveClient.Call(
&txHash,
"eth_sendRawTransaction",
hexutil.Encode(txBytes)
)
return &mevshareAddr, &txHash, nil
}
Later, we'll incorporate the mevshareAddr
into our transaction's allowed contracts, granting access for the contract to compute over our confidential data.
Next we deploy a simple block builder contract which we will also store to later grant access to. The block builder takes in a boostRelayUrl
which is where it will send blocks to when finished building.
blockSenderAddrPtr, txHash, err := sendBlockSenderCreationTx(
suaveClient,
suaveSigner,
privKey,
boostRelayUrl
)
if err != nil {
panic(err.Error())
}
waitForTransactionToBeConfirmed(suaveClient, txHash)
blockSenderAddr := *blockSenderAddrPtr
Similar as above, sendBlockSenderCreationTx
operates like any other contract deployment on an EVM chain.
Once our contracts have been succesfully deployed we will craft a goerli bundle and send it to our newly deployed mev-share contract.
mevShareTx, err := sendMevShareBidTx(suaveClient, goerliClient, suaveSigner, goerliSigner, 5, mevShareAddr, blockSenderAddr, kettleAddress, privKey)
if err != nil {
err = errors.Wrap(err, unwrapPeekerError(err).Error())
panic(err.Error())
}
waitForTransactionToBeConfirmed(suaveClient, &mevShareTx.txHash)
Let's take a deeper look at sendMevShareBidTx
which looks similar to a normal Ethereum transaction but has a couple key differences. We explore those below the following code snippet.
func sendMevShareBidTx(
// function inputs removed for brevity
) (mevShareBidData, error) {
var startingGoerliBlockNum uint64
err = goerliClient.Call(
(*hexutil.Uint64)(&startingGoerliBlockNum),
"eth_blockNumber"
)
if err != nil {
utils.Fatalf("could not get goerli block: %v", err)
}
_, ethBundleBytes, err := prepareEthBundle(
goerliClient,
goerliSigner,
privKey
)
// Prepare bundle bid
var suaveAccNonce hexutil.Uint64
err = suaveClient.Call(
&suaveAccNonce,
"eth_getTransactionCount",
crypto.PubkeyToAddress(privKey.PublicKey),
"pending"
)
confidentialDataBytes, err := mevShareABI.Methods["fetchBidConfidentialBundleData"].Outputs.Pack(ethBundleBytes)
allowedPeekers := []common.Address{
newBlockBidAddress,
extractHintAddress,
buildEthBlockAddress,
mevShareAddr,
blockBuilderAddr
}
calldata, err := mevShareABI.Pack("newBid", blockNum, allowedPeekers)
if err != nil {
return mevShareBidData{}, err
}
wrappedTxData := &types.DynamicFeeTx{
Nonce: suaveAccNonce,
To: &mevShareAddr,
Value: nil,
Gas: 10000000,
GasTipCap: big.NewInt(10),
GasFeeCap: big.NewInt(33000000000),
Data: calldata,
}
mevShareTx, err := types.SignTx(types.NewTx(&types.ConfidentialComputeRequestTx{
KettleAddress: kettleAddress,
Wrapped: *types.NewTx(wrappedTxData),
}), suaveSigner, privKey)
if err != nil {
return nil, nil, err
}
mevShareTxBytes, err := mevShareTx.MarshalBinary()
if err != nil {
return nil, nil, err
}
var confidentialRequestTxHash common.Hash
err = suaveClient.Call(
&confidentialRequestTxHash,
"eth_sendRawTransaction",
hexutil.Encode(mevShareTxBytes),
hexutil.Encode(confidentialDataBytes)
)
if err != nil {
return mevShareBidData{}, err
}
mevShareTxHash= mevShareBidData{blockNumber: blockNum, txHash: confidentialRequestTxHash}
return mevShareTxHash, nil
}
A SUAVE transaction, referred to as a mevshare bid in the code, takes in two extra arguments: allowedPeekers
and kettleAddress
. These arguement are to utilize a new transaction primitive types.ConfidentialComputeRequest
, which you can read more about here. The role of allowedPeekers
is to dictate which contracts can view the confidential data, in our scenario, the goerli bundle being submitted. Meanwhile, kettleAddress
points to the intended execution node for the transaction. Lastly, Suave nodes have a modified ethSendRawTransaction
to support this new transaction type.
Now that a MEV-share bid has been sent in we can simulate sending in a match. Once live on a testnet, searchers can monitor the SUAVE chain looking for hints emitted as logs for protocols they specialize in. In our example you could monitor the mevShareAddr
for emitted events. Using these hints they can get a BidId
to reference in their match. Below we see the code.
bidIdBytes, err := extractBidId(suaveClient, mevShareTx.txHash)
if err != nil {
panic(err.Error())
}
_, err = sendMevShareMatchTx(
suaveClient,
goerliClient,
suaveSigner,
goerliSigner,
mevShareTx.blockNumber,
mevShareAddr,
blockSenderAddr,
kettleAddress,
bidIdBytes,
privKey,
)
if err != nil {
err = errors.Wrap(err, unwrapPeekerError(err).Error())
panic(err.Error())
}
Now that our SUAVE node's bidpool has a mevshare bid and match, we can trigger block building to combine these transactions, simulate for validity, and insert the refund transaction.
_, err = sendBuildShareBlockTx(suaveClient, suaveSigner, privKey, kettleAddress, blockSenderAddr, payloadArgsTuple, uint64(goerliBlockNum)+1)
if err != nil {
err = errors.Wrap(err, unwrapPeekerError(err).Error())
if strings.Contains(err.Error(), "no bids") {
log.Error("Failed to build a block, no bids")
}
log.Error("Failed to send BuildShareBlockTx", "err", err)
}
Once the execution node has received this transaction it will build your block and send it off to a relay. If you used the flashbots goerli relay you should be able to check it out using the builder blocks received endpoint.