diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b42163..b51c6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +1.35.4: + - provide consensus and execution client info in block info output + 1.35.3: - provide better error message on context deadlline exceeded - update launchpad output to match latest version diff --git a/cmd/block/info/output.go b/cmd/block/info/output.go index 8950a83..8dc6953 100644 --- a/cmd/block/info/output.go +++ b/cmd/block/info/output.go @@ -19,6 +19,7 @@ import ( "encoding/hex" "fmt" "math/big" + "regexp" "sort" "strconv" "strings" @@ -54,7 +55,7 @@ func output(_ context.Context, data *dataOut) (string, error) { return "", nil } -func outputBlockGeneral(_ context.Context, +func outputBlockGeneral(ctx context.Context, verbose bool, slot phase0.Slot, proposerIndex phase0.ValidatorIndex, @@ -82,14 +83,7 @@ func outputBlockGeneral(_ context.Context, res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot)) res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot)) } - if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" { - graffiti = bytes.TrimRight(graffiti, "\u0000") - if utf8.Valid(graffiti) { - res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti))) - } else { - res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti)) - } - } + res.WriteString(blockGraffiti(ctx, graffiti)) return res.String(), nil } @@ -1062,3 +1056,94 @@ func attestingIndices(input bitfield.Bitlist, indices []phase0.ValidatorIndex) s } return strings.TrimSpace(res) } + +func blockGraffiti(_ context.Context, graffiti []byte) string { + if len(graffiti) == 0 || hex.EncodeToString(graffiti) == "0000000000000000000000000000000000000000000000000000000000000000" { + // No graffiti. + return "" + } + + // Remove any trailing null characters. + graffiti = bytes.TrimRight(graffiti, "\u0000") + + if !utf8.Valid(graffiti) { + // Graffiti is not valid UTF-8, return hex. + return fmt.Sprintf("Graffiti: %#x\n", graffiti) + } + + // See if there is client identification information present in the graffiti. + // The client identification will always be the last entry in the graffiti, with a space beforehand. + parts := bytes.Split(graffiti, []byte{' '}) + + // Consensus and execution client values come from + // https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md + consensusClients := map[string]string{ + "GR": "grandine", + "LH": "lighthouse", + "LS": "lodestar", + "NB": "nimbus", + "PM": "prysm", + "TK": "teku", + } + consensusRegex := regexp.MustCompile(`(GR|LH|LS|NB|PM|TK)([0-9a-f]*)`) + consensusData := consensusRegex.Find(parts[len(parts)-1]) + + executionClients := map[string]string{ + "BU": "besu", + "EG": "erigon", + "EJ": "ethereumJS", + "GE": "go-ethereum", + "NM": "nethermind", + "RH": "reth", + } + executionRegex := regexp.MustCompile(`(BU|EG|EJ|GE|NM|RH)([0-9a-f]*)`) + executionData := executionRegex.Find(parts[len(parts)-1]) + + if len(consensusData) == 0 && len(executionData) == 0 { + // There is no identifier; return the graffiti as-is. + return fmt.Sprintf("Graffiti: %s\n", string(graffiti)) + } + + res := strings.Builder{} + + truncatedGraffiti := bytes.Join(parts[0:len(parts)-1], []byte(" ")) + if len(truncatedGraffiti) > 0 { + res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(truncatedGraffiti))) + } + + if len(consensusData) > 0 { + consensusClient := consensusData[0:2] + consensusHash := "" + if len(consensusData) > 2 { + consensusHash = string(consensusData[2:]) + } + + res.WriteString("Consensus client: ") + res.WriteString(consensusClients[string(consensusClient)]) + if consensusHash != "" { + res.WriteString(" (version hash ") + res.WriteString(consensusHash) + res.WriteString(")") + } + res.WriteString("\n") + } + + if len(executionData) > 0 { + executionClient := executionData[0:2] + executionHash := "" + if len(executionData) > 2 { + executionHash = string(executionData[2:]) + } + + res.WriteString("Execution client: ") + res.WriteString(executionClients[string(executionClient)]) + if executionHash != "" { + res.WriteString(" (version hash ") + res.WriteString(executionHash) + res.WriteString(")") + } + res.WriteString("\n") + } + + return res.String() +} diff --git a/cmd/block/info/output_internal_test.go b/cmd/block/info/output_internal_test.go index 8023f74..70f40f9 100644 --- a/cmd/block/info/output_internal_test.go +++ b/cmd/block/info/output_internal_test.go @@ -175,3 +175,58 @@ func TestOutputBlockETH1Data(t *testing.T) { }) } } + +func TestBlockGraffiti(t *testing.T) { + tests := []struct { + name string + graffiti []byte + res string + }{ + { + name: "Empty", + graffiti: []byte(""), + }, + { + name: "NoID", + graffiti: []byte("No identifier"), + res: "Graffiti: No identifier\n", + }, + { + name: "SingleClient", + graffiti: []byte("Graffiti TK"), + res: "Graffiti: Graffiti\nConsensus client: teku\n", + }, + { + name: "SingleClientImmediate", + graffiti: []byte("TK"), + res: "Consensus client: teku\n", + }, + { + name: "SingleClientAndHashImmediate", + graffiti: []byte("TKa9f98260"), + res: "Consensus client: teku (version hash a9f98260)\n", + }, + { + name: "DualClients", + graffiti: []byte("LHGE"), + res: "Consensus client: lighthouse\nExecution client: go-ethereum\n", + }, + { + name: "DualClientsReverseOrder", + graffiti: []byte("GELH"), + res: "Consensus client: lighthouse\nExecution client: go-ethereum\n", + }, + { + name: "DualClientsTruncatedHash", + graffiti: []byte("Freedom To Transact TKa9f9NM220b"), + res: "Graffiti: Freedom To Transact\nConsensus client: teku (version hash a9f9)\nExecution client: nethermind (version hash 220b)\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res := blockGraffiti(context.Background(), test.graffiti) + require.Equal(t, test.res, res) + }) + } +} diff --git a/cmd/version.go b/cmd/version.go index 0edd0b6..d2df7c4 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -24,7 +24,7 @@ import ( // ReleaseVersion is the release version of the codebase. // Usually overridden by tag names when building binaries. -var ReleaseVersion = "local build (latest release 1.35.3)" +var ReleaseVersion = "local build (latest release 1.35.4)" // versionCmd represents the version command. var versionCmd = &cobra.Command{