Skip to content

Commit f8a3be2

Browse files
committed
update eth config checker
1 parent 6b02315 commit f8a3be2

6 files changed

Lines changed: 310 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ test-*.yaml
77

88
.hack/devnet/generated-**
99
.hack/devnet/custom-**
10+
CLAUDE.md
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package rpc
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
)
7+
8+
// EthConfigResponse represents the response from eth_config RPC call (EIP-7910)
9+
type EthConfigResponse struct {
10+
Current *ForkConfig `json:"current"`
11+
Next *ForkConfig `json:"next"`
12+
Last *ForkConfig `json:"last"`
13+
}
14+
15+
// ForkConfig represents a fork configuration
16+
type ForkConfig struct {
17+
ActivationTime int64 `json:"activationTime"`
18+
BlobSchedule map[string]interface{} `json:"blobSchedule,omitempty"`
19+
ChainID string `json:"chainId"`
20+
ForkID string `json:"forkId"`
21+
Precompiles map[string]interface{} `json:"precompiles,omitempty"`
22+
SystemContracts map[string]interface{} `json:"systemContracts,omitempty"`
23+
}
24+
25+
// GetEthConfig queries the eth_config RPC method (EIP-7910)
26+
func (ec *ExecutionClient) GetEthConfig(ctx context.Context) (*EthConfigResponse, error) {
27+
closeFn := ec.enforceConcurrencyLimit(ctx)
28+
if closeFn == nil {
29+
return nil, nil
30+
}
31+
32+
defer closeFn()
33+
34+
reqCtx, reqCtxCancel := context.WithTimeout(ctx, ec.requestTimeout)
35+
defer reqCtxCancel()
36+
37+
var result json.RawMessage
38+
err := ec.rpcClient.CallContext(reqCtx, &result, "eth_config")
39+
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
var config EthConfigResponse
45+
if err := json.Unmarshal(result, &config); err != nil {
46+
return nil, err
47+
}
48+
49+
return &config, nil
50+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package checkethconfig
2+
3+
type Config struct {
4+
ClientPattern string `yaml:"clientPattern" json:"clientPattern"`
5+
ExcludeClientPattern string `yaml:"excludeClientPattern" json:"excludeClientPattern"`
6+
FailOnMismatch bool `yaml:"failOnMismatch" json:"failOnMismatch"`
7+
}
8+
9+
func DefaultConfig() Config {
10+
return Config{
11+
FailOnMismatch: true,
12+
}
13+
}
14+
15+
func (c *Config) Validate() error {
16+
return nil
17+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package checkethconfig
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
"time"
9+
10+
"github.com/ethpandaops/assertoor/pkg/coordinator/clients/execution"
11+
"github.com/ethpandaops/assertoor/pkg/coordinator/types"
12+
"github.com/sirupsen/logrus"
13+
)
14+
15+
var (
16+
TaskName = "check_eth_config"
17+
TaskDescriptor = &types.TaskDescriptor{
18+
Name: TaskName,
19+
Description: "Checks that all execution clients return the same eth_config (EIP-7910)",
20+
Config: DefaultConfig(),
21+
NewTask: NewTask,
22+
}
23+
)
24+
25+
type Task struct {
26+
ctx *types.TaskContext
27+
options *types.TaskOptions
28+
config Config
29+
logger logrus.FieldLogger
30+
}
31+
32+
func NewTask(ctx *types.TaskContext, options *types.TaskOptions) (types.Task, error) {
33+
return &Task{
34+
ctx: ctx,
35+
options: options,
36+
logger: ctx.Logger.GetLogger(),
37+
}, nil
38+
}
39+
40+
func (t *Task) Config() interface{} {
41+
return t.config
42+
}
43+
44+
func (t *Task) Timeout() time.Duration {
45+
return t.options.Timeout.Duration
46+
}
47+
48+
func (t *Task) LoadConfig() error {
49+
config := DefaultConfig()
50+
51+
// parse static config
52+
if t.options.Config != nil {
53+
if err := t.options.Config.Unmarshal(&config); err != nil {
54+
return fmt.Errorf("error parsing task config for %v: %w", TaskName, err)
55+
}
56+
}
57+
58+
// load dynamic vars
59+
err := t.ctx.Vars.ConsumeVars(&config, t.options.ConfigVars)
60+
if err != nil {
61+
return err
62+
}
63+
64+
// validate config
65+
if err := config.Validate(); err != nil {
66+
return err
67+
}
68+
69+
t.config = config
70+
71+
return nil
72+
}
73+
74+
func (t *Task) Execute(ctx context.Context) error {
75+
// Get the client pool from the scheduler
76+
clientPool := t.ctx.Scheduler.GetServices().ClientPool()
77+
78+
// Get matching clients from the pool
79+
var clients []*execution.Client
80+
81+
if t.config.ClientPattern == "" && t.config.ExcludeClientPattern == "" {
82+
clients = clientPool.GetExecutionPool().GetReadyEndpoints(true)
83+
if len(clients) == 0 {
84+
t.logger.Error("check failed: no matching clients found")
85+
t.ctx.SetResult(types.TaskResultFailure)
86+
87+
return nil
88+
}
89+
} else {
90+
poolClients := clientPool.GetClientsByNamePatterns(t.config.ClientPattern, t.config.ExcludeClientPattern)
91+
if len(poolClients) == 0 {
92+
t.logger.Errorf("check failed: no matching clients found with pattern %v", t.config.ClientPattern)
93+
t.ctx.SetResult(types.TaskResultFailure)
94+
95+
return nil
96+
}
97+
98+
clients = make([]*execution.Client, len(poolClients))
99+
for i, c := range poolClients {
100+
clients[i] = c.ExecutionClient
101+
}
102+
}
103+
104+
// Query eth_config from all clients
105+
type clientResult struct {
106+
client *execution.Client
107+
configJSON string
108+
err error
109+
}
110+
111+
results := make([]clientResult, len(clients))
112+
113+
for i, client := range clients {
114+
t.logger.Infof("querying eth_config from client %v", client.GetName())
115+
116+
ethConfig, err := client.GetRPCClient().GetEthConfig(ctx)
117+
if err != nil {
118+
results[i] = clientResult{
119+
client: client,
120+
err: err,
121+
}
122+
123+
t.logger.WithField("client", client.GetName()).Errorf("RPC error when querying eth_config: %v", err)
124+
125+
if t.config.FailOnMismatch {
126+
t.ctx.SetResult(types.TaskResultFailure)
127+
return nil
128+
}
129+
130+
continue
131+
}
132+
133+
// Convert to JSON for comparison
134+
configBytes, err := json.MarshalIndent(ethConfig, "", " ")
135+
if err != nil {
136+
results[i] = clientResult{
137+
client: client,
138+
err: fmt.Errorf("failed to marshal config: %w", err),
139+
}
140+
141+
t.logger.WithField("client", client.GetName()).Errorf("error marshaling eth_config: %v", err)
142+
143+
if t.config.FailOnMismatch {
144+
t.ctx.SetResult(types.TaskResultFailure)
145+
return nil
146+
}
147+
148+
continue
149+
}
150+
151+
results[i] = clientResult{
152+
client: client,
153+
configJSON: string(configBytes),
154+
}
155+
156+
t.logger.WithField("client", client.GetName()).Debugf("eth_config response:\n%s", string(configBytes))
157+
t.logger.Infof("client %v returned eth_config successfully", client.GetName())
158+
}
159+
160+
// Check for consistency
161+
var referenceConfig string
162+
163+
mismatchFound := false
164+
configMap := make(map[string][]string) // config JSON -> list of client names
165+
166+
for _, result := range results {
167+
if result.err != nil {
168+
continue
169+
}
170+
171+
if referenceConfig == "" {
172+
referenceConfig = result.configJSON
173+
t.ctx.Outputs.SetVar("ethConfig", result.configJSON)
174+
}
175+
176+
// Track which clients returned which config
177+
configMap[result.configJSON] = append(configMap[result.configJSON], result.client.GetName())
178+
179+
if result.configJSON != referenceConfig {
180+
mismatchFound = true
181+
}
182+
}
183+
184+
if mismatchFound {
185+
// Build diff output
186+
var diffBuilder strings.Builder
187+
188+
diffBuilder.WriteString("eth_config mismatch detected across clients:\n\n")
189+
190+
configIndex := 1
191+
for config, clientNames := range configMap {
192+
diffBuilder.WriteString(fmt.Sprintf("Config variant #%d (clients: %v):\n", configIndex, clientNames))
193+
diffBuilder.WriteString(config)
194+
diffBuilder.WriteString("\n\n")
195+
196+
configIndex++
197+
}
198+
199+
t.logger.Error(diffBuilder.String())
200+
201+
if t.config.FailOnMismatch {
202+
t.ctx.SetResult(types.TaskResultFailure)
203+
} else {
204+
t.ctx.SetResult(types.TaskResultNone)
205+
}
206+
207+
return nil
208+
}
209+
210+
// All checks passed
211+
if len(results) > 0 {
212+
t.logger.Infof("all %d clients returned consistent eth_config", len(results))
213+
t.logger.Debugf("consistent eth_config:\n%s", referenceConfig)
214+
t.ctx.SetResult(types.TaskResultSuccess)
215+
}
216+
217+
return nil
218+
}

pkg/coordinator/tasks/tasks.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
checkconsensussyncstatus "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/check_consensus_sync_status"
1616
checkconsensusvalidatorstatus "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/check_consensus_validator_status"
1717
checkethcall "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/check_eth_call"
18+
checkethconfig "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/check_eth_config"
1819
checkexecutionsyncstatus "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/check_execution_sync_status"
1920
generateblobtransactions "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_blob_transactions"
2021
generateblschanges "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_bls_changes"
@@ -57,6 +58,7 @@ var AvailableTaskDescriptors = []*types.TaskDescriptor{
5758
checkconsensusvalidatorstatus.TaskDescriptor,
5859
checkexecutionblock.TaskDescriptor,
5960
checkethcall.TaskDescriptor,
61+
checkethconfig.TaskDescriptor,
6062
checkexecutionsyncstatus.TaskDescriptor,
6163
generateblobtransactions.TaskDescriptor,
6264
generateblschanges.TaskDescriptor,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
id: ethconfig-test
3+
name: "eth_config RPC consistency test (EIP-7910)"
4+
timeout: 1h
5+
config:
6+
#walletPrivkey: ""
7+
tasks:
8+
- name: check_clients_are_healthy
9+
title: "Check if at least one client is ready"
10+
timeout: 5m
11+
config:
12+
minClientCount: 1
13+
- name: check_consensus_slot_range
14+
title: "Wait for slot >= 10"
15+
timeout: 10m
16+
config:
17+
minSlotNumber: 10
18+
- name: check_eth_config
19+
title: "Check eth_config consistency across all execution clients"
20+
timeout: 5m
21+
config:
22+
failOnMismatch: true

0 commit comments

Comments
 (0)