Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 50 additions & 14 deletions server/configuration_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,85 @@ package server

import (
"errors"
"fmt"
"maps"
"net/url"
"os"

"github.com/ethereum/go-ethereum/log"
"gopkg.in/yaml.v3"
)

var ErrCustomerNotConfigured = errors.New("customer is not configured")

type CustomersConfig struct {
URLs map[string][]string `yaml:"urls"`
URLs map[string][]string `yaml:"urls"`
Presets map[string]string `yaml:"presets,omitempty"`
}

// ConfigurationWatcher
// all params are normilized
type ConfigurationWatcher struct {
// CustomersConfig represents config for each custom with allowed list of configuration parameters
ParsedCustomersConfig map[string][]URLParameters
// ParsedPresets contains pre-parsed preset configurations for header-based override
ParsedPresets map[string]URLParameters
}

// parseURLToParameters converts a raw URL string to URLParameters
func parseURLToParameters(rawURL string) (URLParameters, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return URLParameters{}, fmt.Errorf("failed to parse URL: %w", err)
}

params, err := ExtractParametersFromUrl(parsedURL, nil)
if err != nil {
return URLParameters{}, fmt.Errorf("failed to extract parameters: %w", err)
}

return params, nil
}

func NewConfigurationWatcher(customersConfig CustomersConfig) (*ConfigurationWatcher, error) {
parsedCustomersConfig := make(map[string][]URLParameters)
for k, v := range customersConfig.URLs {
var allowedConfigs []URLParameters
for _, rawUrl := range v {
parsedUrl, err := url.Parse(rawUrl)
for customerID, urls := range customersConfig.URLs {
allowedConfigs := make([]URLParameters, 0, len(urls))
for _, rawURL := range urls {
urlParam, err := parseURLToParameters(rawURL)
if err != nil {
return nil, err
return nil, fmt.Errorf("invalid URL for customer %s: %w", customerID, err)
}
URLParam, err := ExtractParametersFromUrl(parsedUrl, nil)
if err != nil {
return nil, err
}
allowedConfigs = append(allowedConfigs, URLParam)
allowedConfigs = append(allowedConfigs, urlParam)
}
parsedCustomersConfig[customerID] = allowedConfigs
}

// Parse presets for header-based override
parsedPresets := make(map[string]URLParameters)
for originID, presetURL := range customersConfig.Presets {
params, err := parseURLToParameters(presetURL)
if err != nil {
// Log error but continue - graceful degradation
log.Error("Failed to parse preset configuration", "originID", originID, "url", presetURL, "error", err)
continue
}
parsedCustomersConfig[k] = allowedConfigs
parsedPresets[originID] = params
log.Info("Loaded preset configuration", "originID", originID)
}
return &ConfigurationWatcher{ParsedCustomersConfig: parsedCustomersConfig}, nil

return &ConfigurationWatcher{
ParsedCustomersConfig: parsedCustomersConfig,
ParsedPresets: parsedPresets,
}, nil
}

func ReadCustomerConfigFromFile(fileName string) (*ConfigurationWatcher, error) {
if fileName == "" {
return &ConfigurationWatcher{ParsedCustomersConfig: make(map[string][]URLParameters)}, nil
return &ConfigurationWatcher{
ParsedCustomersConfig: make(map[string][]URLParameters),
ParsedPresets: make(map[string]URLParameters),
}, nil
}
data, err := os.ReadFile(fileName)
if err != nil {
Expand Down
54 changes: 54 additions & 0 deletions server/configuration_watcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package server

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestConfigurationWatcherPresets(t *testing.T) {
// Test the core business logic: valid presets are parsed and available
config := CustomersConfig{
URLs: map[string][]string{
"quicknode": {"/fast?originId=quicknode"},
},
Presets: map[string]string{
"quicknode": "/fast?originId=quicknode&refund=0x1234567890123456789012345678901234567890:90",
},
}

watcher, err := NewConfigurationWatcher(config)
require.NoError(t, err)
require.NotNil(t, watcher)

// Core functionality: preset should be parsed and available
preset, exists := watcher.ParsedPresets["quicknode"]
require.True(t, exists)
require.Equal(t, "quicknode", preset.originId)
require.True(t, preset.fast)
require.Equal(t, 1, len(preset.pref.Validity.Refund))
}

func TestConfigurationWatcherInvalidPresets(t *testing.T) {
// Test graceful degradation: invalid presets are skipped, don't break startup
config := CustomersConfig{
URLs: map[string][]string{
"test": {"/fast?originId=test"},
},
Presets: map[string]string{
"valid": "/fast?originId=valid",
"invalid": "://invalid-url", // This should be skipped
},
}

watcher, err := NewConfigurationWatcher(config)
require.NoError(t, err) // Should not fail startup

// Valid preset loaded
_, exists := watcher.ParsedPresets["valid"]
require.True(t, exists)

// Invalid preset skipped
_, exists = watcher.ParsedPresets["invalid"]
require.False(t, exists)
}
24 changes: 23 additions & 1 deletion server/request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ func NewRpcRequestHandler(
}
}

// getEffectiveParameters determines the URL parameters to use for this request.
// It checks for header-based preset override first, then falls back to URL parsing.
func (r *RpcRequestHandler) getEffectiveParameters() (URLParameters, error) {
extracted, err := ExtractParametersFromUrl(r.req.URL, r.builderNames)
if err != nil {
return extracted, err
}
if r.configurationWatcher == nil {
return extracted, nil
}
originID := extracted.originId
if headerOriginID := r.req.Header.Get("X-Flashbots-Origin"); headerOriginID != "" {
originID = headerOriginID
}
if preset, exists := r.configurationWatcher.ParsedPresets[originID]; exists {
r.logger.Info("Using preset configuration", "originID", originID)
return preset, nil
}

return extracted, nil
}

// nolint
func (r *RpcRequestHandler) process() {
r.logger = r.logger.New("uid", r.uid)
Expand Down Expand Up @@ -132,7 +154,7 @@ func (r *RpcRequestHandler) process() {
}

// mev-share parameters
urlParams, err := ExtractParametersFromUrl(r.req.URL, r.builderNames)
urlParams, err := r.getEffectiveParameters()
if err != nil {
r.logger.Warn("[process] Invalid auction preference", "error", err, "url", r.req.URL)
res := AuctionPreferenceErrorToJSONRPCResponse(jsonReq, err)
Expand Down
94 changes: 94 additions & 0 deletions server/request_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package server

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)

func TestGetEffectiveParameters(t *testing.T) {
// Core business logic test: header with preset uses preset (ignores URL)
config := CustomersConfig{
Presets: map[string]string{
"quicknode": "/fast?originId=quicknode&refund=0x1234567890123456789012345678901234567890:90",
},
}

watcher, err := NewConfigurationWatcher(config)
require.NoError(t, err)

// Request with header and different URL parameters
req := httptest.NewRequest(http.MethodPost, "/fast?originId=user-provided&refund=0xdadB0d80178819F2319190D340ce9A924f783711:10", nil)
req.Header.Set("X-Flashbots-Origin", "quicknode")

w := httptest.NewRecorder()
respw := http.ResponseWriter(w)

handler := &RpcRequestHandler{
respw: &respw,
req: req,
logger: log.New(),
builderNames: []string{"flashbots"},
configurationWatcher: watcher,
}

params, err := handler.getEffectiveParameters()
require.NoError(t, err)

// Should use preset values, ignore URL
require.Equal(t, "quicknode", params.originId)
require.True(t, params.fast)
require.Equal(t, 1, len(params.pref.Validity.Refund)) // Preset refund, not URL refund
require.Equal(t, params.pref.Validity.Refund[0].Address, common.HexToAddress("0x1234567890123456789012345678901234567890"))
}

func TestGetEffectiveParametersNoHeader(t *testing.T) {
// Fallback behavior: no header uses URL normally
req := httptest.NewRequest(http.MethodPost, "/fast?originId=normal-user", nil)
// No X-Flashbots-Origin-ID header

w := httptest.NewRecorder()
respw := http.ResponseWriter(w)

handler := &RpcRequestHandler{
respw: &respw,
req: req,
logger: log.New(),
builderNames: []string{"flashbots"},
configurationWatcher: &ConfigurationWatcher{
ParsedPresets: make(map[string]URLParameters),
},
}

params, err := handler.getEffectiveParameters()
require.NoError(t, err)
require.Equal(t, "normal-user", params.originId)
require.True(t, params.fast)
}

func TestGetEffectiveParametersHeaderNoPreset(t *testing.T) {
// Edge case: header present but no matching preset falls back to URL
req := httptest.NewRequest(http.MethodPost, "/fast?originId=fallback-user", nil)
req.Header.Set("X-Flashbots-Origin", "unknown")

w := httptest.NewRecorder()
respw := http.ResponseWriter(w)

handler := &RpcRequestHandler{
respw: &respw,
req: req,
logger: log.New(),
builderNames: []string{"flashbots"},
configurationWatcher: &ConfigurationWatcher{
ParsedPresets: make(map[string]URLParameters),
},
}

params, err := handler.getEffectiveParameters()
require.NoError(t, err)
require.Equal(t, "fallback-user", params.originId)
}