mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
add suave package and builder
This commit is contained in:
parent
c66ca8bf7a
commit
28a45e3492
21 changed files with 1115 additions and 5 deletions
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
95
core/types/sbundle.go
Normal 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"`
|
||||||
|
}
|
||||||
32
core/types/suave_structs.go
Normal file
32
core/types/suave_structs.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())...)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
227
miner/worker.go
227
miner/worker.go
|
|
@ -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
12
suave/builder/api/api.go
Normal 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)
|
||||||
|
}
|
||||||
42
suave/builder/api/api_client.go
Normal file
42
suave/builder/api/api_client.go
Normal 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
|
||||||
|
}
|
||||||
43
suave/builder/api/api_server.go
Normal file
43
suave/builder/api/api_server.go
Normal 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
|
||||||
|
}
|
||||||
39
suave/builder/api/api_test.go
Normal file
39
suave/builder/api/api_test.go
Normal 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
77
suave/builder/builder.go
Normal 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
|
||||||
|
}
|
||||||
122
suave/builder/builder_test.go
Normal file
122
suave/builder/builder_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
suave/builder/session_manager.go
Normal file
138
suave/builder/session_manager.go
Normal 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)
|
||||||
|
}
|
||||||
183
suave/builder/session_manager_test.go
Normal file
183
suave/builder/session_manager_test.go
Normal 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
9
suave/config.go
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package suave
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
Enabled: false,
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue