Skip to content

Commit cf63d11

Browse files
authored
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?** - #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.
1 parent 6c04508 commit cf63d11

14 files changed

Lines changed: 490 additions & 101 deletions

File tree

beacon-chain/execution/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ go_library(
88
"deposit.go",
99
"engine_client.go",
1010
"errors.go",
11+
"graffiti_info.go",
1112
"log.go",
1213
"log_processing.go",
1314
"metrics.go",
@@ -89,6 +90,7 @@ go_test(
8990
"engine_client_fuzz_test.go",
9091
"engine_client_test.go",
9192
"execution_chain_test.go",
93+
"graffiti_info_test.go",
9294
"init_test.go",
9395
"log_processing_test.go",
9496
"mock_test.go",

beacon-chain/execution/engine_client.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,17 @@ var (
6161
}
6262
)
6363

64+
// ClientVersionV1 represents the response from engine_getClientVersionV1.
65+
type ClientVersionV1 struct {
66+
Code string `json:"code"`
67+
Name string `json:"name"`
68+
Version string `json:"version"`
69+
Commit string `json:"commit"`
70+
}
71+
6472
const (
73+
// GetClientVersionMethod is the engine_getClientVersionV1 method for JSON-RPC.
74+
GetClientVersionMethod = "engine_getClientVersionV1"
6575
// NewPayloadMethod v1 request string for JSON-RPC.
6676
NewPayloadMethod = "engine_newPayloadV1"
6777
// NewPayloadMethodV2 v2 request string for JSON-RPC.
@@ -350,6 +360,24 @@ func (s *Service) ExchangeCapabilities(ctx context.Context) ([]string, error) {
350360
return elSupportedEndpointsSlice, nil
351361
}
352362

363+
// GetClientVersion calls engine_getClientVersionV1 to retrieve EL client information.
364+
func (s *Service) GetClientVersion(ctx context.Context) ([]ClientVersionV1, error) {
365+
ctx, span := trace.StartSpan(ctx, "powchain.engine-api-client.GetClientVersion")
366+
defer span.End()
367+
368+
// Per spec, we send our own client info as the parameter
369+
clVersion := ClientVersionV1{
370+
Code: CLCode,
371+
Name: Name,
372+
Version: version.SemanticVersion(),
373+
Commit: version.GetCommitPrefix(),
374+
}
375+
376+
var result []ClientVersionV1
377+
err := s.rpcClient.CallContext(ctx, &result, GetClientVersionMethod, clVersion)
378+
return result, handleRPCError(err)
379+
}
380+
353381
// GetTerminalBlockHash returns the valid terminal block hash based on total difficulty.
354382
//
355383
// Spec code:
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package execution
2+
3+
import (
4+
"strings"
5+
"sync"
6+
7+
"github.com/OffchainLabs/prysm/v7/runtime/version"
8+
)
9+
10+
const (
11+
// CLCode is the two-letter client code for Prysm.
12+
CLCode = "PR"
13+
Name = "Prysm"
14+
)
15+
16+
// GraffitiInfo holds version information for generating block graffiti.
17+
// It is thread-safe and can be updated by the execution service and read by the validator server.
18+
type GraffitiInfo struct {
19+
mu sync.RWMutex
20+
elCode string // From engine_getClientVersionV1
21+
elCommit string // From engine_getClientVersionV1
22+
logOnce sync.Once
23+
}
24+
25+
// NewGraffitiInfo creates a new GraffitiInfo.
26+
func NewGraffitiInfo() *GraffitiInfo {
27+
return &GraffitiInfo{}
28+
}
29+
30+
// UpdateFromEngine updates the EL client information.
31+
func (g *GraffitiInfo) UpdateFromEngine(code, commit string) {
32+
g.mu.Lock()
33+
defer g.mu.Unlock()
34+
g.elCode = code
35+
g.elCommit = strings.TrimPrefix(commit, "0x")
36+
}
37+
38+
// GenerateGraffiti generates graffiti using the flexible standard
39+
// with the provided user graffiti from the validator client request.
40+
// It places user graffiti first, then appends as much client info as space allows.
41+
//
42+
// A space separator is added between user graffiti and client info when it
43+
// fits without reducing the client version tier.
44+
//
45+
// Available Space | Format
46+
// ≥13 bytes | user + space + EL(2)+commit(4)+CL(2)+commit(4) e.g. "Sushi GEabcdPRe4f6"
47+
// 12 bytes | user + EL(2)+commit(4)+CL(2)+commit(4) e.g. "12345678901234567890GEabcdPRe4f6"
48+
// 9-11 bytes | user + space + EL(2)+commit(2)+CL(2)+commit(2) e.g. "12345678901234567890123 GEabPRe4"
49+
// 8 bytes | user + EL(2)+commit(2)+CL(2)+commit(2) e.g. "123456789012345678901234GEabPRe4"
50+
// 5-7 bytes | user + space + EL(2)+CL(2) e.g. "123456789012345678901234567 GEPR"
51+
// 4 bytes | user + EL(2)+CL(2) e.g. "1234567890123456789012345678GEPR"
52+
// 3 bytes | user + space + code(2) e.g. "12345678901234567890123456789 GE"
53+
// 2 bytes | user + code(2) e.g. "123456789012345678901234567890GE"
54+
// <2 bytes | user only e.g. "1234567890123456789012345678901x"
55+
func (g *GraffitiInfo) GenerateGraffiti(userGraffiti []byte) [32]byte {
56+
g.mu.RLock()
57+
defer g.mu.RUnlock()
58+
59+
var result [32]byte
60+
userStr := string(userGraffiti)
61+
// Trim trailing null bytes
62+
for len(userStr) > 0 && userStr[len(userStr)-1] == 0 {
63+
userStr = userStr[:len(userStr)-1]
64+
}
65+
66+
available := 32 - len(userStr)
67+
68+
clCommit := version.GetCommitPrefix()
69+
clCommit4 := truncateCommit(clCommit, 4)
70+
clCommit2 := truncateCommit(clCommit, 2)
71+
72+
// If no EL info, clear EL commits but still include CL info
73+
var elCommit4, elCommit2 string
74+
if g.elCode != "" {
75+
elCommit4 = truncateCommit(g.elCommit, 4)
76+
elCommit2 = truncateCommit(g.elCommit, 2)
77+
}
78+
79+
// Add a space separator between user graffiti and client info,
80+
// but only if it won't reduce the space available for client version info.
81+
space := func(minForTier int) string {
82+
if len(userStr) > 0 && available >= minForTier+1 {
83+
return " "
84+
}
85+
return ""
86+
}
87+
88+
var graffiti string
89+
switch {
90+
case available >= 12:
91+
// Full: user+EL(2)+commit(4)+CL(2)+commit(4)
92+
graffiti = userStr + space(12) + g.elCode + elCommit4 + CLCode + clCommit4
93+
case available >= 8:
94+
// Reduced commits: user+EL(2)+commit(2)+CL(2)+commit(2)
95+
graffiti = userStr + space(8) + g.elCode + elCommit2 + CLCode + clCommit2
96+
case available >= 4:
97+
// Codes only: user+EL(2)+CL(2)
98+
graffiti = userStr + space(4) + g.elCode + CLCode
99+
case available >= 2:
100+
// Single code: user+code(2)
101+
if g.elCode != "" {
102+
graffiti = userStr + space(2) + g.elCode
103+
} else {
104+
graffiti = userStr + space(2) + CLCode
105+
}
106+
default:
107+
// User graffiti only
108+
graffiti = userStr
109+
}
110+
111+
g.logOnce.Do(func() {
112+
logGraffitiInfo(graffiti, available)
113+
})
114+
115+
copy(result[:], graffiti)
116+
return result
117+
}
118+
119+
// logGraffitiInfo logs the graffiti that will be used.
120+
func logGraffitiInfo(graffiti string, available int) {
121+
if available >= 2 {
122+
log.WithField("graffiti", graffiti).Info("Graffiti includes client version info appended after user graffiti")
123+
return
124+
}
125+
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")
126+
}
127+
128+
// truncateCommit returns the first n characters of the commit string.
129+
func truncateCommit(commit string, n int) string {
130+
if len(commit) <= n {
131+
return commit
132+
}
133+
return commit[:n]
134+
}

0 commit comments

Comments
 (0)