Skip to content

Commit

Permalink
app/eth2wrap: add support for extra headers (#3449)
Browse files Browse the repository at this point in the history
Add `--beacon-node-headers` flag to `run` and `exit` command for passing HTTP header values to go-eth2-client client to allow for beacon node authentication.

category: feature
ticket: #3388
  • Loading branch information
DiogoSantoss authored Jan 16, 2025
1 parent d3bf94e commit d27aebf
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 29 deletions.
14 changes: 10 additions & 4 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type Config struct {
ProcDirectory string
ConsensusProtocol string
Nickname string
BeaconNodeHeaders []string

TestConfig TestConfig
}
Expand Down Expand Up @@ -909,12 +910,17 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, cl
log.Info(ctx, "Synthetic block proposals enabled")
}

eth2Cl, err := configureEth2Client(ctx, forkVersion, conf.BeaconNodeAddrs, bnTimeout, conf.SyntheticBlockProposals)
beaconNodeHeaders, err := eth2util.ParseBeaconNodeHeaders(conf.BeaconNodeHeaders)
if err != nil {
return nil, nil, err
}

eth2Cl, err := configureEth2Client(ctx, forkVersion, conf.BeaconNodeAddrs, beaconNodeHeaders, bnTimeout, conf.SyntheticBlockProposals)
if err != nil {
return nil, nil, errors.Wrap(err, "new eth2 http client")
}

submissionEth2Cl, err := configureEth2Client(ctx, forkVersion, conf.BeaconNodeAddrs, submissionBnTimeout, conf.SyntheticBlockProposals)
submissionEth2Cl, err := configureEth2Client(ctx, forkVersion, conf.BeaconNodeAddrs, beaconNodeHeaders, submissionBnTimeout, conf.SyntheticBlockProposals)
if err != nil {
return nil, nil, errors.Wrap(err, "new submission eth2 http client")
}
Expand All @@ -923,8 +929,8 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, cl
}

// configureEth2Client configures a beacon node client with the provided settings.
func configureEth2Client(ctx context.Context, forkVersion []byte, addrs []string, timeout time.Duration, syntheticBlockProposals bool) (eth2wrap.Client, error) {
eth2Cl, err := eth2wrap.NewMultiHTTP(timeout, [4]byte(forkVersion), addrs...)
func configureEth2Client(ctx context.Context, forkVersion []byte, addrs []string, headers map[string]string, timeout time.Duration, syntheticBlockProposals bool) (eth2wrap.Client, error) {
eth2Cl, err := eth2wrap.NewMultiHTTP(timeout, [4]byte(forkVersion), headers, addrs...)
if err != nil {
return nil, errors.Wrap(err, "new eth2 http client")
}
Expand Down
11 changes: 6 additions & 5 deletions app/eth2wrap/eth2wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,28 +85,29 @@ func WithSyntheticDuties(cl Client) Client {
}

// NewMultiHTTP returns a new instrumented multi eth2 http client.
func NewMultiHTTP(timeout time.Duration, forkVersion [4]byte, addresses ...string) (Client, error) {
return Instrument(newClients(timeout, forkVersion, addresses)...)
func NewMultiHTTP(timeout time.Duration, forkVersion [4]byte, headers map[string]string, addresses ...string) (Client, error) {
return Instrument(newClients(timeout, forkVersion, headers, addresses)...)
}

// newClients returns a slice of Client initialized with the provided settings.
func newClients(timeout time.Duration, forkVersion [4]byte, addresses []string) []Client {
func newClients(timeout time.Duration, forkVersion [4]byte, headers map[string]string, addresses []string) []Client {
var clients []Client
for _, address := range addresses {
clients = append(clients, newBeaconClient(timeout, forkVersion, address))
clients = append(clients, newBeaconClient(timeout, forkVersion, headers, address))
}

return clients
}

// newBeaconClient returns a Client with the provided settings.
func newBeaconClient(timeout time.Duration, forkVersion [4]byte, address string) Client {
func newBeaconClient(timeout time.Duration, forkVersion [4]byte, headers map[string]string, address string) Client {
parameters := []eth2http.Parameter{
eth2http.WithLogLevel(zeroLogInfo),
eth2http.WithAddress(address),
eth2http.WithTimeout(timeout),
eth2http.WithAllowDelayedStart(true),
eth2http.WithEnforceJSON(featureset.Enabled(featureset.JSONRequests)),
eth2http.WithExtraHeaders(headers),
}

cl := newLazy(func(ctx context.Context) (Client, error) {
Expand Down
18 changes: 9 additions & 9 deletions app/eth2wrap/eth2wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func TestErrors(t *testing.T) {
ctx := context.Background()

t.Run("network dial error", func(t *testing.T) {
cl, err := eth2wrap.NewMultiHTTP(time.Hour, [4]byte{}, "localhost:22222")
cl, err := eth2wrap.NewMultiHTTP(time.Hour, [4]byte{}, map[string]string{}, "localhost:22222")
require.NoError(t, err)

_, err = cl.SlotsPerEpoch(ctx)
Expand All @@ -186,7 +186,7 @@ func TestErrors(t *testing.T) {
}))

t.Run("http timeout", func(t *testing.T) {
cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, srv.URL)
cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, map[string]string{}, srv.URL)
require.NoError(t, err)

_, err = cl.SlotsPerEpoch(ctx)
Expand All @@ -199,7 +199,7 @@ func TestErrors(t *testing.T) {
ctx, cancel := context.WithCancel(ctx)
cancel()

cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, srv.URL)
cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, map[string]string{}, srv.URL)
require.NoError(t, err)

_, err = cl.SlotsPerEpoch(ctx)
Expand Down Expand Up @@ -251,7 +251,7 @@ func TestCtxCancel(t *testing.T) {

bmock, err := beaconmock.New()
require.NoError(t, err)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, bmock.Address())
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, map[string]string{}, bmock.Address())
require.NoError(t, err)

cancel() // Cancel context before calling method.
Expand Down Expand Up @@ -310,7 +310,7 @@ func TestOneError(t *testing.T) {
bmock.Address(), // Valid
}

eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, addresses...)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, map[string]string{}, addresses...)
require.NoError(t, err)

eth2Resp, err := eth2Cl.Spec(ctx, &eth2api.SpecOpts{})
Expand Down Expand Up @@ -341,7 +341,7 @@ func TestOneTimeout(t *testing.T) {
bmock.Address(), // Valid
}

eth2Cl, err := eth2wrap.NewMultiHTTP(time.Minute, [4]byte{}, addresses...)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Minute, [4]byte{}, map[string]string{}, addresses...)
require.NoError(t, err)

eth2Resp, err := eth2Cl.Spec(ctx, &eth2api.SpecOpts{})
Expand All @@ -364,7 +364,7 @@ func TestOnlyTimeout(t *testing.T) {
defer srv.Close()
defer cancel() // Cancel the context before stopping the server.

eth2Cl, err := eth2wrap.NewMultiHTTP(time.Minute, [4]byte{}, srv.URL)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Minute, [4]byte{}, map[string]string{}, srv.URL)
require.NoError(t, err)

// Start goroutine that is blocking trying to create the client.
Expand Down Expand Up @@ -426,7 +426,7 @@ func TestLazy(t *testing.T) {
httputil.NewSingleHostReverseProxy(target).ServeHTTP(w, r)
}))

eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, srv1.URL, srv2.URL)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, map[string]string{}, srv1.URL, srv2.URL)
require.NoError(t, err)

// Both proxies are disabled, so this should fail.
Expand Down Expand Up @@ -505,7 +505,7 @@ func TestLazyDomain(t *testing.T) {

forkVersionHex, err := hex.DecodeString(test.in)
require.NoError(t, err)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte(forkVersionHex), srv.URL)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte(forkVersionHex), map[string]string{}, srv.URL)
require.NoError(t, err)

voluntaryExitDomain := eth2p0.DomainType{0x04, 0x00, 0x00, 0x00}
Expand Down
2 changes: 1 addition & 1 deletion app/eth2wrap/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type FallbackClient struct {
// NewFallbackClient initializes a FallbackClient with the provided settings
func NewFallbackClient(timeout time.Duration, forkVersion [4]byte, addresses []string) *FallbackClient {
return &FallbackClient{
clients: newClients(timeout, forkVersion, addresses),
clients: newClients(timeout, forkVersion, map[string]string{}, addresses),
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/peerinfo/peerinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (p *PeerInfo) sendOnce(ctx context.Context, now time.Time) {

name := p2p.PeerName(peerID)

p.nicknames[name] = resp.Nickname
p.nicknames[name] = resp.GetNickname()
log.Info(ctx, "Peer name to nickname mappings", z.Any("nicknames", p.nicknames))

// Validator git hash with regex.
Expand Down
10 changes: 8 additions & 2 deletions cmd/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type exitConfig struct {
Log log.Config
All bool
testnetConfig eth2util.Network
BeaconNodeHeaders []string
}

func newExitCmd(cmds ...*cobra.Command) *cobra.Command {
Expand Down Expand Up @@ -74,6 +75,7 @@ const (
testnetChainID
testnetGenesisTimestamp
testnetCapellaHardFork
beaconNodeHeaders
)

func (ef exitFlag) String() string {
Expand Down Expand Up @@ -116,6 +118,8 @@ func (ef exitFlag) String() string {
return "testnet-genesis-timestamp"
case testnetCapellaHardFork:
return "testnet-capella-hard-fork"
case beaconNodeHeaders:
return "beacon-node-headers"
default:
return "unknown"
}
Expand Down Expand Up @@ -177,6 +181,8 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag)
cmd.Flags().Int64Var(&config.testnetConfig.GenesisTimestamp, "testnet-genesis-timestamp", 0, "Genesis timestamp of the custom test network.")
case testnetCapellaHardFork:
cmd.Flags().StringVar(&config.testnetConfig.CapellaHardFork, "testnet-capella-hard-fork", "", "Capella hard fork version of the custom test network.")
case beaconNodeHeaders:
cmd.Flags().StringSliceVar(&config.BeaconNodeHeaders, "beacon-node-headers", nil, "Comma separated list of headers formatted as header=value")
}

if f.required {
Expand All @@ -185,8 +191,8 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag)
}
}

func eth2Client(ctx context.Context, u []string, timeout time.Duration, forkVersion [4]byte) (eth2wrap.Client, error) {
cl, err := eth2wrap.NewMultiHTTP(timeout, forkVersion, u...)
func eth2Client(ctx context.Context, headers map[string]string, u []string, timeout time.Duration, forkVersion [4]byte) (eth2wrap.Client, error) {
cl, err := eth2wrap.NewMultiHTTP(timeout, forkVersion, headers, u...)
if err != nil {
return nil, err
}
Expand Down
13 changes: 12 additions & 1 deletion cmd/exit_broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func newBcastFullExitCmd(runFunc func(context.Context, exitConfig) error) *cobra
{testnetChainID, false},
{testnetGenesisTimestamp, false},
{testnetCapellaHardFork, false},
{beaconNodeHeaders, false},
})

bindLogFlags(cmd.Flags(), &config.Log)
Expand Down Expand Up @@ -98,6 +99,11 @@ func newBcastFullExitCmd(runFunc func(context.Context, exitConfig) error) *cobra
return errors.New(fmt.Sprintf("if you want to specify exit file directory for all validators, you must provide %s and not %s.", exitFromDir.String(), exitFromFile.String()))
}

err := eth2util.ValidateBeaconNodeHeaders(config.BeaconNodeHeaders)
if err != nil {
return err
}

return nil
})

Expand All @@ -121,7 +127,12 @@ func runBcastFullExit(ctx context.Context, config exitConfig) error {
return errors.Wrap(err, "load cluster lock", z.Str("lock_file_path", config.LockFilePath))
}

eth2Cl, err := eth2Client(ctx, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion()))
beaconNodeHeaders, err := eth2util.ParseBeaconNodeHeaders(config.BeaconNodeHeaders)
if err != nil {
return err
}

eth2Cl, err := eth2Client(ctx, beaconNodeHeaders, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion()))
if err != nil {
return errors.Wrap(err, "create eth2 client for specified beacon node(s)", z.Any("beacon_nodes_endpoints", config.BeaconNodeEndpoints))
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/exit_broadcast_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool, all bool) {
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, []string{beaconMock.Address()}, 10*time.Second, [4]byte(lock.ForkVersion))
eth2Cl, err := eth2Client(ctx, map[string]string{}, []string{beaconMock.Address()}, 10*time.Second, [4]byte(lock.ForkVersion))
require.NoError(t, err)

handler, addLockFiles := obolapimock.MockServer(false, eth2Cl)
Expand Down
2 changes: 1 addition & 1 deletion cmd/exit_fetch_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func testRunFetchExitFullFlow(t *testing.T, all bool) {
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, []string{beaconMock.Address()}, 10*time.Second, [4]byte(lock.ForkVersion))
eth2Cl, err := eth2Client(ctx, map[string]string{}, []string{beaconMock.Address()}, 10*time.Second, [4]byte(lock.ForkVersion))
require.NoError(t, err)

handler, addLockFiles := obolapimock.MockServer(false, eth2Cl)
Expand Down
12 changes: 11 additions & 1 deletion cmd/exit_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ func newListActiveValidatorsCmd(runFunc func(context.Context, exitConfig) error)
{testnetChainID, false},
{testnetGenesisTimestamp, false},
{testnetCapellaHardFork, false},
{beaconNodeHeaders, false},
})

bindLogFlags(cmd.Flags(), &config.Log)

wrapPreRunE(cmd, func(*cobra.Command, []string) error {
return eth2util.ValidateBeaconNodeHeaders(config.BeaconNodeHeaders)
})

return cmd
}

Expand Down Expand Up @@ -87,7 +92,12 @@ func listActiveVals(ctx context.Context, config exitConfig) ([]string, error) {
return nil, errors.Wrap(err, "load cluster lock", z.Str("lock_file_path", config.LockFilePath))
}

eth2Cl, err := eth2Client(ctx, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte{}) // fine to avoid initializing a fork version, we're just querying the BN
beaconNodeHeaders, err := eth2util.ParseBeaconNodeHeaders(config.BeaconNodeHeaders)
if err != nil {
return nil, err
}

eth2Cl, err := eth2Client(ctx, beaconNodeHeaders, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte{}) // fine to avoid initializing a fork version, we're just querying the BN
if err != nil {
return nil, errors.Wrap(err, "create eth2 client for specified beacon node(s)", z.Any("beacon_nodes_endpoints", config.BeaconNodeEndpoints))
}
Expand Down
13 changes: 12 additions & 1 deletion cmd/exit_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func newSignPartialExitCmd(runFunc func(context.Context, exitConfig) error) *cob
{testnetChainID, false},
{testnetGenesisTimestamp, false},
{testnetCapellaHardFork, false},
{beaconNodeHeaders, false},
})

bindLogFlags(cmd.Flags(), &config.Log)
Expand All @@ -78,6 +79,11 @@ func newSignPartialExitCmd(runFunc func(context.Context, exitConfig) error) *cob
return errors.New(fmt.Sprintf("%s or %s should not be specified when %s is, as they are obsolete and misleading.", validatorIndex.String(), validatorPubkey.String(), all.String()))
}

err := eth2util.ValidateBeaconNodeHeaders(config.BeaconNodeHeaders)
if err != nil {
return err
}

config.ValidatorIndexPresent = valIdxPresent
config.SkipBeaconNodeCheck = valIdxPresent && valPubkPresent

Expand Down Expand Up @@ -129,7 +135,12 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error {
return errors.Wrap(err, "create Obol API client", z.Str("publish_address", config.PublishAddress))
}

eth2Cl, err := eth2Client(ctx, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion()))
beaconNodeHeaders, err := eth2util.ParseBeaconNodeHeaders(config.BeaconNodeHeaders)
if err != nil {
return err
}

eth2Cl, err := eth2Client(ctx, beaconNodeHeaders, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion()))
if err != nil {
return errors.Wrap(err, "create eth2 client for specified beacon node(s)", z.Any("beacon_nodes_endpoints", config.BeaconNodeEndpoints))
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/exit_sign_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func runSubmitPartialExitFlowTest(t *testing.T, useValIdx bool, skipBeaconNodeCh
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, []string{beaconMock.Address()}, 10*time.Second, [4]byte(lock.ForkVersion))
eth2Cl, err := eth2Client(ctx, map[string]string{}, []string{beaconMock.Address()}, 10*time.Second, [4]byte(lock.ForkVersion))
require.NoError(t, err)

handler, addLockFiles := obolapimock.MockServer(false, eth2Cl)
Expand Down
6 changes: 6 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/eth2util"
"github.com/obolnetwork/charon/p2p"
)

Expand Down Expand Up @@ -94,6 +95,7 @@ func bindRunFlags(cmd *cobra.Command, config *app.Config) {
cmd.Flags().StringVar(&config.ProcDirectory, "proc-directory", "", "Directory to look into in order to detect other stack components running on the host.")
cmd.Flags().StringVar(&config.ConsensusProtocol, "consensus-protocol", "", "Preferred consensus protocol name for the node. Selected automatically when not specified.")
cmd.Flags().StringVar(&config.Nickname, "nickname", "", "Human friendly peer nickname. Maximum 32 characters.")
cmd.Flags().StringSliceVar(&config.BeaconNodeHeaders, "beacon-node-headers", nil, "Comma separated list of headers formatted as header=value")

wrapPreRunE(cmd, func(*cobra.Command, []string) error {
if len(config.BeaconNodeAddrs) == 0 && !config.SimnetBMock {
Expand All @@ -102,6 +104,10 @@ func bindRunFlags(cmd *cobra.Command, config *app.Config) {
if len(config.Nickname) > 32 {
return errors.New("flag 'nickname' can not exceed 32 characters")
}
err := eth2util.ValidateBeaconNodeHeaders(config.BeaconNodeHeaders)
if err != nil {
return err
}

return nil
})
Expand Down
2 changes: 1 addition & 1 deletion core/scheduler/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestIntegration(t *testing.T) {
t.Fatal("BEACON_URL env var not set")
}

eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second*2, [4]byte{}, beaconURL)
eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second*2, [4]byte{}, map[string]string{}, beaconURL)
require.NoError(t, err)

// Use random actual mainnet validators
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Usage:
Flags:
--beacon-node-endpoints strings Comma separated list of one or more beacon node endpoint URLs.
--beacon-node-headers strings Comma separated list of headers formatted as header=value
--beacon-node-submit-timeout duration Timeout for the submission-related HTTP requests Charon makes to the configured beacon nodes. (default 2s)
--beacon-node-timeout duration Timeout for the HTTP requests Charon makes to the configured beacon nodes. (default 2s)
--builder-api Enables the builder api. Will only produce builder blocks. Builder API must also be enabled on the validator client. Beacon node must be connected to a builder-relay to access the builder network.
Expand Down
Loading

0 comments on commit d27aebf

Please sign in to comment.