diff --git a/cmd/hermes/cmd_eth.go b/cmd/hermes/cmd_eth.go index 6609144..e3b11cf 100644 --- a/cmd/hermes/cmd_eth.go +++ b/cmd/hermes/cmd_eth.go @@ -33,6 +33,10 @@ var ethConfig = &struct { DialConcurrency int DialTimeout time.Duration MaxPeers int + GenesisSSZURL string + ConfigURL string + BootnodesURL string + DepositContractBlockURL string }{ PrivateKeyStr: "", // unset means it'll be generated Chain: params.MainnetName, @@ -48,6 +52,10 @@ var ethConfig = &struct { DialConcurrency: 16, DialTimeout: 5 * time.Second, MaxPeers: 30, // arbitrary + GenesisSSZURL: "", + ConfigURL: "", + BootnodesURL: "", + DepositContractBlockURL: "", } var cmdEth = &cli.Command{ @@ -167,6 +175,34 @@ var cmdEthFlags = []cli.Flag{ Value: ethConfig.MaxPeers, Destination: ðConfig.MaxPeers, }, + &cli.StringFlag{ + Name: "genesis.ssz.url", + EnvVars: []string{"HERMES_ETH_GENESIS_SSZ_URL"}, + Usage: "The .ssz URL from which to fetch the genesis data, requires 'chain=devnet'", + Value: ethConfig.GenesisSSZURL, + Destination: ðConfig.GenesisSSZURL, + }, + &cli.StringFlag{ + Name: "config.yaml.url", + EnvVars: []string{"HERMES_ETH_CONFIG_URL"}, + Usage: "The .yaml URL from which to fetch the beacon chain config, requires 'chain=devnet'", + Value: ethConfig.ConfigURL, + Destination: ðConfig.ConfigURL, + }, + &cli.StringFlag{ + Name: "bootnodes.yaml.url", + EnvVars: []string{"HERMES_ETH_BOOTNODES_URL"}, + Usage: "The .yaml URL from which to fetch the bootnode ENRs, requires 'chain=devnet'", + Value: ethConfig.BootnodesURL, + Destination: ðConfig.BootnodesURL, + }, + &cli.StringFlag{ + Name: "deposit-contract-block.txt.url", + EnvVars: []string{"HERMES_ETH_DEPOSIT_CONTRACT_BLOCK_URL"}, + Usage: "The .txt URL from which to fetch the deposit contract block. Requires 'chain=devnet'", + Value: ethConfig.DepositContractBlockURL, + Destination: ðConfig.DepositContractBlockURL, + }, } func cmdEthAction(c *cli.Context) error { @@ -176,20 +212,45 @@ func cmdEthAction(c *cli.Context) error { // Print hermes configuration for debugging purposes printEthConfig() - // Extract chain configuration parameters based on the given chain name - genConfig, netConfig, beaConfig, err := eth.GetConfigsByNetworkName(ethConfig.Chain) - if err != nil { - return fmt.Errorf("get config for %s: %w", ethConfig.Chain, err) + var config *eth.NetworkConfig + // Derive network configuration + if ethConfig.Chain != params.DevnetName { + slog.Info("Deriving known network config:", "chain", ethConfig.Chain) + + c, err := eth.DeriveKnownNetworkConfig(c.Context, ethConfig.Chain) + if err != nil { + return fmt.Errorf("derive network config: %w", err) + } + + config = c + } else { + slog.Info("Deriving devnet network config") + + c, err := eth.DeriveDevnetConfig(c.Context, eth.DevnetOptions{ + ConfigURL: ethConfig.ConfigURL, + BootnodesURL: ethConfig.BootnodesURL, + DepositContractBlockURL: ethConfig.DepositContractBlockURL, + GenesisSSZURL: ethConfig.GenesisSSZURL, + }) + if err != nil { + return fmt.Errorf("failed to derive devnet network config: %w", err) + } + config = c } - genesisRoot := genConfig.GenesisValidatorRoot - genesisTime := genConfig.GenesisTime + // Overriding configuration so that functions like ComputForkDigest take the + // correct input data from the global configuration. + params.OverrideBeaconConfig(config.Beacon) + params.OverrideBeaconNetworkConfig(config.Network) + + genesisRoot := config.Genesis.GenesisValidatorRoot + genesisTime := config.Genesis.GenesisTime // compute fork version and fork digest currentSlot := slots.Since(genesisTime) currentEpoch := slots.ToEpoch(currentSlot) - currentForkVersion, err := eth.GetCurrentForkVersion(currentEpoch, beaConfig) + currentForkVersion, err := eth.GetCurrentForkVersion(currentEpoch, config.Beacon) if err != nil { return fmt.Errorf("compute fork version for epoch %d: %w", currentEpoch, err) } @@ -201,13 +262,13 @@ func cmdEthAction(c *cli.Context) error { // Overriding configuration so that functions like ComputForkDigest take the // correct input data from the global configuration. - params.OverrideBeaconConfig(beaConfig) - params.OverrideBeaconNetworkConfig(netConfig) + params.OverrideBeaconConfig(config.Beacon) + params.OverrideBeaconNetworkConfig(config.Network) cfg := ð.NodeConfig{ - GenesisConfig: genConfig, - NetworkConfig: netConfig, - BeaconConfig: beaConfig, + GenesisConfig: config.Genesis, + NetworkConfig: config.Network, + BeaconConfig: config.Beacon, ForkDigest: forkDigest, ForkVersion: currentForkVersion, PrivateKeyStr: ethConfig.PrivateKeyStr, diff --git a/cmd/hermes/cmd_eth_chains.go b/cmd/hermes/cmd_eth_chains.go index 73e2da2..026e8e4 100644 --- a/cmd/hermes/cmd_eth_chains.go +++ b/cmd/hermes/cmd_eth_chains.go @@ -28,28 +28,27 @@ func cmdEthChainsAction(c *cli.Context) error { slog.Info("Supported chains:") for _, chain := range chains { - - genConfig, _, beaConfig, err := eth.GetConfigsByNetworkName(chain) + config, err := eth.DeriveKnownNetworkConfig(c.Context, chain) if err != nil { return fmt.Errorf("get config for %s: %w", chain, err) } slog.Info(chain) forkVersions := [][]byte{ - beaConfig.GenesisForkVersion, - beaConfig.AltairForkVersion, - beaConfig.BellatrixForkVersion, - beaConfig.CapellaForkVersion, - beaConfig.DenebForkVersion, + config.Beacon.GenesisForkVersion, + config.Beacon.AltairForkVersion, + config.Beacon.BellatrixForkVersion, + config.Beacon.CapellaForkVersion, + config.Beacon.DenebForkVersion, } for _, forkVersion := range forkVersions { - epoch, found := beaConfig.ForkVersionSchedule[[4]byte(forkVersion)] + epoch, found := config.Beacon.ForkVersionSchedule[[4]byte(forkVersion)] if !found { return fmt.Errorf("fork version schedule not found for %x", forkVersion) } - forkName, found := beaConfig.ForkVersionNames[[4]byte(forkVersion)] + forkName, found := config.Beacon.ForkVersionNames[[4]byte(forkVersion)] if !found { return fmt.Errorf("fork version name not found for %x", forkVersion) } @@ -58,7 +57,7 @@ func cmdEthChainsAction(c *cli.Context) error { continue } - digest, err := signing.ComputeForkDigest(forkVersion, genConfig.GenesisValidatorRoot) + digest, err := signing.ComputeForkDigest(forkVersion, config.Genesis.GenesisValidatorRoot) if err != nil { return err } diff --git a/eth/fetch.go b/eth/fetch.go new file mode 100644 index 0000000..70971bc --- /dev/null +++ b/eth/fetch.go @@ -0,0 +1,121 @@ +package eth + +import ( + "context" + "encoding/binary" + "io" + "net/http" + + "github.com/prysmaticlabs/prysm/v5/config/params" + "gopkg.in/yaml.v3" +) + +// FetchConfigFromURL fetches the beacon chain config from a given URL. +func FetchConfigFromURL(ctx context.Context, url string) (*params.BeaconChainConfig, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + response, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer response.Body.Close() + + data, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + config := params.MainnetConfig().Copy() + + out, err := params.UnmarshalConfig(data, config) + if err != nil { + return nil, err + } + + return out, nil +} + +// FetchBootnodeENRsFromURL fetches the bootnode ENRs from a given URL. +func FetchBootnodeENRsFromURL(ctx context.Context, url string) ([]string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + response, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer response.Body.Close() + + data, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var enrs []string + err = yaml.Unmarshal(data, &enrs) + if err != nil { + return nil, err + } + + return enrs, nil +} + +// FetchDepositContractBlockFromURL fetches the deposit contract block from a given URL. +func FetchDepositContractBlockFromURL(ctx context.Context, url string) (uint64, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return 0, err + } + + response, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + defer response.Body.Close() + + data, err := io.ReadAll(response.Body) + if err != nil { + return 0, err + } + + var block uint64 + + err = yaml.Unmarshal(data, &block) + if err != nil { + return 0, err + } + + return block, nil +} + +// FetchGenesisDetailsFromURL fetches the genesis time and validators root from a given URL. +func FetchGenesisDetailsFromURL(ctx context.Context, url string) (uint64, [32]byte, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return 0, [32]byte{}, err + } + + response, err := http.DefaultClient.Do(req) + if err != nil { + return 0, [32]byte{}, err + } + defer response.Body.Close() + + // Read only the first 40 bytes (8 bytes for GenesisTime + 32 bytes for GenesisValidatorsRoot) + data := make([]byte, 40) + _, err = io.ReadFull(response.Body, data) + if err != nil { + return 0, [32]byte{}, err + } + + genesisTime := binary.LittleEndian.Uint64(data[:8]) + var genesisValidatorsRoot [32]byte + copy(genesisValidatorsRoot[:], data[8:]) + + return genesisTime, genesisValidatorsRoot, nil +} diff --git a/eth/genesis.go b/eth/genesis.go index 13d1c85..ad11e39 100644 --- a/eth/genesis.go +++ b/eth/genesis.go @@ -44,23 +44,6 @@ type GenesisConfig struct { GenesisTime time.Time // Time at Genesis } -// GetConfigsByNetworkName returns the GenesisConfig, NetworkConfig, -// BeaconChainConfig and any error based on the input network name -func GetConfigsByNetworkName(net string) (*GenesisConfig, *params.NetworkConfig, *params.BeaconChainConfig, error) { - switch net { - case params.MainnetName: - return GenesisConfigs[net], params.BeaconNetworkConfig(), params.MainnetConfig(), nil - case params.SepoliaName: - return GenesisConfigs[net], params.BeaconNetworkConfig(), params.SepoliaConfig(), nil - case params.PraterName: - return GenesisConfigs[net], params.BeaconNetworkConfig(), params.PraterConfig(), nil - case params.HoleskyName: - return GenesisConfigs[net], params.BeaconNetworkConfig(), params.HoleskyConfig(), nil - default: - return nil, nil, nil, fmt.Errorf("network %s not found", net) - } -} - var GenesisConfigs = map[string]*GenesisConfig{ params.MainnetName: { GenesisValidatorRoot: hexToBytes("4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"), diff --git a/eth/network_config.go b/eth/network_config.go new file mode 100644 index 0000000..3844137 --- /dev/null +++ b/eth/network_config.go @@ -0,0 +1,126 @@ +package eth + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/prysmaticlabs/prysm/v5/config/params" +) + +type NetworkConfig struct { + Genesis *GenesisConfig + Network *params.NetworkConfig + Beacon *params.BeaconChainConfig +} + +func DeriveKnownNetworkConfig(ctx context.Context, network string) (*NetworkConfig, error) { + if network == params.DevnetName { + return nil, errors.New("network devnet not supported - use DeriveDevnetConfig instead") + } + + defaultBeaconNetworkConfig := params.BeaconNetworkConfig() + + switch network { + case params.MainnetName: + return &NetworkConfig{ + Genesis: GenesisConfigs[network], + Beacon: params.MainnetConfig(), + Network: defaultBeaconNetworkConfig, + }, nil + case params.SepoliaName: + return &NetworkConfig{ + Genesis: GenesisConfigs[network], + Beacon: params.SepoliaConfig(), + Network: defaultBeaconNetworkConfig, + }, nil + case params.PraterName: + return &NetworkConfig{ + Genesis: GenesisConfigs[network], + Beacon: params.PraterConfig(), + Network: defaultBeaconNetworkConfig, + }, nil + case params.HoleskyName: + return &NetworkConfig{ + Genesis: GenesisConfigs[network], + Beacon: params.HoleskyConfig(), + Network: defaultBeaconNetworkConfig, + }, nil + case params.DevnetName: + return nil, errors.New("network devnet not supported") + default: + return nil, fmt.Errorf("network %s not found", network) + } +} + +type DevnetOptions struct { + ConfigURL string + BootnodesURL string + DepositContractBlockURL string + GenesisSSZURL string +} + +func (o *DevnetOptions) Validate() error { + if o.ConfigURL == "" { + return errors.New("config URL is required") + } + + if o.BootnodesURL == "" { + return errors.New("bootnodes URL is required") + } + + if o.DepositContractBlockURL == "" { + return errors.New("deposit contract block URL is required") + } + + if o.GenesisSSZURL == "" { + return errors.New("genesis SSZ URL is required") + } + + return nil +} + +func DeriveDevnetConfig(ctx context.Context, options DevnetOptions) (*NetworkConfig, error) { + if err := options.Validate(); err != nil { + return nil, fmt.Errorf("invalid options: %w", err) + } + + // Fetch the beacon chain config from the provided URL + beaconConfig, err := FetchConfigFromURL(ctx, options.ConfigURL) + if err != nil { + return nil, fmt.Errorf("fetch beacon config: %w", err) + } + + // Fetch bootnode ENRs from the provided URL + bootnodeENRs, err := FetchBootnodeENRsFromURL(ctx, options.BootnodesURL) + if err != nil { + return nil, fmt.Errorf("fetch bootnode ENRs: %w", err) + } + + // Fetch deposit contract block from the provided URL + depositContractBlock, err := FetchDepositContractBlockFromURL(ctx, options.DepositContractBlockURL) + if err != nil { + return nil, fmt.Errorf("fetch deposit contract block: %w", err) + } + + // Fetch genesis details from the provided URL + genesisTime, genesisValidatorsRoot, err := FetchGenesisDetailsFromURL(ctx, options.GenesisSSZURL) + if err != nil { + return nil, fmt.Errorf("fetch genesis details: %w", err) + } + + network := params.BeaconNetworkConfig() + + network.BootstrapNodes = bootnodeENRs + network.ContractDeploymentBlock = depositContractBlock + + return &NetworkConfig{ + Genesis: &GenesisConfig{ + GenesisTime: time.Unix(int64(genesisTime), 0), + GenesisValidatorRoot: genesisValidatorsRoot[:], + }, + Network: network, + Beacon: beaconConfig, + }, nil +} diff --git a/eth/node.go b/eth/node.go index dfa45f0..12ebf2c 100644 --- a/eth/node.go +++ b/eth/node.go @@ -185,7 +185,7 @@ func NewNode(cfg *NodeConfig) (*Node, error) { } // initialize the custom Prysm client to communicate with its API - pryClient, err := NewPrysmClient(cfg.PrysmHost, cfg.PrysmPortHTTP, cfg.PrysmPortGRPC, cfg.DialTimeout) + pryClient, err := NewPrysmClient(cfg.PrysmHost, cfg.PrysmPortHTTP, cfg.PrysmPortGRPC, cfg.DialTimeout, cfg.GenesisConfig) if err != nil { return nil, fmt.Errorf("new prysm client") } diff --git a/eth/node_config.go b/eth/node_config.go index 1653741..d16a66f 100644 --- a/eth/node_config.go +++ b/eth/node_config.go @@ -374,18 +374,18 @@ func pubsubGossipParam() pubsub.GossipSubParams { // desiredPubSubBaseTopics returns the list of gossip_topics we want to subscribe to func desiredPubSubBaseTopics() []string { return []string{ - // p2p.GossipBlockMessage, - // p2p.GossipAggregateAndProofMessage, - // p2p.GossipAttestationMessage, + p2p.GossipBlockMessage, + p2p.GossipAggregateAndProofMessage, + p2p.GossipAttestationMessage, // In relation to https://github.com/probe-lab/hermes/issues/24 // we unfortunatelly can't validate the messages (yet) // thus, better not to forward invalid messages // p2p.GossipExitMessage, - // p2p.GossipAttesterSlashingMessage, - // p2p.GossipProposerSlashingMessage, - // p2p.GossipContributionAndProofMessage, - // p2p.GossipSyncCommitteeMessage, - // p2p.GossipBlsToExecutionChangeMessage, + p2p.GossipAttesterSlashingMessage, + p2p.GossipProposerSlashingMessage, + p2p.GossipContributionAndProofMessage, + p2p.GossipSyncCommitteeMessage, + p2p.GossipBlsToExecutionChangeMessage, p2p.GossipBlobSidecarMessage, } } diff --git a/eth/prysm.go b/eth/prysm.go index 7d0ce96..6920494 100644 --- a/eth/prysm.go +++ b/eth/prysm.go @@ -38,9 +38,10 @@ type PrysmClient struct { tracer trace.Tracer beaconClient eth.BeaconChainClient beaconApiClient *apiCli.Client + genesis *GenesisConfig } -func NewPrysmClient(host string, portHTTP int, portGRPC int, timeout time.Duration) (*PrysmClient, error) { +func NewPrysmClient(host string, portHTTP int, portGRPC int, timeout time.Duration, genesis *GenesisConfig) (*PrysmClient, error) { tracer := otel.GetTracerProvider().Tracer("prysm_client") conn, err := grpc.Dial(fmt.Sprintf("%s:%d", host, portGRPC), @@ -63,6 +64,7 @@ func NewPrysmClient(host string, portHTTP int, portGRPC int, timeout time.Durati beaconApiClient: apiCli, timeout: timeout, tracer: tracer, + genesis: genesis, }, nil } @@ -320,26 +322,17 @@ func (p *PrysmClient) isOnNetwork(ctx context.Context, hermesForkDigest [4]byte) } span.End() }() + // this checks whether the local fork_digest at hermes matches the one that the remote node keeps // request the genesis - nodeCnf, err := p.beaconApiClient.GetConfigSpec(ctx) - if err != nil { - return false, fmt.Errorf("request prysm node config to compose forkdigest: %w", err) - } - cnf := nodeCnf.Data.(map[string]interface{}) - genesisConf, _, _, err := GetConfigsByNetworkName(cnf["CONFIG_NAME"].(string)) - if err != nil { - return false, fmt.Errorf("not identified network from trusted node (%s): %w", cnf["CONFIG_NAME"].(string), err) - } - nodeFork, err := p.beaconApiClient.GetFork(ctx, apiCli.StateOrBlockId("head")) if err != nil { return false, fmt.Errorf("request beacon fork to compose forkdigest: %w", err) } - forkDigest, err := signing.ComputeForkDigest(nodeFork.CurrentVersion, genesisConf.GenesisValidatorRoot) + forkDigest, err := signing.ComputeForkDigest(nodeFork.CurrentVersion, p.genesis.GenesisValidatorRoot) if err != nil { - return false, fmt.Errorf("create fork digest (%s, %x): %w", hex.EncodeToString(nodeFork.CurrentVersion), genesisConf.GenesisValidatorRoot, err) + return false, fmt.Errorf("create fork digest (%s, %x): %w", hex.EncodeToString(nodeFork.CurrentVersion), p.genesis.GenesisValidatorRoot, err) } // check if our version is within the versions of the node if forkDigest == hermesForkDigest { diff --git a/eth/prysm_test.go b/eth/prysm_test.go index 6f14862..7d0753c 100644 --- a/eth/prysm_test.go +++ b/eth/prysm_test.go @@ -86,7 +86,7 @@ func TestPrysmClient_AddTrustedPeer(t *testing.T) { port, err := strconv.Atoi(serverURL.Port()) require.NoError(t, err) - p, err := NewPrysmClient(serverURL.Hostname(), port, 0, time.Second) + p, err := NewPrysmClient(serverURL.Hostname(), port, 0, time.Second, nil) require.NoError(t, err) err = p.AddTrustedPeer(context.Background(), pid, maddr) @@ -150,7 +150,7 @@ func TestPrysmClient_RemoveTrustedPeer(t *testing.T) { port, err := strconv.Atoi(serverURL.Port()) require.NoError(t, err) - p, err := NewPrysmClient(serverURL.Hostname(), port, 0, time.Second) + p, err := NewPrysmClient(serverURL.Hostname(), port, 0, time.Second, nil) require.NoError(t, err) err = p.RemoveTrustedPeer(context.Background(), pid) diff --git a/go.mod b/go.mod index 3ed7cf8..51887e8 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( golang.org/x/time v0.5.0 google.golang.org/grpc v1.62.1 google.golang.org/protobuf v1.33.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -202,7 +203,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.20.0 // indirect k8s.io/client-go v0.20.0 // indirect k8s.io/klog/v2 v2.80.0 // indirect