mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-13 06:25:06 -05:00
Graffiti implementation (#16089)
<!-- Thanks for sending a PR! Before submitting: 1. If this is your first PR, check out our contribution guide here https://docs.prylabs.network/docs/contribute/contribution-guidelines You will then need to sign our Contributor License Agreement (CLA), which will show up as a comment from a bot in this pull request after you open it. We cannot review code without a signed CLA. 2. Please file an associated tracking issue if this pull request is non-trivial and requires context for our team to understand. All features and most bug fixes should have an associated issue with a design discussed and decided upon. Small bug fixes and documentation improvements don't need issues. 3. New features and bug fixes must have tests. Documentation may need to be updated. If you're unsure what to update, send the PR, and we'll discuss in review. 4. Note that PRs updating dependencies and new Go versions are not accepted. Please file an issue instead. 5. A changelog entry is required for user facing issues. --> **What type of PR is this?** Feature **What does this PR do? Why is it needed?** This PR implements graffiti as described in the corresponding spec doc `graffiti-proposal-brief.md ` **Which issues(s) does this PR fix?** - https://github.com/OffchainLabs/prysm/issues/13558 **Other notes for review** **Acknowledgements** - [ ] I have read [CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md). - [ ] I have included a uniquely named [changelog fragment file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd). - [ ] I have added a description to this PR with sufficient context for reviewers to understand this PR.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
134
beacon-chain/execution/graffiti_info.go
Normal file
134
beacon-chain/execution/graffiti_info.go
Normal file
@@ -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]
|
||||
}
|
||||
250
beacon-chain/execution/graffiti_info_test.go
Normal file
250
beacon-chain/execution/graffiti_info_test.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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[:])
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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{
|
||||
|
||||
3
changelog/satushh-graffiti-impl.md
Normal file
3
changelog/satushh-graffiti-impl.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Graffiti implementation based on the design doc.
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user