diff --git a/beacon-chain/execution/BUILD.bazel b/beacon-chain/execution/BUILD.bazel index 4d4ce47689..1f0d20c465 100644 --- a/beacon-chain/execution/BUILD.bazel +++ b/beacon-chain/execution/BUILD.bazel @@ -8,6 +8,7 @@ go_library( "deposit.go", "engine_client.go", "errors.go", + "graffiti_info.go", "log.go", "log_processing.go", "metrics.go", @@ -89,6 +90,7 @@ go_test( "engine_client_fuzz_test.go", "engine_client_test.go", "execution_chain_test.go", + "graffiti_info_test.go", "init_test.go", "log_processing_test.go", "mock_test.go", diff --git a/beacon-chain/execution/engine_client.go b/beacon-chain/execution/engine_client.go index 82eb5ff634..7d35c59508 100644 --- a/beacon-chain/execution/engine_client.go +++ b/beacon-chain/execution/engine_client.go @@ -61,7 +61,17 @@ var ( } ) +// ClientVersionV1 represents the response from engine_getClientVersionV1. +type ClientVersionV1 struct { + Code string `json:"code"` + Name string `json:"name"` + Version string `json:"version"` + Commit string `json:"commit"` +} + const ( + // GetClientVersionMethod is the engine_getClientVersionV1 method for JSON-RPC. + GetClientVersionMethod = "engine_getClientVersionV1" // NewPayloadMethod v1 request string for JSON-RPC. NewPayloadMethod = "engine_newPayloadV1" // NewPayloadMethodV2 v2 request string for JSON-RPC. @@ -350,6 +360,24 @@ func (s *Service) ExchangeCapabilities(ctx context.Context) ([]string, error) { return elSupportedEndpointsSlice, nil } +// GetClientVersion calls engine_getClientVersionV1 to retrieve EL client information. +func (s *Service) GetClientVersion(ctx context.Context) ([]ClientVersionV1, error) { + ctx, span := trace.StartSpan(ctx, "powchain.engine-api-client.GetClientVersion") + defer span.End() + + // Per spec, we send our own client info as the parameter + clVersion := ClientVersionV1{ + Code: CLCode, + Name: Name, + Version: version.SemanticVersion(), + Commit: version.GetCommitPrefix(), + } + + var result []ClientVersionV1 + err := s.rpcClient.CallContext(ctx, &result, GetClientVersionMethod, clVersion) + return result, handleRPCError(err) +} + // GetTerminalBlockHash returns the valid terminal block hash based on total difficulty. // // Spec code: diff --git a/beacon-chain/execution/graffiti_info.go b/beacon-chain/execution/graffiti_info.go new file mode 100644 index 0000000000..2b12693e12 --- /dev/null +++ b/beacon-chain/execution/graffiti_info.go @@ -0,0 +1,134 @@ +package execution + +import ( + "strings" + "sync" + + "github.com/OffchainLabs/prysm/v7/runtime/version" +) + +const ( + // CLCode is the two-letter client code for Prysm. + CLCode = "PR" + Name = "Prysm" +) + +// GraffitiInfo holds version information for generating block graffiti. +// It is thread-safe and can be updated by the execution service and read by the validator server. +type GraffitiInfo struct { + mu sync.RWMutex + elCode string // From engine_getClientVersionV1 + elCommit string // From engine_getClientVersionV1 + logOnce sync.Once +} + +// NewGraffitiInfo creates a new GraffitiInfo. +func NewGraffitiInfo() *GraffitiInfo { + return &GraffitiInfo{} +} + +// UpdateFromEngine updates the EL client information. +func (g *GraffitiInfo) UpdateFromEngine(code, commit string) { + g.mu.Lock() + defer g.mu.Unlock() + g.elCode = code + g.elCommit = strings.TrimPrefix(commit, "0x") +} + +// GenerateGraffiti generates graffiti using the flexible standard +// with the provided user graffiti from the validator client request. +// It places user graffiti first, then appends as much client info as space allows. +// +// A space separator is added between user graffiti and client info when it +// fits without reducing the client version tier. +// +// Available Space | Format +// ≥13 bytes | user + space + EL(2)+commit(4)+CL(2)+commit(4) e.g. "Sushi GEabcdPRe4f6" +// 12 bytes | user + EL(2)+commit(4)+CL(2)+commit(4) e.g. "12345678901234567890GEabcdPRe4f6" +// 9-11 bytes | user + space + EL(2)+commit(2)+CL(2)+commit(2) e.g. "12345678901234567890123 GEabPRe4" +// 8 bytes | user + EL(2)+commit(2)+CL(2)+commit(2) e.g. "123456789012345678901234GEabPRe4" +// 5-7 bytes | user + space + EL(2)+CL(2) e.g. "123456789012345678901234567 GEPR" +// 4 bytes | user + EL(2)+CL(2) e.g. "1234567890123456789012345678GEPR" +// 3 bytes | user + space + code(2) e.g. "12345678901234567890123456789 GE" +// 2 bytes | user + code(2) e.g. "123456789012345678901234567890GE" +// <2 bytes | user only e.g. "1234567890123456789012345678901x" +func (g *GraffitiInfo) GenerateGraffiti(userGraffiti []byte) [32]byte { + g.mu.RLock() + defer g.mu.RUnlock() + + var result [32]byte + userStr := string(userGraffiti) + // Trim trailing null bytes + for len(userStr) > 0 && userStr[len(userStr)-1] == 0 { + userStr = userStr[:len(userStr)-1] + } + + available := 32 - len(userStr) + + clCommit := version.GetCommitPrefix() + clCommit4 := truncateCommit(clCommit, 4) + clCommit2 := truncateCommit(clCommit, 2) + + // If no EL info, clear EL commits but still include CL info + var elCommit4, elCommit2 string + if g.elCode != "" { + elCommit4 = truncateCommit(g.elCommit, 4) + elCommit2 = truncateCommit(g.elCommit, 2) + } + + // Add a space separator between user graffiti and client info, + // but only if it won't reduce the space available for client version info. + space := func(minForTier int) string { + if len(userStr) > 0 && available >= minForTier+1 { + return " " + } + return "" + } + + var graffiti string + switch { + case available >= 12: + // Full: user+EL(2)+commit(4)+CL(2)+commit(4) + graffiti = userStr + space(12) + g.elCode + elCommit4 + CLCode + clCommit4 + case available >= 8: + // Reduced commits: user+EL(2)+commit(2)+CL(2)+commit(2) + graffiti = userStr + space(8) + g.elCode + elCommit2 + CLCode + clCommit2 + case available >= 4: + // Codes only: user+EL(2)+CL(2) + graffiti = userStr + space(4) + g.elCode + CLCode + case available >= 2: + // Single code: user+code(2) + if g.elCode != "" { + graffiti = userStr + space(2) + g.elCode + } else { + graffiti = userStr + space(2) + CLCode + } + default: + // User graffiti only + graffiti = userStr + } + + g.logOnce.Do(func() { + logGraffitiInfo(graffiti, available) + }) + + copy(result[:], graffiti) + return result +} + +// logGraffitiInfo logs the graffiti that will be used. +func logGraffitiInfo(graffiti string, available int) { + if available >= 2 { + log.WithField("graffiti", graffiti).Info("Graffiti includes client version info appended after user graffiti") + return + } + log.WithField("graffiti", graffiti).Info("Prysm adds consensus and execution debugging information to the end of the graffiti field when possible. To prevent deletion of debugging info, please consider using a shorter graffiti") +} + +// truncateCommit returns the first n characters of the commit string. +func truncateCommit(commit string, n int) string { + if len(commit) <= n { + return commit + } + return commit[:n] +} diff --git a/beacon-chain/execution/graffiti_info_test.go b/beacon-chain/execution/graffiti_info_test.go new file mode 100644 index 0000000000..1dcdcd9c75 --- /dev/null +++ b/beacon-chain/execution/graffiti_info_test.go @@ -0,0 +1,250 @@ +package execution + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestGraffitiInfo_GenerateGraffiti(t *testing.T) { + tests := []struct { + name string + elCode string + elCommit string + userGraffiti []byte + wantPrefix string // user graffiti appears first + wantSuffix string // client version info appended after + }{ + // No EL info cases (CL info "PR" + commit still included when space allows) + { + name: "No EL - empty user graffiti", + elCode: "", + elCommit: "", + userGraffiti: []byte{}, + wantPrefix: "PR", // Only CL code + commit (no user graffiti to prefix) + }, + { + name: "No EL - short user graffiti", + elCode: "", + elCommit: "", + userGraffiti: []byte("my validator"), + wantPrefix: "my validator", + wantSuffix: " PR", // space + CL code appended + }, + { + name: "No EL - 28 char user graffiti (4 bytes available)", + elCode: "", + elCommit: "", + userGraffiti: []byte("1234567890123456789012345678"), // 28 chars, 4 bytes available = codes only + wantPrefix: "1234567890123456789012345678", + wantSuffix: "PR", // CL code (no EL, so just PR) + }, + { + name: "No EL - 30 char user graffiti (2 bytes available)", + elCode: "", + elCommit: "", + userGraffiti: []byte("123456789012345678901234567890"), // 30 chars, 2 bytes available = fits PR + wantPrefix: "123456789012345678901234567890", + wantSuffix: "PR", + }, + { + name: "No EL - 31 char user graffiti (1 byte available)", + elCode: "", + elCommit: "", + userGraffiti: []byte("1234567890123456789012345678901"), // 31 chars, 1 byte available = not enough for code + wantPrefix: "1234567890123456789012345678901", // User only + }, + { + name: "No EL - 32 char user graffiti (0 bytes available)", + elCode: "", + elCommit: "", + userGraffiti: []byte("12345678901234567890123456789012"), + wantPrefix: "12345678901234567890123456789012", // User only + }, + // With EL info - flexible standard format cases + { + name: "With EL - full format (empty user graffiti)", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte{}, + wantPrefix: "GEabcdPR", // No user graffiti, starts with client info + }, + { + name: "With EL - full format (short user graffiti)", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("Bob"), + wantPrefix: "Bob", + wantSuffix: " GEabcdPR", // space + EL(2)+commit(4)+CL(2)+commit(4) + }, + { + name: "With EL - full format (20 char user, 12 bytes available) - no space, would reduce tier", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("12345678901234567890"), // 20 chars, leaves exactly 12 bytes = full format, no room for space + wantPrefix: "12345678901234567890", + wantSuffix: "GEabcdPR", + }, + { + name: "With EL - full format (19 char user, 13 bytes available) - space fits", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("1234567890123456789"), // 19 chars, leaves 13 bytes = full format + space + wantPrefix: "1234567890123456789", + wantSuffix: " GEabcdPR", + }, + { + name: "With EL - reduced commits (24 char user, 8 bytes available) - no space, would reduce tier", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("123456789012345678901234"), // 24 chars, leaves exactly 8 bytes = reduced format, no room for space + wantPrefix: "123456789012345678901234", + wantSuffix: "GEabPR", + }, + { + name: "With EL - reduced commits (23 char user, 9 bytes available) - space fits", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("12345678901234567890123"), // 23 chars, leaves 9 bytes = reduced format + space + wantPrefix: "12345678901234567890123", + wantSuffix: " GEabPR", + }, + { + name: "With EL - codes only (28 char user, 4 bytes available) - no space, would reduce tier", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("1234567890123456789012345678"), // 28 chars, leaves exactly 4 bytes = codes only, no room for space + wantPrefix: "1234567890123456789012345678", + wantSuffix: "GEPR", + }, + { + name: "With EL - codes only (27 char user, 5 bytes available) - space fits", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("123456789012345678901234567"), // 27 chars, leaves 5 bytes = codes only + space + wantPrefix: "123456789012345678901234567", + wantSuffix: " GEPR", + }, + { + name: "With EL - EL code only (30 char user, 2 bytes available) - no space, would reduce tier", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("123456789012345678901234567890"), // 30 chars, leaves exactly 2 bytes = EL code only, no room for space + wantPrefix: "123456789012345678901234567890", + wantSuffix: "GE", + }, + { + name: "With EL - EL code only (29 char user, 3 bytes available) - space fits", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("12345678901234567890123456789"), // 29 chars, leaves 3 bytes = EL code + space + wantPrefix: "12345678901234567890123456789", + wantSuffix: " GE", + }, + { + name: "With EL - user only (31 char user, 1 byte available)", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("1234567890123456789012345678901"), // 31 chars, leaves 1 byte = not enough for code + wantPrefix: "1234567890123456789012345678901", // User only + }, + { + name: "With EL - user only (32 char user, 0 bytes available)", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: []byte("12345678901234567890123456789012"), + wantPrefix: "12345678901234567890123456789012", + }, + // Null byte handling + { + name: "Null bytes - input with trailing nulls", + elCode: "GE", + elCommit: "abcd1234", + userGraffiti: append([]byte("test"), 0, 0, 0), + wantPrefix: "test", + wantSuffix: " GEabcdPR", + }, + // 0x prefix handling - some ELs return 0x-prefixed commits + { + name: "0x prefix - stripped from EL commit", + elCode: "GE", + elCommit: "0xabcd1234", + userGraffiti: []byte{}, + wantPrefix: "GEabcdPR", + }, + { + name: "No 0x prefix - commit used as-is", + elCode: "NM", + elCommit: "abcd1234", + userGraffiti: []byte{}, + wantPrefix: "NMabcdPR", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewGraffitiInfo() + if tt.elCode != "" { + g.UpdateFromEngine(tt.elCode, tt.elCommit) + } + + result := g.GenerateGraffiti(tt.userGraffiti) + resultStr := string(result[:]) + trimmed := trimNullBytes(resultStr) + + // Check prefix (user graffiti comes first) + require.Equal(t, true, len(trimmed) >= len(tt.wantPrefix), "Result too short for prefix check") + require.Equal(t, tt.wantPrefix, trimmed[:len(tt.wantPrefix)], "Prefix mismatch") + + // Check suffix if specified (client version info appended) + if tt.wantSuffix != "" { + require.Equal(t, true, len(trimmed) >= len(tt.wantSuffix), "Result too short for suffix check") + // The suffix should appear somewhere after the prefix + afterPrefix := trimmed[len(tt.wantPrefix):] + require.Equal(t, true, len(afterPrefix) >= len(tt.wantSuffix), "Not enough room for suffix after prefix") + require.Equal(t, tt.wantSuffix, afterPrefix[:len(tt.wantSuffix)], "Suffix mismatch") + } + }) + } +} + +func TestGraffitiInfo_UpdateFromEngine(t *testing.T) { + g := NewGraffitiInfo() + + // Initially no EL info - should still have CL info (PR + commit) + result := g.GenerateGraffiti([]byte{}) + resultStr := trimNullBytes(string(result[:])) + require.Equal(t, "PR", resultStr[:2], "Expected CL info before update") + + // Update with EL info + g.UpdateFromEngine("GE", "1234abcd") + + result = g.GenerateGraffiti([]byte{}) + resultStr = trimNullBytes(string(result[:])) + require.Equal(t, "GE1234PR", resultStr[:8], "Expected EL+CL info after update") +} + +func TestTruncateCommit(t *testing.T) { + tests := []struct { + commit string + n int + want string + }{ + {"abcd1234", 4, "abcd"}, + {"ab", 4, "ab"}, + {"", 4, ""}, + {"abcdef", 2, "ab"}, + } + + for _, tt := range tests { + got := truncateCommit(tt.commit, tt.n) + require.Equal(t, tt.want, got) + } +} + +func trimNullBytes(s string) string { + for len(s) > 0 && s[len(s)-1] == 0 { + s = s[:len(s)-1] + } + return s +} diff --git a/beacon-chain/execution/options.go b/beacon-chain/execution/options.go index 7d178671ce..e7918b1c1b 100644 --- a/beacon-chain/execution/options.go +++ b/beacon-chain/execution/options.go @@ -124,3 +124,11 @@ func WithVerifierWaiter(v *verification.InitializerWaiter) Option { return nil } } + +// WithGraffitiInfo sets the GraffitiInfo for client version tracking. +func WithGraffitiInfo(g *GraffitiInfo) Option { + return func(s *Service) error { + s.graffitiInfo = g + return nil + } +} diff --git a/beacon-chain/execution/service.go b/beacon-chain/execution/service.go index f9d35fd7a5..d9b095ade4 100644 --- a/beacon-chain/execution/service.go +++ b/beacon-chain/execution/service.go @@ -162,6 +162,7 @@ type Service struct { verifierWaiter *verification.InitializerWaiter blobVerifier verification.NewBlobVerifier capabilityCache *capabilityCache + graffitiInfo *GraffitiInfo } // NewService sets up a new instance with an ethclient when given a web3 endpoint as a string in the config. @@ -318,6 +319,28 @@ func (s *Service) updateConnectedETH1(state bool) { s.updateBeaconNodeStats() } +// GraffitiInfo returns the GraffitiInfo struct for graffiti generation. +func (s *Service) GraffitiInfo() *GraffitiInfo { + return s.graffitiInfo +} + +// updateGraffitiInfo fetches EL client version and updates the graffiti info. +func (s *Service) updateGraffitiInfo() { + if s.graffitiInfo == nil { + return + } + ctx, cancel := context.WithTimeout(s.ctx, time.Second) + defer cancel() + versions, err := s.GetClientVersion(ctx) + if err != nil { + log.WithError(err).Debug("Could not get execution client version for graffiti") + return + } + if len(versions) >= 1 { + s.graffitiInfo.UpdateFromEngine(versions[0].Code, versions[0].Commit) + } +} + // refers to the latest eth1 block which follows the condition: eth1_timestamp + // SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= current_unix_time func (s *Service) followedBlockHeight(ctx context.Context) (uint64, error) { @@ -598,6 +621,12 @@ func (s *Service) run(done <-chan struct{}) { chainstartTicker := time.NewTicker(logPeriod) defer chainstartTicker.Stop() + // Update graffiti info 4 times per epoch (~96 seconds with 12s slots and 32 slots/epoch) + graffitiTicker := time.NewTicker(96 * time.Second) + defer graffitiTicker.Stop() + // Initial update + s.updateGraffitiInfo() + for { select { case <-done: @@ -622,6 +651,8 @@ func (s *Service) run(done <-chan struct{}) { continue } s.logTillChainStart(context.Background()) + case <-graffitiTicker.C: + s.updateGraffitiInfo() } } } diff --git a/beacon-chain/graffiti/graffiti-proposal-brief.md b/beacon-chain/graffiti/graffiti-proposal-brief.md deleted file mode 100644 index 448ae29d13..0000000000 --- a/beacon-chain/graffiti/graffiti-proposal-brief.md +++ /dev/null @@ -1,95 +0,0 @@ -# Graffiti Version Info Implementation - -## Summary -Add automatic EL+CL version info to block graffiti following [ethereum/execution-apis#517](https://github.com/ethereum/execution-apis/pull/517). Uses the [flexible standard](https://hackmd.io/@wmoBhF17RAOH2NZ5bNXJVg/BJX2c9gja) to pack client info into leftover space after user graffiti. - -More details: https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md - -## Implementation - -### Core Component: GraffitiInfo Struct -Thread-safe struct holding version information: -```go -const clCode = "PR" - -type GraffitiInfo struct { - mu sync.RWMutex - userGraffiti string // From --graffiti flag (set once at startup) - clCommit string // From version.GetCommitPrefix() helper function - elCode string // From engine_getClientVersionV1 - elCommit string // From engine_getClientVersionV1 -} -``` - -### Flow -1. **Startup**: Parse flags, create GraffitiInfo with user graffiti and CL info. -2. **Wiring**: Pass struct to both execution service and RPC validator server -3. **Runtime**: Execution service goroutine periodically calls `engine_getClientVersionV1` and updates EL fields -4. **Block Proposal**: RPC validator server calls `GenerateGraffiti()` to get formatted graffiti - -### Flexible Graffiti Format -Packs as much client info as space allows (after user graffiti): - -| Available Space | Format | Example | -|----------------|--------|---------| -| ≥12 bytes | `EL(2)+commit(4)+CL(2)+commit(4)+user` | `GE168dPR63afBob` | -| 8-11 bytes | `EL(2)+commit(2)+CL(2)+commit(2)+user` | `GE16PR63my node here` | -| 4-7 bytes | `EL(2)+CL(2)+user` | `GEPRthis is my graffiti msg` | -| 2-3 bytes | `EL(2)+user` | `GEalmost full graffiti message` | -| <2 bytes | user only | `full 32 byte user graffiti here` | - -```go -func (g *GraffitiInfo) GenerateGraffiti() [32]byte { - available := 32 - len(userGraffiti) - - if elCode == "" { - elCommit2 = elCommit4 = "" - } - - switch { - case available >= 12: - return elCode + elCommit4 + clCode + clCommit4 + userGraffiti - case available >= 8: - return elCode + elCommit2 + clCode + clCommit2 + userGraffiti - case available >= 4: - return elCode + clCode + userGraffiti - case available >= 2: - return elCode + userGraffiti - default: - return userGraffiti - } -} -``` - -### Update Logic -Single testable function in execution service: -```go -func (s *Service) updateGraffitiInfo() { - versions, err := s.GetClientVersion(ctx) - if err != nil { - return // Keep last good value - } - if len(versions) == 1 { - s.graffitiInfo.UpdateFromEngine(versions[0].Code, versions[0].Commit) - } -} -``` - -Goroutine calls this on `slot % 8 == 4` timing (4 times per epoch, avoids slot boundaries). - -### Files Changes Required - -**New:** -- `beacon-chain/execution/graffiti_info.go` - The struct and methods -- `beacon-chain/execution/graffiti_info_test.go` - Unit tests -- `runtime/version/version.go` - Add `GetCommitPrefix()` helper that extracts first 4 hex chars from the git commit injected via Bazel ldflags at build time - -**Modified:** -- `beacon-chain/execution/service.go` - Add goroutine + updateGraffitiInfo() -- `beacon-chain/execution/engine_client.go` - Add GetClientVersion() method that does engine call -- `beacon-chain/rpc/.../validator/proposer.go` - Call GenerateGraffiti() -- `beacon-chain/node/node.go` - Wire GraffitiInfo to services - -### Testing Strategy -- Unit test GraffitiInfo methods (priority logic, thread safety) -- Unit test updateGraffitiInfo() with mocked engine client diff --git a/beacon-chain/node/node.go b/beacon-chain/node/node.go index fd8e885a7a..74834fb611 100644 --- a/beacon-chain/node/node.go +++ b/beacon-chain/node/node.go @@ -785,6 +785,9 @@ func (b *BeaconNode) registerPOWChainService() error { return err } + // Create GraffitiInfo for client version tracking in block graffiti + graffitiInfo := execution.NewGraffitiInfo() + // skipcq: CRT-D0001 opts := append( b.serviceFlagOpts.executionChainFlagOpts, @@ -797,6 +800,7 @@ func (b *BeaconNode) registerPOWChainService() error { execution.WithFinalizedStateAtStartup(b.finalizedStateAtStartUp), execution.WithJwtId(b.cliCtx.String(flags.JwtId.Name)), execution.WithVerifierWaiter(b.verifyInitWaiter), + execution.WithGraffitiInfo(graffitiInfo), ) web3Service, err := execution.NewService(b.ctx, opts...) if err != nil { @@ -1003,6 +1007,7 @@ func (b *BeaconNode) registerRPCService(router *http.ServeMux) error { TrackedValidatorsCache: b.trackedValidatorsCache, PayloadIDCache: b.payloadIDCache, LCStore: b.lcStore, + GraffitiInfo: web3Service.GraffitiInfo(), }) return b.services.RegisterService(rpcService) diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go index 356eeee03f..32f450ab21 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go @@ -89,7 +89,13 @@ func (vs *Server) GetBeaconBlock(ctx context.Context, req *ethpb.BlockRequest) ( } // Set slot, graffiti, randao reveal, and parent root. sBlk.SetSlot(req.Slot) - sBlk.SetGraffiti(req.Graffiti) + // Generate graffiti with client version info using flexible standard + if vs.GraffitiInfo != nil { + graffiti := vs.GraffitiInfo.GenerateGraffiti(req.Graffiti) + sBlk.SetGraffiti(graffiti[:]) + } else { + sBlk.SetGraffiti(req.Graffiti) + } sBlk.SetRandaoReveal(req.RandaoReveal) sBlk.SetParentRoot(parentRoot[:]) diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/server.go b/beacon-chain/rpc/prysm/v1alpha1/validator/server.go index aaba034c12..ece565f211 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/server.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/server.go @@ -83,6 +83,7 @@ type Server struct { ClockWaiter startup.ClockWaiter CoreService *core.Service AttestationStateFetcher blockchain.AttestationStateFetcher + GraffitiInfo *execution.GraffitiInfo } // Deprecated: The gRPC API will remain the default and fully supported through v8 (expected in 2026) but will be eventually removed in favor of REST API. diff --git a/beacon-chain/rpc/service.go b/beacon-chain/rpc/service.go index 032ee2e465..6cb4073294 100644 --- a/beacon-chain/rpc/service.go +++ b/beacon-chain/rpc/service.go @@ -125,6 +125,7 @@ type Config struct { TrackedValidatorsCache *cache.TrackedValidatorsCache PayloadIDCache *cache.PayloadIDCache LCStore *lightClient.Store + GraffitiInfo *execution.GraffitiInfo } // NewService instantiates a new RPC service instance that will @@ -256,6 +257,7 @@ func NewService(ctx context.Context, cfg *Config) *Service { TrackedValidatorsCache: s.cfg.TrackedValidatorsCache, PayloadIDCache: s.cfg.PayloadIDCache, AttestationStateFetcher: s.cfg.AttestationReceiver, + GraffitiInfo: s.cfg.GraffitiInfo, } s.validatorServer = validatorServer nodeServer := &nodev1alpha1.Server{ diff --git a/changelog/satushh-graffiti-impl.md b/changelog/satushh-graffiti-impl.md new file mode 100644 index 0000000000..b2f80a029c --- /dev/null +++ b/changelog/satushh-graffiti-impl.md @@ -0,0 +1,3 @@ +### Added + +- Graffiti implementation based on the design doc. \ No newline at end of file diff --git a/runtime/version/version.go b/runtime/version/version.go index a7ba869747..7014950171 100644 --- a/runtime/version/version.go +++ b/runtime/version/version.go @@ -47,3 +47,13 @@ func BuildData() string { } return fmt.Sprintf("Prysm/%s/%s", gitTag, gitCommit) } + +// GetCommitPrefix returns the first 4 characters of the git commit. +// This is used for graffiti generation per the client identification spec. +// Note: BuildData() must be called before this (happens at startup via Version()). +func GetCommitPrefix() string { + if len(gitCommit) < 4 { + return gitCommit + } + return gitCommit[:4] +} diff --git a/testing/endtoend/evaluators/operations.go b/testing/endtoend/evaluators/operations.go index 7b383ecbc6..7b55ff1c25 100644 --- a/testing/endtoend/evaluators/operations.go +++ b/testing/endtoend/evaluators/operations.go @@ -260,17 +260,21 @@ func verifyGraffitiInBlocks(_ *e2etypes.EvaluationContext, conns ...*grpc.Client if err != nil { return err } - var e bool + var found bool slot := blk.Block().Slot() graffitiInBlock := blk.Block().Body().Graffiti() + // Trim trailing null bytes from graffiti. + // Example: "SushiGEabcdPRxxxx\x00\x00\x00..." becomes "SushiGEabcdPRxxxx" + graffitiTrimmed := bytes.TrimRight(graffitiInBlock[:], "\x00") for _, graffiti := range helpers.Graffiti { - if bytes.Equal(bytesutil.PadTo([]byte(graffiti), 32), graffitiInBlock[:]) { - e = true + // Check prefix match since user graffiti comes first, with EL/CL version info appended after. + if bytes.HasPrefix(graffitiTrimmed, []byte(graffiti)) { + found = true break } } - if !e && slot != 0 { - return errors.New("could not get graffiti from the list") + if !found && slot != 0 { + return fmt.Errorf("block at slot %d has graffiti %q which does not start with any expected graffiti", slot, string(graffitiTrimmed)) } }