From 5c54ca9ad44bb6bdcb7d8fd75fd27b284c0ef4e5 Mon Sep 17 00:00:00 2001 From: Vitaly Drogan Date: Tue, 25 Jul 2023 17:34:05 +0300 Subject: [PATCH] API v0.2 (external), builder file config * implements api v0.2 (inputs and outputs) * builders are configured with a yaml file --- builders.yaml | 5 + cmd/node/main.go | 23 +- mevshare/api.go | 51 ++- mevshare/backend.go | 89 +++-- mevshare/builders.go | 201 +++++++++++ mevshare/bundle_v0.1.go | 116 +++++++ mevshare/bundle_v0.2.go | 256 ++++++++++++++ mevshare/bundle_v0.2_test.go | 614 +++++++++++++++++++++++++++++++++ mevshare/bundle_validation.go | 12 +- mevshare/database.go | 8 +- mevshare/external_builders.go | 142 -------- mevshare/hints.go | 4 +- mevshare/refund_recipient.go | 6 +- mevshare/sim_queue.go | 4 +- mevshare/sim_result_backend.go | 25 +- mevshare/types.go | 137 +++----- mevshare/utils.go | 54 +++ 17 files changed, 1411 insertions(+), 336 deletions(-) create mode 100644 builders.yaml create mode 100644 mevshare/builders.go create mode 100644 mevshare/bundle_v0.1.go create mode 100644 mevshare/bundle_v0.2.go create mode 100644 mevshare/bundle_v0.2_test.go delete mode 100644 mevshare/external_builders.go diff --git a/builders.yaml b/builders.yaml new file mode 100644 index 0000000..232d9e9 --- /dev/null +++ b/builders.yaml @@ -0,0 +1,5 @@ +builders: + - name: builder + api: v0.1 + url: http://127.0.0.1:8545 + internal: true diff --git a/cmd/node/main.go b/cmd/node/main.go index 80b2620..ecd3b4a 100644 --- a/cmd/node/main.go +++ b/cmd/node/main.go @@ -37,12 +37,11 @@ var ( defaultRedisEndpoint = getEnvOrDefault("REDIS_ENDPOINT", "redis://localhost:6379") defaultSimulationsEndpoint = getEnvOrDefault("SIMULATION_ENDPOINTS", "http://127.0.0.1:8545") defaultWorkersPerNode = getEnvOrDefault("WORKERS_PER_SIM_ENDPOINT", "2") - defaultBuildersEndpoint = getEnvOrDefault("BUILDER_ENDPOINTS", "http://127.0.0.1:8545") defaultPostgresDSN = getEnvOrDefault("POSTGRES_DSN", "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable") defaultEthEndpoint = getEnvOrDefault("ETH_ENDPOINT", "http://127.0.0.1:8545") defaultMevSimBundleRateLimit = getEnvOrDefault("MEV_SIM_BUNDLE_RATE_LIMIT", "5") - // See `ParseExternalBuilders` external_builders.go for more info - defaultExternalBuilders = getEnvOrDefault("EXTERNAL_BUILDERS", "") + // See `LoadBuilderConfig` builders.go for more info + defaultBuildersConfig = getEnvOrDefault("BUILDERS_CONFIG", "builders.yaml") defaultShareGasUsed = getEnvOrDefault("SHARE_GAS_USED", "0") defaultShareMevGasPrice = getEnvOrDefault("SHARE_MEV_GAS_PRICE", "1") @@ -55,11 +54,10 @@ var ( redisPtr = flag.String("redis", defaultRedisEndpoint, "redis url string") simEndpointPtr = flag.String("sim-endpoint", defaultSimulationsEndpoint, "simulation endpoints (comma separated)") workersPerNodePtr = flag.String("workers-per-node", defaultWorkersPerNode, "number of workers per simulation node") - buildersEndpointPtr = flag.String("builder-endpoint", defaultBuildersEndpoint, "builder endpoint") postgresDSNPtr = flag.String("postgres-dsn", defaultPostgresDSN, "postgres dsn") ethPtr = flag.String("eth", defaultEthEndpoint, "eth endpoint") meVSimBundleRateLimitPtr = flag.String("mev-sim-bundle-rate-limit", defaultMevSimBundleRateLimit, "mev sim bundle rate limit for external users (calls per second)") - externalBuildersPtr = flag.String("external-builders", defaultExternalBuilders, "external builders (e.g. name,rpc1,api;name,rpc2,api)") + buildersPtr = flag.String("external-builders", defaultBuildersConfig, "external builders YAML config file") shareGasUsedPtr = flag.String("share-gas-used", defaultShareGasUsed, "share gas used in hints (0-1)") shareMevGasPricePtr = flag.String("share-mev-gas-price", defaultShareMevGasPrice, "share mev gas price in hints (0-1)") ) @@ -108,12 +106,6 @@ func main() { logger.Fatal("Failed to create redis hint backend", zap.Error(err)) } - var builderBackends []mevshare.BuilderBackend //nolint:prealloc - for _, builderEndpoint := range strings.Split(*buildersEndpointPtr, ",") { - builderBackend := mevshare.NewJSONRPCBuilder(builderEndpoint) - builderBackends = append(builderBackends, builderBackend) - } - ethBackend, err := ethclient.Dial(*ethPtr) if err != nil { logger.Fatal("Failed to connect to ethBackend endpoint", zap.Error(err)) @@ -124,14 +116,14 @@ func main() { logger.Fatal("Failed to create postgres backend", zap.Error(err)) } - externalBuilders, err := mevshare.ParseExternalBuilders(*externalBuildersPtr) + builders, err := mevshare.LoadBuilderConfig(*buildersPtr) if err != nil { - logger.Fatal("Failed to parse external builders", zap.Error(err)) + logger.Fatal("Failed to load builder config", zap.Error(err)) } shareGasUsed := *shareGasUsedPtr == "1" shareMevGasPrice := *shareMevGasPricePtr == "1" - simResultBackend := mevshare.NewSimulationResultBackend(logger, hintBackend, builderBackends, ethBackend, dbBackend, externalBuilders, shareGasUsed, shareMevGasPrice) + simResultBackend := mevshare.NewSimulationResultBackend(logger, hintBackend, builders, ethBackend, dbBackend, shareGasUsed, shareMevGasPrice) redisQueue := simqueue.NewRedisQueue(logger, redisClient, "node") @@ -159,10 +151,11 @@ func main() { cachingEthBackend := mevshare.NewCachingEthClient(ethBackend) - api := mevshare.NewAPI(logger, simQueue, dbBackend, cachingEthBackend, signer, simBackends, rate.Limit(rateLimit), builderBackends) + api := mevshare.NewAPI(logger, simQueue, dbBackend, cachingEthBackend, signer, simBackends, rate.Limit(rateLimit), builders) jsonRPCServer, err := jsonrpcserver.NewHandler(jsonrpcserver.Methods{ "mev_sendBundle": api.SendBundle, + "mev_sendMergedBundle": api.SendMergedBundle, "mev_simBundle": api.SimBundle, "mev_cancelBundleByHash": api.CancelBundleByHash, }) diff --git a/mevshare/api.go b/mevshare/api.go index 73e5e49..f9ed5c6 100644 --- a/mevshare/api.go +++ b/mevshare/api.go @@ -16,6 +16,9 @@ import ( ) var ( + ErrInvalidSendBundleArgument = errors.New("invalid mev_sendBundle argument") + ErrInvalidSendMergedBundleArgument = errors.New("invalid mev_sendMergedBundle argument") + ErrInvalidInclusion = errors.New("invalid inclusion") ErrInvalidBundleBodySize = errors.New("invalid bundle body size") ErrInvalidBundleBody = errors.New("invalid bundle body") @@ -32,11 +35,11 @@ var ( ) type SimScheduler interface { - ScheduleBundleSimulation(ctx context.Context, bundle *SendMevBundleArgs, highPriority bool) error + ScheduleBundleSimulation(ctx context.Context, bundle *SendMevBundleArgsV1, highPriority bool) error } type BundleStorage interface { - GetBundle(ctx context.Context, hash common.Hash) (*SendMevBundleArgs, error) + GetBundle(ctx context.Context, hash common.Hash) (*SendMevBundleArgsV1, error) CancelBundleByHash(ctx context.Context, hash common.Hash, signer common.Address) error } @@ -53,12 +56,12 @@ type API struct { signer types.Signer simBackends []SimulationBackend simRateLimiter *rate.Limiter - builders []BuilderBackend + builders BuildersBackend knownBundleCache *lru.Cache[common.Hash, struct{}] } -func NewAPI(log *zap.Logger, scheduler SimScheduler, bundleStorage BundleStorage, eth EthClient, signer types.Signer, simBackends []SimulationBackend, simRateLimit rate.Limit, builders []BuilderBackend) *API { +func NewAPI(log *zap.Logger, scheduler SimScheduler, bundleStorage BundleStorage, eth EthClient, signer types.Signer, simBackends []SimulationBackend, simRateLimit rate.Limit, builders BuildersBackend) *API { return &API{ log: log, @@ -74,16 +77,30 @@ func NewAPI(log *zap.Logger, scheduler SimScheduler, bundleStorage BundleStorage } } -func (m *API) SendBundle(ctx context.Context, bundle SendMevBundleArgs) (SendMevBundleResponse, error) { +func (m *API) SendBundle(ctx context.Context, union SendBundleUnion) (SendMevBundleResponse, error) { logger := m.log + var bundle *SendMevBundleArgsV1 + if union.v1bundle != nil { + bundle = union.v1bundle + } else if union.v2bundle != nil { + b, err := union.v2bundle.ToV1Bundle() + if err != nil { + logger.Warn("failed to convert bundle", zap.Error(err)) + return SendMevBundleResponse{}, ErrInvalidSendBundleArgument + } + bundle = b + } else { + return SendMevBundleResponse{}, ErrInvalidSendBundleArgument + } + currentBlock, err := m.eth.BlockNumber(ctx) if err != nil { logger.Error("failed to get current block", zap.Error(err)) return SendMevBundleResponse{}, ErrInternalServiceError } - hash, hasUnmatchedHash, err := ValidateBundle(&bundle, currentBlock, m.signer) + hash, hasUnmatchedHash, err := ValidateBundle(bundle, currentBlock, m.signer) if err != nil { logger.Warn("failed to validate bundle", zap.Error(err)) return SendMevBundleResponse{}, err @@ -129,7 +146,7 @@ func (m *API) SendBundle(ctx context.Context, bundle SendMevBundleArgs) (SendMev refundPercent = *unmatchedBundle.Privacy.WantRefund } bundle.Validity.Refund = []RefundConstraint{{0, refundPercent}} - MergePrivacyBuilders(&bundle) + MergePrivacyBuilders(bundle) err = MergeInclusionIntervals(&bundle.Inclusion, &unmatchedBundle.Inclusion) if err != nil { return SendMevBundleResponse{}, ErrBackrunInclusion @@ -137,7 +154,7 @@ func (m *API) SendBundle(ctx context.Context, bundle SendMevBundleArgs) (SendMev } highPriority := jsonrpcserver.GetPriority(ctx) - err = m.scheduler.ScheduleBundleSimulation(ctx, &bundle, highPriority) + err = m.scheduler.ScheduleBundleSimulation(ctx, bundle, highPriority) if err != nil { logger.Error("Failed to schedule bundle simulation", zap.Error(err)) return SendMevBundleResponse{}, ErrInternalServiceError @@ -148,7 +165,16 @@ func (m *API) SendBundle(ctx context.Context, bundle SendMevBundleArgs) (SendMev }, nil } -func (m *API) SimBundle(ctx context.Context, bundle SendMevBundleArgs, aux SimMevBundleAuxArgs) (*SimMevBundleResponse, error) { +func (m *API) SendMergedBundle(ctx context.Context, union SendMergedBundleArgsV2) (SendMevBundleResponse, error) { + bundle, err := union.ToV1Bundle() + if err != nil { + return SendMevBundleResponse{}, ErrInvalidSendMergedBundleArgument + } + + return m.SendBundle(ctx, SendBundleUnion{v1bundle: bundle, v2bundle: nil}) +} + +func (m *API) SimBundle(ctx context.Context, bundle SendMevBundleArgsV1, aux SimMevBundleAuxArgs) (*SimMevBundleResponse, error) { if len(m.simBackends) == 0 { return nil, ErrInternalServiceError } @@ -181,12 +207,7 @@ func (m *API) CancelBundleByHash(ctx context.Context, hash common.Hash) error { return ErrBundleNotCancelled } - for _, builder := range m.builders { - err := builder.CancelBundleByHash(ctx, hash) - if err != nil { - m.log.Warn("Failed to cancel bundle by hash", zap.Error(err), zap.String("builder", builder.String())) - } - } + m.builders.CancelBundleByHash(ctx, m.log, hash) m.log.Info("Bundle cancelled", zap.String("hash", hash.Hex())) return nil } diff --git a/mevshare/backend.go b/mevshare/backend.go index c02f0f4..2c2f862 100644 --- a/mevshare/backend.go +++ b/mevshare/backend.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" - "github.com/ethereum/go-ethereum/common" "github.com/redis/go-redis/v9" "github.com/ybbus/jsonrpc/v3" ) @@ -13,16 +12,16 @@ type HintBackend interface { NotifyHint(ctx context.Context, hint *Hint) error } -type BuilderBackend interface { - String() string - SendMatchedShareBundle(ctx context.Context, bundle *SendMevBundleArgs) error - CancelBundleByHash(ctx context.Context, hash common.Hash) error -} +//type BuilderBackend interface { +// String() string +// SendMatchedShareBundle(ctx context.Context, bundle *SendMevBundleArgsV1) error +// CancelBundleByHash(ctx context.Context, hash common.Hash) error +//} // SimulationBackend is an interface for simulating transactions // There should be one simulation backend per worker node type SimulationBackend interface { - SimulateBundle(ctx context.Context, bundle *SendMevBundleArgs, aux *SimMevBundleAuxArgs) (*SimMevBundleResponse, error) + SimulateBundle(ctx context.Context, bundle *SendMevBundleArgsV1, aux *SimMevBundleAuxArgs) (*SimMevBundleResponse, error) } type JSONRPCSimulationBackend struct { @@ -35,7 +34,7 @@ func NewJSONRPCSimulationBackend(url string) *JSONRPCSimulationBackend { } } -func (b *JSONRPCSimulationBackend) SimulateBundle(ctx context.Context, bundle *SendMevBundleArgs, aux *SimMevBundleAuxArgs) (*SimMevBundleResponse, error) { +func (b *JSONRPCSimulationBackend) SimulateBundle(ctx context.Context, bundle *SendMevBundleArgsV1, aux *SimMevBundleAuxArgs) (*SimMevBundleResponse, error) { var result SimMevBundleResponse err := b.client.CallFor(ctx, &result, "mev_simBundle", bundle, aux) return &result, err @@ -61,40 +60,40 @@ func (b *RedisHintBackend) NotifyHint(ctx context.Context, hint *Hint) error { return b.client.Publish(ctx, b.pubChannel, data).Err() } -type JSONRPCBuilder struct { - url string - client jsonrpc.RPCClient -} - -func NewJSONRPCBuilder(url string) *JSONRPCBuilder { - return &JSONRPCBuilder{ - url: url, - client: jsonrpc.NewClient(url), - } -} - -func (b *JSONRPCBuilder) String() string { - return b.url -} - -func (b *JSONRPCBuilder) SendMatchedShareBundle(ctx context.Context, bundle *SendMevBundleArgs) error { - res, err := b.client.Call(ctx, "mev_sendBundle", []*SendMevBundleArgs{bundle}) - if err != nil { - return err - } - if res.Error != nil { - return res.Error - } - return nil -} - -func (b *JSONRPCBuilder) CancelBundleByHash(ctx context.Context, hash common.Hash) error { - res, err := b.client.Call(ctx, "mev_cancelBundleByHash", []common.Hash{hash}) - if err != nil { - return err - } - if res.Error != nil { - return res.Error - } - return nil -} +//type JSONRPCBuilder struct { +// url string +// client jsonrpc.RPCClient +//} +// +//func NewJSONRPCBuilder(url string) *JSONRPCBuilder { +// return &JSONRPCBuilder{ +// url: url, +// client: jsonrpc.NewClient(url), +// } +//} +// +//func (b *JSONRPCBuilder) String() string { +// return b.url +//} +// +//func (b *JSONRPCBuilder) SendMatchedShareBundle(ctx context.Context, bundle *SendMevBundleArgsV1) error { +// res, err := b.client.Call(ctx, "mev_sendBundle", []*SendMevBundleArgsV1{bundle}) +// if err != nil { +// return err +// } +// if res.Error != nil { +// return res.Error +// } +// return nil +//} +// +//func (b *JSONRPCBuilder) CancelBundleByHash(ctx context.Context, hash common.Hash) error { +// res, err := b.client.Call(ctx, "mev_cancelBundleByHash", []common.Hash{hash}) +// if err != nil { +// return err +// } +// if res.Error != nil { +// return res.Error +// } +// return nil +//} diff --git a/mevshare/builders.go b/mevshare/builders.go new file mode 100644 index 0000000..053f152 --- /dev/null +++ b/mevshare/builders.go @@ -0,0 +1,201 @@ +package mevshare + +import ( + "context" + "errors" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ybbus/jsonrpc/v3" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +type BuilderAPI uint8 + +const ( + BuilderAPIRefRecipient BuilderAPI = iota + BuilderAPIMevShareBeta1 + BuilderAPIMevShareV2 +) + +var ErrInvalidBuilder = errors.New("invalid builder specification") + +type BuildersConfig struct { + Builders []struct { + Name string `yaml:"name"` + URL string `yaml:"url"` + API string `yaml:"api"` + Internal bool `yaml:"internal"` + } `yaml:"builders"` +} + +// LoadBuilderConfig parses a builder config from a file +func LoadBuilderConfig(file string) (BuildersBackend, error) { + data, err := os.ReadFile(file) + if err != nil { + return BuildersBackend{}, err + } + + var config BuildersConfig + err = yaml.Unmarshal(data, &config) + if err != nil { + return BuildersBackend{}, err + } + + externalBuilders := make([]JSONRPCBuilderBackend, 0) + internalBuilders := make([]JSONRPCBuilderBackend, 0) + for _, builder := range config.Builders { + var api BuilderAPI + switch builder.API { + case "refund-recipient": + api = BuilderAPIRefRecipient + case VersionV1: + api = BuilderAPIMevShareBeta1 + case VersionV2: + api = BuilderAPIMevShareV2 + default: + return BuildersBackend{}, ErrInvalidBuilder + } + + builderBackend := JSONRPCBuilderBackend{ + Name: builder.Name, + Client: jsonrpc.NewClient(builder.URL), + API: api, + } + + if builder.Internal { + internalBuilders = append(internalBuilders, builderBackend) + } else { + externalBuilders = append(externalBuilders, builderBackend) + } + } + + externalBuilderMap := make(map[string]JSONRPCBuilderBackend) + for _, builder := range externalBuilders { + externalBuilderMap[builder.Name] = builder + } + + return BuildersBackend{ + externalBuilders: externalBuilderMap, + internalBuilders: internalBuilders, + }, nil +} + +type JSONRPCBuilderBackend struct { + Name string + Client jsonrpc.RPCClient + API BuilderAPI +} + +func (b *JSONRPCBuilderBackend) SendBundle(ctx context.Context, bundle *SendMevBundleArgsV1) error { + switch b.API { + case BuilderAPIRefRecipient: + refRec, err := ConvertBundleToRefundRecipient(bundle) + if err != nil { + return err + } + res, err := b.Client.Call(ctx, "eth_sendBundle", []SendRefundRecBundleArgs{refRec}) + if err != nil { + return err + } + if res.Error != nil { + return res.Error + } + case BuilderAPIMevShareBeta1: + res, err := b.Client.Call(ctx, "mev_sendBundle", []SendMevBundleArgsV1{*bundle}) + if err != nil { + return err + } + if res.Error != nil { + return res.Error + } + case BuilderAPIMevShareV2: + mergedBundle, err := ConvertV1BundleToMergedBundleV2(bundle) + if err != nil { + return err + } + res, err := b.Client.Call(ctx, "mev_sendMergedBundle", []SendMergedBundleArgsV2{*mergedBundle}) + if err != nil { + return err + } + if res.Error != nil { + return res.Error + } + } + return nil +} + +func (b *JSONRPCBuilderBackend) CancelBundleByHash(ctx context.Context, hash common.Hash) error { + res, err := b.Client.Call(ctx, "mev_cancelBundleByHash", []common.Hash{hash}) + if err != nil { + return err + } + if res.Error != nil { + return res.Error + } + return nil +} + +type BuildersBackend struct { + externalBuilders map[string]JSONRPCBuilderBackend + internalBuilders []JSONRPCBuilderBackend +} + +func (b *BuildersBackend) SendBundle(ctx context.Context, logger *zap.Logger, bundle *SendMevBundleArgsV1) { + for _, builder := range b.internalBuilders { + err := builder.SendBundle(ctx, bundle) + if err != nil { + logger.Warn("failed to send bundle to internal builder", zap.Error(err), zap.String("builder", builder.Name)) + } + } + + if bundle.Privacy != nil && len(bundle.Privacy.Builders) > 0 { + // clean metadata, privacy + args := *bundle + // it should already be cleaned while matching, but just in case we do it again here + MergePrivacyBuilders(&args) + builders := args.Privacy.Builders + cleanBundle(&args) + + buildersUsed := make(map[string]struct{}) + for _, target := range builders { + if target == "default" || target == "flashbots" { + // right now we always send to flashbots and default means flashbots + continue + } + if _, ok := buildersUsed[target]; ok { + continue + } + buildersUsed[target] = struct{}{} + if builder, ok := b.externalBuilders[target]; ok { + err := builder.SendBundle(ctx, &args) + if err != nil { + logger.Warn("failed to send bundle to external builder", zap.Error(err), zap.String("builder", builder.Name)) + } + } else { + logger.Warn("unknown external builder", zap.String("builder", target)) + } + } + } +} + +func (b *BuildersBackend) CancelBundleByHash(ctx context.Context, logger *zap.Logger, hash common.Hash) { + // we cancel bundle only in the internal builders, external cancellations are not supported + for _, builder := range b.internalBuilders { + err := builder.CancelBundleByHash(ctx, hash) + if err != nil { + logger.Warn("failed to cancel bundle to internal builder", zap.Error(err), zap.String("builder", builder.Name)) + } + } +} + +func cleanBundle(bundle *SendMevBundleArgsV1) { + for _, el := range bundle.Body { + if el.Bundle != nil { + cleanBundle(el.Bundle) + } + } + bundle.Privacy = nil + bundle.Metadata = nil +} diff --git a/mevshare/bundle_v0.1.go b/mevshare/bundle_v0.1.go new file mode 100644 index 0000000..1a3480a --- /dev/null +++ b/mevshare/bundle_v0.1.go @@ -0,0 +1,116 @@ +package mevshare + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +type SendMevBundleArgsV1 struct { + Version string `json:"version"` + Inclusion MevBundleInclusion `json:"inclusion"` + Body []MevBundleBody `json:"body"` + Validity MevBundleValidity `json:"validity"` + Privacy *MevBundlePrivacy `json:"privacy,omitempty"` + Metadata *MevBundleMetadata `json:"metadata,omitempty"` +} + +type MevBundleInclusion struct { + BlockNumber hexutil.Uint64 `json:"block"` + MaxBlock hexutil.Uint64 `json:"maxBlock"` +} + +type MevBundleBody struct { + Hash *common.Hash `json:"hash,omitempty"` + Tx *hexutil.Bytes `json:"tx,omitempty"` + Bundle *SendMevBundleArgsV1 `json:"bundle,omitempty"` + CanRevert bool `json:"canRevert,omitempty"` +} + +type MevBundleValidity struct { + Refund []RefundConstraint `json:"refund,omitempty"` + RefundConfig []RefundConfig `json:"refundConfig,omitempty"` +} + +type RefundConstraint struct { + BodyIdx int `json:"bodyIdx"` + Percent int `json:"percent"` +} + +type RefundConfig struct { + Address common.Address `json:"address"` + Percent int `json:"percent"` +} + +type MevBundlePrivacy struct { + Hints HintIntent `json:"hints,omitempty"` + Builders []string `json:"builders,omitempty"` + WantRefund *int `json:"wantRefund,omitempty"` +} + +type MevBundleMetadata struct { + BundleHash common.Hash `json:"bundleHash,omitempty"` + BodyHashes []common.Hash `json:"bodyHashes,omitempty"` + Signer common.Address `json:"signer,omitempty"` + OriginID string `json:"originId,omitempty"` + ReceivedAt hexutil.Uint64 `json:"receivedAt,omitempty"` +} + +type SendMevBundleResponse struct { + BundleHash common.Hash `json:"bundleHash"` +} + +type SimMevBundleResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + StateBlock hexutil.Uint64 `json:"stateBlock"` + MevGasPrice hexutil.Big `json:"mevGasPrice"` + Profit hexutil.Big `json:"profit"` + RefundableValue hexutil.Big `json:"refundableValue"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + BodyLogs []SimMevBodyLogs `json:"logs,omitempty"` +} + +type SimMevBundleAuxArgs struct { + ParentBlock *rpc.BlockNumberOrHash `json:"parentBlock"` + // override the default values for the block header + BlockNumber *hexutil.Big `json:"blockNumber"` + Coinbase *common.Address `json:"coinbase"` + Timestamp *hexutil.Uint64 `json:"timestamp"` + GasLimit *hexutil.Uint64 `json:"gasLimit"` + BaseFee *hexutil.Big `json:"baseFee"` + Timeout *int64 `json:"timeout"` +} + +type SimMevBodyLogs struct { + TxLogs []*types.Log `json:"txLogs,omitempty"` + BundleLogs []SimMevBodyLogs `json:"bundleLogs,omitempty"` +} + +func GetRefundConfigV1FromBody(args *MevBundleBody) ([]RefundConfig, error) { + if args.Tx != nil { + var tx types.Transaction + err := tx.UnmarshalBinary(*args.Tx) + if err != nil { + return nil, err + } + signer := types.LatestSignerForChainID(tx.ChainId()) + from, err := types.Sender(signer, &tx) + if err != nil { + return nil, err + } + return []RefundConfig{{Address: from, Percent: 100}}, nil + } else if args.Bundle != nil { + if len(args.Bundle.Validity.RefundConfig) > 0 { + return args.Bundle.Validity.RefundConfig, nil + } else { + if len(args.Bundle.Body) == 0 { + return nil, ErrInvalidBundleBodySize + } + return GetRefundConfigV1FromBody(&args.Bundle.Body[0]) + } + } else { + return nil, ErrInvalidBundleBody + } +} diff --git a/mevshare/bundle_v0.2.go b/mevshare/bundle_v0.2.go new file mode 100644 index 0000000..2a13638 --- /dev/null +++ b/mevshare/bundle_v0.2.go @@ -0,0 +1,256 @@ +package mevshare + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var ( + ErrMustBeV2Bundle = errors.New("must be v2 bundle") + ErrExtensionNotSupported = errors.New("extension not supported") + ErrMergedBundleTooComplex = errors.New("external merged bundles can only have 2 elements in the body with possible refund in position 1") + ErrMergedV1BundleTooComplex = errors.New("v0.1 merged bundles can only be all txs or a simple backruns to be converted to v0.2") +) + +type SendMevBundleArgsV2 struct { + Version string `json:"version"` + Extensions []string `json:"extensions"` + Inclusion MevBundleInclusion `json:"inclusion"` + Body []MevBundleBodyV2 `json:"body"` + Privacy *MevBundlePrivacyV2 `json:"privacy,omitempty"` + Validity *MevBundleValidityV2 `json:"validity,omitempty"` +} + +type MevBundleBodyV2 struct { + Hash *common.Hash `json:"hash,omitempty"` + Tx *hexutil.Bytes `json:"tx,omitempty"` + Optional bool `json:"optional,omitempty"` +} + +type MevBundlePrivacyV2 struct { + Hints HintIntent `json:"hints,omitempty"` + Builders []string `json:"builders,omitempty"` +} + +type MevBundleValidityV2 struct { + Refund []RefundConfig `json:"refund,omitempty"` +} + +func (b *SendMevBundleArgsV2) ToV1Bundle() (*SendMevBundleArgsV1, error) { + var result SendMevBundleArgsV1 + result.Version = VersionV1 + + if b.Version != VersionV2 { + return nil, ErrMustBeV2Bundle + } + + if len(b.Extensions) > 0 { + return nil, ErrExtensionNotSupported + } + + result.Inclusion.BlockNumber = b.Inclusion.BlockNumber + result.Inclusion.MaxBlock = b.Inclusion.MaxBlock + + result.Body = make([]MevBundleBody, len(b.Body)) + for i, body := range b.Body { + result.Body[i].CanRevert = body.Optional + if body.Hash != nil { + result.Body[i].Hash = body.Hash + } + if body.Tx != nil { + result.Body[i].Tx = body.Tx + } + } + + if b.Privacy != nil { + result.Privacy = &MevBundlePrivacy{ + Hints: b.Privacy.Hints, + Builders: b.Privacy.Builders, + WantRefund: nil, + } + } + + if b.Validity != nil { + wantRefund, refundConfig, err := ConvertTotalRefundConfigToBundleV1Params(b.Validity.Refund) + if err != nil { + return nil, err + } + result.Validity.RefundConfig = refundConfig + + if result.Privacy == nil { + result.Privacy = &MevBundlePrivacy{HintNone, nil, nil} + } + result.Privacy.WantRefund = &wantRefund + } + + return &result, nil +} + +type SendMergedBundleArgsV2 struct { + Version string `json:"version"` + Extensions []string `json:"extensions"` + Inclusion MergedBundleInclusionV2 `json:"inclusion"` + Body []MergedBundleBodyV2 `json:"body"` +} + +type MergedBundleInclusionV2 struct { + BlockNumber hexutil.Uint64 `json:"block"` +} + +type MergedBundleBodyV2 struct { + Items []MergedBundleBodyItemV2 `json:"items"` + Validity *MergedBundleValidityV2 `json:"validity"` + Optional bool `json:"optional,omitempty"` +} + +type MergedBundleBodyItemV2 struct { + Tx hexutil.Bytes `json:"tx,omitempty"` + Optional bool `json:"optional,omitempty"` +} + +type MergedBundleValidityV2 struct { + Refund []RefundConfig `json:"refund,omitempty"` +} + +func (b *SendMergedBundleArgsV2) ToV1Bundle() (*SendMevBundleArgsV1, error) { + var result SendMevBundleArgsV1 + result.Version = VersionV1 + + if b.Version != VersionV2 { + return nil, ErrMustBeV2Bundle + } + if len(b.Extensions) > 0 { + return nil, ErrExtensionNotSupported + } + + result.Inclusion.BlockNumber = b.Inclusion.BlockNumber + + if len(b.Body) == 1 { + el := b.Body[0] + if el.Validity != nil && len(el.Validity.Refund) > 0 { + return nil, ErrMergedBundleTooComplex + } + result.Body = make([]MevBundleBody, len(el.Items)) + for i, item := range el.Items { + tx := item.Tx + result.Body[i].Tx = &tx + result.Body[i].CanRevert = item.Optional + } + return &result, nil + } + + if len(b.Body) != 2 { + return nil, ErrMergedBundleTooComplex + } + + b1 := b.Body[0] + b2 := b.Body[1] + + if b1.Validity != nil && len(b1.Validity.Refund) > 0 { + return nil, ErrMergedBundleTooComplex + } + + var innerBundle SendMevBundleArgsV1 + innerBundle.Version = result.Version + innerBundle.Inclusion = result.Inclusion + innerBundle.Body = make([]MevBundleBody, len(b1.Items)) + for i, item := range b1.Items { + tx := item.Tx + innerBundle.Body[i].Tx = &tx + if item.Optional { + innerBundle.Body[i].CanRevert = true + } + } + + if b2.Validity != nil && len(b2.Validity.Refund) > 0 { + wantRefund, refundConfig, err := ConvertTotalRefundConfigToBundleV1Params(b2.Validity.Refund) + if err != nil { + return nil, err + } + innerBundle.Validity.RefundConfig = refundConfig + result.Validity.Refund = []RefundConstraint{{BodyIdx: 0, Percent: wantRefund}} + } + + result.Body = []MevBundleBody{{Bundle: &innerBundle}} + for _, item := range b2.Items { + tx := item.Tx + result.Body = append(result.Body, MevBundleBody{ + Tx: &tx, + Hash: nil, + Bundle: nil, + CanRevert: item.Optional, + }) + } + + return &result, nil +} + +func ConvertV1BundleToMergedBundleV2(bundle *SendMevBundleArgsV1) (*SendMergedBundleArgsV2, error) { + var result SendMergedBundleArgsV2 + result.Version = VersionV2 + result.Inclusion.BlockNumber = bundle.Inclusion.BlockNumber + + if len(bundle.Validity.Refund) == 0 { + items := make([]MergedBundleBodyItemV2, len(bundle.Body)) + for i, body := range bundle.Body { + if body.Tx == nil { + return nil, ErrMergedV1BundleTooComplex + } + items[i].Tx = *body.Tx + items[i].Optional = body.CanRevert + } + result.Body = []MergedBundleBodyV2{{Items: items}} + return &result, nil + } + + if len(bundle.Validity.Refund) != 1 { + return nil, ErrMergedV1BundleTooComplex + } + + if len(bundle.Body) != 2 { + return nil, ErrMergedV1BundleTooComplex + } + + var body0 MergedBundleBodyV2 + if innerBundle := bundle.Body[0].Bundle; innerBundle != nil { + items := make([]MergedBundleBodyItemV2, len(innerBundle.Body)) + for i, el := range innerBundle.Body { + if el.Tx == nil { + return nil, ErrMergedV1BundleTooComplex + } + items[i].Tx = *el.Tx + items[i].Optional = el.CanRevert + } + body0.Items = items + body0.Optional = bundle.Body[0].CanRevert + } else if bundle.Body[0].Tx != nil { + body0.Items = append(body0.Items, MergedBundleBodyItemV2{Tx: *bundle.Body[0].Tx, Optional: bundle.Body[0].CanRevert}) + } else { + return nil, ErrMergedV1BundleTooComplex + } + + var body1 MergedBundleBodyV2 + if bundle.Body[1].Tx != nil { + body1.Items = append(body1.Items, MergedBundleBodyItemV2{Tx: *bundle.Body[1].Tx, Optional: bundle.Body[1].CanRevert}) + } else { + return nil, ErrMergedV1BundleTooComplex + } + + // refund for body1 + refConstraint := bundle.Validity.Refund[0] + if refConstraint.BodyIdx != 0 { + return nil, ErrMergedV1BundleTooComplex + } + refundPercent := refConstraint.Percent + refundConfig, err := GetRefundConfigV1FromBody(&bundle.Body[0]) + if err != nil { + return nil, err + } + totalRefundConfig := ConvertBundleV1ParamsToTotalRefundConfig(refundPercent, refundConfig) + body1.Validity = &MergedBundleValidityV2{Refund: totalRefundConfig} + + result.Body = []MergedBundleBodyV2{body0, body1} + return &result, nil +} diff --git a/mevshare/bundle_v0.2_test.go b/mevshare/bundle_v0.2_test.go new file mode 100644 index 0000000..604ccd4 --- /dev/null +++ b/mevshare/bundle_v0.2_test.go @@ -0,0 +1,614 @@ +package mevshare + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/require" +) + +var ( + hash = common.HexToHash("0xaabbcc") + // two transactions signed by addr1 + tx1 = common.Hex2Bytes("02f8700118843b9aca00850ec14a8d608255f094324a74db217d5c12add31c5f3545f45c57da7dd080850102030405c001a0ba6c9d75a7dba4eb26879d465a8a7505c0caa3fa6a50c763cb511b57d06ae996a047a84667d4a0fef94f08372538cfedc96e0d027caf46914b3c436337d3a0e82c") + tx2 = common.Hex2Bytes("02f870010784059a5381851599296b148255f094324a74db217d5c12add31c5f3545f45c57da7dd080850102030415c001a0ef4027807cbf414334458cee0b6dc2b4741b381ca01da91f2759473cd9c05c41a040a947ab6273e390dc0a12bae6e7d2cf51cebf1cc7ed8f1d7b1365cd0b9fb219") + addr1 = common.HexToAddress("0x324A74DB217D5c12aDd31c5f3545F45C57DA7dd0") + addr2 = common.HexToAddress("0x222") +) + +func TestSendMevBundleArgsV2_ToV1Bundle(t *testing.T) { + iptr := func(i int) *int { return &i } + + input := SendMevBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBodyV2{ + { + Hash: &hash, + }, + { + Hash: &hash, + Optional: true, + }, + { + Tx: (*hexutil.Bytes)(&tx1), + }, + { + Tx: (*hexutil.Bytes)(&tx1), + Optional: true, + }, + }, + Privacy: &MevBundlePrivacyV2{ + Hints: HintsAll, + Builders: []string{"builder1", "builder2"}, + }, + Validity: &MevBundleValidityV2{ + Refund: []RefundConfig{ + { + Address: addr1, + Percent: 10, + }, + { + Address: addr2, + Percent: 20, + }, + }, + }, + } + + expectedOutput := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Hash: &hash}, + {Hash: &hash, CanRevert: true}, + {Tx: (*hexutil.Bytes)(&tx1)}, + {Tx: (*hexutil.Bytes)(&tx1), CanRevert: true}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: []RefundConfig{ + { + Address: addr1, + Percent: 34, + }, + { + Address: addr2, + Percent: 66, + }, + }, + }, + Privacy: &MevBundlePrivacy{ + Hints: HintsAll, + Builders: []string{"builder1", "builder2"}, + WantRefund: iptr(30), + }, + Metadata: nil, + } + + result, err := input.ToV1Bundle() + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestConvertV1BundleToMergedBundleV2_WithInnerBundleRefundConfig(t *testing.T) { + innerbundle := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: []RefundConfig{ + {Address: addr1, Percent: 34}, + {Address: addr2, Percent: 66}, + }, + }, + Privacy: nil, + Metadata: nil, + } + + input := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Bundle: &innerbundle}, + {Tx: (*hexutil.Bytes)(&tx2)}, + }, + Validity: MevBundleValidity{ + Refund: []RefundConstraint{{BodyIdx: 0, Percent: 30}}, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{{Tx: hexutil.Bytes(tx1)}}, + }, + { + Items: []MergedBundleBodyItemV2{{Tx: hexutil.Bytes(tx2)}}, + Validity: &MergedBundleValidityV2{ + Refund: []RefundConfig{ + {Address: addr1, Percent: 10}, + {Address: addr2, Percent: 19}, + }, + }, + }, + }, + } + + result, err := ConvertV1BundleToMergedBundleV2(&input) + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestConvertV1BundleToMergedBundleV2_WithInnerBundleNoRefundConfig(t *testing.T) { + innerbundle := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + input := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Bundle: &innerbundle}, + {Tx: (*hexutil.Bytes)(&tx2)}, + }, + Validity: MevBundleValidity{ + Refund: []RefundConstraint{{BodyIdx: 0, Percent: 30}}, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{{Tx: hexutil.Bytes(tx1)}}, + }, + { + Items: []MergedBundleBodyItemV2{{Tx: hexutil.Bytes(tx2)}}, + Validity: &MergedBundleValidityV2{ + Refund: []RefundConfig{ + {Address: addr1, Percent: 30}, + }, + }, + }, + }, + } + + result, err := ConvertV1BundleToMergedBundleV2(&input) + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestConvertV1BundleToMergedBundleV2_RefundToTx(t *testing.T) { //nolint:dupl + input := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + {Tx: (*hexutil.Bytes)(&tx2)}, + }, + Validity: MevBundleValidity{ + Refund: []RefundConstraint{{BodyIdx: 0, Percent: 30}}, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{{Tx: hexutil.Bytes(tx1)}}, + }, + { + Items: []MergedBundleBodyItemV2{{Tx: hexutil.Bytes(tx2)}}, + Validity: &MergedBundleValidityV2{ + Refund: []RefundConfig{ + {Address: addr1, Percent: 30}, + }, + }, + }, + }, + } + + result, err := ConvertV1BundleToMergedBundleV2(&input) + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestConvertV1BundleToMergedBundleV2_AllTxsBundle(t *testing.T) { + input := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + {Tx: (*hexutil.Bytes)(&tx2), CanRevert: true}, + {Tx: (*hexutil.Bytes)(&tx1)}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx1)}, + {Tx: hexutil.Bytes(tx2), Optional: true}, + {Tx: hexutil.Bytes(tx1)}, + }, + }, + }, + } + + result, err := ConvertV1BundleToMergedBundleV2(&input) + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestConvertV1BundleToMergedBundleV2_BundleWithRefundToTx(t *testing.T) { //nolint:dupl + input := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + {Tx: (*hexutil.Bytes)(&tx2)}, + }, + Validity: MevBundleValidity{ + Refund: []RefundConstraint{{BodyIdx: 0, Percent: 30}}, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx1)}, + }, + }, + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx2)}, + }, + Validity: &MergedBundleValidityV2{ + Refund: []RefundConfig{ + {Address: addr1, Percent: 30}, + }, + }, + }, + }, + } + + result, err := ConvertV1BundleToMergedBundleV2(&input) + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestConvertV1BundleToMergedBundleV2_BundleWithRefundToInnerBundleWithRefundConfig(t *testing.T) { + inner := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + {Tx: (*hexutil.Bytes)(&tx2), CanRevert: true}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: []RefundConfig{ + {Address: addr1, Percent: 34}, + {Address: addr2, Percent: 66}, + }, + }, + Privacy: nil, + Metadata: nil, + } + input := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Bundle: &inner, CanRevert: true}, + {Tx: (*hexutil.Bytes)(&tx2)}, + }, + Validity: MevBundleValidity{ + Refund: []RefundConstraint{{BodyIdx: 0, Percent: 30}}, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx1)}, + {Tx: hexutil.Bytes(tx2), Optional: true}, + }, + Optional: true, + }, + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx2)}, + }, + Validity: &MergedBundleValidityV2{ + Refund: []RefundConfig{ + {Address: addr1, Percent: 10}, + {Address: addr2, Percent: 19}, + }, + }, + }, + }, + } + + result, err := ConvertV1BundleToMergedBundleV2(&input) + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestConvertV1BundleToMergedBundleV2_BundleWithRefundToInnerBundleWithoutRefundConfig(t *testing.T) { + inner := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + {Tx: (*hexutil.Bytes)(&tx2), CanRevert: true}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + input := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 3, + }, + Body: []MevBundleBody{ + {Bundle: &inner, CanRevert: true}, + {Tx: (*hexutil.Bytes)(&tx2)}, + }, + Validity: MevBundleValidity{ + Refund: []RefundConstraint{{BodyIdx: 0, Percent: 30}}, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx1)}, + {Tx: hexutil.Bytes(tx2), Optional: true}, + }, + Optional: true, + }, + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx2)}, + }, + Validity: &MergedBundleValidityV2{ + Refund: []RefundConfig{ + {Address: addr1, Percent: 30}, + }, + }, + }, + }, + } + + result, err := ConvertV1BundleToMergedBundleV2(&input) + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestSendMergedBundleArgsV2_ToV1Bundle_AllTxOneBundle(t *testing.T) { + input := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx1)}, + {Tx: hexutil.Bytes(tx2), Optional: true}, + }, + }, + }, + } + + expectedOutput := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 0, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + {Tx: (*hexutil.Bytes)(&tx2), CanRevert: true}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + result, err := input.ToV1Bundle() + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} + +func TestSendMergedBundleArgsV2_ToV1Bundle_WithRefund(t *testing.T) { + input := SendMergedBundleArgsV2{ + Version: VersionV2, + Extensions: nil, + Inclusion: MergedBundleInclusionV2{ + BlockNumber: 1, + }, + Body: []MergedBundleBodyV2{ + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx1)}, + }, + }, + { + Items: []MergedBundleBodyItemV2{ + {Tx: hexutil.Bytes(tx2)}, + }, + Validity: &MergedBundleValidityV2{ + Refund: []RefundConfig{ + {Address: addr1, Percent: 10}, + {Address: addr2, Percent: 20}, + }, + }, + }, + }, + } + + innerBundle := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 0, + }, + Body: []MevBundleBody{ + {Tx: (*hexutil.Bytes)(&tx1)}, + }, + Validity: MevBundleValidity{ + Refund: nil, + RefundConfig: []RefundConfig{ + {Address: addr1, Percent: 34}, + {Address: addr2, Percent: 66}, + }, + }, + Privacy: nil, + Metadata: nil, + } + + expectedOutput := SendMevBundleArgsV1{ + Version: VersionV1, + Inclusion: MevBundleInclusion{ + BlockNumber: 1, + MaxBlock: 0, + }, + Body: []MevBundleBody{ + {Bundle: &innerBundle}, + {Tx: (*hexutil.Bytes)(&tx2)}, + }, + Validity: MevBundleValidity{ + Refund: []RefundConstraint{{BodyIdx: 0, Percent: 30}}, + RefundConfig: nil, + }, + Privacy: nil, + Metadata: nil, + } + + result, err := input.ToV1Bundle() + require.NoError(t, err) + require.Equal(t, expectedOutput, *result) +} diff --git a/mevshare/bundle_validation.go b/mevshare/bundle_validation.go index 87672cb..7b08cf3 100644 --- a/mevshare/bundle_validation.go +++ b/mevshare/bundle_validation.go @@ -15,7 +15,7 @@ var ( ErrInvalidBundlePrivacy = errors.New("invalid bundle privacy") ) -func cleanBody(bundle *SendMevBundleArgs) { +func cleanBody(bundle *SendMevBundleArgsV1) { for _, el := range bundle.Body { if el.Hash != nil { el.Tx = nil @@ -60,11 +60,11 @@ func MergeBuilders(topLevel, inner *MevBundlePrivacy) { topLevel.Builders = Intersect(topLevel.Builders, inner.Builders) } -func validateBundleInner(level int, bundle *SendMevBundleArgs, currentBlock uint64, signer types.Signer) (hash common.Hash, txs int, unmatched bool, err error) { //nolint:gocognit,gocyclo +func validateBundleInner(level int, bundle *SendMevBundleArgsV1, currentBlock uint64, signer types.Signer) (hash common.Hash, txs int, unmatched bool, err error) { //nolint:gocognit,gocyclo if level > MaxNestingLevel { return hash, txs, unmatched, ErrBundleTooDeep } - if bundle.Version != "beta-1" && bundle.Version != "v0.1" { + if bundle.Version != "beta-1" && bundle.Version != VersionV1 { return hash, txs, unmatched, ErrUnsupportedBundleVersion } @@ -209,12 +209,12 @@ func validateBundleInner(level int, bundle *SendMevBundleArgs, currentBlock uint return hash, txs, unmatched, nil } -func ValidateBundle(bundle *SendMevBundleArgs, currentBlock uint64, signer types.Signer) (hash common.Hash, unmatched bool, err error) { +func ValidateBundle(bundle *SendMevBundleArgsV1, currentBlock uint64, signer types.Signer) (hash common.Hash, unmatched bool, err error) { hash, _, unmatched, err = validateBundleInner(0, bundle, currentBlock, signer) return hash, unmatched, err } -func mergePrivacyBuildersInner(bundle *SendMevBundleArgs, topLevel *MevBundlePrivacy) { +func mergePrivacyBuildersInner(bundle *SendMevBundleArgsV1, topLevel *MevBundlePrivacy) { MergeBuilders(topLevel, bundle.Privacy) for _, el := range bundle.Body { if el.Bundle != nil { @@ -224,6 +224,6 @@ func mergePrivacyBuildersInner(bundle *SendMevBundleArgs, topLevel *MevBundlePri } // MergePrivacyBuilders Sets privacy.builders to the intersection of all privacy.builders in the bundle -func MergePrivacyBuilders(bundle *SendMevBundleArgs) { +func MergePrivacyBuilders(bundle *SendMevBundleArgsV1) { mergePrivacyBuildersInner(bundle, bundle.Privacy) } diff --git a/mevshare/database.go b/mevshare/database.go index 165623c..3cd1b05 100644 --- a/mevshare/database.go +++ b/mevshare/database.go @@ -146,7 +146,7 @@ func NewDBBackend(postgresDSN string) (*DBBackend, error) { }, nil } -func (b *DBBackend) GetBundle(ctx context.Context, hash common.Hash) (*SendMevBundleArgs, error) { +func (b *DBBackend) GetBundle(ctx context.Context, hash common.Hash) (*SendMevBundleArgsV1, error) { var dbSbundle DBSbundle err := b.getBundle.GetContext(ctx, &dbSbundle, hash.Bytes()) if errors.Is(err, sql.ErrNoRows) { @@ -155,7 +155,7 @@ func (b *DBBackend) GetBundle(ctx context.Context, hash common.Hash) (*SendMevBu return nil, err } - var bundle SendMevBundleArgs + var bundle SendMevBundleArgsV1 err = json.Unmarshal(dbSbundle.Body, &bundle) if err != nil { return nil, err @@ -163,7 +163,7 @@ func (b *DBBackend) GetBundle(ctx context.Context, hash common.Hash) (*SendMevBu return &bundle, nil } -func (b *DBBackend) InsertBundleForStats(ctx context.Context, bundle *SendMevBundleArgs, result *SimMevBundleResponse) error { +func (b *DBBackend) InsertBundleForStats(ctx context.Context, bundle *SendMevBundleArgsV1, result *SimMevBundleResponse) error { var dbBundle DBSbundle var err error if bundle.Metadata == nil { @@ -241,7 +241,7 @@ func (b *DBBackend) CancelBundleByHash(ctx context.Context, hash common.Hash, si return nil } -func (b *DBBackend) InsertBundleForBuilder(ctx context.Context, bundle *SendMevBundleArgs, result *SimMevBundleResponse) error { +func (b *DBBackend) InsertBundleForBuilder(ctx context.Context, bundle *SendMevBundleArgsV1, result *SimMevBundleResponse) error { var dbBundle DBSbundleBuilder var err error if bundle.Metadata == nil { diff --git a/mevshare/external_builders.go b/mevshare/external_builders.go deleted file mode 100644 index f1b3d9e..0000000 --- a/mevshare/external_builders.go +++ /dev/null @@ -1,142 +0,0 @@ -package mevshare - -import ( - "context" - "errors" - "strings" - - "github.com/ybbus/jsonrpc/v3" - "go.uber.org/zap" -) - -type BuilderAPI uint8 - -const ( - BuilderAPIRefRecipient BuilderAPI = iota - BuilderAPIMevShareBeta1 -) - -var ErrInvalidExternalBuilder = errors.New("invalid external builder") - -type ExternalBuilder struct { - Name string - Client jsonrpc.RPCClient - API BuilderAPI -} - -func (b *ExternalBuilder) SendBundle(ctx context.Context, bundle *SendMevBundleArgs) error { - switch b.API { - case BuilderAPIRefRecipient: - refRec, err := ConvertBundleToRefundRecipient(bundle) - if err != nil { - return err - } - res, err := b.Client.Call(ctx, "eth_sendBundle", []SendRefundRecBundleArgs{refRec}) - if err != nil { - return err - } - if res.Error != nil { - return res.Error - } - case BuilderAPIMevShareBeta1: - // clean metadata, privacy - args := *bundle - // it should already be cleaned while matching, but just in case we do it again here - MergePrivacyBuilders(&args) - cleanBundle(&args) - res, err := b.Client.Call(ctx, "mev_sendBundle", []SendMevBundleArgs{args}) - if err != nil { - return err - } - if res.Error != nil { - return res.Error - } - } - return nil -} - -type ExternalBuildersBackend struct { - Builders map[string]ExternalBuilder -} - -func NewExternalBuildersBackend(builders []ExternalBuilder) *ExternalBuildersBackend { - builderMap := make(map[string]ExternalBuilder) - for _, builder := range builders { - builderMap[builder.Name] = builder - } - return &ExternalBuildersBackend{ - Builders: builderMap, - } -} - -// ParseExternalBuilders parses a string of the form "name,url,api;name,url,api;..." -// where -// - name is a builder name (same as in the privacy.builders field) -// - url is the url of the builder endpoint -// - api is one of "refund-recipient", "v0.1" -// For example: "builder-1,http://url1,refund-recipient;builder-2,http://url2,v0.1" -func ParseExternalBuilders(str string) (*ExternalBuildersBackend, error) { - if str == "" { - return NewExternalBuildersBackend(nil), nil - } - builders := strings.Split(str, ";") - externalBuilders := make([]ExternalBuilder, 0, len(builders)) - for _, builder := range builders { - builderParts := strings.Split(builder, ",") - if len(builderParts) != 3 { - return nil, ErrInvalidExternalBuilder - } - - var api BuilderAPI - switch builderParts[2] { - case "refund-recipient": - api = BuilderAPIRefRecipient - case "v0.1": - api = BuilderAPIMevShareBeta1 - default: - return nil, ErrInvalidExternalBuilder - } - - externalBuilders = append(externalBuilders, ExternalBuilder{ - Name: builderParts[0], - Client: jsonrpc.NewClient(builderParts[1]), - API: api, - }) - } - - return NewExternalBuildersBackend(externalBuilders), nil -} - -func (b *ExternalBuildersBackend) SendBundle(ctx context.Context, logger *zap.Logger, bundle *SendMevBundleArgs) { - if bundle.Privacy != nil && len(bundle.Privacy.Builders) > 0 { - buildersUsed := make(map[string]struct{}) - for _, target := range bundle.Privacy.Builders { - if target == "default" || target == "flashbots" { - // right now we always send to flashbots and default means flashbots - continue - } - if _, ok := buildersUsed[target]; ok { - continue - } - buildersUsed[target] = struct{}{} - if builder, ok := b.Builders[target]; ok { - err := builder.SendBundle(ctx, bundle) - if err != nil { - logger.Warn("failed to send bundle to external builder", zap.Error(err), zap.String("builder", builder.Name)) - } - } else { - logger.Warn("unknown external builder", zap.String("builder", target)) - } - } - } -} - -func cleanBundle(bundle *SendMevBundleArgs) { - for _, el := range bundle.Body { - if el.Bundle != nil { - cleanBundle(el.Bundle) - } - } - bundle.Privacy = nil - bundle.Metadata = nil -} diff --git a/mevshare/hints.go b/mevshare/hints.go index cb189df..64841a1 100644 --- a/mevshare/hints.go +++ b/mevshare/hints.go @@ -45,7 +45,7 @@ func extractTxHints(tx *types.Transaction, want HintIntent) (hint *TxHint) { return hint } -func extractHintsInner(bundle *SendMevBundleArgs, bundleLogs []SimMevBodyLogs) (logs []CleanLog, txs []TxHint, err error) { +func extractHintsInner(bundle *SendMevBundleArgsV1, bundleLogs []SimMevBodyLogs) (logs []CleanLog, txs []TxHint, err error) { var want HintIntent = HintNone if bundle.Privacy != nil { want = bundle.Privacy.Hints @@ -92,7 +92,7 @@ func extractHintsInner(bundle *SendMevBundleArgs, bundleLogs []SimMevBodyLogs) ( return logs, txs, err } -func ExtractHints(bundle *SendMevBundleArgs, simRes *SimMevBundleResponse, shareGasUsed, shareMevGasPrice bool) (Hint, error) { +func ExtractHints(bundle *SendMevBundleArgsV1, simRes *SimMevBundleResponse, shareGasUsed, shareMevGasPrice bool) (Hint, error) { var want HintIntent = HintNone if bundle.Privacy != nil { want = bundle.Privacy.Hints diff --git a/mevshare/refund_recipient.go b/mevshare/refund_recipient.go index f783cfd..8391330 100644 --- a/mevshare/refund_recipient.go +++ b/mevshare/refund_recipient.go @@ -9,7 +9,7 @@ import ( var ErrCantConvertToRefRecBundle = errors.New("can't convert bundle to ref recipient bundle") -func extractTxsForRefundRecipientBundle(depth int, bundle *SendMevBundleArgs) (txs []hexutil.Bytes, canRevert []common.Hash, err error) { +func extractTxsForRefundRecipientBundle(depth int, bundle *SendMevBundleArgsV1) (txs []hexutil.Bytes, canRevert []common.Hash, err error) { if depth > MaxNestingLevel { return nil, nil, ErrInvalidBundleBody } @@ -44,7 +44,7 @@ func extractTxsForRefundRecipientBundle(depth int, bundle *SendMevBundleArgs) (t return txs, canRevert, nil } -func extractRefundDataForRefundRecipientData(bundle *SendMevBundleArgs) (percent *int, address *common.Address, err error) { +func extractRefundDataForRefundRecipientData(bundle *SendMevBundleArgsV1) (percent *int, address *common.Address, err error) { if len(bundle.Validity.Refund) == 0 { // no refund is specified return nil, nil, nil @@ -94,7 +94,7 @@ func extractRefundDataForRefundRecipientData(bundle *SendMevBundleArgs) (percent } } -func ConvertBundleToRefundRecipient(bundle *SendMevBundleArgs) (res SendRefundRecBundleArgs, err error) { +func ConvertBundleToRefundRecipient(bundle *SendMevBundleArgsV1) (res SendRefundRecBundleArgs, err error) { res.BlockNumber = bundle.Inclusion.BlockNumber txs, canRevert, err := extractTxsForRefundRecipientBundle(0, bundle) if err != nil { diff --git a/mevshare/sim_queue.go b/mevshare/sim_queue.go index 52adccd..d9f7798 100644 --- a/mevshare/sim_queue.go +++ b/mevshare/sim_queue.go @@ -96,7 +96,7 @@ func (q *SimQueue) Start(ctx context.Context) *sync.WaitGroup { return wg } -func (q *SimQueue) ScheduleBundleSimulation(ctx context.Context, bundle *SendMevBundleArgs, highPriority bool) error { +func (q *SimQueue) ScheduleBundleSimulation(ctx context.Context, bundle *SendMevBundleArgsV1, highPriority bool) error { data, err := json.Marshal(bundle) if err != nil { return err @@ -112,7 +112,7 @@ type SimulationWorker struct { } func (w *SimulationWorker) Process(ctx context.Context, data []byte, info simqueue.QueueItemInfo) error { - var bundle SendMevBundleArgs + var bundle SendMevBundleArgsV1 err := json.Unmarshal(data, &bundle) if err != nil { w.log.Error("Failed to unmarshal bundle simulation data", zap.Error(err)) diff --git a/mevshare/sim_result_backend.go b/mevshare/sim_result_backend.go index 0991a9f..da08241 100644 --- a/mevshare/sim_result_backend.go +++ b/mevshare/sim_result_backend.go @@ -12,34 +12,32 @@ import ( // SimulationResult is responsible for processing simulation results // NOTE: That error should be returned only if simulation should be retried, for example if redis is down or none of the builders responded type SimulationResult interface { - SimulatedBundle(ctx context.Context, args *SendMevBundleArgs, result *SimMevBundleResponse, info simqueue.QueueItemInfo) error + SimulatedBundle(ctx context.Context, args *SendMevBundleArgsV1, result *SimMevBundleResponse, info simqueue.QueueItemInfo) error } type Storage interface { - InsertBundleForStats(ctx context.Context, bundle *SendMevBundleArgs, result *SimMevBundleResponse) error - InsertBundleForBuilder(ctx context.Context, bundle *SendMevBundleArgs, result *SimMevBundleResponse) error + InsertBundleForStats(ctx context.Context, bundle *SendMevBundleArgsV1, result *SimMevBundleResponse) error + InsertBundleForBuilder(ctx context.Context, bundle *SendMevBundleArgsV1, result *SimMevBundleResponse) error InsertHistoricalHint(ctx context.Context, currentBlock uint64, hint *Hint) error } type SimulationResultBackend struct { log *zap.Logger hint HintBackend - builders []BuilderBackend eth EthClient store Storage - externalBuilders *ExternalBuildersBackend + builders BuildersBackend shareGasUsed bool shareMevGasPrice bool } -func NewSimulationResultBackend(log *zap.Logger, hint HintBackend, builders []BuilderBackend, eth EthClient, store Storage, externalBuilders *ExternalBuildersBackend, shareGasUsed, shareMevGasPrice bool) *SimulationResultBackend { +func NewSimulationResultBackend(log *zap.Logger, hint HintBackend, builders BuildersBackend, eth EthClient, store Storage, shareGasUsed, shareMevGasPrice bool) *SimulationResultBackend { return &SimulationResultBackend{ log: log, hint: hint, builders: builders, eth: eth, store: store, - externalBuilders: externalBuilders, shareGasUsed: shareGasUsed, shareMevGasPrice: shareMevGasPrice, } @@ -48,7 +46,7 @@ func NewSimulationResultBackend(log *zap.Logger, hint HintBackend, builders []Bu // SimulatedBundle is called when simulation is done // NOTE: we return error only if we want to retry the simulation func (s *SimulationResultBackend) SimulatedBundle(ctx context.Context, - bundle *SendMevBundleArgs, sim *SimMevBundleResponse, _ simqueue.QueueItemInfo, + bundle *SendMevBundleArgsV1, sim *SimMevBundleResponse, _ simqueue.QueueItemInfo, ) error { var hash common.Hash if bundle.Metadata != nil { @@ -74,24 +72,17 @@ func (s *SimulationResultBackend) SimulatedBundle(ctx context.Context, logger.Error("Failed to process hints", zap.Error(err)) } - for _, builder := range s.builders { - err := builder.SendMatchedShareBundle(ctx, bundle) - if err != nil { - logger.Warn("Failed to send bundle to builder", zap.Error(err)) - } - } + s.builders.SendBundle(ctx, logger, bundle) err = s.store.InsertBundleForBuilder(ctx, bundle, sim) if err != nil { logger.Error("Failed to insert bundle for builder", zap.Error(err)) } - s.externalBuilders.SendBundle(ctx, logger, bundle) - return nil } -func (s *SimulationResultBackend) ProcessHints(ctx context.Context, bundle *SendMevBundleArgs, sim *SimMevBundleResponse) error { +func (s *SimulationResultBackend) ProcessHints(ctx context.Context, bundle *SendMevBundleArgsV1, sim *SimMevBundleResponse) error { if bundle.Privacy == nil { return nil } diff --git a/mevshare/types.go b/mevshare/types.go index f6d77ba..929b466 100644 --- a/mevshare/types.go +++ b/mevshare/types.go @@ -6,13 +6,17 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" ) var ( - ErrInvalidHintIntent = errors.New("invalid hint intent") - ErrNilBundleMetadata = errors.New("bundle metadata is nil") + ErrInvalidHintIntent = errors.New("invalid hint intent") + ErrNilBundleMetadata = errors.New("bundle metadata is nil") + ErrInvalidUnionBundle = errors.New("invalid union bundle") +) + +const ( + VersionV1 = "v0.1" + VersionV2 = "v0.2" ) // HintIntent is a set of hint intents @@ -27,6 +31,7 @@ const ( HintHash HintSpecialLogs HintTxHash + HintsAll = HintContractAddress | HintFunctionSelector | HintLogs | HintCallData | HintHash | HintSpecialLogs | HintTxHash HintNone = 0 ) @@ -107,87 +112,6 @@ type TxHint struct { CallData *hexutil.Bytes `json:"callData,omitempty"` } -type SendMevBundleArgs struct { - Version string `json:"version"` - Inclusion MevBundleInclusion `json:"inclusion"` - Body []MevBundleBody `json:"body"` - Validity MevBundleValidity `json:"validity"` - Privacy *MevBundlePrivacy `json:"privacy,omitempty"` - Metadata *MevBundleMetadata `json:"metadata,omitempty"` -} - -type MevBundleInclusion struct { - BlockNumber hexutil.Uint64 `json:"block"` - MaxBlock hexutil.Uint64 `json:"maxBlock"` -} - -type MevBundleBody struct { - Hash *common.Hash `json:"hash,omitempty"` - Tx *hexutil.Bytes `json:"tx,omitempty"` - Bundle *SendMevBundleArgs `json:"bundle,omitempty"` - CanRevert bool `json:"canRevert,omitempty"` -} - -type MevBundleValidity struct { - Refund []RefundConstraint `json:"refund,omitempty"` - RefundConfig []RefundConfig `json:"refundConfig,omitempty"` -} - -type RefundConstraint struct { - BodyIdx int `json:"bodyIdx"` - Percent int `json:"percent"` -} - -type RefundConfig struct { - Address common.Address `json:"address"` - Percent int `json:"percent"` -} - -type MevBundlePrivacy struct { - Hints HintIntent `json:"hints,omitempty"` - Builders []string `json:"builders,omitempty"` - WantRefund *int `json:"wantRefund,omitempty"` -} - -type MevBundleMetadata struct { - BundleHash common.Hash `json:"bundleHash,omitempty"` - BodyHashes []common.Hash `json:"bodyHashes,omitempty"` - Signer common.Address `json:"signer,omitempty"` - OriginID string `json:"originId,omitempty"` - ReceivedAt hexutil.Uint64 `json:"receivedAt,omitempty"` -} - -type SendMevBundleResponse struct { - BundleHash common.Hash `json:"bundleHash"` -} - -type SimMevBundleResponse struct { - Success bool `json:"success"` - Error string `json:"error,omitempty"` - StateBlock hexutil.Uint64 `json:"stateBlock"` - MevGasPrice hexutil.Big `json:"mevGasPrice"` - Profit hexutil.Big `json:"profit"` - RefundableValue hexutil.Big `json:"refundableValue"` - GasUsed hexutil.Uint64 `json:"gasUsed"` - BodyLogs []SimMevBodyLogs `json:"logs,omitempty"` -} - -type SimMevBundleAuxArgs struct { - ParentBlock *rpc.BlockNumberOrHash `json:"parentBlock"` - // override the default values for the block header - BlockNumber *hexutil.Big `json:"blockNumber"` - Coinbase *common.Address `json:"coinbase"` - Timestamp *hexutil.Uint64 `json:"timestamp"` - GasLimit *hexutil.Uint64 `json:"gasLimit"` - BaseFee *hexutil.Big `json:"baseFee"` - Timeout *int64 `json:"timeout"` -} - -type SimMevBodyLogs struct { - TxLogs []*types.Log `json:"txLogs,omitempty"` - BundleLogs []SimMevBodyLogs `json:"bundleLogs,omitempty"` -} - type CleanLog struct { // address of the contract that generated the event Address common.Address `json:"address"` @@ -204,3 +128,46 @@ type SendRefundRecBundleArgs struct { RefundPercent *int `json:"refundPercent,omitempty"` RefundRecipient *common.Address `json:"refundRecipient,omitempty"` } + +type SendBundleArgsVersioned struct { + Version string `json:"version"` +} + +type SendBundleUnion struct { + v1bundle *SendMevBundleArgsV1 + v2bundle *SendMevBundleArgsV2 +} + +func (p *SendBundleUnion) UnmarshalJSON(data []byte) error { + var versioned SendBundleArgsVersioned + if err := json.Unmarshal(data, &versioned); err != nil { + return err + } + + // if version is v0.1 or beta-1 use V1, if v0.2 use V2 + switch versioned.Version { + case VersionV1, "beta-1": + var bundle SendMevBundleArgsV1 + if err := json.Unmarshal(data, &bundle); err != nil { + return err + } + p.v1bundle = &bundle + case VersionV2: + var bundle SendMevBundleArgsV2 + if err := json.Unmarshal(data, &bundle); err != nil { + return err + } + p.v2bundle = &bundle + } + return nil +} + +func (p *SendBundleUnion) MarshalJSON() ([]byte, error) { + if p.v1bundle != nil { + return json.Marshal(p.v1bundle) + } + if p.v2bundle != nil { + return json.Marshal(p.v2bundle) + } + return nil, ErrInvalidUnionBundle +} diff --git a/mevshare/utils.go b/mevshare/utils.go index f07db6a..3ec7f76 100644 --- a/mevshare/utils.go +++ b/mevshare/utils.go @@ -2,6 +2,7 @@ package mevshare import ( "context" + "errors" "math/big" "sync" "time" @@ -16,6 +17,8 @@ var ( big1 = big.NewInt(1) big10 = big.NewInt(10) + + ErrInvalidRefundConfig = errors.New("invalid refund config") ) func formatUnits(value *big.Int, unit string) string { @@ -115,3 +118,54 @@ func RoundUpWithPrecision(number *big.Int, precisionDigits int) *big.Int { return result } + +// ConvertTotalRefundConfigToBundleV1Params converts total refund config used in v0.2 to params used in mev_sendBundle v0.1 +func ConvertTotalRefundConfigToBundleV1Params(totalRefundConfig []RefundConfig) (wantRefund int, refundConfig []RefundConfig, err error) { + if len(totalRefundConfig) == 0 { + return wantRefund, refundConfig, nil + } + + totalRefund := 0 + for _, r := range totalRefundConfig { + if r.Percent <= 0 || r.Percent >= 100 { + return wantRefund, refundConfig, ErrInvalidRefundConfig + } + totalRefund += r.Percent + } + if totalRefund <= 0 || totalRefund >= 100 { + return wantRefund, refundConfig, ErrInvalidRefundConfig + } + + refundConfig = make([]RefundConfig, len(totalRefundConfig)) + copy(refundConfig, totalRefundConfig) + + // normalize refund config percentages + for i := range refundConfig { + refundConfig[i].Percent = (refundConfig[i].Percent * 100) / totalRefund + } + + // should sum to 100 + totalRefundConfDelta := 0 + for _, r := range refundConfig { + totalRefundConfDelta += r.Percent + } + totalRefundConfDelta = 100 - totalRefundConfDelta + + // try to remove delta + for i, r := range refundConfig { + if fixed := totalRefundConfDelta + r.Percent; fixed <= 100 && fixed >= 0 { + refundConfig[i].Percent = fixed + break + } + } + return totalRefund, refundConfig, nil +} + +func ConvertBundleV1ParamsToTotalRefundConfig(wantRefund int, refundConfig []RefundConfig) (totalRefundConfig []RefundConfig) { + totalRefundConfig = make([]RefundConfig, len(refundConfig)) + for i := range refundConfig { + totalRefundConfig[i].Address = refundConfig[i].Address + totalRefundConfig[i].Percent = (refundConfig[i].Percent * wantRefund) / 100 + } + return totalRefundConfig +}