diff --git a/clients/execution/client.go b/clients/execution/client.go index 23eb6c71..b77376b0 100644 --- a/clients/execution/client.go +++ b/clients/execution/client.go @@ -44,6 +44,8 @@ type Client struct { nodeInfo *p2p.NodeInfo peers []*p2p.PeerInfo didFetchPeers bool + ethConfig *EthConfig + ethConfigMutex sync.RWMutex } func (pool *Pool) newPoolClient(clientIdx uint16, endpoint *ClientConfig) (*Client, error) { @@ -92,6 +94,16 @@ func (client *Client) GetNodeInfo() *p2p.NodeInfo { return client.nodeInfo } +func (client *Client) GetEthConfig(ctx context.Context) (map[string]interface{}, error) { + return client.rpcClient.GetEthConfig(ctx) +} + +func (client *Client) GetCachedEthConfig() *EthConfig { + client.ethConfigMutex.RLock() + defer client.ethConfigMutex.RUnlock() + return client.ethConfig +} + func (client *Client) GetEndpointConfig() *ClientConfig { return client.endpointConfig } diff --git a/clients/execution/clientlogic.go b/clients/execution/clientlogic.go index e0cef017..9aeacee2 100644 --- a/clients/execution/clientlogic.go +++ b/clients/execution/clientlogic.go @@ -122,6 +122,23 @@ func (client *Client) updateNodeMetadata(ctx context.Context) error { client.peers = peers client.didFetchPeers = true + // get eth_config + rawEthConfig, err := client.rpcClient.GetEthConfig(ctx) + if err != nil { + client.logger.Debugf("could not get eth_config: %v", err) + // Don't return error since eth_config is optional + } else { + parsedConfig, err := ParseEthConfig(rawEthConfig) + if err != nil { + client.logger.Warnf("could not parse eth_config: %v", err) + } else { + client.ethConfigMutex.Lock() + client.ethConfig = parsedConfig + client.ethConfigMutex.Unlock() + client.logger.Debugf("updated eth_config data") + } + } + return nil } diff --git a/clients/execution/ethconfig.go b/clients/execution/ethconfig.go new file mode 100644 index 00000000..96c5a7cc --- /dev/null +++ b/clients/execution/ethconfig.go @@ -0,0 +1,138 @@ +package execution + +import ( + "fmt" + "strconv" + "time" +) + +type EthConfigFork struct { + ActivationTime uint64 `json:"activationTime"` + ChainID string `json:"chainId"` + ForkID string `json:"forkId"` + BlobSchedule map[string]string `json:"blobSchedule"` + Precompiles map[string]string `json:"precompiles"` + SystemContracts map[string]string `json:"systemContracts"` +} + +type EthConfig struct { + Current *EthConfigFork `json:"current"` + Next *EthConfigFork `json:"next"` + Last *EthConfigFork `json:"last"` +} + +// ParseEthConfig parses the raw eth_config response into a structured EthConfig +func ParseEthConfig(rawConfig map[string]interface{}) (*EthConfig, error) { + if rawConfig == nil { + return nil, nil + } + + config := &EthConfig{} + + if current, ok := rawConfig["current"].(map[string]interface{}); ok { + parsed, err := parseEthConfigFork(current) + if err != nil { + return nil, fmt.Errorf("failed to parse current config: %w", err) + } + config.Current = parsed + } + + if next, ok := rawConfig["next"].(map[string]interface{}); ok { + parsed, err := parseEthConfigFork(next) + if err != nil { + return nil, fmt.Errorf("failed to parse next config: %w", err) + } + config.Next = parsed + } + + if last, ok := rawConfig["last"].(map[string]interface{}); ok { + parsed, err := parseEthConfigFork(last) + if err != nil { + return nil, fmt.Errorf("failed to parse last config: %w", err) + } + config.Last = parsed + } + + return config, nil +} + +func parseEthConfigFork(raw map[string]interface{}) (*EthConfigFork, error) { + fork := &EthConfigFork{} + + // Parse activation time + if activationTime, ok := raw["activationTime"]; ok { + switch v := activationTime.(type) { + case float64: + fork.ActivationTime = uint64(v) + case string: + parsed, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse activationTime: %w", err) + } + fork.ActivationTime = parsed + } + } + + // Parse chain ID + if chainID, ok := raw["chainId"].(string); ok { + fork.ChainID = chainID + } + + // Parse fork ID + if forkID, ok := raw["forkId"].(string); ok { + fork.ForkID = forkID + } + + // Parse blob schedule + if blobSchedule, ok := raw["blobSchedule"].(map[string]interface{}); ok { + fork.BlobSchedule = parseStringMap(blobSchedule) + } + + // Parse precompiles + if precompiles, ok := raw["precompiles"].(map[string]interface{}); ok { + fork.Precompiles = parseStringMap(precompiles) + } + + // Parse system contracts + if systemContracts, ok := raw["systemContracts"].(map[string]interface{}); ok { + fork.SystemContracts = parseStringMap(systemContracts) + } + + return fork, nil +} + +func parseStringMap(raw map[string]interface{}) map[string]string { + result := make(map[string]string) + for key, value := range raw { + if str, ok := value.(string); ok { + result[key] = str + } + } + return result +} + +// GetActivationTime returns the activation time as a time.Time +func (f *EthConfigFork) GetActivationTime() time.Time { + return time.Unix(int64(f.ActivationTime), 0) +} + +// GetSystemContractAddress returns the address of a specific system contract +func (f *EthConfigFork) GetSystemContractAddress(contractType string) string { + if f.SystemContracts == nil { + return "" + } + + // Map common contract type names to their actual keys + keyMap := map[string]string{ + "deposit": "depositContract", + "withdrawal": "withdrawalContract", + "consolidation": "consolidationContract", + } + + if key, exists := keyMap[contractType]; exists { + return f.SystemContracts[key] + } + + // Also allow direct key lookup for flexibility + return f.SystemContracts[contractType] +} diff --git a/clients/execution/rpc/executionapi.go b/clients/execution/rpc/executionapi.go index 9e30843c..4681933a 100644 --- a/clients/execution/rpc/executionapi.go +++ b/clients/execution/rpc/executionapi.go @@ -144,6 +144,12 @@ func (ec *ExecutionClient) GetAdminNodeInfo(ctx context.Context) (*p2p.NodeInfo, return result, err } +func (ec *ExecutionClient) GetEthConfig(ctx context.Context) (map[string]interface{}, error) { + var result map[string]interface{} + err := ec.rpcClient.CallContext(ctx, &result, "eth_config") + return result, err +} + func (ec *ExecutionClient) GetNodeSyncing(ctx context.Context) (*SyncStatus, error) { status, err := ec.ethClient.SyncProgress(ctx) if err != nil { diff --git a/handlers/clients_el.go b/handlers/clients_el.go index d2e069eb..deb52e29 100644 --- a/handlers/clients_el.go +++ b/handlers/clients_el.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethpandaops/dora/clients/execution" "github.com/ethpandaops/dora/services" "github.com/ethpandaops/dora/templates" "github.com/ethpandaops/dora/types/models" @@ -283,6 +284,8 @@ func buildELClientsPageData(sortOrder string) (*models.ClientsELPageData, time.D PeerID: peerID, } + forkConfig := buildForkConfig(client) + resNode := &models.ClientsELPageDataNode{ Name: client.GetName(), Version: client.GetVersion(), @@ -291,6 +294,7 @@ func buildELClientsPageData(sortOrder string) (*models.ClientsELPageData, time.D PeerID: peerID, PeerName: peerName, DidFetchPeers: client.DidFetchPeers(), + ForkConfig: forkConfig, } if pageData.ShowSensitivePeerInfos { @@ -397,3 +401,52 @@ func buildELClientsPageData(sortOrder string) (*models.ClientsELPageData, time.D return pageData, cacheTime } + +func buildForkConfig(client *execution.Client) *models.ClientELPageDataForkConfig { + ethConfig := client.GetCachedEthConfig() + if ethConfig == nil { + return nil + } + + forkConfig := &models.ClientELPageDataForkConfig{} + + if ethConfig.Current != nil { + forkConfig.Current = convertEthConfigFork(ethConfig.Current) + } + + if ethConfig.Next != nil { + forkConfig.Next = convertEthConfigFork(ethConfig.Next) + } + + if ethConfig.Last != nil { + forkConfig.Last = convertEthConfigFork(ethConfig.Last) + } + + return forkConfig +} + +func convertEthConfigFork(fork *execution.EthConfigFork) *models.EthConfigObject { + obj := &models.EthConfigObject{} + + obj.ActivationTime = fork.ActivationTime + obj.ChainId = fork.ChainID + obj.ForkId = fork.ForkID + + // Convert string maps to interface{} maps for model compatibility + obj.BlobSchedule = convertStringMapToInterface(fork.BlobSchedule) + obj.Precompiles = convertStringMapToInterface(fork.Precompiles) + obj.SystemContracts = convertStringMapToInterface(fork.SystemContracts) + + return obj +} + +func convertStringMapToInterface(stringMap map[string]string) map[string]interface{} { + if stringMap == nil { + return nil + } + interfaceMap := make(map[string]interface{}) + for key, value := range stringMap { + interfaceMap[key] = value + } + return interfaceMap +} diff --git a/handlers/submit_consolidation.go b/handlers/submit_consolidation.go index 3a6545c7..a8759610 100644 --- a/handlers/submit_consolidation.go +++ b/handlers/submit_consolidation.go @@ -92,12 +92,18 @@ func buildSubmitConsolidationPageData() (*models.SubmitConsolidationPageData, ti chainState := services.GlobalBeaconService.GetChainState() specs := chainState.GetSpecs() + // Get consolidation contract address from client config, fallback to default + consolidationContract := services.GlobalBeaconService.GetSystemContractAddress("consolidation") + if consolidationContract == "" { + consolidationContract = execution.DefaultConsolidationContractAddr + } + pageData := &models.SubmitConsolidationPageData{ NetworkName: specs.ConfigName, PublicRPCUrl: utils.Config.Frontend.PublicRPCUrl, RainbowkitProjectId: utils.Config.Frontend.RainbowkitProjectId, ChainId: specs.DepositChainId, - ConsolidationContract: execution.ConsolidationContractAddr, + ConsolidationContract: consolidationContract, ExplorerUrl: utils.Config.Frontend.EthExplorerLink, } diff --git a/handlers/submit_withdrawal.go b/handlers/submit_withdrawal.go index 4b3c7396..9c094c7d 100644 --- a/handlers/submit_withdrawal.go +++ b/handlers/submit_withdrawal.go @@ -89,12 +89,18 @@ func buildSubmitWithdrawalPageData() (*models.SubmitWithdrawalPageData, time.Dur chainState := services.GlobalBeaconService.GetChainState() specs := chainState.GetSpecs() + // Get withdrawal contract address from client config, fallback to default + withdrawalContract := services.GlobalBeaconService.GetSystemContractAddress("withdrawal") + if withdrawalContract == "" { + withdrawalContract = execution.DefaultWithdrawalContractAddr + } + pageData := &models.SubmitWithdrawalPageData{ NetworkName: specs.ConfigName, PublicRPCUrl: utils.Config.Frontend.PublicRPCUrl, RainbowkitProjectId: utils.Config.Frontend.RainbowkitProjectId, ChainId: specs.DepositChainId, - WithdrawalContract: execution.WithdrawalContractAddr, + WithdrawalContract: withdrawalContract, ExplorerUrl: utils.Config.Frontend.EthExplorerLink, MinValidatorBalance: specs.MinActivationBalance, } diff --git a/indexer/execution/consolidation_indexer.go b/indexer/execution/consolidation_indexer.go index 1110a297..7782e915 100644 --- a/indexer/execution/consolidation_indexer.go +++ b/indexer/execution/consolidation_indexer.go @@ -16,7 +16,7 @@ import ( "github.com/ethpandaops/dora/utils" ) -const ConsolidationContractAddr = "0x0000BBdDc7CE488642fb579F8B00f3a590007251" +const DefaultConsolidationContractAddr = "0x0000BBdDc7CE488642fb579F8B00f3a590007251" // ConsolidationIndexer is the indexer for the eip-7251 consolidation system contract type ConsolidationIndexer struct { @@ -53,7 +53,7 @@ func NewConsolidationIndexer(indexer *IndexerCtx) *ConsolidationIndexer { &contractIndexerOptions[dbtypes.ConsolidationRequestTx]{ stateKey: "indexer.consolidationindexer", batchSize: batchSize, - contractAddress: common.HexToAddress(ConsolidationContractAddr), + contractAddress: common.HexToAddress(ci.getConsolidationContractAddr()), deployBlock: uint64(utils.Config.ExecutionApi.ElectraDeployBlock), dequeueRate: specs.MaxConsolidationRequestsPerPayload, @@ -82,6 +82,15 @@ func NewConsolidationIndexer(indexer *IndexerCtx) *ConsolidationIndexer { return ci } +// getConsolidationContractAddr returns the consolidation contract address from config or falls back to default +func (ci *ConsolidationIndexer) getConsolidationContractAddr() string { + if addr := ci.indexerCtx.GetSystemContractAddress("consolidation"); addr != "" { + return addr + } + ci.logger.Warnf("using default consolidation contract address, could not get from client config") + return DefaultConsolidationContractAddr +} + // GetMatcherHeight returns the last processed el block number from the transaction matcher func (ci *ConsolidationIndexer) GetMatcherHeight() uint64 { return ci.matcher.GetMatcherHeight() diff --git a/indexer/execution/indexerctx.go b/indexer/execution/indexerctx.go index c4fa7a26..dad5c8d4 100644 --- a/indexer/execution/indexerctx.go +++ b/indexer/execution/indexerctx.go @@ -145,3 +145,31 @@ func (ictx *IndexerCtx) getForksWithClients(clientType execution.ClientType) []* return forksWithClients } + +// GetSystemContractAddress returns the address of a system contract from the first available client's config +func (ictx *IndexerCtx) GetSystemContractAddress(contractType string) string { + // Try canonical clients first + for _, forkWithClients := range ictx.getForksWithClients(execution.AnyClient) { + if forkWithClients.canonical && len(forkWithClients.clients) > 0 { + client := forkWithClients.clients[0] + config := client.GetCachedEthConfig() + if config != nil && config.Current != nil { + if addr := config.Current.GetSystemContractAddress(contractType); addr != "" { + return addr + } + } + } + } + + // Fallback to any available client if no canonical fork found + for client := range ictx.executionClients { + config := client.GetCachedEthConfig() + if config != nil && config.Current != nil { + if addr := config.Current.GetSystemContractAddress(contractType); addr != "" { + return addr + } + } + } + + return "" +} diff --git a/indexer/execution/withdrawal_indexer.go b/indexer/execution/withdrawal_indexer.go index 60a36804..918ba1ee 100644 --- a/indexer/execution/withdrawal_indexer.go +++ b/indexer/execution/withdrawal_indexer.go @@ -17,7 +17,7 @@ import ( "github.com/ethpandaops/dora/utils" ) -const WithdrawalContractAddr = "0x00000961Ef480Eb55e80D19ad83579A64c007002" +const DefaultWithdrawalContractAddr = "0x00000961Ef480Eb55e80D19ad83579A64c007002" // WithdrawalIndexer is the indexer for the eip-7002 consolidation system contract type WithdrawalIndexer struct { @@ -54,7 +54,7 @@ func NewWithdrawalIndexer(indexer *IndexerCtx) *WithdrawalIndexer { &contractIndexerOptions[dbtypes.WithdrawalRequestTx]{ stateKey: "indexer.withdrawalindexer", batchSize: batchSize, - contractAddress: common.HexToAddress(WithdrawalContractAddr), + contractAddress: common.HexToAddress(wi.getWithdrawalContractAddr()), deployBlock: uint64(utils.Config.ExecutionApi.ElectraDeployBlock), dequeueRate: specs.MaxWithdrawalRequestsPerPayload, @@ -83,6 +83,15 @@ func NewWithdrawalIndexer(indexer *IndexerCtx) *WithdrawalIndexer { return wi } +// getWithdrawalContractAddr returns the withdrawal contract address from config or falls back to default +func (wi *WithdrawalIndexer) getWithdrawalContractAddr() string { + if addr := wi.indexerCtx.GetSystemContractAddress("withdrawal"); addr != "" { + return addr + } + wi.logger.Warnf("using default withdrawal contract address, could not get from client config") + return DefaultWithdrawalContractAddr +} + // GetMatcherHeight returns the last processed el block number from the transaction matcher func (wi *WithdrawalIndexer) GetMatcherHeight() uint64 { return wi.matcher.GetMatcherHeight() diff --git a/services/chainservice.go b/services/chainservice.go index 8ed6d2af..c2c6148c 100644 --- a/services/chainservice.go +++ b/services/chainservice.go @@ -351,6 +351,24 @@ func (bs *ChainService) GetExecutionClients() []*execution.Client { return bs.executionPool.GetAllEndpoints() } +func (bs *ChainService) GetSystemContractAddress(contractType string) string { + if bs == nil || bs.executionPool == nil { + return "" + } + + // Get any ready client and check its config + for _, client := range bs.executionPool.GetReadyEndpoints(execution.AnyClient) { + config := client.GetCachedEthConfig() + if config != nil && config.Current != nil { + if addr := config.Current.GetSystemContractAddress(contractType); addr != "" { + return addr + } + } + } + + return "" +} + func (bs *ChainService) GetChainState() *consensus.ChainState { if bs == nil || bs.consensusPool == nil { return nil diff --git a/templates/clients/clients_el.html b/templates/clients/clients_el.html index 65861e5d..561fd2ce 100644 --- a/templates/clients/clients_el.html +++ b/templates/clients/clients_el.html @@ -169,14 +169,14 @@

- + - + {{ html "" }} - + - + - +
NameName
Peer IDPeer ID @@ -184,7 +184,7 @@

EnodeEnode
@@ -193,7 +193,7 @@

IP AddressIP Address
@@ -202,7 +202,7 @@

Listen AddressListen Address
@@ -214,6 +214,76 @@

+ +
+ Fork Configuration + + {{ html "" }} + Chain ID: + + {{ html "" }} + +
+ {{ html "" }} +
+ + + + + + + + + + {{ html "" }} + + + + + + {{ html "" }} + {{ html "" }} + + + + + + {{ html "" }} + {{ html "" }} + + + + + + {{ html "" }} + +
ForkActivation Time (UTC)Fork ID
Current + + + + + +
Next + + + + + +
Last + + + + + +
+
+ {{ html "" }} + {{ html "" }} +
+

Fork configuration data not available for this client.

+
+ {{ html "" }} +
Peers