Repository: ObolNetwork/charon

Issue:
Add a `nickname` field to `peerinfo` protocol
## Problem to be solved

We have a hard time giving useful names to peers in a cluster. It would be great if we could give people an easier way to identify their node places. We could have people label their prometheus writes, but that doesn't get shared with peers, so might help us more than it helps users, which might not be what we are after. Instead, what if we included an optional field in the peerinfo protocol, or another protocol that allows us to share short strings with one another along with peerId/nodeId information. That way, we could (hopefully safely), use it in logs and monitoring on the connected person's side, to improve context and understanding of issues by 'making them human'. 

## Proposed solution

  Add a optional --nickname string flag to `charon run` (and optionally `charon dkg`)
  Share that with peers via peerinfo protocol
  Add app_peerinfo_nickname constant metric with nickname label (including own)

  (And even lazier MVP is adding the flag and just creating the prom metric for it, that would mean we could show it in obol's shared grafana but not the charon peer logs)	

### In Scope

Add a field to the grafana dashboard for these nicknames. Update the dashboard in charon and [cdvn](https://github.com/ObolNetwork/charon-distributed-validator-node), no need for cdvc.

<img width="706" alt="image" src="https://github.com/user-attachments/assets/64b71034-c7fd-4546-acc7-b305ecdbea2b" />

Asserting the maximum string is enforced at CLI and wire, and that it is safe from weird attacks such as terminal takeovers and stuff. It would also be great to prevent setting nickname equal to any peer name in the cluster.

### Out of Scope

Consensus on these nicknames across parties


Discussion:
@corverroos: Why don't we add it to the lock file rather? Make it part of the protocol properly. CLI flags is very loose, people will change it over time and joke around. If in the closter lock, it could be a nice initial migration to add.: updating your node moniker/nickname/name.

Also wiring the result from peerinfo throughout the codebase is going to be hard to implement. It would require a big refactor switching from immutable types to mutable types which is a pain and something I want to avoid.@corverroos: 100 chars is also too long imo, it doesn't display nicely in grafana and people will write sentences. Suggest 32 chars.@corverroos: another term could also be `nickname`, feels a bit simpler and more approachable to non-english speakers.
@corverroos: I'd also suggest we not try to "replace" existing `cluster_peer` names, as that will just result in a mess in grafana as timeseries chop and change, deduplicating is impossible I think. Suggest we only include the new nicks/monikers in the Peer panel, but keep on using deterministic peer names for everything else. @corverroos: Note my suggested solution:
 - Add a optional `--nickname` string flag
 - Share that with peers via `peerinfo` protocol
 - Add `app_peerinfo_nickname` constant metric with nickname label (including own)@corverroos: Note that adding this to the lock file is still the best solution as then it is also supported by DKG, see #1912. Requiring users to add consistent moniker fields to DKG introduces risk of users needs to modify the copied launchpad command which will result in more DKG failures. Best just copy and paste that command directly.@KaloyanTanev: Done in the above mentioned PRs

Solution patch:
diff --git a/app/app.go b/app/app.go
index 693309a9..01768b0d 100644
--- a/app/app.go
+++ b/app/app.go
@@ -94,6 +94,7 @@ type Config struct {
 	TestnetConfig           eth2util.Network
 	ProcDirectory           string
 	ConsensusProtocol       string
+	Nickname                string
 
 	TestConfig TestConfig
 }
@@ -257,7 +258,10 @@ func Run(ctx context.Context, conf Config) (err error) {
 
 	sender := new(p2p.Sender)
 
-	wirePeerInfo(life, tcpNode, peerIDs, cluster.GetInitialMutationHash(), sender, conf.BuilderAPI)
+	if len(conf.Nickname) > 32 {
+		return errors.New("nickname can not exceed 32 characters")
+	}
+	wirePeerInfo(life, tcpNode, peerIDs, cluster.GetInitialMutationHash(), sender, conf.BuilderAPI, conf.Nickname)
 
 	// seenPubkeys channel to send seen public keys from validatorapi to monitoringapi.
 	seenPubkeys := make(chan core.PubKey)
@@ -297,9 +301,9 @@ func Run(ctx context.Context, conf Config) (err error) {
 }
 
 // wirePeerInfo wires the peerinfo protocol.
-func wirePeerInfo(life *lifecycle.Manager, tcpNode host.Host, peers []peer.ID, lockHash []byte, sender *p2p.Sender, builderEnabled bool) {
+func wirePeerInfo(life *lifecycle.Manager, tcpNode host.Host, peers []peer.ID, lockHash []byte, sender *p2p.Sender, builderEnabled bool, nickname string) {
 	gitHash, _ := version.GitCommit()
-	peerInfo := peerinfo.New(tcpNode, peers, version.Version, lockHash, gitHash, sender.SendReceive, builderEnabled)
+	peerInfo := peerinfo.New(tcpNode, peers, version.Version, lockHash, gitHash, sender.SendReceive, builderEnabled, nickname)
 	life.RegisterStart(lifecycle.AsyncAppCtx, lifecycle.StartPeerInfo, lifecycle.HookFuncCtx(peerInfo.Run))
 }
 
diff --git a/app/peerinfo/metrics.go b/app/peerinfo/metrics.go
index 14f3ecad..25e2a0f3 100644
--- a/app/peerinfo/metrics.go
+++ b/app/peerinfo/metrics.go
@@ -60,4 +60,12 @@ var (
 		Name:      "builder_api_enabled",
 		Help:      "Set to 1 if builder API is enabled on this peer, else 0 if disabled.",
 	}, []string{"peer"})
+
+	peerNickname = promauto.NewResetGaugeVec(prometheus.GaugeOpts{
+		Namespace:   "app",
+		Subsystem:   "peerinfo",
+		Name:        "nickname",
+		Help:        "Constant gauge with nickname label set to peer's charon nickname.",
+		ConstLabels: nil,
+	}, []string{"peer", "nickname"})
 )
diff --git a/app/peerinfo/peerinfo.go b/app/peerinfo/peerinfo.go
index 7eb1e746..788af6c5 100644
--- a/app/peerinfo/peerinfo.go
+++ b/app/peerinfo/peerinfo.go
@@ -39,17 +39,18 @@ func Protocols() []protocol.ID {
 type (
 	tickerProvider  func() (<-chan time.Time, func())
 	nowFunc         func() time.Time
-	metricSubmitter func(peerID peer.ID, clockOffset time.Duration, version, gitHash string, startTime time.Time, builderAPIEnabled bool)
+	metricSubmitter func(peerID peer.ID, clockOffset time.Duration, version, gitHash string, startTime time.Time, builderAPIEnabled bool, nickname string)
 )
 
 // New returns a new peer info protocol instance.
 func New(tcpNode host.Host, peers []peer.ID, version version.SemVer, lockHash []byte, gitHash string,
-	sendFunc p2p.SendReceiveFunc, builderEnabled bool,
+	sendFunc p2p.SendReceiveFunc, builderEnabled bool, nickname string,
 ) *PeerInfo {
-	// Set own version and git hash and start time metrics.
+	// Set own version, git hash and nickname and start time and metrics.
 	name := p2p.PeerName(tcpNode.ID())
 	peerVersion.WithLabelValues(name, version.String()).Set(1)
 	peerGitHash.WithLabelValues(name, gitHash).Set(1)
+	peerNickname.WithLabelValues(name, nickname).Set(1)
 	peerStartGauge.WithLabelValues(name).Set(float64(time.Now().Unix()))
 
 	if builderEnabled {
@@ -68,24 +69,24 @@ func New(tcpNode host.Host, peers []peer.ID, version version.SemVer, lockHash []
 	}
 
 	return newInternal(tcpNode, peers, version, lockHash, gitHash, sendFunc, p2p.RegisterHandler,
-		tickerProvider, time.Now, newMetricsSubmitter(), builderEnabled)
+		tickerProvider, time.Now, newMetricsSubmitter(), builderEnabled, nickname)
 }
 
 // NewForT returns a new peer info protocol instance for testing only.
 func NewForT(_ *testing.T, tcpNode host.Host, peers []peer.ID, version version.SemVer, lockHash []byte, gitHash string,
 	sendFunc p2p.SendReceiveFunc, registerHandler p2p.RegisterHandlerFunc,
 	tickerProvider tickerProvider, nowFunc nowFunc, metricSubmitter metricSubmitter,
-	builderAPIEnabled bool,
+	builderAPIEnabled bool, nickname string,
 ) *PeerInfo {
 	return newInternal(tcpNode, peers, version, lockHash, gitHash, sendFunc, registerHandler,
-		tickerProvider, nowFunc, metricSubmitter, builderAPIEnabled)
+		tickerProvider, nowFunc, metricSubmitter, builderAPIEnabled, nickname)
 }
 
 // newInternal returns a new instance for New or NewForT.
 func newInternal(tcpNode host.Host, peers []peer.ID, version version.SemVer, lockHash []byte, gitHash string,
 	sendFunc p2p.SendReceiveFunc, registerHandler p2p.RegisterHandlerFunc,
 	tickerProvider tickerProvider, nowFunc nowFunc, metricSubmitter metricSubmitter,
-	builderAPIEnabled bool,
+	builderAPIEnabled bool, nickname string,
 ) *PeerInfo {
 	startTime := timestamppb.New(nowFunc())
 
@@ -100,10 +101,14 @@ func newInternal(tcpNode host.Host, peers []peer.ID, version version.SemVer, loc
 				SentAt:            timestamppb.New(nowFunc()),
 				StartedAt:         startTime,
 				BuilderApiEnabled: builderAPIEnabled,
+				Nickname:          nickname,
 			}, true, nil
 		},
 	)
 
+	// Maps peers to their nickname
+	nicknames := map[string]string{p2p.PeerName(tcpNode.ID()): nickname}
+
 	// Create log filters
 	lockHashFilters := make(map[peer.ID]z.Field)
 	versionFilters := make(map[peer.ID]z.Field)
@@ -125,6 +130,7 @@ func newInternal(tcpNode host.Host, peers []peer.ID, version version.SemVer, loc
 		nowFunc:           nowFunc,
 		lockHashFilters:   lockHashFilters,
 		versionFilters:    versionFilters,
+		nicknames:         nicknames,
 	}
 }
 
@@ -142,6 +148,7 @@ type PeerInfo struct {
 	nowFunc           func() time.Time
 	lockHashFilters   map[peer.ID]z.Field
 	versionFilters    map[peer.ID]z.Field
+	nicknames         map[string]string
 }
 
 // Run runs the peer info protocol until the context is cancelled.
@@ -175,6 +182,7 @@ func (p *PeerInfo) sendOnce(ctx context.Context, now time.Time) {
 			SentAt:            timestamppb.New(now),
 			StartedAt:         p.startTime,
 			BuilderApiEnabled: p.builderAPIEnabled,
+			Nickname:          p.nicknames[p2p.PeerName(p.tcpNode.ID())],
 		}
 
 		go func(peerID peer.ID) {
@@ -194,6 +202,9 @@ func (p *PeerInfo) sendOnce(ctx context.Context, now time.Time) {
 
 			name := p2p.PeerName(peerID)
 
+			p.nicknames[name] = resp.Nickname
+			log.Info(ctx, "Peer name to nickname mappings", z.Any("nicknames", p.nicknames))
+
 			// Validator git hash with regex.
 			if !gitHashMatch.MatchString(resp.GetGitHash()) {
 				log.Warn(ctx, "Invalid peer git hash", nil, z.Str("peer", name))
@@ -221,7 +232,7 @@ func (p *PeerInfo) sendOnce(ctx context.Context, now time.Time) {
 			// Set peer compatibility to true.
 			peerCompatibleGauge.WithLabelValues(name).Set(1)
 
-			p.metricSubmitter(peerID, clockOffset, resp.GetCharonVersion(), resp.GetGitHash(), resp.GetStartedAt().AsTime(), resp.GetBuilderApiEnabled())
+			p.metricSubmitter(peerID, clockOffset, resp.GetCharonVersion(), resp.GetGitHash(), resp.GetStartedAt().AsTime(), resp.GetBuilderApiEnabled(), resp.GetNickname())
 
 			// Log unexpected lock hash
 			if !bytes.Equal(resp.GetLockHash(), p.lockHash) {
@@ -269,10 +280,13 @@ func supportedPeerVersion(peerVersion string, supported []version.SemVer) error
 // newMetricsSubmitter returns a prometheus metric submitter.
 func newMetricsSubmitter() metricSubmitter {
 	return func(peerID peer.ID, clockOffset time.Duration, version string, gitHash string,
-		startTime time.Time, builderAPIEnabled bool,
+		startTime time.Time, builderAPIEnabled bool, nickname string,
 	) {
 		peerName := p2p.PeerName(peerID)
 
+		peerNickname.Reset(peerName)
+		peerNickname.WithLabelValues(peerName, nickname).Set(1)
+
 		// Limit range of possible values
 		if clockOffset < -time.Hour {
 			clockOffset = -time.Hour
diff --git a/app/peerinfo/peerinfopb/v1/peerinfo.pb.go b/app/peerinfo/peerinfopb/v1/peerinfo.pb.go
index 09d3f611..7fff156d 100644
--- a/app/peerinfo/peerinfopb/v1/peerinfo.pb.go
+++ b/app/peerinfo/peerinfopb/v1/peerinfo.pb.go
@@ -29,6 +29,7 @@ type PeerInfo struct {
 	GitHash           string                 `protobuf:"bytes,4,opt,name=git_hash,json=gitHash,proto3" json:"git_hash,omitempty"`
 	StartedAt         *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=started_at,json=startedAt,proto3,oneof" json:"started_at,omitempty"`
 	BuilderApiEnabled bool                   `protobuf:"varint,6,opt,name=builder_api_enabled,json=builderApiEnabled,proto3" json:"builder_api_enabled,omitempty"`
+	Nickname          string                 `protobuf:"bytes,7,opt,name=nickname,proto3" json:"nickname,omitempty"`
 	unknownFields     protoimpl.UnknownFields
 	sizeCache         protoimpl.SizeCache
 }
@@ -105,6 +106,13 @@ func (x *PeerInfo) GetBuilderApiEnabled() bool {
 	return false
 }
 
+func (x *PeerInfo) GetNickname() string {
+	if x != nil {
+		return x.Nickname
+	}
+	return ""
+}
+
 var File_app_peerinfo_peerinfopb_v1_peerinfo_proto protoreflect.FileDescriptor
 
 var file_app_peerinfo_peerinfopb_v1_peerinfo_proto_rawDesc = []byte{
@@ -114,7 +122,7 @@ var file_app_peerinfo_peerinfopb_v1_peerinfo_proto_rawDesc = []byte{
 	0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e,
 	0x66, 0x6f, 0x70, 0x62, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
 	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
-	0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xae, 0x02, 0x0a, 0x08, 0x50, 0x65, 0x65,
+	0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xca, 0x02, 0x0a, 0x08, 0x50, 0x65, 0x65,
 	0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x72, 0x6f, 0x6e, 0x5f,
 	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63,
 	0x68, 0x61, 0x72, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09,
@@ -131,13 +139,15 @@ var file_app_peerinfo_peerinfopb_v1_peerinfo_proto_rawDesc = []byte{
 	0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2e,
 	0x0a, 0x13, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x65, 0x6e,
 	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x62, 0x75, 0x69,
-	0x6c, 0x64, 0x65, 0x72, 0x41, 0x70, 0x69, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x0a,
-	0x0a, 0x08, 0x5f, 0x73, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x73,
-	0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74,
-	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x62, 0x6f, 0x6c, 0x6e, 0x65, 0x74, 0x77,
-	0x6f, 0x72, 0x6b, 0x2f, 0x63, 0x68, 0x61, 0x72, 0x6f, 0x6e, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x70,
-	0x65, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f,
-	0x70, 0x62, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6c, 0x64, 0x65, 0x72, 0x41, 0x70, 0x69, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a,
+	0x0a, 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73,
+	0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74,
+	0x65, 0x64, 0x5f, 0x61, 0x74, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
+	0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x62, 0x6f, 0x6c, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f,
+	0x63, 0x68, 0x61, 0x72, 0x6f, 0x6e, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x69,
+	0x6e, 0x66, 0x6f, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x70, 0x62, 0x2f, 0x76,
+	0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/app/peerinfo/peerinfopb/v1/peerinfo.proto b/app/peerinfo/peerinfopb/v1/peerinfo.proto
index bd63ff20..564c747e 100644
--- a/app/peerinfo/peerinfopb/v1/peerinfo.proto
+++ b/app/peerinfo/peerinfopb/v1/peerinfo.proto
@@ -13,6 +13,7 @@ message PeerInfo {
   string                               git_hash = 4;
   optional google.protobuf.Timestamp started_at = 5;
   bool                      builder_api_enabled = 6;
+  string                               nickname = 7;
 
   // NOTE: Always populate timestamps when sending, then make them required after subsequent release.
 }
diff --git a/cmd/run.go b/cmd/run.go
index 99c60b99..03582ec7 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -93,11 +93,15 @@ func bindRunFlags(cmd *cobra.Command, config *app.Config) {
 	cmd.Flags().StringVar(&config.TestnetConfig.CapellaHardFork, "testnet-capella-hard-fork", "", "Capella hard fork version of the custom test network.")
 	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.")
 
 	wrapPreRunE(cmd, func(*cobra.Command, []string) error {
 		if len(config.BeaconNodeAddrs) == 0 && !config.SimnetBMock {
 			return errors.New("either flag 'beacon-node-endpoints' or flag 'simnet-beacon-mock=true' must be specified")
 		}
+		if len(config.Nickname) > 32 {
+			return errors.New("flag 'nickname' can not exceed 32 characters")
+		}
 
 		return nil
 	})
diff --git a/dkg/dkg.go b/dkg/dkg.go
index 652e35b0..ee890dde 100644
--- a/dkg/dkg.go
+++ b/dkg/dkg.go
@@ -418,7 +418,8 @@ func setupP2P(ctx context.Context, key *k1.PrivateKey, conf Config, peers []p2p.
 
 	// Register peerinfo server handler for identification to relays (but do not run peerinfo client).
 	gitHash, _ := version.GitCommit()
-	_ = peerinfo.New(tcpNode, peerIDs, version.Version, defHash, gitHash, nil, false)
+
+	_ = peerinfo.New(tcpNode, peerIDs, version.Version, defHash, gitHash, nil, false, "")
 
 	return tcpNode, func() {
 		_ = tcpNode.Close()
diff --git a/docs/configuration.md b/docs/configuration.md
index 45dd9777..3dac8ee6 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -170,6 +170,7 @@ Flags:
       --loki-service string                   Service label sent with logs to Loki. (default "charon")
       --manifest-file string                  The path to the cluster manifest file. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence. (default ".charon/cluster-manifest.pb")
       --monitoring-address string             Listening address (ip and port) for the monitoring API (prometheus). (default "127.0.0.1:3620")
+      --nickname string                       Human friendly peer nickname. Maximum 32 characters.
       --no-verify                             Disables cluster definition and lock file verification.
       --p2p-disable-reuseport                 Disables TCP port reuse for outgoing libp2p connections.
       --p2p-external-hostname string          The DNS hostname advertised by libp2p. This may be used to advertise an external DNS.
diff --git a/docs/metrics.md b/docs/metrics.md
index ef8173f0..0070daf6 100644
--- a/docs/metrics.md
+++ b/docs/metrics.md
@@ -28,6 +28,7 @@ when storing metrics from multiple nodes or clusters in one Prometheus instance.
 | `app_peerinfo_clock_offset_seconds` | Gauge | Peer clock offset in seconds | `peer` |
 | `app_peerinfo_git_commit` | Gauge | Constant gauge with git_hash label set to peer`s git commit hash. | `peer, git_hash` |
 | `app_peerinfo_index` | Gauge | Constant gauge set to the peer index in the cluster definition | `peer` |
+| `app_peerinfo_nickname` | Gauge | Constant gauge with nickname label set to peer`s charon nickname. | `peer, nickname` |
 | `app_peerinfo_start_time_secs` | Gauge | Constant gauge set to the peer start time of the binary in unix seconds | `peer` |
 | `app_peerinfo_version` | Gauge | Constant gauge with version label set to peer`s charon version. | `peer, version` |
 | `app_peerinfo_version_support` | Gauge | Set to 1 if the peer`s version is supported by (compatible with) the current version, else 0 if unsupported. | `peer` |
diff --git a/go.mod b/go.mod
index 8adb5f74..98d1cf17 100644
--- a/go.mod
+++ b/go.mod
@@ -32,22 +32,22 @@ require (
 	github.com/spf13/viper v1.19.0
 	github.com/stretchr/testify v1.10.0
 	github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
 	go.opentelemetry.io/otel v1.33.0
 	go.opentelemetry.io/otel/exporters/jaeger v1.17.0
-	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0
-	go.opentelemetry.io/otel/sdk v1.32.0
+	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0
+	go.opentelemetry.io/otel/sdk v1.33.0
 	go.opentelemetry.io/otel/trace v1.33.0
 	go.uber.org/automaxprocs v1.6.0
 	go.uber.org/goleak v1.3.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.31.0
+	golang.org/x/crypto v0.32.0
 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
 	golang.org/x/sync v0.10.0
-	golang.org/x/term v0.27.0
+	golang.org/x/term v0.28.0
 	golang.org/x/time v0.8.0
-	golang.org/x/tools v0.28.0
-	google.golang.org/protobuf v1.35.2
+	golang.org/x/tools v0.29.0
+	google.golang.org/protobuf v1.36.2
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
@@ -210,8 +210,8 @@ require (
 	go.uber.org/mock v0.5.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/mod v0.22.0 // indirect
-	golang.org/x/net v0.32.0 // indirect
-	golang.org/x/sys v0.28.0 // indirect
+	golang.org/x/net v0.34.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
 	golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
diff --git a/go.sum b/go.sum
index 794a9921..02364dc1 100644
--- a/go.sum
+++ b/go.sum
@@ -594,8 +594,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
 go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
 go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
 go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
@@ -604,12 +604,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYa
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
-go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw=
-go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk=
 go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
 go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
-go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
-go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
+go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
+go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
 go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k=
 go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY=
 go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
@@ -650,8 +650,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
@@ -691,8 +691,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
-golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -744,8 +744,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -753,8 +753,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
 golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -784,8 +784,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
-golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
+golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
+golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -814,8 +814,8 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
 google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
-google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
-google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
+google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
 gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
 gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=



Test patch:
diff --git a/app/peerinfo/adhoc_test.go b/app/peerinfo/adhoc_test.go
index 83d5813b..69f9e90b 100644
--- a/app/peerinfo/adhoc_test.go
+++ b/app/peerinfo/adhoc_test.go
@@ -25,8 +25,10 @@ func TestDoOnce(t *testing.T) {
 	vers := version.Version
 	lockHash := []byte("123")
 	gitHash := "abc"
+	nickname := "johndoe"
+
 	// Register the server handler that either
-	_ = peerinfo.New(server, []peer.ID{server.ID(), client.ID()}, vers, lockHash, gitHash, p2p.SendReceive, true)
+	_ = peerinfo.New(server, []peer.ID{server.ID(), client.ID()}, vers, lockHash, gitHash, p2p.SendReceive, true, nickname)
 
 	info, _, ok, err := peerinfo.DoOnce(context.Background(), client, server.ID())
 	require.NoError(t, err)
@@ -35,4 +37,5 @@ func TestDoOnce(t *testing.T) {
 	require.Equal(t, gitHash, info.GetGitHash())
 	require.Equal(t, lockHash, info.GetLockHash())
 	require.True(t, info.GetBuilderApiEnabled())
+	require.Equal(t, nickname, info.GetNickname())
 }
diff --git a/app/peerinfo/peerinfo_internal_test.go b/app/peerinfo/peerinfo_internal_test.go
index 0fcb9e14..e9aa5e6f 100644
--- a/app/peerinfo/peerinfo_internal_test.go
+++ b/app/peerinfo/peerinfo_internal_test.go
@@ -72,6 +72,7 @@ func TestPeerBuilderAPIEnabledGauge(t *testing.T) {
 	lockHash := []byte("123")
 	gitHash := "abc"
 	peerName := p2p.PeerName(server.ID())
+	peerNickname := "johndoe"
 
 	tests := []struct {
 		name           string
@@ -84,7 +85,7 @@ func TestPeerBuilderAPIEnabledGauge(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			_ = New(server, []peer.ID{server.ID(), client.ID()}, version.Version, lockHash, gitHash, nil, test.builderEnabled)
+			_ = New(server, []peer.ID{server.ID(), client.ID()}, version.Version, lockHash, gitHash, nil, test.builderEnabled, peerNickname)
 
 			expectedMetric := fmt.Sprintf(`
 			# HELP app_peerinfo_builder_api_enabled Set to 1 if builder API is enabled on this peer, else 0 if disabled.
diff --git a/app/peerinfo/peerinfo_test.go b/app/peerinfo/peerinfo_test.go
index 5e615b5f..68ac0ec2 100644
--- a/app/peerinfo/peerinfo_test.go
+++ b/app/peerinfo/peerinfo_test.go
@@ -22,6 +22,7 @@ import (
 func TestPeerInfo(t *testing.T) {
 	now := time.Now()
 	const gitCommit = "1234567"
+	baseNickname := "john-"
 
 	// when a major release happens, charon will only be compatible with a single version
 	versions := version.Supported()
@@ -49,7 +50,7 @@ func TestPeerInfo(t *testing.T) {
 			Offset:   time.Minute,
 		},
 		{
-			Version: semver(t, "v0.0"),
+			Version: semver(t, "v1.0"),
 			Ignore:  true,
 		},
 	}
@@ -92,7 +93,7 @@ func TestPeerInfo(t *testing.T) {
 		tickProvider := func() (<-chan time.Time, func()) {
 			return nil, func() {}
 		}
-		metricSubmitter := func(peer.ID, time.Duration, string, string, time.Time, bool) {
+		metricSubmitter := func(peer.ID, time.Duration, string, string, time.Time, bool, string) {
 			panic("unexpected metric submitted")
 		}
 
@@ -107,7 +108,7 @@ func TestPeerInfo(t *testing.T) {
 
 			var submittedMutex sync.Mutex
 			var submitted int
-			metricSubmitter = func(peerID peer.ID, clockOffset time.Duration, version, gitHash string, startTime time.Time, builderEnabled bool) {
+			metricSubmitter = func(peerID peer.ID, clockOffset time.Duration, version, gitHash string, startTime time.Time, builderEnabled bool, nickname string) {
 				for i, tcpNode := range tcpNodes {
 					if tcpNode.ID() != peerID {
 						continue
@@ -117,6 +118,7 @@ func TestPeerInfo(t *testing.T) {
 					require.Equal(t, gitCommit, gitHash)
 					require.Equal(t, nowFunc(i)().Unix(), startTime.Unix())
 					require.True(t, builderEnabled)
+					require.Equal(t, baseNickname+p2p.PeerName(peerID), nickname)
 
 					submittedMutex.Lock()
 					submitted++
@@ -132,7 +134,7 @@ func TestPeerInfo(t *testing.T) {
 		}
 
 		peerInfo := peerinfo.NewForT(t, tcpNodes[i], peers, node.Version, node.LockHash, gitCommit, p2p.SendReceive, p2p.RegisterHandler,
-			tickProvider, nowFunc(i), metricSubmitter, true)
+			tickProvider, nowFunc(i), metricSubmitter, true, baseNickname+p2p.PeerName(peers[i]))
 
 		peerInfos = append(peerInfos, peerInfo)
 	}