add suave package and builder

This commit is contained in:
dmarzzz 2024-01-16 16:06:59 -05:00
parent c66ca8bf7a
commit 28a45e3492
21 changed files with 1115 additions and 5 deletions

View file

@ -196,6 +196,9 @@ var (
utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBBucketFlag,
utils.MetricsInfluxDBOrganizationFlag, utils.MetricsInfluxDBOrganizationFlag,
} }
suaveApiFlags = []cli.Flag{
utils.SuaveEnabled,
}
) )
var app = flags.NewApp("the go-ethereum command line interface") var app = flags.NewApp("the go-ethereum command line interface")
@ -245,6 +248,7 @@ func init() {
consoleFlags, consoleFlags,
debug.Flags, debug.Flags,
metricsFlags, metricsFlags,
suaveApiFlags,
) )
flags.AutoEnvVars(app.Flags, "GETH") flags.AutoEnvVars(app.Flags, "GETH")

View file

@ -69,6 +69,7 @@ import (
"github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/p2p/netutil"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/suave"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/triedb/hashdb" "github.com/ethereum/go-ethereum/trie/triedb/hashdb"
"github.com/ethereum/go-ethereum/trie/triedb/pathdb" "github.com/ethereum/go-ethereum/trie/triedb/pathdb"
@ -908,6 +909,13 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server.
Value: metrics.DefaultConfig.InfluxDBOrganization, Value: metrics.DefaultConfig.InfluxDBOrganization,
Category: flags.MetricsCategory, Category: flags.MetricsCategory,
} }
// SUAVE namespace rpc settings
SuaveEnabled = &cli.BoolFlag{
Name: "suave",
Usage: "Enable the suave",
Category: flags.SuaveCategory,
}
) )
var ( var (
@ -1344,6 +1352,10 @@ func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) {
} }
} }
func SetSuaveConfig(ctx *cli.Context, cfg *suave.Config) {
cfg.Enabled = ctx.IsSet(SuaveEnabled.Name)
}
// SetNodeConfig applies node-related command line flags to the config. // SetNodeConfig applies node-related command line flags to the config.
func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
SetP2PConfig(ctx, &cfg.P2P) SetP2PConfig(ctx, &cfg.P2P)
@ -1859,11 +1871,18 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) {
// RegisterEthService adds an Ethereum client to the stack. // RegisterEthService adds an Ethereum client to the stack.
// The second return value is the full node instance. // The second return value is the full node instance.
func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) { func RegisterEthService(stack *node.Node, cfg *ethconfig.Config, suaveConfig *suave.Config) (ethapi.Backend, *eth.Ethereum) {
backend, err := eth.New(stack, cfg) backend, err := eth.New(stack, cfg)
if err != nil { if err != nil {
Fatalf("Failed to register the Ethereum service: %v", err) Fatalf("Failed to register the Ethereum service: %v", err)
} }
if suaveConfig.Enabled {
log.Info("Enable suave service")
if err := suave.Register(stack, backend, suaveConfig); err != nil {
Fatalf("Failed to register the suave service: %v", err)
}
}
stack.RegisterAPIs(tracers.APIs(backend.APIBackend)) stack.RegisterAPIs(tracers.APIs(backend.APIBackend))
return backend.APIBackend, backend return backend.APIBackend, backend
} }

95
core/types/sbundle.go Normal file
View file

@ -0,0 +1,95 @@
package types
import (
"encoding/json"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// Simplified Share Bundle Type for PoC
type SBundle struct {
BlockNumber *big.Int `json:"blockNumber,omitempty"` // if BlockNumber is set it must match DecryptionCondition!
MaxBlock *big.Int `json:"maxBlock,omitempty"`
Txs Transactions `json:"txs"`
RevertingHashes []common.Hash `json:"revertingHashes,omitempty"`
RefundPercent *int `json:"percent,omitempty"`
}
type RpcSBundle struct {
BlockNumber *hexutil.Big `json:"blockNumber,omitempty"`
MaxBlock *hexutil.Big `json:"maxBlock,omitempty"`
Txs []hexutil.Bytes `json:"txs"`
RevertingHashes []common.Hash `json:"revertingHashes,omitempty"`
RefundPercent *int `json:"percent,omitempty"`
}
func (s *SBundle) MarshalJSON() ([]byte, error) {
txs := []hexutil.Bytes{}
for _, tx := range s.Txs {
txBytes, err := tx.MarshalBinary()
if err != nil {
return nil, err
}
txs = append(txs, txBytes)
}
var blockNumber *hexutil.Big
if s.BlockNumber != nil {
blockNumber = new(hexutil.Big)
*blockNumber = hexutil.Big(*s.BlockNumber)
}
return json.Marshal(&RpcSBundle{
BlockNumber: blockNumber,
Txs: txs,
RevertingHashes: s.RevertingHashes,
RefundPercent: s.RefundPercent,
})
}
func (s *SBundle) UnmarshalJSON(data []byte) error {
var rpcSBundle RpcSBundle
if err := json.Unmarshal(data, &rpcSBundle); err != nil {
return err
}
var txs Transactions
for _, txBytes := range rpcSBundle.Txs {
var tx Transaction
err := tx.UnmarshalBinary(txBytes)
if err != nil {
return err
}
txs = append(txs, &tx)
}
s.BlockNumber = (*big.Int)(rpcSBundle.BlockNumber)
s.MaxBlock = (*big.Int)(rpcSBundle.MaxBlock)
s.Txs = txs
s.RevertingHashes = rpcSBundle.RevertingHashes
s.RefundPercent = rpcSBundle.RefundPercent
return nil
}
type RPCMevShareBundle struct {
Version string `json:"version"`
Inclusion struct {
Block string `json:"block"`
MaxBlock string `json:"maxBlock"`
} `json:"inclusion"`
Body []struct {
Tx string `json:"tx"`
CanRevert bool `json:"canRevert"`
} `json:"body"`
Validity struct {
Refund []struct {
BodyIdx int `json:"bodyIdx"`
Percent int `json:"percent"`
} `json:"refund"`
} `json:"validity"`
}

View file

@ -0,0 +1,32 @@
// Code generated by suave/gen in https://github.com/flashbots/suave-geth.
// DO NOT EDIT.
// Hash: c60f303834fbdbbd940aae7cb3679cf3755a25f7384f1052c20bf6c38d9a0451
package types
import "github.com/ethereum/go-ethereum/common"
type DataId [16]byte
// Structs
type BuildBlockArgs struct {
Slot uint64
ProposerPubkey []byte
Parent common.Hash
Timestamp uint64
FeeRecipient common.Address
GasLimit uint64
Random common.Hash
Withdrawals []*Withdrawal
Extra []byte
FillPending bool
}
type DataRecord struct {
Id DataId
Salt DataId
DecryptionCondition uint64
AllowedPeekers []common.Address
AllowedStores []common.Address
Version string
}

View file

@ -415,3 +415,11 @@ func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, re
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) return b.eth.stateAtTransaction(ctx, block, txIndex, reexec)
} }
func (b *EthAPIBackend) BuildBlockFromTxs(ctx context.Context, buildArgs *types.BuildBlockArgs, txs types.Transactions) (*types.Block, *big.Int, error) {
return b.eth.Miner().BuildBlockFromTxs(ctx, buildArgs, txs)
}
func (b *EthAPIBackend) BuildBlockFromBundles(ctx context.Context, buildArgs *types.BuildBlockArgs, bundles []types.SBundle) (*types.Block, *big.Int, error) {
return b.eth.Miner().BuildBlockFromBundles(ctx, buildArgs, bundles)
}

View file

@ -57,6 +57,8 @@ import (
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
suave_builder "github.com/ethereum/go-ethereum/suave/builder"
suave_builder_api "github.com/ethereum/go-ethereum/suave/builder/api"
) )
// Config contains the configuration options of the ETH protocol. // Config contains the configuration options of the ETH protocol.
@ -309,6 +311,19 @@ func makeExtraData(extra []byte) []byte {
func (s *Ethereum) APIs() []rpc.API { func (s *Ethereum) APIs() []rpc.API {
apis := ethapi.GetAPIs(s.APIBackend) apis := ethapi.GetAPIs(s.APIBackend)
// Append SUAVE-enabled node backend
apis = append(apis, rpc.API{
Namespace: "suavex",
Service: backends.NewEthBackendServer(s.APIBackend),
})
sessionManager := suave_builder.NewSessionManager(s.blockchain, &suave_builder.Config{})
apis = append(apis, rpc.API{
Namespace: "suavex",
Service: suave_builder_api.NewServer(sessionManager),
})
// Append any APIs exposed explicitly by the consensus engine // Append any APIs exposed explicitly by the consensus engine
apis = append(apis, s.engine.APIs(s.BlockChain())...) apis = append(apis, s.engine.APIs(s.BlockChain())...)

View file

@ -48,6 +48,7 @@ import (
"github.com/ethereum/go-ethereum/internal/blocktest" "github.com/ethereum/go-ethereum/internal/blocktest"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@ -597,6 +598,24 @@ func (b testBackend) ServiceFilter(ctx context.Context, session *bloombits.Match
panic("implement me") panic("implement me")
} }
func (n *testBackend) BuildBlockFromTxs(ctx context.Context, buildArgs *types.BuildBlockArgs, txs types.Transactions) (*types.Block, *big.Int, error) {
block := types.NewBlock(&types.Header{GasUsed: 1000, BaseFee: big.NewInt(1)}, txs, nil, nil, trie.NewStackTrie(nil))
return block, big.NewInt(11000), nil
}
func (n *testBackend) BuildBlockFromBundles(ctx context.Context, buildArgs *types.BuildBlockArgs, bundles []types.SBundle) (*types.Block, *big.Int, error) {
var txs types.Transactions
for _, bundle := range bundles {
txs = append(txs, bundle.Txs...)
}
block := types.NewBlock(&types.Header{GasUsed: 1000, BaseFee: big.NewInt(1)}, txs, nil, nil, trie.NewStackTrie(nil))
return block, big.NewInt(11000), nil
}
func (n *testBackend) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) {
return []byte{0x1}, nil
}
func TestEstimateGas(t *testing.T) { func TestEstimateGas(t *testing.T) {
t.Parallel() t.Parallel()
// Initialize test accounts // Initialize test accounts

View file

@ -97,6 +97,10 @@ type Backend interface {
SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription
BloomStatus() (uint64, uint64) BloomStatus() (uint64, uint64)
ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession)
// SUAVE Execution Methods
BuildBlockFromTxs(ctx context.Context, buildArgs *types.BuildBlockArgs, txs types.Transactions) (*types.Block, *big.Int, error)
BuildBlockFromBundles(ctx context.Context, buildArgs *types.BuildBlockArgs, bundles []types.SBundle) (*types.Block, *big.Int, error)
} }
func GetAPIs(apiBackend Backend) []rpc.API { func GetAPIs(apiBackend Backend) []rpc.API {

View file

@ -365,3 +365,15 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent)
} }
func (b *backendMock) Engine() consensus.Engine { return nil } func (b *backendMock) Engine() consensus.Engine { return nil }
func (n *backendMock) BuildBlockFromTxs(ctx context.Context, buildArgs *types.BuildBlockArgs, txs types.Transactions) (*types.Block, *big.Int, error) {
return nil, nil, nil
}
func (n *backendMock) BuildBlockFromBundles(ctx context.Context, buildArgs *types.BuildBlockArgs, bundles []types.SBundle) (*types.Block, *big.Int, error) {
return nil, nil, nil
}
func (n *backendMock) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) {
return nil, nil
}

View file

@ -37,6 +37,7 @@ const (
MiscCategory = "MISC" MiscCategory = "MISC"
TestingCategory = "TESTING" TestingCategory = "TESTING"
DeprecatedCategory = "ALIASED (deprecated)" DeprecatedCategory = "ALIASED (deprecated)"
SuaveCategory = "SUAVE"
) )
func init() { func init() {

View file

@ -18,6 +18,7 @@
package miner package miner
import ( import (
"context"
"fmt" "fmt"
"math/big" "math/big"
"sync" "sync"
@ -244,3 +245,11 @@ func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscript
func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) { func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) {
return miner.worker.buildPayload(args) return miner.worker.buildPayload(args)
} }
func (miner *Miner) BuildBlockFromTxs(ctx context.Context, buildArgs *types.BuildBlockArgs, txs types.Transactions) (*types.Block, *big.Int, error) {
return miner.worker.buildBlockFromTxs(ctx, buildArgs, txs)
}
func (miner *Miner) BuildBlockFromBundles(ctx context.Context, buildArgs *types.BuildBlockArgs, bundles []types.SBundle) (*types.Block, *big.Int, error) {
return miner.worker.buildBlockFromBundles(ctx, buildArgs, bundles)
}

View file

@ -17,6 +17,7 @@
package miner package miner
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
@ -33,6 +34,7 @@ import (
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
@ -892,7 +894,9 @@ type generateParams struct {
forceTime bool // Flag whether the given timestamp is immutable or not forceTime bool // Flag whether the given timestamp is immutable or not
parentHash common.Hash // Parent block hash, empty means the latest chain head parentHash common.Hash // Parent block hash, empty means the latest chain head
coinbase common.Address // The fee recipient address for including transaction coinbase common.Address // The fee recipient address for including transaction
gasLimit uint64
random common.Hash // The randomness generated by beacon chain, empty before the merge random common.Hash // The randomness generated by beacon chain, empty before the merge
extra []byte // The extra data to include in the block header
withdrawals types.Withdrawals // List of withdrawals to include in block. withdrawals types.Withdrawals // List of withdrawals to include in block.
beaconRoot *common.Hash // The beacon root (cancun field). beaconRoot *common.Hash // The beacon root (cancun field).
noTxs bool // Flag whether an empty block without any transaction is expected noTxs bool // Flag whether an empty block without any transaction is expected
@ -1212,3 +1216,226 @@ func signalToErr(signal int32) error {
panic(fmt.Errorf("undefined signal %d", signal)) panic(fmt.Errorf("undefined signal %d", signal))
} }
} }
// SUAVE
func (w *worker) rawCommitTransactions(env *environment, txs types.Transactions) error {
gasLimit := env.header.GasLimit
if env.gasPool == nil {
env.gasPool = new(core.GasPool).AddGas(gasLimit)
}
// TODO: logs should be a part of the env and returned to whoever requested the block
// var coalescedLogs []*types.Log
for _, tx := range txs {
// If we don't have enough gas for any further transactions then we're done.
if env.gasPool.Gas() < params.TxGas {
log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas)
break
}
// Error may be ignored here. The error has already been checked
// during transaction acceptance is the transaction pool.
from, _ := types.Sender(env.signer, tx)
// Check whether the tx is replay protected. If we're not in the EIP155 hf
// phase, start ignoring the sender until we do.
if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) {
log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", w.chainConfig.EIP155Block)
return fmt.Errorf("invalid reply protected tx %s", tx.Hash())
}
// Start executing the transaction
env.state.SetTxContext(tx.Hash(), env.tcount)
// logs, err := w.commitTransaction(env, tx)
_, err := w.commitTransaction(env, tx)
switch {
case errors.Is(err, core.ErrNonceTooLow):
log.Debug("Skipping transaction with low nonce", "hash", tx.Hash(), "sender", from, "nonce", tx.Nonce())
return err
case errors.Is(err, nil):
// coalescedLogs = append(coalescedLogs, logs...)
env.tcount++
default:
// Transaction is regarded as invalid, drop all consecutive transactions from
// the same sender because of `nonce-too-high` clause.
log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
return err
}
}
return nil
}
func (w *worker) commitPendingTxs(work *environment) error {
interrupt := new(atomic.Int32)
timer := time.AfterFunc(w.newpayloadTimeout, func() {
interrupt.Store(commitInterruptTimeout)
})
defer timer.Stop()
if err := w.fillTransactions(nil, work); err != nil {
return err
}
return nil
}
func (w *worker) buildBlockFromTxs(ctx context.Context, args *types.BuildBlockArgs, txs types.Transactions) (*types.Block, *big.Int, error) {
params := &generateParams{
timestamp: args.Timestamp,
forceTime: true,
parentHash: args.Parent,
coinbase: args.FeeRecipient,
gasLimit: args.GasLimit,
random: args.Random,
extra: args.Extra,
withdrawals: args.Withdrawals,
// noUncle: true,
noTxs: false,
}
work, err := w.prepareWork(params)
if err != nil {
return nil, nil, err
}
defer work.discard()
profitPre := work.state.GetBalance(args.FeeRecipient)
if err := w.rawCommitTransactions(work, txs); err != nil {
return nil, nil, err
}
if args.FillPending {
if err := w.commitPendingTxs(work); err != nil {
return nil, nil, err
}
}
profitPost := work.state.GetBalance(args.FeeRecipient)
// TODO : Is it okay to set Uncle List to nil?
block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, nil, work.receipts, params.withdrawals)
if err != nil {
return nil, nil, err
}
blockProfit := new(big.Int).Sub(profitPost, profitPre)
return block, blockProfit, nil
}
func (w *worker) buildBlockFromBundles(ctx context.Context, args *types.BuildBlockArgs, bundles []types.SBundle) (*types.Block, *big.Int, error) {
// create ephemeral addr and private key for payment txn
ephemeralPrivKey, err := crypto.GenerateKey()
if err != nil {
return nil, nil, err
}
ephemeralAddr := crypto.PubkeyToAddress(ephemeralPrivKey.PublicKey)
params := &generateParams{
timestamp: args.Timestamp,
forceTime: true,
parentHash: args.Parent,
coinbase: ephemeralAddr, // NOTE : overriding BuildBlockArgs.FeeRecipient TODO : make customizable
gasLimit: args.GasLimit,
random: args.Random,
extra: args.Extra,
withdrawals: args.Withdrawals,
// noUncle: true,
noTxs: false,
}
work, err := w.prepareWork(params)
if err != nil {
return nil, nil, err
}
defer work.discard()
// Assume static 28000 gas transfers for both mev-share and proposer payments
refundTransferCost := new(big.Int).Mul(big.NewInt(28000), work.header.BaseFee)
profitPre := work.state.GetBalance(params.coinbase)
for _, bundle := range bundles {
// NOTE: failing bundles will cause the block to not be built!
// apply bundle
profitPreBundle := work.state.GetBalance(params.coinbase)
if err := w.rawCommitTransactions(work, bundle.Txs); err != nil {
return nil, nil, err
}
profitPostBundle := work.state.GetBalance(params.coinbase)
// calc & refund user if bundle has multiple txns and wants refund
if len(bundle.Txs) > 1 && bundle.RefundPercent != nil {
// Note: PoC logic, this could be gamed by not sending any eth to coinbase
refundPrct := *bundle.RefundPercent
if refundPrct == 0 {
// default refund
refundPrct = 10
}
bundleProfit := new(big.Int).Sub(profitPostBundle, profitPreBundle)
refundAmt := new(big.Int).Div(bundleProfit, big.NewInt(int64(refundPrct)))
// subtract payment txn transfer costs
refundAmt = new(big.Int).Sub(refundAmt, refundTransferCost)
currNonce := work.state.GetNonce(ephemeralAddr)
// HACK to include payment txn
// multi refund block untested
userTx := bundle.Txs[0] // NOTE : assumes first txn is refund recipient
refundAddr, err := types.Sender(types.LatestSignerForChainID(userTx.ChainId()), userTx)
if err != nil {
return nil, nil, err
}
paymentTx, err := types.SignTx(types.NewTx(&types.LegacyTx{
Nonce: currNonce,
To: &refundAddr,
Value: refundAmt,
Gas: 28000,
GasPrice: work.header.BaseFee,
}), work.signer, ephemeralPrivKey)
if err != nil {
return nil, nil, err
}
// commit payment txn
if err := w.rawCommitTransactions(work, types.Transactions{paymentTx}); err != nil {
return nil, nil, err
}
}
}
if args.FillPending {
if err := w.commitPendingTxs(work); err != nil {
return nil, nil, err
}
}
profitPost := work.state.GetBalance(params.coinbase)
proposerProfit := new(big.Int).Set(profitPost) // = post-pre-transfer_cost
proposerProfit = proposerProfit.Sub(profitPost, profitPre)
proposerProfit = proposerProfit.Sub(proposerProfit, refundTransferCost)
currNonce := work.state.GetNonce(ephemeralAddr)
paymentTx, err := types.SignTx(types.NewTx(&types.LegacyTx{
Nonce: currNonce,
To: &args.FeeRecipient,
Value: proposerProfit,
Gas: 28000,
GasPrice: work.header.BaseFee,
}), work.signer, ephemeralPrivKey)
if err != nil {
return nil, nil, fmt.Errorf("could not sign proposer payment: %w", err)
}
// commit payment txn
if err := w.rawCommitTransactions(work, types.Transactions{paymentTx}); err != nil {
return nil, nil, fmt.Errorf("could not sign proposer payment: %w", err)
}
log.Info("buildBlockFromBundles", "num_bundles", len(bundles), "num_txns", len(work.txs), "profit", proposerProfit)
// TODO : Is it okay to set Uncle List to nil?
block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, nil, work.receipts, params.withdrawals)
if err != nil {
return nil, nil, err
}
return block, proposerProfit, nil
}

12
suave/builder/api/api.go Normal file
View file

@ -0,0 +1,12 @@
package api
import (
"context"
"github.com/ethereum/go-ethereum/core/types"
)
type API interface {
NewSession(ctx context.Context) (string, error)
AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error)
}

View file

@ -0,0 +1,42 @@
package api
import (
"context"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
)
var _ API = (*APIClient)(nil)
type APIClient struct {
rpc rpcClient
}
func NewClient(endpoint string) (*APIClient, error) {
clt, err := rpc.Dial(endpoint)
if err != nil {
return nil, err
}
return NewClientFromRPC(clt), nil
}
type rpcClient interface {
CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error
}
func NewClientFromRPC(rpc rpcClient) *APIClient {
return &APIClient{rpc: rpc}
}
func (a *APIClient) NewSession(ctx context.Context) (string, error) {
var id string
err := a.rpc.CallContext(ctx, &id, "suavex_newSession")
return id, err
}
func (a *APIClient) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) {
var receipt *types.SimulateTransactionResult
err := a.rpc.CallContext(ctx, &receipt, "suavex_addTransaction", sessionId, tx)
return receipt, err
}

View file

@ -0,0 +1,43 @@
package api
import (
"context"
"github.com/ethereum/go-ethereum/core/types"
)
// sessionManager is the backend that manages the session state of the builder API.
type sessionManager interface {
NewSession() (string, error)
AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error)
}
func NewServer(s sessionManager) *Server {
api := &Server{
sessionMngr: s,
}
return api
}
type Server struct {
sessionMngr sessionManager
}
func (s *Server) NewSession(ctx context.Context) (string, error) {
return s.sessionMngr.NewSession()
}
func (s *Server) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) {
return s.sessionMngr.AddTransaction(sessionId, tx)
}
type MockServer struct {
}
func (s *MockServer) NewSession(ctx context.Context) (string, error) {
return "", nil
}
func (s *MockServer) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) {
return &types.SimulateTransactionResult{}, nil
}

View file

@ -0,0 +1,39 @@
package api
import (
"context"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
)
func TestAPI(t *testing.T) {
srv := rpc.NewServer()
builderAPI := NewServer(&nullSessionManager{})
srv.RegisterName("suavex", builderAPI)
c := NewClientFromRPC(rpc.DialInProc(srv))
res0, err := c.NewSession(context.Background())
require.NoError(t, err)
require.Equal(t, res0, "1")
txn := types.NewTransaction(0, common.Address{}, big.NewInt(1), 1, big.NewInt(1), []byte{})
_, err = c.AddTransaction(context.Background(), "1", txn)
require.NoError(t, err)
}
type nullSessionManager struct{}
func (n *nullSessionManager) NewSession() (string, error) {
return "1", nil
}
func (n *nullSessionManager) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) {
return &types.SimulateTransactionResult{Logs: []*types.SimulatedLog{}}, nil
}

77
suave/builder/builder.go Normal file
View file

@ -0,0 +1,77 @@
package builder
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
)
type builder struct {
config *builderConfig
txns []*types.Transaction
receipts []*types.Receipt
state *state.StateDB
gasPool *core.GasPool
gasUsed *uint64
}
type builderConfig struct {
preState *state.StateDB
header *types.Header
config *params.ChainConfig
context core.ChainContext
}
func newBuilder(config *builderConfig) *builder {
gp := core.GasPool(config.header.GasLimit)
var gasUsed uint64
return &builder{
config: config,
state: config.preState.Copy(),
gasPool: &gp,
gasUsed: &gasUsed,
}
}
func (b *builder) AddTransaction(txn *types.Transaction) (*types.SimulateTransactionResult, error) {
dummyAuthor := common.Address{}
vmConfig := vm.Config{
NoBaseFee: true,
}
snap := b.state.Snapshot()
b.state.SetTxContext(txn.Hash(), len(b.txns))
receipt, err := core.ApplyTransaction(b.config.config, b.config.context, &dummyAuthor, b.gasPool, b.state, b.config.header, txn, b.gasUsed, vmConfig)
if err != nil {
b.state.RevertToSnapshot(snap)
result := &types.SimulateTransactionResult{
Success: false,
Error: err.Error(),
}
return result, nil
}
b.txns = append(b.txns, txn)
b.receipts = append(b.receipts, receipt)
result := &types.SimulateTransactionResult{
Success: true,
Logs: []*types.SimulatedLog{},
}
for _, log := range receipt.Logs {
result.Logs = append(result.Logs, &types.SimulatedLog{
Addr: log.Address,
Topics: log.Topics,
Data: log.Data,
})
}
return result, nil
}

View file

@ -0,0 +1,122 @@
package builder
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"
)
func TestBuilder_AddTxn_Simple(t *testing.T) {
to := common.Address{0x01, 0x10, 0xab}
mock := newMockBuilder(t)
txn := mock.state.newTransfer(t, to, big.NewInt(1))
_, err := mock.builder.AddTransaction(txn)
require.NoError(t, err)
mock.expect(t, expectedResult{
txns: []*types.Transaction{
txn,
},
balances: map[common.Address]*big.Int{
to: big.NewInt(1),
},
})
}
func newMockBuilder(t *testing.T) *mockBuilder {
// create a dummy header at 0
header := &types.Header{
Number: big.NewInt(0),
GasLimit: 1000000000000,
Time: 1000,
Difficulty: big.NewInt(1),
}
mState := newMockState(t)
m := &mockBuilder{
state: mState,
}
stateRef, err := mState.stateAt(mState.stateRoot)
require.NoError(t, err)
config := &builderConfig{
header: header,
preState: stateRef,
config: mState.chainConfig,
context: m, // m implements ChainContext with panics
}
m.builder = newBuilder(config)
return m
}
type mockBuilder struct {
builder *builder
state *mockState
}
func (m *mockBuilder) Engine() consensus.Engine {
panic("TODO")
}
func (m *mockBuilder) GetHeader(common.Hash, uint64) *types.Header {
panic("TODO")
}
type expectedResult struct {
txns []*types.Transaction
balances map[common.Address]*big.Int
}
func (m *mockBuilder) expect(t *testing.T, res expectedResult) {
// validate txns
if len(res.txns) != len(m.builder.txns) {
t.Fatalf("expected %d txns, got %d", len(res.txns), len(m.builder.txns))
}
for indx, txn := range res.txns {
if txn.Hash() != m.builder.txns[indx].Hash() {
t.Fatalf("expected txn %d to be %s, got %s", indx, txn.Hash(), m.builder.txns[indx].Hash())
}
}
// The receipts must be the same as the txns
if len(res.txns) != len(m.builder.receipts) {
t.Fatalf("expected %d receipts, got %d", len(res.txns), len(m.builder.receipts))
}
for indx, txn := range res.txns {
if txn.Hash() != m.builder.receipts[indx].TxHash {
t.Fatalf("expected receipt %d to be %s, got %s", indx, txn.Hash(), m.builder.receipts[indx].TxHash)
}
}
// The gas left in the pool must be the header gas limit minus
// the total gas consumed by all the transactions in the block.
totalGasConsumed := uint64(0)
for _, receipt := range m.builder.receipts {
totalGasConsumed += receipt.GasUsed
}
if m.builder.gasPool.Gas() != m.builder.config.header.GasLimit-totalGasConsumed {
t.Fatalf("expected gas pool to be %d, got %d", m.builder.config.header.GasLimit-totalGasConsumed, m.builder.gasPool.Gas())
}
// The 'gasUsed' must match the total gas consumed by all the transactions
if *m.builder.gasUsed != totalGasConsumed {
t.Fatalf("expected gas used to be %d, got %d", totalGasConsumed, m.builder.gasUsed)
}
// The state must match the expected balances
for addr, expectedBalance := range res.balances {
balance := m.builder.state.GetBalance(addr)
if balance.Cmp(expectedBalance) != 0 {
t.Fatalf("expected balance of %s to be %d, got %d", addr, expectedBalance, balance)
}
}
}

View file

@ -0,0 +1,138 @@
package builder
import (
"fmt"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/google/uuid"
)
// blockchain is the minimum interface to the blockchain
// required to build a block
type blockchain interface {
core.ChainContext
// Header returns the current tip of the chain
CurrentHeader() *types.Header
// StateAt returns the state at the given root
StateAt(root common.Hash) (*state.StateDB, error)
// Config returns the chain config
Config() *params.ChainConfig
}
type Config struct {
GasCeil uint64
SessionIdleTimeout time.Duration
}
type SessionManager struct {
sessions map[string]*builder
sessionTimers map[string]*time.Timer
sessionsLock sync.RWMutex
blockchain blockchain
config *Config
}
func NewSessionManager(blockchain blockchain, config *Config) *SessionManager {
if config.GasCeil == 0 {
config.GasCeil = 1000000000000000000
}
if config.SessionIdleTimeout == 0 {
config.SessionIdleTimeout = 5 * time.Second
}
s := &SessionManager{
sessions: make(map[string]*builder),
sessionTimers: make(map[string]*time.Timer),
blockchain: blockchain,
config: config,
}
return s
}
// NewSession creates a new builder session and returns the session id
func (s *SessionManager) NewSession() (string, error) {
s.sessionsLock.Lock()
defer s.sessionsLock.Unlock()
parent := s.blockchain.CurrentHeader()
chainConfig := s.blockchain.Config()
header := &types.Header{
ParentHash: parent.Hash(),
Number: new(big.Int).Add(parent.Number, common.Big1),
GasLimit: core.CalcGasLimit(parent.GasLimit, s.config.GasCeil),
Time: 1000, // TODO: fix this
Coinbase: common.Address{}, // TODO: fix this
Difficulty: big.NewInt(1),
}
// Set baseFee and GasLimit if we are on an EIP-1559 chain
if chainConfig.IsLondon(header.Number) {
header.BaseFee = misc.CalcBaseFee(chainConfig, parent)
if !chainConfig.IsLondon(parent.Number) {
parentGasLimit := parent.GasLimit * chainConfig.ElasticityMultiplier()
header.GasLimit = core.CalcGasLimit(parentGasLimit, s.config.GasCeil)
}
}
stateRef, err := s.blockchain.StateAt(parent.Root)
if err != nil {
return "", err
}
cfg := &builderConfig{
preState: stateRef,
header: header,
config: s.blockchain.Config(),
context: s.blockchain,
}
id := uuid.New().String()[:7]
s.sessions[id] = newBuilder(cfg)
// start session timer
s.sessionTimers[id] = time.AfterFunc(s.config.SessionIdleTimeout, func() {
s.sessionsLock.Lock()
defer s.sessionsLock.Unlock()
delete(s.sessions, id)
delete(s.sessionTimers, id)
})
return id, nil
}
func (s *SessionManager) getSession(sessionId string) (*builder, error) {
s.sessionsLock.RLock()
defer s.sessionsLock.RUnlock()
session, ok := s.sessions[sessionId]
if !ok {
return nil, fmt.Errorf("session %s not found", sessionId)
}
// reset session timer
s.sessionTimers[sessionId].Reset(s.config.SessionIdleTimeout)
return session, nil
}
func (s *SessionManager) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) {
builder, err := s.getSession(sessionId)
if err != nil {
return nil, err
}
return builder.AddTransaction(tx)
}

View file

@ -0,0 +1,183 @@
package builder
import (
"crypto/ecdsa"
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)
func TestSessionManager_SessionTimeout(t *testing.T) {
mngr, _ := newSessionManager(t, &Config{
SessionIdleTimeout: 500 * time.Millisecond,
})
id, err := mngr.NewSession()
require.NoError(t, err)
time.Sleep(1 * time.Second)
_, err = mngr.getSession(id)
require.Error(t, err)
}
func TestSessionManager_SessionRefresh(t *testing.T) {
mngr, _ := newSessionManager(t, &Config{
SessionIdleTimeout: 500 * time.Millisecond,
})
id, err := mngr.NewSession()
require.NoError(t, err)
// if we query the session under the idle timeout,
// we should be able to refresh it
for i := 0; i < 5; i++ {
time.Sleep(250 * time.Millisecond)
_, err = mngr.getSession(id)
require.NoError(t, err)
}
// if we query the session after the idle timeout,
// we should get an error
time.Sleep(1 * time.Second)
_, err = mngr.getSession(id)
require.Error(t, err)
}
func TestSessionManager_StartSession(t *testing.T) {
// test that the session starts and it can simulate transactions
mngr, bMock := newSessionManager(t, &Config{})
id, err := mngr.NewSession()
require.NoError(t, err)
txn := bMock.state.newTransfer(t, common.Address{}, big.NewInt(1))
receipt, err := mngr.AddTransaction(id, txn)
require.NoError(t, err)
require.NotNil(t, receipt)
}
func newSessionManager(t *testing.T, cfg *Config) (*SessionManager, *blockchainMock) {
if cfg == nil {
cfg = &Config{}
}
state := newMockState(t)
bMock := &blockchainMock{
state: state,
}
return NewSessionManager(bMock, cfg), bMock
}
type blockchainMock struct {
state *mockState
}
func (b *blockchainMock) Engine() consensus.Engine {
panic("TODO")
}
func (b *blockchainMock) GetHeader(common.Hash, uint64) *types.Header {
panic("TODO")
}
func (b *blockchainMock) Config() *params.ChainConfig {
return b.state.chainConfig
}
func (b *blockchainMock) CurrentHeader() *types.Header {
return &types.Header{
Number: big.NewInt(1),
Difficulty: big.NewInt(1),
Root: b.state.stateRoot,
}
}
func (b *blockchainMock) StateAt(root common.Hash) (*state.StateDB, error) {
return b.state.stateAt(root)
}
type mockState struct {
stateRoot common.Hash
statedb state.Database
premineKey *ecdsa.PrivateKey
premineKeyAdd common.Address
nextNonce uint64 // figure out a better way
signer types.Signer
chainConfig *params.ChainConfig
}
func newMockState(t *testing.T) *mockState {
premineKey, _ := crypto.GenerateKey() // TODO: it would be nice to have it deterministic
premineKeyAddr := crypto.PubkeyToAddress(premineKey.PublicKey)
// create a state reference with at least one premined account
// In order to test the statedb in isolation, we are going
// to commit this pre-state to a memory database
db := state.NewDatabase(rawdb.NewMemoryDatabase())
preState, err := state.New(types.EmptyRootHash, db, nil)
require.NoError(t, err)
preState.AddBalance(premineKeyAddr, big.NewInt(1000000000000000000))
root, err := preState.Commit(true)
require.NoError(t, err)
// for the sake of this test, we only need all the forks enabled
chainConfig := params.SuaveChainConfig
// Disable london so that we do not check gasFeeCap (TODO: Fix)
chainConfig.LondonBlock = big.NewInt(100)
return &mockState{
statedb: db,
stateRoot: root,
premineKey: premineKey,
premineKeyAdd: premineKeyAddr,
signer: types.NewEIP155Signer(chainConfig.ChainID),
chainConfig: chainConfig,
}
}
func (m *mockState) stateAt(root common.Hash) (*state.StateDB, error) {
return state.New(root, m.statedb, nil)
}
func (m *mockState) getNonce() uint64 {
next := m.nextNonce
m.nextNonce++
return next
}
func (m *mockState) newTransfer(t *testing.T, to common.Address, amount *big.Int) *types.Transaction {
tx := types.NewTransaction(m.getNonce(), to, amount, 1000000, big.NewInt(1), nil)
return m.newTxn(t, tx)
}
func (m *mockState) newTxn(t *testing.T, tx *types.Transaction) *types.Transaction {
// sign the transaction
signature, err := crypto.Sign(m.signer.Hash(tx).Bytes(), m.premineKey)
require.NoError(t, err)
// include the signature in the transaction
tx, err = tx.WithSignature(m.signer, signature)
require.NoError(t, err)
return tx
}

9
suave/config.go Normal file
View file

@ -0,0 +1,9 @@
package suave
type Config struct {
Enabled bool
}
var DefaultConfig = Config{
Enabled: false,
}