From 2c64ffdbacb59e401f1884f5ac7a2a3d346455aa Mon Sep 17 00:00:00 2001 From: George Zhang Date: Sun, 25 Jan 2026 11:58:43 -0800 Subject: [PATCH 1/3] rpcserver: add header extraction and transformation helpers Add utilities for extracting and transforming HTTP headers in the JSON-RPC request chain: - headers.go: ExtractHeaders, TransformToFlashbots, header constants - Header(ctx, name): nil-safe helper to get header from request context Used by protect-rpc (edge transformation), protect-of-api (passthrough), and mev-share-node (storage) for User-Agent and Origin header logging. Co-Authored-By: Claude Opus 4.5 --- rpcserver/headers.go | 74 ++++++++++++++++ rpcserver/headers_test.go | 169 ++++++++++++++++++++++++++++++++++++ rpcserver/jsonrpc_server.go | 10 +++ 3 files changed, 253 insertions(+) create mode 100644 rpcserver/headers.go create mode 100644 rpcserver/headers_test.go diff --git a/rpcserver/headers.go b/rpcserver/headers.go new file mode 100644 index 0000000..25fec6f --- /dev/null +++ b/rpcserver/headers.go @@ -0,0 +1,74 @@ +package rpcserver + +import "net/http" + +// Standard HTTP headers +const ( + HeaderUserAgent = "User-Agent" + HeaderOrigin = "Origin" +) + +// X-Flashbots-* headers for propagation through the service chain. +const ( + HeaderFlashbotsUserAgent = "X-Flashbots-User-Agent" + HeaderFlashbotsHttpOrigin = "X-Flashbots-Http-Origin" + HeaderFlashbotsOrigin = "X-Flashbots-Origin" + HeaderFlashbotsSignature = "X-Flashbots-Signature" +) + +// EdgeHeaders are standard HTTP headers captured at the edge service. +var EdgeHeaders = []string{HeaderUserAgent, HeaderOrigin} + +// FlashbotsHeaders are X-Flashbots-* headers that propagate through the service chain. +var FlashbotsHeaders = []string{ + HeaderFlashbotsUserAgent, + HeaderFlashbotsHttpOrigin, + HeaderFlashbotsOrigin, + HeaderFlashbotsSignature, +} + +// transformMap defines how standard HTTP headers map to X-Flashbots-* headers. +var transformMap = map[string]string{ + HeaderUserAgent: HeaderFlashbotsUserAgent, + HeaderOrigin: HeaderFlashbotsHttpOrigin, +} + +// ExtractHeaders returns header values for the specified names. +// Returns nil if no matching headers are found. +func ExtractHeaders(req *http.Request, names []string) map[string]string { + if req == nil { + return nil + } + + result := make(map[string]string) + for _, name := range names { + if value := req.Header.Get(name); value != "" { + result[name] = value + } + } + + if len(result) == 0 { + return nil + } + return result +} + +// TransformToFlashbots converts standard HTTP headers to X-Flashbots-* format. +// User-Agent → X-Flashbots-User-Agent +// Origin → X-Flashbots-Http-Origin +// Returns nil if input is nil or empty. +func TransformToFlashbots(headers map[string]string) map[string]string { + if len(headers) == 0 { + return nil + } + + result := make(map[string]string, len(headers)) + for key, value := range headers { + if flashbotsKey, ok := transformMap[key]; ok { + result[flashbotsKey] = value + } else { + result[key] = value + } + } + return result +} diff --git a/rpcserver/headers_test.go b/rpcserver/headers_test.go new file mode 100644 index 0000000..1572e19 --- /dev/null +++ b/rpcserver/headers_test.go @@ -0,0 +1,169 @@ +package rpcserver + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExtractHeaders(t *testing.T) { + tests := []struct { + name string + headers map[string]string + names []string + expected map[string]string + }{ + { + name: "extract edge headers", + headers: map[string]string{ + "User-Agent": "MetaMask/16.4", + "Origin": "https://app.uniswap.org", + }, + names: EdgeHeaders, + expected: map[string]string{ + "User-Agent": "MetaMask/16.4", + "Origin": "https://app.uniswap.org", + }, + }, + { + name: "extract flashbots headers", + headers: map[string]string{ + "X-Flashbots-User-Agent": "MetaMask/16.4", + "X-Flashbots-Http-Origin": "https://app.uniswap.org", + "X-Flashbots-Origin": "wallet", + }, + names: FlashbotsHeaders, + expected: map[string]string{ + "X-Flashbots-User-Agent": "MetaMask/16.4", + "X-Flashbots-Http-Origin": "https://app.uniswap.org", + "X-Flashbots-Origin": "wallet", + }, + }, + { + name: "partial match", + headers: map[string]string{ + "User-Agent": "MetaMask/16.4", + }, + names: EdgeHeaders, + expected: map[string]string{ + "User-Agent": "MetaMask/16.4", + }, + }, + { + name: "no matching headers", + headers: map[string]string{}, + names: EdgeHeaders, + expected: nil, + }, + { + name: "unrelated headers ignored", + headers: map[string]string{ + "Content-Type": "application/json", + "User-Agent": "TestClient", + }, + names: EdgeHeaders, + expected: map[string]string{ + "User-Agent": "TestClient", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest("POST", "/", nil) + require.NoError(t, err) + + for k, v := range tt.headers { + req.Header.Set(k, v) + } + + result := ExtractHeaders(req, tt.names) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestExtractHeaders_NilRequest(t *testing.T) { + result := ExtractHeaders(nil, EdgeHeaders) + require.Nil(t, result) +} + +func TestTransformToFlashbots(t *testing.T) { + tests := []struct { + name string + input map[string]string + expected map[string]string + }{ + { + name: "transform standard headers", + input: map[string]string{ + "User-Agent": "MetaMask/16.4", + "Origin": "https://app.uniswap.org", + }, + expected: map[string]string{ + "X-Flashbots-User-Agent": "MetaMask/16.4", + "X-Flashbots-Http-Origin": "https://app.uniswap.org", + }, + }, + { + name: "partial transformation", + input: map[string]string{ + "User-Agent": "TestClient", + }, + expected: map[string]string{ + "X-Flashbots-User-Agent": "TestClient", + }, + }, + { + name: "nil input", + input: nil, + expected: nil, + }, + { + name: "empty input", + input: map[string]string{}, + expected: nil, + }, + { + name: "unknown headers passed through", + input: map[string]string{ + "User-Agent": "TestClient", + "Custom-Header": "custom-value", + }, + expected: map[string]string{ + "X-Flashbots-User-Agent": "TestClient", + "Custom-Header": "custom-value", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TransformToFlashbots(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestHeaderConstants(t *testing.T) { + // Verify constants match expected values + require.Equal(t, "User-Agent", HeaderUserAgent) + require.Equal(t, "Origin", HeaderOrigin) + require.Equal(t, "X-Flashbots-User-Agent", HeaderFlashbotsUserAgent) + require.Equal(t, "X-Flashbots-Http-Origin", HeaderFlashbotsHttpOrigin) + require.Equal(t, "X-Flashbots-Origin", HeaderFlashbotsOrigin) + require.Equal(t, "X-Flashbots-Signature", HeaderFlashbotsSignature) +} + +func TestDefaultLists(t *testing.T) { + // EdgeHeaders should contain standard HTTP headers + require.Contains(t, EdgeHeaders, HeaderUserAgent) + require.Contains(t, EdgeHeaders, HeaderOrigin) + + // FlashbotsHeaders should contain X-Flashbots-* headers + require.Contains(t, FlashbotsHeaders, HeaderFlashbotsUserAgent) + require.Contains(t, FlashbotsHeaders, HeaderFlashbotsHttpOrigin) + require.Contains(t, FlashbotsHeaders, HeaderFlashbotsOrigin) + require.Contains(t, FlashbotsHeaders, HeaderFlashbotsSignature) +} diff --git a/rpcserver/jsonrpc_server.go b/rpcserver/jsonrpc_server.go index 6746c73..6be1a93 100644 --- a/rpcserver/jsonrpc_server.go +++ b/rpcserver/jsonrpc_server.go @@ -445,6 +445,16 @@ func GetRequest(ctx context.Context) *http.Request { return req } +// Header returns a header value from the request in context. +// Returns empty string if header is not set or request is not in context. +func Header(ctx context.Context, name string) string { + req := GetRequest(ctx) + if req == nil { + return "" + } + return req.Header.Get(name) +} + // TestContextWithRequest returns a context with the given HTTP request. // This is intended for use in tests only. func TestContextWithRequest(ctx context.Context, req *http.Request) context.Context { From 28999611a65f487ad05e729e15293f606144ae4d Mon Sep 17 00:00:00 2001 From: George Zhang Date: Sun, 25 Jan 2026 12:04:55 -0800 Subject: [PATCH 2/3] feat: add header extraction helpers for User-Agent and Origin Add ExtractHeaders() to extract specified headers from HTTP requests. Keep headers as-is (no transformation to X-Flashbots-* format). Co-Authored-By: Claude Opus 4.5 --- rpcserver/headers.go | 49 +++------------------- rpcserver/headers_test.go | 88 +++------------------------------------ 2 files changed, 10 insertions(+), 127 deletions(-) diff --git a/rpcserver/headers.go b/rpcserver/headers.go index 25fec6f..4a79179 100644 --- a/rpcserver/headers.go +++ b/rpcserver/headers.go @@ -8,38 +8,19 @@ const ( HeaderOrigin = "Origin" ) -// X-Flashbots-* headers for propagation through the service chain. +// X-Flashbots-* headers for internal service communication. const ( - HeaderFlashbotsUserAgent = "X-Flashbots-User-Agent" - HeaderFlashbotsHttpOrigin = "X-Flashbots-Http-Origin" - HeaderFlashbotsOrigin = "X-Flashbots-Origin" - HeaderFlashbotsSignature = "X-Flashbots-Signature" + HeaderFlashbotsOrigin = "X-Flashbots-Origin" + HeaderFlashbotsSignature = "X-Flashbots-Signature" ) -// EdgeHeaders are standard HTTP headers captured at the edge service. +// EdgeHeaders are standard HTTP headers captured at the edge and propagated through the service chain. var EdgeHeaders = []string{HeaderUserAgent, HeaderOrigin} -// FlashbotsHeaders are X-Flashbots-* headers that propagate through the service chain. -var FlashbotsHeaders = []string{ - HeaderFlashbotsUserAgent, - HeaderFlashbotsHttpOrigin, - HeaderFlashbotsOrigin, - HeaderFlashbotsSignature, -} - -// transformMap defines how standard HTTP headers map to X-Flashbots-* headers. -var transformMap = map[string]string{ - HeaderUserAgent: HeaderFlashbotsUserAgent, - HeaderOrigin: HeaderFlashbotsHttpOrigin, -} - // ExtractHeaders returns header values for the specified names. // Returns nil if no matching headers are found. +// Panics if req is nil - callers must ensure valid request context. func ExtractHeaders(req *http.Request, names []string) map[string]string { - if req == nil { - return nil - } - result := make(map[string]string) for _, name := range names { if value := req.Header.Get(name); value != "" { @@ -52,23 +33,3 @@ func ExtractHeaders(req *http.Request, names []string) map[string]string { } return result } - -// TransformToFlashbots converts standard HTTP headers to X-Flashbots-* format. -// User-Agent → X-Flashbots-User-Agent -// Origin → X-Flashbots-Http-Origin -// Returns nil if input is nil or empty. -func TransformToFlashbots(headers map[string]string) map[string]string { - if len(headers) == 0 { - return nil - } - - result := make(map[string]string, len(headers)) - for key, value := range headers { - if flashbotsKey, ok := transformMap[key]; ok { - result[flashbotsKey] = value - } else { - result[key] = value - } - } - return result -} diff --git a/rpcserver/headers_test.go b/rpcserver/headers_test.go index 1572e19..0c63004 100644 --- a/rpcserver/headers_test.go +++ b/rpcserver/headers_test.go @@ -26,20 +26,6 @@ func TestExtractHeaders(t *testing.T) { "Origin": "https://app.uniswap.org", }, }, - { - name: "extract flashbots headers", - headers: map[string]string{ - "X-Flashbots-User-Agent": "MetaMask/16.4", - "X-Flashbots-Http-Origin": "https://app.uniswap.org", - "X-Flashbots-Origin": "wallet", - }, - names: FlashbotsHeaders, - expected: map[string]string{ - "X-Flashbots-User-Agent": "MetaMask/16.4", - "X-Flashbots-Http-Origin": "https://app.uniswap.org", - "X-Flashbots-Origin": "wallet", - }, - }, { name: "partial match", headers: map[string]string{ @@ -84,86 +70,22 @@ func TestExtractHeaders(t *testing.T) { } } -func TestExtractHeaders_NilRequest(t *testing.T) { - result := ExtractHeaders(nil, EdgeHeaders) - require.Nil(t, result) -} - -func TestTransformToFlashbots(t *testing.T) { - tests := []struct { - name string - input map[string]string - expected map[string]string - }{ - { - name: "transform standard headers", - input: map[string]string{ - "User-Agent": "MetaMask/16.4", - "Origin": "https://app.uniswap.org", - }, - expected: map[string]string{ - "X-Flashbots-User-Agent": "MetaMask/16.4", - "X-Flashbots-Http-Origin": "https://app.uniswap.org", - }, - }, - { - name: "partial transformation", - input: map[string]string{ - "User-Agent": "TestClient", - }, - expected: map[string]string{ - "X-Flashbots-User-Agent": "TestClient", - }, - }, - { - name: "nil input", - input: nil, - expected: nil, - }, - { - name: "empty input", - input: map[string]string{}, - expected: nil, - }, - { - name: "unknown headers passed through", - input: map[string]string{ - "User-Agent": "TestClient", - "Custom-Header": "custom-value", - }, - expected: map[string]string{ - "X-Flashbots-User-Agent": "TestClient", - "Custom-Header": "custom-value", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := TransformToFlashbots(tt.input) - require.Equal(t, tt.expected, result) - }) - } +func TestExtractHeaders_NilRequest_Panics(t *testing.T) { + require.Panics(t, func() { + ExtractHeaders(nil, EdgeHeaders) + }) } func TestHeaderConstants(t *testing.T) { // Verify constants match expected values require.Equal(t, "User-Agent", HeaderUserAgent) require.Equal(t, "Origin", HeaderOrigin) - require.Equal(t, "X-Flashbots-User-Agent", HeaderFlashbotsUserAgent) - require.Equal(t, "X-Flashbots-Http-Origin", HeaderFlashbotsHttpOrigin) require.Equal(t, "X-Flashbots-Origin", HeaderFlashbotsOrigin) require.Equal(t, "X-Flashbots-Signature", HeaderFlashbotsSignature) } -func TestDefaultLists(t *testing.T) { +func TestEdgeHeaders(t *testing.T) { // EdgeHeaders should contain standard HTTP headers require.Contains(t, EdgeHeaders, HeaderUserAgent) require.Contains(t, EdgeHeaders, HeaderOrigin) - - // FlashbotsHeaders should contain X-Flashbots-* headers - require.Contains(t, FlashbotsHeaders, HeaderFlashbotsUserAgent) - require.Contains(t, FlashbotsHeaders, HeaderFlashbotsHttpOrigin) - require.Contains(t, FlashbotsHeaders, HeaderFlashbotsOrigin) - require.Contains(t, FlashbotsHeaders, HeaderFlashbotsSignature) } From ae702ea8fa1d746aaef793fc845e4cf96aee8df4 Mon Sep 17 00:00:00 2001 From: George Zhang Date: Wed, 28 Jan 2026 09:32:41 -0800 Subject: [PATCH 3/3] Revert header extraction helpers Removes Header(ctx, name) wrapper and headers.go utilities. Keep GetRequest(ctx) - callers can use GetRequest(ctx).Header.Get() directly. Reverts 2c64ffd and 2899961. Co-Authored-By: Claude Opus 4.5 --- rpcserver/headers.go | 35 -------------- rpcserver/headers_test.go | 91 ------------------------------------- rpcserver/jsonrpc_server.go | 10 ---- 3 files changed, 136 deletions(-) delete mode 100644 rpcserver/headers.go delete mode 100644 rpcserver/headers_test.go diff --git a/rpcserver/headers.go b/rpcserver/headers.go deleted file mode 100644 index 4a79179..0000000 --- a/rpcserver/headers.go +++ /dev/null @@ -1,35 +0,0 @@ -package rpcserver - -import "net/http" - -// Standard HTTP headers -const ( - HeaderUserAgent = "User-Agent" - HeaderOrigin = "Origin" -) - -// X-Flashbots-* headers for internal service communication. -const ( - HeaderFlashbotsOrigin = "X-Flashbots-Origin" - HeaderFlashbotsSignature = "X-Flashbots-Signature" -) - -// EdgeHeaders are standard HTTP headers captured at the edge and propagated through the service chain. -var EdgeHeaders = []string{HeaderUserAgent, HeaderOrigin} - -// ExtractHeaders returns header values for the specified names. -// Returns nil if no matching headers are found. -// Panics if req is nil - callers must ensure valid request context. -func ExtractHeaders(req *http.Request, names []string) map[string]string { - result := make(map[string]string) - for _, name := range names { - if value := req.Header.Get(name); value != "" { - result[name] = value - } - } - - if len(result) == 0 { - return nil - } - return result -} diff --git a/rpcserver/headers_test.go b/rpcserver/headers_test.go deleted file mode 100644 index 0c63004..0000000 --- a/rpcserver/headers_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package rpcserver - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestExtractHeaders(t *testing.T) { - tests := []struct { - name string - headers map[string]string - names []string - expected map[string]string - }{ - { - name: "extract edge headers", - headers: map[string]string{ - "User-Agent": "MetaMask/16.4", - "Origin": "https://app.uniswap.org", - }, - names: EdgeHeaders, - expected: map[string]string{ - "User-Agent": "MetaMask/16.4", - "Origin": "https://app.uniswap.org", - }, - }, - { - name: "partial match", - headers: map[string]string{ - "User-Agent": "MetaMask/16.4", - }, - names: EdgeHeaders, - expected: map[string]string{ - "User-Agent": "MetaMask/16.4", - }, - }, - { - name: "no matching headers", - headers: map[string]string{}, - names: EdgeHeaders, - expected: nil, - }, - { - name: "unrelated headers ignored", - headers: map[string]string{ - "Content-Type": "application/json", - "User-Agent": "TestClient", - }, - names: EdgeHeaders, - expected: map[string]string{ - "User-Agent": "TestClient", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest("POST", "/", nil) - require.NoError(t, err) - - for k, v := range tt.headers { - req.Header.Set(k, v) - } - - result := ExtractHeaders(req, tt.names) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestExtractHeaders_NilRequest_Panics(t *testing.T) { - require.Panics(t, func() { - ExtractHeaders(nil, EdgeHeaders) - }) -} - -func TestHeaderConstants(t *testing.T) { - // Verify constants match expected values - require.Equal(t, "User-Agent", HeaderUserAgent) - require.Equal(t, "Origin", HeaderOrigin) - require.Equal(t, "X-Flashbots-Origin", HeaderFlashbotsOrigin) - require.Equal(t, "X-Flashbots-Signature", HeaderFlashbotsSignature) -} - -func TestEdgeHeaders(t *testing.T) { - // EdgeHeaders should contain standard HTTP headers - require.Contains(t, EdgeHeaders, HeaderUserAgent) - require.Contains(t, EdgeHeaders, HeaderOrigin) -} diff --git a/rpcserver/jsonrpc_server.go b/rpcserver/jsonrpc_server.go index 6be1a93..6746c73 100644 --- a/rpcserver/jsonrpc_server.go +++ b/rpcserver/jsonrpc_server.go @@ -445,16 +445,6 @@ func GetRequest(ctx context.Context) *http.Request { return req } -// Header returns a header value from the request in context. -// Returns empty string if header is not set or request is not in context. -func Header(ctx context.Context, name string) string { - req := GetRequest(ctx) - if req == nil { - return "" - } - return req.Header.Get(name) -} - // TestContextWithRequest returns a context with the given HTTP request. // This is intended for use in tests only. func TestContextWithRequest(ctx context.Context, req *http.Request) context.Context {