From 28a45e3492f4823b48f2f97dc6387d88adc4d4c1 Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Tue, 16 Jan 2024 16:06:59 -0500 Subject: [PATCH 01/17] add suave package and builder --- cmd/geth/main.go | 4 + cmd/utils/flags.go | 21 +- core/types/sbundle.go | 95 +++++++++ core/types/suave_structs.go | 32 +++ eth/api_backend.go | 8 + eth/backend.go | 15 ++ internal/ethapi/api_test.go | 19 ++ internal/ethapi/backend.go | 4 + internal/ethapi/transaction_args_test.go | 12 ++ internal/flags/categories.go | 1 + miner/miner.go | 9 + miner/worker.go | 235 ++++++++++++++++++++++- suave/builder/api/api.go | 12 ++ suave/builder/api/api_client.go | 42 ++++ suave/builder/api/api_server.go | 43 +++++ suave/builder/api/api_test.go | 39 ++++ suave/builder/builder.go | 77 ++++++++ suave/builder/builder_test.go | 122 ++++++++++++ suave/builder/session_manager.go | 138 +++++++++++++ suave/builder/session_manager_test.go | 183 ++++++++++++++++++ suave/config.go | 9 + 21 files changed, 1115 insertions(+), 5 deletions(-) create mode 100644 core/types/sbundle.go create mode 100644 core/types/suave_structs.go create mode 100644 suave/builder/api/api.go create mode 100644 suave/builder/api/api_client.go create mode 100644 suave/builder/api/api_server.go create mode 100644 suave/builder/api/api_test.go create mode 100644 suave/builder/builder.go create mode 100644 suave/builder/builder_test.go create mode 100644 suave/builder/session_manager.go create mode 100644 suave/builder/session_manager_test.go create mode 100644 suave/config.go diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 4438cef560a1..f55bc8ed29c9 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -196,6 +196,9 @@ var ( utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, } + suaveApiFlags = []cli.Flag{ + utils.SuaveEnabled, + } ) var app = flags.NewApp("the go-ethereum command line interface") @@ -245,6 +248,7 @@ func init() { consoleFlags, debug.Flags, metricsFlags, + suaveApiFlags, ) flags.AutoEnvVars(app.Flags, "GETH") diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 159c47ca0191..5d9f9fa55544 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -69,6 +69,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/suave" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/triedb/hashdb" "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, Category: flags.MetricsCategory, } + + // SUAVE namespace rpc settings + SuaveEnabled = &cli.BoolFlag{ + Name: "suave", + Usage: "Enable the suave", + Category: flags.SuaveCategory, + } ) 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. func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { SetP2PConfig(ctx, &cfg.P2P) @@ -1859,11 +1871,18 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) { // RegisterEthService adds an Ethereum client to the stack. // 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) if err != nil { 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)) return backend.APIBackend, backend } diff --git a/core/types/sbundle.go b/core/types/sbundle.go new file mode 100644 index 000000000000..c88dd4f3bc2e --- /dev/null +++ b/core/types/sbundle.go @@ -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"` +} diff --git a/core/types/suave_structs.go b/core/types/suave_structs.go new file mode 100644 index 000000000000..0d9bab5fec13 --- /dev/null +++ b/core/types/suave_structs.go @@ -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 +} diff --git a/eth/api_backend.go b/eth/api_backend.go index bc8398d217a1..d4148ee11a5e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -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) { 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) +} diff --git a/eth/backend.go b/eth/backend.go index 774ffaf24877..cb410d365516 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -57,6 +57,8 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "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. @@ -309,6 +311,19 @@ func makeExtraData(extra []byte) []byte { func (s *Ethereum) APIs() []rpc.API { 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 apis = append(apis, s.engine.APIs(s.BlockChain())...) diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index c2490ac70315..831b865a0381 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -48,6 +48,7 @@ import ( "github.com/ethereum/go-ethereum/internal/blocktest" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" "github.com/holiman/uint256" "github.com/stretchr/testify/require" "golang.org/x/exp/slices" @@ -597,6 +598,24 @@ func (b testBackend) ServiceFilter(ctx context.Context, session *bloombits.Match 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) { t.Parallel() // Initialize test accounts diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 50f338f5cab3..48ba0e7fd71a 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -97,6 +97,10 @@ type Backend interface { SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription BloomStatus() (uint64, uint64) 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 { diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index ab7c2f70edfa..7c99cff14805 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -365,3 +365,15 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) } 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 +} diff --git a/internal/flags/categories.go b/internal/flags/categories.go index 3ff0767921b9..4373f7e97424 100644 --- a/internal/flags/categories.go +++ b/internal/flags/categories.go @@ -37,6 +37,7 @@ const ( MiscCategory = "MISC" TestingCategory = "TESTING" DeprecatedCategory = "ALIASED (deprecated)" + SuaveCategory = "SUAVE" ) func init() { diff --git a/miner/miner.go b/miner/miner.go index b7273948f5e7..20bade3da2f6 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -18,6 +18,7 @@ package miner import ( + "context" "fmt" "math/big" "sync" @@ -244,3 +245,11 @@ func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscript func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) { 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) +} diff --git a/miner/worker.go b/miner/worker.go index 2ed91cc18781..6282aecc5b1b 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -17,6 +17,7 @@ package miner import ( + "context" "errors" "fmt" "math/big" @@ -33,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -888,11 +890,13 @@ func (w *worker) commitTransactions(env *environment, txs *transactionsByPriceAn // generateParams wraps various of settings for generating sealing task. type generateParams struct { - timestamp uint64 // The timstamp for sealing task - forceTime bool // Flag whether the given timestamp is immutable or not - parentHash common.Hash // Parent block hash, empty means the latest chain head - coinbase common.Address // The fee recipient address for including transaction + timestamp uint64 // The timstamp for sealing task + forceTime bool // Flag whether the given timestamp is immutable or not + parentHash common.Hash // Parent block hash, empty means the latest chain head + 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 + extra []byte // The extra data to include in the block header withdrawals types.Withdrawals // List of withdrawals to include in block. beaconRoot *common.Hash // The beacon root (cancun field). 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)) } } + +// 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 +} diff --git a/suave/builder/api/api.go b/suave/builder/api/api.go new file mode 100644 index 000000000000..b11d237ba2c9 --- /dev/null +++ b/suave/builder/api/api.go @@ -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) +} diff --git a/suave/builder/api/api_client.go b/suave/builder/api/api_client.go new file mode 100644 index 000000000000..3baefde531dd --- /dev/null +++ b/suave/builder/api/api_client.go @@ -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 +} diff --git a/suave/builder/api/api_server.go b/suave/builder/api/api_server.go new file mode 100644 index 000000000000..7eb6daf795c4 --- /dev/null +++ b/suave/builder/api/api_server.go @@ -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 +} diff --git a/suave/builder/api/api_test.go b/suave/builder/api/api_test.go new file mode 100644 index 000000000000..51122eef0289 --- /dev/null +++ b/suave/builder/api/api_test.go @@ -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 +} diff --git a/suave/builder/builder.go b/suave/builder/builder.go new file mode 100644 index 000000000000..0d5c30ce9ecf --- /dev/null +++ b/suave/builder/builder.go @@ -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 +} diff --git a/suave/builder/builder_test.go b/suave/builder/builder_test.go new file mode 100644 index 000000000000..0ceae209ae18 --- /dev/null +++ b/suave/builder/builder_test.go @@ -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) + } + } +} diff --git a/suave/builder/session_manager.go b/suave/builder/session_manager.go new file mode 100644 index 000000000000..b3e740cbd4a5 --- /dev/null +++ b/suave/builder/session_manager.go @@ -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) +} diff --git a/suave/builder/session_manager_test.go b/suave/builder/session_manager_test.go new file mode 100644 index 000000000000..8d9dbaef4fad --- /dev/null +++ b/suave/builder/session_manager_test.go @@ -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 +} diff --git a/suave/config.go b/suave/config.go new file mode 100644 index 000000000000..2490c60c6199 --- /dev/null +++ b/suave/config.go @@ -0,0 +1,9 @@ +package suave + +type Config struct { + Enabled bool +} + +var DefaultConfig = Config{ + Enabled: false, +} From 2786f36e042281e71729efc187cd523b43ed3713 Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Tue, 16 Jan 2024 16:49:13 -0500 Subject: [PATCH 02/17] working builder tests --- cmd/geth/main.go | 4 - cmd/utils/flags.go | 21 +---- core/types/suave_structs.go | 21 +++++ eth/api_backend.go | 19 ++++ eth/backend.go | 1 + suave/backends/eth_backend_server.go | 89 +++++++++++++++++++ suave/backends/eth_backend_server_test.go | 58 +++++++++++++ suave/backends/eth_backends.go | 100 ++++++++++++++++++++++ suave/builder/session_manager.go | 45 +++++++++- suave/builder/session_manager_test.go | 4 +- suave/{ => core}/config.go | 0 suave/core/types.go | 50 +++++++++++ 12 files changed, 384 insertions(+), 28 deletions(-) create mode 100644 suave/backends/eth_backend_server.go create mode 100644 suave/backends/eth_backend_server_test.go create mode 100644 suave/backends/eth_backends.go rename suave/{ => core}/config.go (100%) create mode 100644 suave/core/types.go diff --git a/cmd/geth/main.go b/cmd/geth/main.go index f55bc8ed29c9..4438cef560a1 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -196,9 +196,6 @@ var ( utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, } - suaveApiFlags = []cli.Flag{ - utils.SuaveEnabled, - } ) var app = flags.NewApp("the go-ethereum command line interface") @@ -248,7 +245,6 @@ func init() { consoleFlags, debug.Flags, metricsFlags, - suaveApiFlags, ) flags.AutoEnvVars(app.Flags, "GETH") diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 5d9f9fa55544..159c47ca0191 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -69,7 +69,6 @@ import ( "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/suave" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/triedb/hashdb" "github.com/ethereum/go-ethereum/trie/triedb/pathdb" @@ -909,13 +908,6 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Value: metrics.DefaultConfig.InfluxDBOrganization, Category: flags.MetricsCategory, } - - // SUAVE namespace rpc settings - SuaveEnabled = &cli.BoolFlag{ - Name: "suave", - Usage: "Enable the suave", - Category: flags.SuaveCategory, - } ) var ( @@ -1352,10 +1344,6 @@ 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. func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { SetP2PConfig(ctx, &cfg.P2P) @@ -1871,18 +1859,11 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) { // RegisterEthService adds an Ethereum client to the stack. // The second return value is the full node instance. -func RegisterEthService(stack *node.Node, cfg *ethconfig.Config, suaveConfig *suave.Config) (ethapi.Backend, *eth.Ethereum) { +func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) { backend, err := eth.New(stack, cfg) if err != nil { 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)) return backend.APIBackend, backend } diff --git a/core/types/suave_structs.go b/core/types/suave_structs.go index 0d9bab5fec13..849acfcf9b0d 100644 --- a/core/types/suave_structs.go +++ b/core/types/suave_structs.go @@ -30,3 +30,24 @@ type DataRecord struct { AllowedStores []common.Address Version string } + +type HttpRequest struct { + Url string + Method string + Headers []string + Body []byte + WithFlashbotsSignature bool +} + +type SimulateTransactionResult struct { + Egp uint64 + Logs []*SimulatedLog + Success bool + Error string +} + +type SimulatedLog struct { + Data []byte + Addr common.Address + Topics []common.Hash +} \ No newline at end of file diff --git a/eth/api_backend.go b/eth/api_backend.go index d4148ee11a5e..63469ca76690 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/bloombits" @@ -37,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" @@ -423,3 +425,20 @@ func (b *EthAPIBackend) BuildBlockFromTxs(ctx context.Context, buildArgs *types. 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) } + +func (b *EthAPIBackend) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) { + // Note: this is pretty close to be a circle dependency. + data := hexutil.Bytes(input) + txnArgs := ethapi.TransactionArgs{ + To: &contractAddr, + Data: &data, + } + + blockNum := rpc.LatestBlockNumber + res, err := ethapi.DoCall(ctx, b, txnArgs, rpc.BlockNumberOrHash{BlockNumber: &blockNum}, nil, nil, 5*time.Second, 100000) + if err != nil { + return nil, err + } + + return res.ReturnData, nil +} diff --git a/eth/backend.go b/eth/backend.go index cb410d365516..327e0094e78a 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -57,6 +57,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/suave/backends" suave_builder "github.com/ethereum/go-ethereum/suave/builder" suave_builder_api "github.com/ethereum/go-ethereum/suave/builder/api" ) diff --git a/suave/backends/eth_backend_server.go b/suave/backends/eth_backend_server.go new file mode 100644 index 000000000000..571f4a4eddce --- /dev/null +++ b/suave/backends/eth_backend_server.go @@ -0,0 +1,89 @@ +package backends + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + suave "github.com/ethereum/go-ethereum/suave/core" +) + +// EthBackend is the set of functions exposed from the SUAVE-enabled node +type EthBackend interface { + BuildEthBlock(ctx context.Context, buildArgs *types.BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) + BuildEthBlockFromBundles(ctx context.Context, buildArgs *types.BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error) + Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) +} + +var _ EthBackend = &EthBackendServer{} + +// EthBackendServerBackend is the interface implemented by the SUAVE-enabled node +// to resolve the EthBackend server queries +type EthBackendServerBackend interface { + CurrentHeader() *types.Header + BuildBlockFromTxs(ctx context.Context, buildArgs *suave.BuildBlockArgs, txs types.Transactions) (*types.Block, *big.Int, error) + BuildBlockFromBundles(ctx context.Context, buildArgs *suave.BuildBlockArgs, bundles []types.SBundle) (*types.Block, *big.Int, error) + Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) +} + +type EthBackendServer struct { + b EthBackendServerBackend +} + +func NewEthBackendServer(b EthBackendServerBackend) *EthBackendServer { + return &EthBackendServer{b} +} + +func (e *EthBackendServer) BuildEthBlock(ctx context.Context, buildArgs *types.BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) { + if buildArgs == nil { + head := e.b.CurrentHeader() + buildArgs = &types.BuildBlockArgs{ + Parent: head.Hash(), + Timestamp: head.Time + uint64(12), + FeeRecipient: common.Address{0x42}, + GasLimit: 30000000, + Random: head.Root, + Withdrawals: nil, + Extra: []byte(""), + FillPending: false, + } + } + + block, profit, err := e.b.BuildBlockFromTxs(ctx, buildArgs, txs) + if err != nil { + return nil, err + } + + // TODO: we're not adding blobs, but this is not where you would do it anyways + return engine.BlockToExecutableData(block, profit, nil), nil +} + +func (e *EthBackendServer) BuildEthBlockFromBundles(ctx context.Context, buildArgs *types.BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error) { + if buildArgs == nil { + head := e.b.CurrentHeader() + buildArgs = &types.BuildBlockArgs{ + Parent: head.Hash(), + Timestamp: head.Time + uint64(12), + FeeRecipient: common.Address{0x42}, + GasLimit: 30000000, + Random: head.Root, + Withdrawals: nil, + Extra: []byte(""), + FillPending: false, + } + } + + block, profit, err := e.b.BuildBlockFromBundles(ctx, buildArgs, bundles) + if err != nil { + return nil, err + } + + // TODO: we're not adding blobs, but this is not where you would do it anyways + return engine.BlockToExecutableData(block, profit, nil), nil +} + +func (e *EthBackendServer) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) { + return e.b.Call(ctx, contractAddr, input) +} diff --git a/suave/backends/eth_backend_server_test.go b/suave/backends/eth_backend_server_test.go new file mode 100644 index 000000000000..60bc0d70b64b --- /dev/null +++ b/suave/backends/eth_backend_server_test.go @@ -0,0 +1,58 @@ +package backends + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/core/types" + suave "github.com/ethereum/go-ethereum/suave/core" +) + +func TestEthBackend_Compatibility(t *testing.T) { + // This test ensures that the client is able to call to the server. + // It does not cover the internal logic implemention of the endpoints. + srv := rpc.NewServer() + require.NoError(t, srv.RegisterName("suavex", NewEthBackendServer(&mockBackend{}))) + + clt := &RemoteEthBackend{client: rpc.DialInProc(srv)} + + _, err := clt.BuildEthBlock(context.Background(), &types.BuildBlockArgs{}, nil) + require.NoError(t, err) + + _, err = clt.BuildEthBlockFromBundles(context.Background(), &types.BuildBlockArgs{}, nil) + require.NoError(t, err) + + _, err = clt.Call(context.Background(), common.Address{}, nil) + require.NoError(t, err) +} + +// mockBackend is a backend for the EthBackendServer that returns mock data +type mockBackend struct{} + +func (n *mockBackend) CurrentHeader() *types.Header { + return &types.Header{} +} + +func (n *mockBackend) BuildBlockFromTxs(ctx context.Context, buildArgs *suave.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 *mockBackend) BuildBlockFromBundles(ctx context.Context, buildArgs *suave.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 *mockBackend) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) { + return []byte{0x1}, nil +} diff --git a/suave/backends/eth_backends.go b/suave/backends/eth_backends.go new file mode 100644 index 000000000000..6f3d21742ac2 --- /dev/null +++ b/suave/backends/eth_backends.go @@ -0,0 +1,100 @@ +package backends + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + builder "github.com/ethereum/go-ethereum/suave/builder/api" + suave "github.com/ethereum/go-ethereum/suave/core" + "github.com/ethereum/go-ethereum/trie" +) + +var ( + _ EthBackend = &EthMock{} + _ EthBackend = &RemoteEthBackend{} +) + +type EthMock struct { + *builder.MockServer +} + +func (e *EthMock) BuildEthBlock(ctx context.Context, args *suave.BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) { + block := types.NewBlock(&types.Header{GasUsed: 1000}, txs, nil, nil, trie.NewStackTrie(nil)) + return engine.BlockToExecutableData(block, big.NewInt(11000), nil), nil +} + +func (e *EthMock) BuildEthBlockFromBundles(ctx context.Context, args *suave.BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error) { + var txs types.Transactions + for _, bundle := range bundles { + txs = append(txs, bundle.Txs...) + } + block := types.NewBlock(&types.Header{GasUsed: 1000}, txs, nil, nil, trie.NewStackTrie(nil)) + return engine.BlockToExecutableData(block, big.NewInt(11000), nil), nil +} + +func (e *EthMock) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) { + return nil, nil +} + +type RemoteEthBackend struct { + endpoint string + client *rpc.Client + + *builder.APIClient +} + +func NewRemoteEthBackend(endpoint string) *RemoteEthBackend { + r := &RemoteEthBackend{ + endpoint: endpoint, + } + + r.APIClient = builder.NewClientFromRPC(r) + return r +} + +func (e *RemoteEthBackend) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + if e.client == nil { + // should lock + var err error + client, err := rpc.DialContext(ctx, e.endpoint) + if err != nil { + return err + } + e.client = client + } + + err := e.client.CallContext(ctx, &result, method, args...) + if err != nil { + client := e.client + e.client = nil + client.Close() + return err + } + + return nil +} + +func (e *RemoteEthBackend) BuildEthBlock(ctx context.Context, args *suave.BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) { + var result engine.ExecutionPayloadEnvelope + err := e.CallContext(ctx, &result, "suavex_buildEthBlock", args, txs) + + return &result, err +} + +func (e *RemoteEthBackend) BuildEthBlockFromBundles(ctx context.Context, args *suave.BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error) { + var result engine.ExecutionPayloadEnvelope + err := e.CallContext(ctx, &result, "suavex_buildEthBlockFromBundles", args, bundles) + + return &result, err +} + +func (e *RemoteEthBackend) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) { + var result []byte + err := e.CallContext(ctx, &result, "suavex_call", contractAddr, input) + + return result, err +} diff --git a/suave/builder/session_manager.go b/suave/builder/session_manager.go index b3e740cbd4a5..2ad4d7376b84 100644 --- a/suave/builder/session_manager.go +++ b/suave/builder/session_manager.go @@ -7,7 +7,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -80,7 +80,7 @@ func (s *SessionManager) NewSession() (string, error) { // Set baseFee and GasLimit if we are on an EIP-1559 chain if chainConfig.IsLondon(header.Number) { - header.BaseFee = misc.CalcBaseFee(chainConfig, parent) + header.BaseFee = CalcBaseFee(chainConfig, parent) if !chainConfig.IsLondon(parent.Number) { parentGasLimit := parent.GasLimit * chainConfig.ElasticityMultiplier() header.GasLimit = core.CalcGasLimit(parentGasLimit, s.config.GasCeil) @@ -136,3 +136,44 @@ func (s *SessionManager) AddTransaction(sessionId string, tx *types.Transaction) } return builder.AddTransaction(tx) } + +// CalcBaseFee calculates the basefee of the header. +func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { + // If the current block is the first EIP-1559 block, return the InitialBaseFee. + if !config.IsLondon(parent.Number) { + return new(big.Int).SetUint64(params.InitialBaseFee) + } + + parentGasTarget := parent.GasLimit / config.ElasticityMultiplier() + // If the parent gasUsed is the same as the target, the baseFee remains unchanged. + if parent.GasUsed == parentGasTarget { + return new(big.Int).Set(parent.BaseFee) + } + + var ( + num = new(big.Int) + denom = new(big.Int) + ) + + if parent.GasUsed > parentGasTarget { + // If the parent block used more gas than its target, the baseFee should increase. + // max(1, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) + num.SetUint64(parent.GasUsed - parentGasTarget) + num.Mul(num, parent.BaseFee) + num.Div(num, denom.SetUint64(parentGasTarget)) + num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator())) + baseFeeDelta := math.BigMax(num, common.Big1) + + return num.Add(parent.BaseFee, baseFeeDelta) + } else { + // Otherwise if the parent block used less gas than its target, the baseFee should decrease. + // max(0, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) + num.SetUint64(parentGasTarget - parent.GasUsed) + num.Mul(num, parent.BaseFee) + num.Div(num, denom.SetUint64(parentGasTarget)) + num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator())) + baseFee := num.Sub(parent.BaseFee, num) + + return math.BigMax(baseFee, common.Big0) + } +} diff --git a/suave/builder/session_manager_test.go b/suave/builder/session_manager_test.go index 8d9dbaef4fad..cdd20a1b4346 100644 --- a/suave/builder/session_manager_test.go +++ b/suave/builder/session_manager_test.go @@ -136,11 +136,11 @@ func newMockState(t *testing.T) *mockState { preState.AddBalance(premineKeyAddr, big.NewInt(1000000000000000000)) - root, err := preState.Commit(true) + root, err := preState.Commit(1, true) require.NoError(t, err) // for the sake of this test, we only need all the forks enabled - chainConfig := params.SuaveChainConfig + chainConfig := params.TestChainConfig // Disable london so that we do not check gasFeeCap (TODO: Fix) chainConfig.LondonBlock = big.NewInt(100) diff --git a/suave/config.go b/suave/core/config.go similarity index 100% rename from suave/config.go rename to suave/core/config.go diff --git a/suave/core/types.go b/suave/core/types.go new file mode 100644 index 000000000000..6071d84db522 --- /dev/null +++ b/suave/core/types.go @@ -0,0 +1,50 @@ +package suave + +import ( + "context" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + builder "github.com/ethereum/go-ethereum/suave/builder/api" +) + +var AllowedPeekerAny = common.HexToAddress("0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829") // "*" + +type Bytes = hexutil.Bytes +type DataId = types.DataId + +type DataRecord struct { + Id types.DataId + Salt types.DataId + DecryptionCondition uint64 + AllowedPeekers []common.Address + AllowedStores []common.Address + Version string + CreationTx *types.Transaction + Signature []byte +} + +func (b *DataRecord) ToInnerRecord() types.DataRecord { + return types.DataRecord{ + Id: b.Id, + Salt: b.Salt, + DecryptionCondition: b.DecryptionCondition, + AllowedPeekers: b.AllowedPeekers, + AllowedStores: b.AllowedStores, + Version: b.Version, + } +} + +type MEVMBid = types.DataRecord + +type BuildBlockArgs = types.BuildBlockArgs + +type ConfidentialEthBackend interface { + BuildEthBlock(ctx context.Context, args *BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) + BuildEthBlockFromBundles(ctx context.Context, args *BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error) + Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) + + builder.API +} From 3de38ffe01267d4aca3fd2728c2f71833faaec43 Mon Sep 17 00:00:00 2001 From: dmarz Date: Mon, 22 Jan 2024 09:53:14 -0500 Subject: [PATCH 03/17] enable suavex by default (#1) --- cmd/geth/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 5f52f1df5442..d1cde3766e2f 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -117,7 +117,7 @@ func defaultNodeConfig() node.Config { cfg := node.DefaultConfig cfg.Name = clientIdentifier cfg.Version = params.VersionWithCommit(git.Commit, git.Date) - cfg.HTTPModules = append(cfg.HTTPModules, "eth") + cfg.HTTPModules = append(cfg.HTTPModules, "eth", "suavex") cfg.WSModules = append(cfg.WSModules, "eth") cfg.IPCPath = "geth.ipc" return cfg From ae2ff276a1bbc16e741947ce1b208b8da996b675 Mon Sep 17 00:00:00 2001 From: dmarz Date: Mon, 22 Jan 2024 09:53:22 -0500 Subject: [PATCH 04/17] Readme (#2) * add description to ReadMe * typo --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d6bc1af05ce8..17cd1cf69cfa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +`suave-execution-geth` is a minimal fork of geth which implements the [SUAVE Execution Namespace](suave-execution-geth). The namespace is turned on by default for http and ipc. + ## Go Ethereum Official Golang execution layer implementation of the Ethereum protocol. From d4817ad29d01fe06a1a77fd9b314a85c57133ac1 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Mon, 22 Jan 2024 16:06:09 -0500 Subject: [PATCH 05/17] Export api.SessionManager. --- suave/builder/api/api_server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/suave/builder/api/api_server.go b/suave/builder/api/api_server.go index 7eb6daf795c4..9ed45465fe1b 100644 --- a/suave/builder/api/api_server.go +++ b/suave/builder/api/api_server.go @@ -6,13 +6,13 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) -// sessionManager is the backend that manages the session state of the builder API. -type sessionManager interface { +// 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 { +func NewServer(s SessionManager) *Server { api := &Server{ sessionMngr: s, } @@ -20,7 +20,7 @@ func NewServer(s sessionManager) *Server { } type Server struct { - sessionMngr sessionManager + sessionMngr SessionManager } func (s *Server) NewSession(ctx context.Context) (string, error) { From 2dfac08e80743022601d3d8928202dbceb54b491 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Mon, 22 Jan 2024 16:22:37 -0500 Subject: [PATCH 06/17] Hack: use a semaphore channel to limit the number of open sessions. This is a hack because we're relying on session timeout behavior. Recommend ref-counting API that releases refs when block-height changes. --- suave/builder/api/api_server.go | 4 +-- suave/builder/api/api_test.go | 6 ++-- suave/builder/session_manager.go | 37 +++++++++++++++++++---- suave/builder/session_manager_test.go | 42 +++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/suave/builder/api/api_server.go b/suave/builder/api/api_server.go index 9ed45465fe1b..4b8336ed0688 100644 --- a/suave/builder/api/api_server.go +++ b/suave/builder/api/api_server.go @@ -8,7 +8,7 @@ import ( // SessionManager is the backend that manages the session state of the builder API. type SessionManager interface { - NewSession() (string, error) + NewSession(context.Context) (string, error) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) } @@ -24,7 +24,7 @@ type Server struct { } func (s *Server) NewSession(ctx context.Context) (string, error) { - return s.sessionMngr.NewSession() + return s.sessionMngr.NewSession(ctx) } func (s *Server) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { diff --git a/suave/builder/api/api_test.go b/suave/builder/api/api_test.go index 51122eef0289..f9d795033cd3 100644 --- a/suave/builder/api/api_test.go +++ b/suave/builder/api/api_test.go @@ -30,10 +30,10 @@ func TestAPI(t *testing.T) { type nullSessionManager struct{} -func (n *nullSessionManager) NewSession() (string, error) { - return "1", nil +func (nullSessionManager) NewSession(ctx context.Context) (string, error) { + return "1", ctx.Err() } -func (n *nullSessionManager) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { +func (nullSessionManager) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { return &types.SimulateTransactionResult{Logs: []*types.SimulatedLog{}}, nil } diff --git a/suave/builder/session_manager.go b/suave/builder/session_manager.go index 2ad4d7376b84..905241a5bd45 100644 --- a/suave/builder/session_manager.go +++ b/suave/builder/session_manager.go @@ -1,6 +1,7 @@ package builder import ( + "context" "fmt" "math/big" "sync" @@ -31,11 +32,13 @@ type blockchain interface { } type Config struct { - GasCeil uint64 - SessionIdleTimeout time.Duration + GasCeil uint64 + SessionIdleTimeout time.Duration + MaxConcurrentSessions int } type SessionManager struct { + sem chan struct{} sessions map[string]*builder sessionTimers map[string]*time.Timer sessionsLock sync.RWMutex @@ -50,8 +53,17 @@ func NewSessionManager(blockchain blockchain, config *Config) *SessionManager { if config.SessionIdleTimeout == 0 { config.SessionIdleTimeout = 5 * time.Second } + if config.MaxConcurrentSessions <= 0 { + config.MaxConcurrentSessions = 16 // chosen arbitrarily + } + + sem := make(chan struct{}, config.MaxConcurrentSessions) + for len(sem) < cap(sem) { + sem <- struct{}{} // fill 'er up + } s := &SessionManager{ + sem: sem, sessions: make(map[string]*builder), sessionTimers: make(map[string]*time.Timer), blockchain: blockchain, @@ -61,12 +73,17 @@ func NewSessionManager(blockchain blockchain, config *Config) *SessionManager { } // NewSession creates a new builder session and returns the session id -func (s *SessionManager) NewSession() (string, error) { - s.sessionsLock.Lock() - defer s.sessionsLock.Unlock() +func (s *SessionManager) NewSession(ctx context.Context) (string, error) { + // Wait for session to become available + select { + case <-s.sem: + s.sessionsLock.Lock() + defer s.sessionsLock.Unlock() + case <-ctx.Done(): + return "", ctx.Err() + } parent := s.blockchain.CurrentHeader() - chainConfig := s.blockchain.Config() header := &types.Header{ @@ -111,6 +128,14 @@ func (s *SessionManager) NewSession() (string, error) { delete(s.sessionTimers, id) }) + // Technically, we are certain that there is an open slot in the semaphore + // channel, but let's be defensive and panic if the invariant is violated. + select { + case s.sem <- struct{}{}: + default: + panic("released more sessions than are open") // unreachable + } + return id, nil } diff --git a/suave/builder/session_manager_test.go b/suave/builder/session_manager_test.go index cdd20a1b4346..d09d9dfc8845 100644 --- a/suave/builder/session_manager_test.go +++ b/suave/builder/session_manager_test.go @@ -1,6 +1,7 @@ package builder import ( + "context" "crypto/ecdsa" "math/big" "testing" @@ -21,7 +22,7 @@ func TestSessionManager_SessionTimeout(t *testing.T) { SessionIdleTimeout: 500 * time.Millisecond, }) - id, err := mngr.NewSession() + id, err := mngr.NewSession(context.TODO()) require.NoError(t, err) time.Sleep(1 * time.Second) @@ -30,12 +31,47 @@ func TestSessionManager_SessionTimeout(t *testing.T) { require.Error(t, err) } +func TestSessionManager_MaxConcurrentSessions(t *testing.T) { + t.Parallel() + + const d = time.Millisecond * 10 + + mngr, _ := newSessionManager(t, &Config{ + MaxConcurrentSessions: 1, + SessionIdleTimeout: d, + }) + + t.Run("SessionAvailable", func(t *testing.T) { + sess, err := mngr.NewSession(context.TODO()) + require.NoError(t, err) + require.NotZero(t, sess) + }) + + t.Run("ContextExpired", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + sess, err := mngr.NewSession(ctx) + require.Zero(t, sess) + require.ErrorIs(t, err, context.Canceled) + }) + + t.Run("SessionExpired", func(t *testing.T) { + time.Sleep(d) // Wait for the session to expire. + + // We should be able to open a session again. + sess, err := mngr.NewSession(context.TODO()) + require.NoError(t, err) + require.NotZero(t, sess) + }) +} + func TestSessionManager_SessionRefresh(t *testing.T) { mngr, _ := newSessionManager(t, &Config{ SessionIdleTimeout: 500 * time.Millisecond, }) - id, err := mngr.NewSession() + id, err := mngr.NewSession(context.TODO()) require.NoError(t, err) // if we query the session under the idle timeout, @@ -60,7 +96,7 @@ 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() + id, err := mngr.NewSession(context.TODO()) require.NoError(t, err) txn := bMock.state.newTransfer(t, common.Address{}, big.NewInt(1)) From 9ed9948dc6ddd032fa59d70d0eed1437bb25acf2 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Mon, 22 Jan 2024 16:43:43 -0500 Subject: [PATCH 07/17] Fix flaky test. --- suave/builder/session_manager_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suave/builder/session_manager_test.go b/suave/builder/session_manager_test.go index d09d9dfc8845..c17c5ea4f051 100644 --- a/suave/builder/session_manager_test.go +++ b/suave/builder/session_manager_test.go @@ -34,7 +34,7 @@ func TestSessionManager_SessionTimeout(t *testing.T) { func TestSessionManager_MaxConcurrentSessions(t *testing.T) { t.Parallel() - const d = time.Millisecond * 10 + const d = time.Millisecond * 100 mngr, _ := newSessionManager(t, &Config{ MaxConcurrentSessions: 1, From 6fc4ead7ec3e35221b02b669922fc24e8f6aab0d Mon Sep 17 00:00:00 2001 From: Daniel Sukoneck Date: Wed, 24 Jan 2024 19:31:05 -0700 Subject: [PATCH 08/17] add image release workflow --- .github/CODEOWNERS | 26 ++------ .github/workflows/release.yml | 35 +++++++++++ .goreleaser.yaml | 111 ++++++++++++++++++++++++++++++++++ Dockerfile.suave | 7 +++ Makefile | 13 ++++ 5 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yaml create mode 100644 Dockerfile.suave diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index faf922df0161..7c5e34984ce9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,22 +1,4 @@ -# Lines starting with '#' are comments. -# Each line is a file pattern followed by one or more owners. - -accounts/usbwallet @karalabe -accounts/scwallet @gballet -accounts/abi @gballet @MariusVanDerWijden -cmd/clef @holiman -consensus @karalabe -core/ @karalabe @holiman @rjl493456442 -eth/ @karalabe @holiman @rjl493456442 -eth/catalyst/ @gballet -eth/tracers/ @s1na -graphql/ @s1na -les/ @zsfelfoldi @rjl493456442 -light/ @zsfelfoldi @rjl493456442 -node/ @fjl -p2p/ @fjl @zsfelfoldi -rpc/ @fjl @holiman -p2p/simulations @fjl -p2p/protocols @fjl -p2p/testing @fjl -signer/ @holiman +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# they will be requested for review when someone opens a pull request. +* @ferranbt @Ruteri @metachris @dmarzzz @lthibault diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..57cabb8a3c9b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +# .github/workflows/release.yml +name: release + +on: + workflow_dispatch: + push: + tags: + - "*" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: setup dependencies + uses: actions/setup-go@v2 + + - name: Login to Docker hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log tag name + run: echo "Build for tag ${{ github.ref_name }}" + + - name: Create release + run: make release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 000000000000..f04ae9991bc2 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,111 @@ +env: + - CGO_ENABLED=1 +builds: + - id: suave-execution-geth-darwin-amd64 + binary: suave-execution-geth + main: ./cmd/geth + goarch: + - amd64 + goos: + - darwin + env: + - CC=o64-clang + - CXX=o64-clang++ + flags: + - -trimpath + - id: suave-execution-geth-darwin-arm64 + binary: suave-execution-geth + main: ./cmd/geth + goarch: + - arm64 + goos: + - darwin + env: + - CC=oa64-clang + - CXX=oa64-clang++ + flags: + - -trimpath + - id: suave-execution-geth-linux-amd64 + binary: suave-execution-geth + main: ./cmd/geth + env: + - CC=x86_64-linux-gnu-gcc + - CXX=x86_64-linux-gnu-g++ + goarch: + - amd64 + goos: + - linux + flags: + - -trimpath + ldflags: + - -extldflags "-Wl,-z,stack-size=0x800000 --static" + tags: + - netgo + - osusergo + - id: suave-execution-geth-linux-arm64 + binary: suave-execution-geth + main: ./cmd/geth + goarch: + - arm64 + goos: + - linux + env: + - CC=aarch64-linux-gnu-gcc + - CXX=aarch64-linux-gnu-g++ + flags: + - -trimpath + ldflags: + - -extldflags "-Wl,-z,stack-size=0x800000 --static" + tags: + - netgo + - osusergo + - id: suave-execution-geth-windows-amd64 + binary: suave-execution-geth + main: ./cmd/geth + goarch: + - amd64 + goos: + - windows + env: + - CC=x86_64-w64-mingw32-gcc + - CXX=x86_64-w64-mingw32-g++ + flags: + - -trimpath + - -buildmode=exe + +archives: + - id: w/version + builds: + - suave-execution-geth-darwin-amd64 + - suave-execution-geth-darwin-arm64 + - suave-execution-geth-linux-amd64 + - suave-execution-geth-linux-arm64 + - suave-execution-geth-windows-amd64 + name_template: "suave-execution-geth_v{{ .Version }}_{{ .Os }}_{{ .Arch }}" + wrap_in_directory: false + format: zip + files: + - none* + +dockers: + - dockerfile: ./Dockerfile.suave + use: buildx + goarch: amd64 + goos: linux + build_flag_templates: + - --platform=linux/amd64 + image_templates: + - "flashbots/suave-execution-geth:{{ .ShortCommit }}" + - "flashbots/suave-execution-geth:{{ .Tag }}" + - "flashbots/suave-execution-geth:latest" + +checksum: + name_template: "checksums.txt" + +release: + draft: true + header: | + # 🚀 Features + # 🎄 Enhancements + # 🐞 Notable bug fixes + # 🎠 Community diff --git a/Dockerfile.suave b/Dockerfile.suave new file mode 100644 index 000000000000..b3b6f40619f2 --- /dev/null +++ b/Dockerfile.suave @@ -0,0 +1,7 @@ +FROM debian:bullseye +LABEL "org.opencontainers.image.source"="https://github.com/flashbots/suave-execution-geth" + +COPY ./suave-execution-geth /bin/ + +EXPOSE 8545 8546 30303 30303/udp +ENTRYPOINT ["suave-execution-geth"] diff --git a/Makefile b/Makefile index d736ef61c001..fbcbd804dd18 100644 --- a/Makefile +++ b/Makefile @@ -36,3 +36,16 @@ devtools: env GOBIN= go install ./cmd/abigen @type "solc" 2> /dev/null || echo 'Please install solc' @type "protoc" 2> /dev/null || echo 'Please install protoc' + +release: + docker run \ + --rm \ + -e CGO_ENABLED=1 \ + -e GITHUB_TOKEN="$(GITHUB_TOKEN)" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $(HOME)/.docker/config.json:/root/.docker/config.json \ + -v `pwd`:/go/src/$(PACKAGE_NAME) \ + -v `pwd`/sysroot:/sysroot \ + -w /go/src/$(PACKAGE_NAME) \ + ghcr.io/goreleaser/goreleaser-cross:v1.19.5 \ + release --clean From 7ff6877946a71455fbe24c473c2c0dc670e8d08d Mon Sep 17 00:00:00 2001 From: Daniel Sukoneck Date: Wed, 24 Jan 2024 19:38:54 -0700 Subject: [PATCH 09/17] fix secret names --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57cabb8a3c9b..8791cea6443d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,8 +22,8 @@ jobs: - name: Login to Docker hub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + username: ${{ secrets.FLASHBOTS_DOCKERHUB_USERNAME }} + password: ${{ secrets.FLASHBOTS_DOCKERHUB_TOKEN }} - name: Log tag name run: echo "Build for tag ${{ github.ref_name }}" From a3732e9a332ad892dcb778427f5a2f288bf9c441 Mon Sep 17 00:00:00 2001 From: Louis Thibault <9452561+lthibault@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:56:59 -0500 Subject: [PATCH 10/17] Port CI pipeline from suave-geth. --- .github/workflows/go.yml | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0c673d15f168..289508eb41d0 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,23 +1,29 @@ -name: i386 linux tests +name: Run unit tests on Linux on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] workflow_dispatch: +env: + CGO_CFLAGS_ALLOW: "-O -D__BLST_PORTABLE__" + CGO_CFLAGS: "-O -D__BLST_PORTABLE__" + jobs: build: - runs-on: self-hosted + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.20', '1.21.x' ] + goarch: [amd64, arm64] + steps: - - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v4 with: - go-version: 1.21.4 - - name: Run tests - run: go test -short ./... - env: - GOOS: linux - GOARCH: 386 + go-version: ${{ matrix.go-version }} + - name: Run unit tests + run: go test -short ./accounts ./cmd/geth ./core ./core/types ./core/vm ./eth/... ./internal/ethapi/... ./miner ./params ./suave/... From 2f5d16e33f909f1e9c8f521eeb97f10fea9c9d3e Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Thu, 25 Jan 2024 19:07:32 -0500 Subject: [PATCH 11/17] Fix failing test. --- cmd/geth/consolecmd_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go index ef6ef5f28836..0559c0566a1d 100644 --- a/cmd/geth/consolecmd_test.go +++ b/cmd/geth/consolecmd_test.go @@ -30,7 +30,7 @@ import ( ) const ( - ipcAPIs = "admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 rpc:1.0 txpool:1.0 web3:1.0" + ipcAPIs = "admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 rpc:1.0 suavex:1.0 txpool:1.0 web3:1.0" httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0" ) From a7b8207783f8d43dc9b9d8b4ffcb900bc7f76893 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Thu, 25 Jan 2024 19:13:00 -0500 Subject: [PATCH 12/17] Add 'suavex' namespace to WS interfaces (fixes test). --- cmd/geth/config.go | 2 +- cmd/geth/consolecmd_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index d1cde3766e2f..d29a8cfc540b 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -118,7 +118,7 @@ func defaultNodeConfig() node.Config { cfg.Name = clientIdentifier cfg.Version = params.VersionWithCommit(git.Commit, git.Date) cfg.HTTPModules = append(cfg.HTTPModules, "eth", "suavex") - cfg.WSModules = append(cfg.WSModules, "eth") + cfg.WSModules = append(cfg.WSModules, "eth", "suavex") cfg.IPCPath = "geth.ipc" return cfg } diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go index 0559c0566a1d..1e28265e9d4b 100644 --- a/cmd/geth/consolecmd_test.go +++ b/cmd/geth/consolecmd_test.go @@ -31,7 +31,7 @@ import ( const ( ipcAPIs = "admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 rpc:1.0 suavex:1.0 txpool:1.0 web3:1.0" - httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0" + httpAPIs = "eth:1.0 net:1.0 rpc:1.0 suavex:1.0 web3:1.0" ) // spawns geth with the given command line args, using a set of flags to minimise From 6eae2010d2645f3c607a6997fbb1131ae7800e4d Mon Sep 17 00:00:00 2001 From: Louis Thibault <9452561+lthibault@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:19:02 -0500 Subject: [PATCH 13/17] Update .github/workflows/go.yml Co-authored-by: Chris Hager --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 289508eb41d0..5106efda990a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ env: CGO_CFLAGS: "-O -D__BLST_PORTABLE__" jobs: - build: + test: runs-on: ubuntu-latest strategy: matrix: From 95276896dbe15bd0ca80bcce28c48398011d1ca5 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Fri, 26 Jan 2024 10:23:07 -0500 Subject: [PATCH 14/17] Rename go.yml to checks.yml --- .github/workflows/{go.yml => checks.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{go.yml => checks.yml} (100%) diff --git a/.github/workflows/go.yml b/.github/workflows/checks.yml similarity index 100% rename from .github/workflows/go.yml rename to .github/workflows/checks.yml From 130c2aa65477ca504e5c24d8c4625b2b38876c91 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Fri, 26 Jan 2024 10:24:24 -0500 Subject: [PATCH 15/17] Run CI on all PRs. --- .github/workflows/checks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5106efda990a..06ca109270be 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -4,7 +4,6 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] workflow_dispatch: env: From 2fd43f93d28724e2311fdf4fce8081caa4ae7098 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Fri, 26 Jan 2024 10:26:05 -0500 Subject: [PATCH 16/17] Test on default goarch. --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 06ca109270be..dced6220e3ae 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: go-version: [ '1.20', '1.21.x' ] - goarch: [amd64, arm64] + # goarch: [amd64, arm64] steps: - uses: actions/checkout@v4 From b34429aa1e7d54cb32e6ac0a3e5a73fcaeb22eda Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Wed, 31 Jan 2024 11:55:23 -0500 Subject: [PATCH 17/17] Use goreleaser-cross:v1.21 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fbcbd804dd18..d885443205c1 100644 --- a/Makefile +++ b/Makefile @@ -47,5 +47,5 @@ release: -v `pwd`:/go/src/$(PACKAGE_NAME) \ -v `pwd`/sysroot:/sysroot \ -w /go/src/$(PACKAGE_NAME) \ - ghcr.io/goreleaser/goreleaser-cross:v1.19.5 \ + ghcr.io/goreleaser/goreleaser-cross:v1.21 \ release --clean