Allow custom headers in validator client HTTP requests (#15884)

* Allow custom headers in validator client HTTP requests

* changelog <3

* improve flag description

* Bastin's review

* James' review

* add godoc for NodeConnectionOption
This commit is contained in:
Radosław Kapka
2025-10-22 15:47:35 +02:00
committed by GitHub
parent 3ecb5d0b67
commit 2f090c52d9
15 changed files with 160 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ go_library(
"client.go", "client.go",
"errors.go", "errors.go",
"options.go", "options.go",
"transport.go",
], ],
importpath = "github.com/OffchainLabs/prysm/v6/api/client", importpath = "github.com/OffchainLabs/prysm/v6/api/client",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
@@ -14,7 +15,13 @@ go_library(
go_test( go_test(
name = "go_default_test", name = "go_default_test",
srcs = ["client_test.go"], srcs = [
"client_test.go",
"transport_test.go",
],
embed = [":go_default_library"], embed = [":go_default_library"],
deps = ["//testing/require:go_default_library"], deps = [
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
],
) )

25
api/client/transport.go Normal file
View File

@@ -0,0 +1,25 @@
package client
import "net/http"
// CustomHeadersTransport adds custom headers to each request
type CustomHeadersTransport struct {
base http.RoundTripper
headers map[string][]string
}
func NewCustomHeadersTransport(base http.RoundTripper, headers map[string][]string) *CustomHeadersTransport {
return &CustomHeadersTransport{
base: base,
headers: headers,
}
}
func (t *CustomHeadersTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for header, values := range t.headers {
for _, value := range values {
req.Header.Add(header, value)
}
}
return t.base.RoundTrip(req)
}

View File

@@ -0,0 +1,25 @@
package client
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/OffchainLabs/prysm/v6/testing/assert"
"github.com/OffchainLabs/prysm/v6/testing/require"
)
type noopTransport struct{}
func (*noopTransport) RoundTrip(*http.Request) (*http.Response, error) {
return nil, nil
}
func TestRoundTrip(t *testing.T) {
tr := &CustomHeadersTransport{base: &noopTransport{}, headers: map[string][]string{"key1": []string{"value1", "value2"}, "key2": []string{"value3"}}}
req := httptest.NewRequest("GET", "http://foo", nil)
_, err := tr.RoundTrip(req)
require.NoError(t, err)
assert.DeepEqual(t, []string{"value1", "value2"}, req.Header.Values("key1"))
assert.DeepEqual(t, []string{"value3"}, req.Header.Values("key2"))
}

View File

@@ -0,0 +1,3 @@
### Added
- Allow custom headers in validator client HTTP requests.

View File

@@ -45,6 +45,13 @@ var (
Usage: "Beacon node REST API provider endpoint.", Usage: "Beacon node REST API provider endpoint.",
Value: "http://127.0.0.1:3500", Value: "http://127.0.0.1:3500",
} }
// BeaconRESTApiHeaders defines a list of headers to send with all HTTP requests to the beacon node.
BeaconRESTApiHeaders = &cli.StringFlag{
Name: "beacon-rest-api-headers",
Usage: `Comma-separated list of key value pairs to pass as headers for all HTTP calls to the beacon node.
To provide multiple values for the same key, specify the same key for each value.
Example: --grpc-headers=key1=value1,key1=value2,key2=value3`,
}
// CertFlag defines a flag for the node's TLS certificate. // CertFlag defines a flag for the node's TLS certificate.
CertFlag = &cli.StringFlag{ CertFlag = &cli.StringFlag{
Name: "tls-cert", Name: "tls-cert",

View File

@@ -51,6 +51,7 @@ func startNode(ctx *cli.Context) error {
var appFlags = []cli.Flag{ var appFlags = []cli.Flag{
flags.BeaconRPCProviderFlag, flags.BeaconRPCProviderFlag,
flags.BeaconRESTApiProviderFlag, flags.BeaconRESTApiProviderFlag,
flags.BeaconRESTApiHeaders,
flags.CertFlag, flags.CertFlag,
flags.GraffitiFlag, flags.GraffitiFlag,
flags.DisablePenaltyRewardLogFlag, flags.DisablePenaltyRewardLogFlag,

View File

@@ -93,6 +93,7 @@ var appHelpFlagGroups = []flagGroup{
Flags: []cli.Flag{ Flags: []cli.Flag{
flags.CertFlag, flags.CertFlag,
flags.BeaconRPCProviderFlag, flags.BeaconRPCProviderFlag,
flags.BeaconRESTApiHeaders,
flags.EnableRPCFlag, flags.EnableRPCFlag,
flags.RPCHost, flags.RPCHost,
flags.RPCPort, flags.RPCPort,

View File

@@ -84,7 +84,7 @@ func (acm *CLIManager) prepareBeaconClients(ctx context.Context) (*iface.Validat
conn := validatorHelpers.NewNodeConnection( conn := validatorHelpers.NewNodeConnection(
grpcConn, grpcConn,
acm.beaconApiEndpoint, acm.beaconApiEndpoint,
acm.beaconApiTimeout, validatorHelpers.WithBeaconApiTimeout(acm.beaconApiTimeout),
) )
restHandler := beaconApi.NewBeaconApiRestHandler( restHandler := beaconApi.NewBeaconApiRestHandler(

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"time" "time"
api "github.com/OffchainLabs/prysm/v6/api/client"
eventClient "github.com/OffchainLabs/prysm/v6/api/client/event" eventClient "github.com/OffchainLabs/prysm/v6/api/client/event"
grpcutil "github.com/OffchainLabs/prysm/v6/api/grpc" grpcutil "github.com/OffchainLabs/prysm/v6/api/grpc"
"github.com/OffchainLabs/prysm/v6/async/event" "github.com/OffchainLabs/prysm/v6/async/event"
@@ -79,6 +80,7 @@ type Config struct {
BeaconNodeGRPCEndpoint string BeaconNodeGRPCEndpoint string
BeaconNodeCert string BeaconNodeCert string
BeaconApiEndpoint string BeaconApiEndpoint string
BeaconApiHeaders map[string][]string
BeaconApiTimeout time.Duration BeaconApiTimeout time.Duration
Graffiti string Graffiti string
GraffitiStruct *graffiti.Graffiti GraffitiStruct *graffiti.Graffiti
@@ -142,7 +144,8 @@ func NewValidatorService(ctx context.Context, cfg *Config) (*ValidatorService, e
s.conn = validatorHelpers.NewNodeConnection( s.conn = validatorHelpers.NewNodeConnection(
grpcConn, grpcConn,
cfg.BeaconApiEndpoint, cfg.BeaconApiEndpoint,
cfg.BeaconApiTimeout, validatorHelpers.WithBeaconApiHeaders(cfg.BeaconApiHeaders),
validatorHelpers.WithBeaconApiTimeout(cfg.BeaconApiTimeout),
) )
return s, nil return s, nil
@@ -185,8 +188,9 @@ func (v *ValidatorService) Start() {
return return
} }
headersTransport := api.NewCustomHeadersTransport(http.DefaultTransport, v.conn.GetBeaconApiHeaders())
restHandler := beaconApi.NewBeaconApiRestHandler( restHandler := beaconApi.NewBeaconApiRestHandler(
http.Client{Timeout: v.conn.GetBeaconApiTimeout(), Transport: otelhttp.NewTransport(http.DefaultTransport)}, http.Client{Timeout: v.conn.GetBeaconApiTimeout(), Transport: otelhttp.NewTransport(headersTransport)},
hosts[0], hosts[0],
) )

View File

@@ -10,16 +10,37 @@ import (
type NodeConnection interface { type NodeConnection interface {
GetGrpcClientConn() *grpc.ClientConn GetGrpcClientConn() *grpc.ClientConn
GetBeaconApiUrl() string GetBeaconApiUrl() string
GetBeaconApiHeaders() map[string][]string
setBeaconApiHeaders(map[string][]string)
GetBeaconApiTimeout() time.Duration GetBeaconApiTimeout() time.Duration
setBeaconApiTimeout(time.Duration)
dummy() dummy()
} }
type nodeConnection struct { type nodeConnection struct {
grpcClientConn *grpc.ClientConn grpcClientConn *grpc.ClientConn
beaconApiUrl string beaconApiUrl string
beaconApiHeaders map[string][]string
beaconApiTimeout time.Duration beaconApiTimeout time.Duration
} }
// NodeConnectionOption is a functional option for configuring the node connection.
type NodeConnectionOption func(nc NodeConnection)
// WithBeaconApiHeaders sets the HTTP headers that should be sent to the server along with each request.
func WithBeaconApiHeaders(headers map[string][]string) NodeConnectionOption {
return func(nc NodeConnection) {
nc.setBeaconApiHeaders(headers)
}
}
// WithBeaconApiTimeout sets the HTTP request timeout.
func WithBeaconApiTimeout(timeout time.Duration) NodeConnectionOption {
return func(nc NodeConnection) {
nc.setBeaconApiTimeout(timeout)
}
}
func (c *nodeConnection) GetGrpcClientConn() *grpc.ClientConn { func (c *nodeConnection) GetGrpcClientConn() *grpc.ClientConn {
return c.grpcClientConn return c.grpcClientConn
} }
@@ -28,16 +49,30 @@ func (c *nodeConnection) GetBeaconApiUrl() string {
return c.beaconApiUrl return c.beaconApiUrl
} }
func (c *nodeConnection) GetBeaconApiHeaders() map[string][]string {
return c.beaconApiHeaders
}
func (c *nodeConnection) setBeaconApiHeaders(headers map[string][]string) {
c.beaconApiHeaders = headers
}
func (c *nodeConnection) GetBeaconApiTimeout() time.Duration { func (c *nodeConnection) GetBeaconApiTimeout() time.Duration {
return c.beaconApiTimeout return c.beaconApiTimeout
} }
func (c *nodeConnection) setBeaconApiTimeout(timeout time.Duration) {
c.beaconApiTimeout = timeout
}
func (*nodeConnection) dummy() {} func (*nodeConnection) dummy() {}
func NewNodeConnection(grpcConn *grpc.ClientConn, beaconApiUrl string, beaconApiTimeout time.Duration) NodeConnection { func NewNodeConnection(grpcConn *grpc.ClientConn, beaconApiUrl string, opts ...NodeConnectionOption) NodeConnection {
conn := &nodeConnection{} conn := &nodeConnection{}
conn.grpcClientConn = grpcConn conn.grpcClientConn = grpcConn
conn.beaconApiUrl = beaconApiUrl conn.beaconApiUrl = beaconApiUrl
conn.beaconApiTimeout = beaconApiTimeout for _, opt := range opts {
opt(conn)
}
return conn return conn
} }

View File

@@ -433,6 +433,7 @@ func (c *ValidatorClient) registerValidatorService(cliCtx *cli.Context) error {
BeaconNodeGRPCEndpoint: cliCtx.String(flags.BeaconRPCProviderFlag.Name), BeaconNodeGRPCEndpoint: cliCtx.String(flags.BeaconRPCProviderFlag.Name),
BeaconNodeCert: cliCtx.String(flags.CertFlag.Name), BeaconNodeCert: cliCtx.String(flags.CertFlag.Name),
BeaconApiEndpoint: cliCtx.String(flags.BeaconRESTApiProviderFlag.Name), BeaconApiEndpoint: cliCtx.String(flags.BeaconRESTApiProviderFlag.Name),
BeaconApiHeaders: parseBeaconApiHeaders(cliCtx.String(flags.BeaconRESTApiHeaders.Name)),
BeaconApiTimeout: time.Second * 30, BeaconApiTimeout: time.Second * 30,
Graffiti: g.ParseHexGraffiti(cliCtx.String(flags.GraffitiFlag.Name)), Graffiti: g.ParseHexGraffiti(cliCtx.String(flags.GraffitiFlag.Name)),
GraffitiStruct: graffitiStruct, GraffitiStruct: graffitiStruct,
@@ -552,6 +553,7 @@ func (c *ValidatorClient) registerRPCService(cliCtx *cli.Context) error {
GRPCHeaders: strings.Split(cliCtx.String(flags.GRPCHeadersFlag.Name), ","), GRPCHeaders: strings.Split(cliCtx.String(flags.GRPCHeadersFlag.Name), ","),
BeaconNodeGRPCEndpoint: cliCtx.String(flags.BeaconRPCProviderFlag.Name), BeaconNodeGRPCEndpoint: cliCtx.String(flags.BeaconRPCProviderFlag.Name),
BeaconApiEndpoint: cliCtx.String(flags.BeaconRESTApiProviderFlag.Name), BeaconApiEndpoint: cliCtx.String(flags.BeaconRESTApiProviderFlag.Name),
BeaconAPIHeaders: parseBeaconApiHeaders(cliCtx.String(flags.BeaconRESTApiHeaders.Name)),
BeaconApiTimeout: time.Second * 30, BeaconApiTimeout: time.Second * 30,
BeaconNodeCert: cliCtx.String(flags.CertFlag.Name), BeaconNodeCert: cliCtx.String(flags.CertFlag.Name),
DB: c.db, DB: c.db,
@@ -636,3 +638,23 @@ func clearDB(ctx context.Context, dataDir string, force bool, isDatabaseMinimal
return nil return nil
} }
func parseBeaconApiHeaders(rawHeaders string) map[string][]string {
result := make(map[string][]string)
pairs := strings.Split(rawHeaders, ",")
for _, pair := range pairs {
key, value, found := strings.Cut(pair, "=")
if !found {
// Skip malformed pairs
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
// Skip malformed pairs
continue
}
result[key] = append(result[key], value)
}
return result
}

View File

@@ -308,3 +308,17 @@ func TestWeb3SignerConfig(t *testing.T) {
}) })
} }
} }
func Test_parseBeaconApiHeaders(t *testing.T) {
t.Run("ok", func(t *testing.T) {
h := parseBeaconApiHeaders("key1=value1,key1=value2,key2=value3")
assert.Equal(t, 2, len(h))
assert.DeepEqual(t, []string{"value1", "value2"}, h["key1"])
assert.DeepEqual(t, []string{"value3"}, h["key2"])
})
t.Run("ignores malformed", func(t *testing.T) {
h := parseBeaconApiHeaders("key1=value1,key2value2,key3=,=key4")
assert.Equal(t, 1, len(h))
assert.DeepEqual(t, []string{"value1"}, h["key1"])
})
}

View File

@@ -23,6 +23,7 @@ go_library(
], ],
deps = [ deps = [
"//api:go_default_library", "//api:go_default_library",
"//api/client:go_default_library",
"//api/grpc:go_default_library", "//api/grpc:go_default_library",
"//api/pagination:go_default_library", "//api/pagination:go_default_library",
"//api/server:go_default_library", "//api/server:go_default_library",

View File

@@ -3,6 +3,7 @@ package rpc
import ( import (
"net/http" "net/http"
api "github.com/OffchainLabs/prysm/v6/api/client"
grpcutil "github.com/OffchainLabs/prysm/v6/api/grpc" grpcutil "github.com/OffchainLabs/prysm/v6/api/grpc"
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v6/validator/client" "github.com/OffchainLabs/prysm/v6/validator/client"
@@ -52,11 +53,13 @@ func (s *Server) registerBeaconClient() error {
conn := validatorHelpers.NewNodeConnection( conn := validatorHelpers.NewNodeConnection(
grpcConn, grpcConn,
s.beaconApiEndpoint, s.beaconApiEndpoint,
s.beaconApiTimeout, validatorHelpers.WithBeaconApiHeaders(s.beaconApiHeaders),
validatorHelpers.WithBeaconApiTimeout(s.beaconApiTimeout),
) )
headersTransport := api.NewCustomHeadersTransport(http.DefaultTransport, conn.GetBeaconApiHeaders())
restHandler := beaconApi.NewBeaconApiRestHandler( restHandler := beaconApi.NewBeaconApiRestHandler(
http.Client{Timeout: s.beaconApiTimeout, Transport: otelhttp.NewTransport(http.DefaultTransport)}, http.Client{Timeout: s.beaconApiTimeout, Transport: otelhttp.NewTransport(headersTransport)},
s.beaconApiEndpoint, s.beaconApiEndpoint,
) )

View File

@@ -34,6 +34,7 @@ type Config struct {
GRPCHeaders []string GRPCHeaders []string
BeaconNodeGRPCEndpoint string BeaconNodeGRPCEndpoint string
BeaconApiEndpoint string BeaconApiEndpoint string
BeaconAPIHeaders map[string][]string
BeaconApiTimeout time.Duration BeaconApiTimeout time.Duration
BeaconNodeCert string BeaconNodeCert string
DB db.Database DB db.Database
@@ -64,6 +65,7 @@ type Server struct {
authTokenPath string authTokenPath string
beaconNodeCert string beaconNodeCert string
beaconApiEndpoint string beaconApiEndpoint string
beaconApiHeaders map[string][]string
beaconNodeEndpoint string beaconNodeEndpoint string
healthClient ethpb.HealthClient healthClient ethpb.HealthClient
nodeClient iface.NodeClient nodeClient iface.NodeClient
@@ -103,6 +105,7 @@ func NewServer(ctx context.Context, cfg *Config) *Server {
wallet: cfg.Wallet, wallet: cfg.Wallet,
beaconApiTimeout: cfg.BeaconApiTimeout, beaconApiTimeout: cfg.BeaconApiTimeout,
beaconApiEndpoint: cfg.BeaconApiEndpoint, beaconApiEndpoint: cfg.BeaconApiEndpoint,
beaconApiHeaders: cfg.BeaconAPIHeaders,
beaconNodeEndpoint: cfg.BeaconNodeGRPCEndpoint, beaconNodeEndpoint: cfg.BeaconNodeGRPCEndpoint,
router: cfg.Router, router: cfg.Router,
} }