Skip to content

Commit 26f9e82

Browse files
odysseus0claudeTymKh
authored
Add header-based configuration override for QuickNode integration (#185)
## Summary - Implements `X-Flashbots-Origin` header support for guaranteed refund collection - Adds preset configuration system to ensure partners receive fixed refund percentages - Maintains backward compatibility with existing URL parameter flows ## Implementation Details - **ConfigurationWatcher**: Extended to support `presets` field with graceful error handling - **Request Handler**: New `getEffectiveParameters()` method checks header first, falls back to URL parsing - **Configuration**: Preset URLs are pre-parsed at startup for performance and early error detection ## Key Behavior When `X-Flashbots-Origin` header is present: 1. Looks up preset configuration for the origin ID 2. Uses preset parameters, **completely ignoring URL parameters** 3. Falls back to normal URL parsing if no preset found ## Testing - Unit tests cover preset parsing and header override logic - Tests graceful degradation for invalid preset configurations - Ready for testnet deployment and integration testing ## Configuration Changes Required ### For Testing (Sepolia testnet) Add to `devops/k8s/eth-l1-testnets/sepolia/protect/rpc/configs/customers.yaml`: ```yaml urls: test1: - /fast?originId=test1 test2: - /fast?originId=test2&auctionTimeout=500&maxBlockRange=7 presets: test-header: /fast?originId=test-header&refund=0xb60e8dd61c5d32be8058bb8eb970870f07233155:50 ``` ### For Production (when QuickNode provides requirements) Add to `devops/k8s/eth-l1-prod/protect-old/rpc.yaml`: ```yaml urls: # ... existing configs ... quicknode: - /fast?originId=quicknode presets: quicknode: [QuickNode to provide: endpoint, refund address, percentage, other params] ``` ## Testing Plan 1. Deploy testnet config changes 2. Deploy code to testnet 3. Test with header: `X-Flashbots-Origin-ID: test-header` 4. Verify preset overrides URL parameters correctly 5. Verify fallback works when no header present 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: TymKh <tkhrushchov@gmail.com>
1 parent 152ab44 commit 26f9e82

File tree

4 files changed

+221
-15
lines changed

4 files changed

+221
-15
lines changed

server/configuration_watcher.go

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,85 @@ package server
22

33
import (
44
"errors"
5+
"fmt"
56
"maps"
67
"net/url"
78
"os"
89

10+
"github.com/ethereum/go-ethereum/log"
911
"gopkg.in/yaml.v3"
1012
)
1113

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

1416
type CustomersConfig struct {
15-
URLs map[string][]string `yaml:"urls"`
17+
URLs map[string][]string `yaml:"urls"`
18+
Presets map[string]string `yaml:"presets,omitempty"`
1619
}
1720

1821
// ConfigurationWatcher
1922
// all params are normilized
2023
type ConfigurationWatcher struct {
2124
// CustomersConfig represents config for each custom with allowed list of configuration parameters
2225
ParsedCustomersConfig map[string][]URLParameters
26+
// ParsedPresets contains pre-parsed preset configurations for header-based override
27+
ParsedPresets map[string]URLParameters
28+
}
29+
30+
// parseURLToParameters converts a raw URL string to URLParameters
31+
func parseURLToParameters(rawURL string) (URLParameters, error) {
32+
parsedURL, err := url.Parse(rawURL)
33+
if err != nil {
34+
return URLParameters{}, fmt.Errorf("failed to parse URL: %w", err)
35+
}
36+
37+
params, err := ExtractParametersFromUrl(parsedURL, nil)
38+
if err != nil {
39+
return URLParameters{}, fmt.Errorf("failed to extract parameters: %w", err)
40+
}
41+
42+
return params, nil
2343
}
2444

2545
func NewConfigurationWatcher(customersConfig CustomersConfig) (*ConfigurationWatcher, error) {
2646
parsedCustomersConfig := make(map[string][]URLParameters)
27-
for k, v := range customersConfig.URLs {
28-
var allowedConfigs []URLParameters
29-
for _, rawUrl := range v {
30-
parsedUrl, err := url.Parse(rawUrl)
47+
for customerID, urls := range customersConfig.URLs {
48+
allowedConfigs := make([]URLParameters, 0, len(urls))
49+
for _, rawURL := range urls {
50+
urlParam, err := parseURLToParameters(rawURL)
3151
if err != nil {
32-
return nil, err
52+
return nil, fmt.Errorf("invalid URL for customer %s: %w", customerID, err)
3353
}
34-
URLParam, err := ExtractParametersFromUrl(parsedUrl, nil)
35-
if err != nil {
36-
return nil, err
37-
}
38-
allowedConfigs = append(allowedConfigs, URLParam)
54+
allowedConfigs = append(allowedConfigs, urlParam)
55+
}
56+
parsedCustomersConfig[customerID] = allowedConfigs
57+
}
58+
59+
// Parse presets for header-based override
60+
parsedPresets := make(map[string]URLParameters)
61+
for originID, presetURL := range customersConfig.Presets {
62+
params, err := parseURLToParameters(presetURL)
63+
if err != nil {
64+
// Log error but continue - graceful degradation
65+
log.Error("Failed to parse preset configuration", "originID", originID, "url", presetURL, "error", err)
66+
continue
3967
}
40-
parsedCustomersConfig[k] = allowedConfigs
68+
parsedPresets[originID] = params
69+
log.Info("Loaded preset configuration", "originID", originID)
4170
}
42-
return &ConfigurationWatcher{ParsedCustomersConfig: parsedCustomersConfig}, nil
71+
72+
return &ConfigurationWatcher{
73+
ParsedCustomersConfig: parsedCustomersConfig,
74+
ParsedPresets: parsedPresets,
75+
}, nil
4376
}
4477

4578
func ReadCustomerConfigFromFile(fileName string) (*ConfigurationWatcher, error) {
4679
if fileName == "" {
47-
return &ConfigurationWatcher{ParsedCustomersConfig: make(map[string][]URLParameters)}, nil
80+
return &ConfigurationWatcher{
81+
ParsedCustomersConfig: make(map[string][]URLParameters),
82+
ParsedPresets: make(map[string]URLParameters),
83+
}, nil
4884
}
4985
data, err := os.ReadFile(fileName)
5086
if err != nil {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package server
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestConfigurationWatcherPresets(t *testing.T) {
10+
// Test the core business logic: valid presets are parsed and available
11+
config := CustomersConfig{
12+
URLs: map[string][]string{
13+
"quicknode": {"/fast?originId=quicknode"},
14+
},
15+
Presets: map[string]string{
16+
"quicknode": "/fast?originId=quicknode&refund=0x1234567890123456789012345678901234567890:90",
17+
},
18+
}
19+
20+
watcher, err := NewConfigurationWatcher(config)
21+
require.NoError(t, err)
22+
require.NotNil(t, watcher)
23+
24+
// Core functionality: preset should be parsed and available
25+
preset, exists := watcher.ParsedPresets["quicknode"]
26+
require.True(t, exists)
27+
require.Equal(t, "quicknode", preset.originId)
28+
require.True(t, preset.fast)
29+
require.Equal(t, 1, len(preset.pref.Validity.Refund))
30+
}
31+
32+
func TestConfigurationWatcherInvalidPresets(t *testing.T) {
33+
// Test graceful degradation: invalid presets are skipped, don't break startup
34+
config := CustomersConfig{
35+
URLs: map[string][]string{
36+
"test": {"/fast?originId=test"},
37+
},
38+
Presets: map[string]string{
39+
"valid": "/fast?originId=valid",
40+
"invalid": "://invalid-url", // This should be skipped
41+
},
42+
}
43+
44+
watcher, err := NewConfigurationWatcher(config)
45+
require.NoError(t, err) // Should not fail startup
46+
47+
// Valid preset loaded
48+
_, exists := watcher.ParsedPresets["valid"]
49+
require.True(t, exists)
50+
51+
// Invalid preset skipped
52+
_, exists = watcher.ParsedPresets["invalid"]
53+
require.False(t, exists)
54+
}

server/request_handler.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ func NewRpcRequestHandler(
7373
}
7474
}
7575

76+
// getEffectiveParameters determines the URL parameters to use for this request.
77+
// It checks for header-based preset override first, then falls back to URL parsing.
78+
func (r *RpcRequestHandler) getEffectiveParameters() (URLParameters, error) {
79+
extracted, err := ExtractParametersFromUrl(r.req.URL, r.builderNames)
80+
if err != nil {
81+
return extracted, err
82+
}
83+
if r.configurationWatcher == nil {
84+
return extracted, nil
85+
}
86+
originID := extracted.originId
87+
if headerOriginID := r.req.Header.Get("X-Flashbots-Origin"); headerOriginID != "" {
88+
originID = headerOriginID
89+
}
90+
if preset, exists := r.configurationWatcher.ParsedPresets[originID]; exists {
91+
r.logger.Info("Using preset configuration", "originID", originID)
92+
return preset, nil
93+
}
94+
95+
return extracted, nil
96+
}
97+
7698
// nolint
7799
func (r *RpcRequestHandler) process() {
78100
r.logger = r.logger.New("uid", r.uid)
@@ -132,7 +154,7 @@ func (r *RpcRequestHandler) process() {
132154
}
133155

134156
// mev-share parameters
135-
urlParams, err := ExtractParametersFromUrl(r.req.URL, r.builderNames)
157+
urlParams, err := r.getEffectiveParameters()
136158
if err != nil {
137159
r.logger.Warn("[process] Invalid auction preference", "error", err, "url", r.req.URL)
138160
res := AuctionPreferenceErrorToJSONRPCResponse(jsonReq, err)

server/request_handler_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package server
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/log"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestGetEffectiveParameters(t *testing.T) {
14+
// Core business logic test: header with preset uses preset (ignores URL)
15+
config := CustomersConfig{
16+
Presets: map[string]string{
17+
"quicknode": "/fast?originId=quicknode&refund=0x1234567890123456789012345678901234567890:90",
18+
},
19+
}
20+
21+
watcher, err := NewConfigurationWatcher(config)
22+
require.NoError(t, err)
23+
24+
// Request with header and different URL parameters
25+
req := httptest.NewRequest(http.MethodPost, "/fast?originId=user-provided&refund=0xdadB0d80178819F2319190D340ce9A924f783711:10", nil)
26+
req.Header.Set("X-Flashbots-Origin", "quicknode")
27+
28+
w := httptest.NewRecorder()
29+
respw := http.ResponseWriter(w)
30+
31+
handler := &RpcRequestHandler{
32+
respw: &respw,
33+
req: req,
34+
logger: log.New(),
35+
builderNames: []string{"flashbots"},
36+
configurationWatcher: watcher,
37+
}
38+
39+
params, err := handler.getEffectiveParameters()
40+
require.NoError(t, err)
41+
42+
// Should use preset values, ignore URL
43+
require.Equal(t, "quicknode", params.originId)
44+
require.True(t, params.fast)
45+
require.Equal(t, 1, len(params.pref.Validity.Refund)) // Preset refund, not URL refund
46+
require.Equal(t, params.pref.Validity.Refund[0].Address, common.HexToAddress("0x1234567890123456789012345678901234567890"))
47+
}
48+
49+
func TestGetEffectiveParametersNoHeader(t *testing.T) {
50+
// Fallback behavior: no header uses URL normally
51+
req := httptest.NewRequest(http.MethodPost, "/fast?originId=normal-user", nil)
52+
// No X-Flashbots-Origin-ID header
53+
54+
w := httptest.NewRecorder()
55+
respw := http.ResponseWriter(w)
56+
57+
handler := &RpcRequestHandler{
58+
respw: &respw,
59+
req: req,
60+
logger: log.New(),
61+
builderNames: []string{"flashbots"},
62+
configurationWatcher: &ConfigurationWatcher{
63+
ParsedPresets: make(map[string]URLParameters),
64+
},
65+
}
66+
67+
params, err := handler.getEffectiveParameters()
68+
require.NoError(t, err)
69+
require.Equal(t, "normal-user", params.originId)
70+
require.True(t, params.fast)
71+
}
72+
73+
func TestGetEffectiveParametersHeaderNoPreset(t *testing.T) {
74+
// Edge case: header present but no matching preset falls back to URL
75+
req := httptest.NewRequest(http.MethodPost, "/fast?originId=fallback-user", nil)
76+
req.Header.Set("X-Flashbots-Origin", "unknown")
77+
78+
w := httptest.NewRecorder()
79+
respw := http.ResponseWriter(w)
80+
81+
handler := &RpcRequestHandler{
82+
respw: &respw,
83+
req: req,
84+
logger: log.New(),
85+
builderNames: []string{"flashbots"},
86+
configurationWatcher: &ConfigurationWatcher{
87+
ParsedPresets: make(map[string]URLParameters),
88+
},
89+
}
90+
91+
params, err := handler.getEffectiveParameters()
92+
require.NoError(t, err)
93+
require.Equal(t, "fallback-user", params.originId)
94+
}

0 commit comments

Comments
 (0)