From 2d5e1acebda80886bfb356715eefcae4700e51de Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:05:44 -0300 Subject: [PATCH] Supporting the erigon client --- .../service/config/settings-execution.go | 5 + rocketpool-cli/service/service.go | 6 + shared/services/config/erigon-params.go | 132 ++++++++++++++++++ .../config/execution-common-config.go | 4 +- shared/services/config/rocket-pool-config.go | 22 +++ .../assets/install/scripts/start-ec.sh | 101 ++++++++++++++ .../assets/install/templates/eth1.tmpl | 5 +- shared/types/config/types.go | 1 + 8 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 shared/services/config/erigon-params.go diff --git a/rocketpool-cli/service/config/settings-execution.go b/rocketpool-cli/service/config/settings-execution.go index 786b709b7..ee35518f6 100644 --- a/rocketpool-cli/service/config/settings-execution.go +++ b/rocketpool-cli/service/config/settings-execution.go @@ -18,6 +18,7 @@ type ExecutionConfigPage struct { nethermindItems []*parameterizedFormItem besuItems []*parameterizedFormItem rethItems []*parameterizedFormItem + erigonItems []*parameterizedFormItem externalEcItems []*parameterizedFormItem } @@ -63,6 +64,7 @@ func (configPage *ExecutionConfigPage) createContent() { configPage.nethermindItems = createParameterizedFormItems(configPage.masterConfig.Nethermind.GetParameters(), configPage.layout) configPage.besuItems = createParameterizedFormItems(configPage.masterConfig.Besu.GetParameters(), configPage.layout) configPage.rethItems = createParameterizedFormItems(configPage.masterConfig.Reth.GetParameters(), configPage.layout) + configPage.erigonItems = createParameterizedFormItems(configPage.masterConfig.Erigon.GetParameters(), configPage.layout) configPage.externalEcItems = createParameterizedFormItems(configPage.masterConfig.ExternalExecution.GetParameters(), configPage.layout) // Map the parameters to the form items in the layout @@ -72,6 +74,7 @@ func (configPage *ExecutionConfigPage) createContent() { configPage.layout.mapParameterizedFormItems(configPage.nethermindItems...) configPage.layout.mapParameterizedFormItems(configPage.besuItems...) configPage.layout.mapParameterizedFormItems(configPage.rethItems...) + configPage.layout.mapParameterizedFormItems(configPage.erigonItems...) configPage.layout.mapParameterizedFormItems(configPage.externalEcItems...) // Set up the setting callbacks @@ -132,6 +135,8 @@ func (configPage *ExecutionConfigPage) handleLocalEcChanged() { configPage.layout.addFormItemsWithCommonParams(configPage.ecCommonItems, configPage.besuItems, configPage.masterConfig.Besu.UnsupportedCommonParams) case cfgtypes.ExecutionClient_Reth: configPage.layout.addFormItemsWithCommonParams(configPage.ecCommonItems, configPage.rethItems, configPage.masterConfig.Reth.UnsupportedCommonParams) + case cfgtypes.ExecutionClient_Erigon: + configPage.layout.addFormItemsWithCommonParams(configPage.ecCommonItems, configPage.erigonItems, configPage.masterConfig.Erigon.UnsupportedCommonParams) } configPage.layout.refresh() diff --git a/rocketpool-cli/service/service.go b/rocketpool-cli/service/service.go index 465a9cd47..b9dfcf4ba 100644 --- a/rocketpool-cli/service/service.go +++ b/rocketpool-cli/service/service.go @@ -966,6 +966,10 @@ func pruneExecutionClient(yes bool) error { } else if selectedEc == cfgtypes.ExecutionClient_Reth && pruningMode == cfgtypes.PruningMode_RollingHistoryExpiry { fmt.Println("This will stop Reth and prune bodies and receipts outside the one-year rolling window.") fmt.Println("This is a resource intensive operation and may lead to an increase in missed attestations until it finishes.") + } else if selectedEc == cfgtypes.ExecutionClient_Erigon { + fmt.Println("Erigon pruning is applied continuously while the client is running. There is no offline prune step.") + fmt.Println("If you just switched pruning modes, you must resync with `rocketpool service resync-eth1` before already-stored history is removed.") + return nil } else { fmt.Println("This will request your main execution client to prune its database, freeing up disk space. This is a resource intensive operation and may lead to an increase in missed attestations until it finishes.") } @@ -1312,6 +1316,8 @@ func serviceVersion() error { eth1ClientString = fmt.Sprintf(format, "Besu", cfg.Besu.ContainerTag.Value.(string)) case cfgtypes.ExecutionClient_Reth: eth1ClientString = fmt.Sprintf(format, "Reth", cfg.Reth.ContainerTag.Value.(string)) + case cfgtypes.ExecutionClient_Erigon: + eth1ClientString = fmt.Sprintf(format, "Erigon", cfg.Erigon.ContainerTag.Value.(string)) default: return fmt.Errorf("unknown local execution client [%v]", eth1Client) } diff --git a/shared/services/config/erigon-params.go b/shared/services/config/erigon-params.go new file mode 100644 index 000000000..cd27b32fa --- /dev/null +++ b/shared/services/config/erigon-params.go @@ -0,0 +1,132 @@ +package config + +import ( + "runtime" + + "github.com/rocket-pool/smartnode/shared/types/config" +) + +// Constants +const ( + erigonTagProd string = "erigontech/erigon:v3.5.5" + erigonTagTest string = "erigontech/erigon:v3.5.5" + erigonEventLogInterval int = 1000 + erigonStopSignal string = "SIGINT" + defaultErigonTorrentPort uint16 = 42069 +) + +// Configuration for Erigon +type ErigonConfig struct { + Title string `yaml:"-"` + + // Common config.Parameters that Erigon doesn't support and should be hidden + UnsupportedCommonParams []string `yaml:"-"` + + // Compatible consensus clients + CompatibleConsensusClients []config.ConsensusClient `yaml:"-"` + + // The max number of events to query in a single event log query + EventLogInterval int `yaml:"-"` + + // Max number of P2P peers to connect to + MaxPeers config.Parameter `yaml:"maxPeers,omitempty"` + + // BitTorrent port used for snapshot sync + TorrentPort config.Parameter `yaml:"torrentPort,omitempty"` + + // The Docker Hub tag for Erigon + ContainerTag config.Parameter `yaml:"containerTag,omitempty"` + + // Custom command line flags + AdditionalFlags config.Parameter `yaml:"additionalFlags,omitempty"` +} + +// Generates a new Erigon configuration +func NewErigonConfig(cfg *RocketPoolConfig) *ErigonConfig { + return &ErigonConfig{ + Title: "Erigon Settings", + + UnsupportedCommonParams: []string{}, + + CompatibleConsensusClients: []config.ConsensusClient{ + config.ConsensusClient_Lighthouse, + config.ConsensusClient_Lodestar, + config.ConsensusClient_Nimbus, + config.ConsensusClient_Prysm, + config.ConsensusClient_Teku, + }, + + EventLogInterval: erigonEventLogInterval, + + MaxPeers: config.Parameter{ + ID: "maxPeers", + Name: "Max Peers", + Description: "The maximum number of peers Erigon should connect to. This can be lowered to improve performance on low-power systems or constrained networks. We recommend keeping it at 12 or higher.", + Type: config.ParameterType_Uint16, + Default: map[config.Network]interface{}{config.Network_All: calculateErigonPeers()}, + AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + + TorrentPort: config.Parameter{ + ID: "torrentPort", + Name: "Torrent Port", + Description: "The port Erigon should use for BitTorrent snapshot sync. This must be reachable from the internet (TCP and UDP), just like the P2P port.", + Type: config.ParameterType_Uint16, + Default: map[config.Network]interface{}{config.Network_All: defaultErigonTorrentPort}, + AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + + ContainerTag: config.Parameter{ + ID: "containerTag", + Name: "Container Tag", + Description: "The tag name of the Erigon container you want to use on Docker Hub.", + Type: config.ParameterType_String, + Default: map[config.Network]interface{}{ + config.Network_Mainnet: erigonTagProd, + config.Network_Devnet: erigonTagTest, + config.Network_Testnet: erigonTagTest, + }, + AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, + CanBeBlank: false, + OverwriteOnUpgrade: true, + }, + + AdditionalFlags: config.Parameter{ + ID: "additionalFlags", + Name: "Additional Flags", + Description: "Additional custom command line flags you want to pass to Erigon, to take advantage of other settings that the Smart Node's configuration doesn't cover.", + Type: config.ParameterType_String, + Default: map[config.Network]interface{}{config.Network_All: ""}, + AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, + CanBeBlank: true, + OverwriteOnUpgrade: false, + }, + } +} + +// Calculate the default number of Erigon peers +func calculateErigonPeers() uint16 { + if runtime.GOARCH == "arm64" { + return 16 + } + return 32 +} + +// Get the config.Parameters for this config +func (cfg *ErigonConfig) GetParameters() []*config.Parameter { + return []*config.Parameter{ + &cfg.MaxPeers, + &cfg.TorrentPort, + &cfg.ContainerTag, + &cfg.AdditionalFlags, + } +} + +// The title for the config +func (cfg *ErigonConfig) GetConfigTitle() string { + return cfg.Title +} diff --git a/shared/services/config/execution-common-config.go b/shared/services/config/execution-common-config.go index 1dd7dc3df..3fe633719 100644 --- a/shared/services/config/execution-common-config.go +++ b/shared/services/config/execution-common-config.go @@ -125,11 +125,11 @@ func NewExecutionCommonConfig(cfg *RocketPoolConfig) *ExecutionCommonConfig { OverwriteOnUpgrade: false, Options: []config.ParameterOption{{ Name: "Rolling History Expiry", - Description: "Drop block bodies and receipts older than about one year. Nethermind, Besu, and Reth support this. Geth does not yet and will use pre-Prague history expiry instead.", + Description: "Drop block bodies and receipts older than about one year. Nethermind, Besu, Reth, and Erigon support this. Geth does not yet and will use pre-Prague history expiry instead.", Value: config.PruningMode_RollingHistoryExpiry, }, { Name: "History Expiry", - Description: "Drop pre-merge block bodies and receipts (EIP-4444 partial history expiry). Keeps all post-merge history.", + Description: "Drop pre-merge block bodies and receipts (EIP-4444 partial history expiry). Keeps all post-merge history. Erigon does not support pre-merge-only expiry and will keep remaining block history with full-mode state pruning.", Value: config.PruningMode_HistoryExpiry, }, { Name: "Full node", diff --git a/shared/services/config/rocket-pool-config.go b/shared/services/config/rocket-pool-config.go index e4f6bea93..01fc21899 100644 --- a/shared/services/config/rocket-pool-config.go +++ b/shared/services/config/rocket-pool-config.go @@ -99,6 +99,7 @@ type RocketPoolConfig struct { Nethermind *NethermindConfig `yaml:"nethermind,omitempty"` Besu *BesuConfig `yaml:"besu,omitempty"` Reth *RethConfig `yaml:"reth,omitempty"` + Erigon *ErigonConfig `yaml:"erigon,omitempty"` ExternalExecution *ExternalExecutionConfig `yaml:"externalExecution,omitempty"` // Consensus client configurations @@ -316,6 +317,10 @@ func NewRocketPoolConfig(rpDir string, isNativeMode bool) *RocketPoolConfig { Name: "Reth", Description: getAugmentedEcDescription(config.ExecutionClient_Reth, "Reth is a new Ethereum full node implementation that is focused on being user-friendly, highly modular, as well as being fast and efficient. Reth is fully open source and written in Rust."), Value: config.ExecutionClient_Reth, + }, { + Name: "Erigon", + Description: getAugmentedEcDescription(config.ExecutionClient_Erigon, "Erigon is an execution client focused on disk efficiency and fast snapshot sync. It is fully open source and written in Go."), + Value: config.ExecutionClient_Erigon, }}, }, @@ -559,6 +564,7 @@ func NewRocketPoolConfig(rpDir string, isNativeMode bool) *RocketPoolConfig { cfg.Nethermind = NewNethermindConfig(cfg) cfg.Besu = NewBesuConfig(cfg) cfg.Reth = NewRethConfig(cfg) + cfg.Erigon = NewErigonConfig(cfg) cfg.ExternalExecution = NewExternalExecutionConfig(cfg) cfg.FallbackNormal = NewFallbackNormalConfig(cfg) cfg.FallbackPrysm = NewFallbackPrysmConfig(cfg) @@ -607,6 +613,11 @@ func getAugmentedEcDescription(client config.ExecutionClient, originalDescriptio if totalMemoryGB < 9 { return fmt.Sprintf("%s\n\n[red]WARNING: Nethermind currently requires over 8 GB of RAM to run smoothly. We do not recommend it for your system. This may be improved in a future release.", originalDescription) } + case config.ExecutionClient_Erigon: + totalMemoryGB := memory.TotalMemory() / 1024 / 1024 / 1024 + if totalMemoryGB < 16 { + return fmt.Sprintf("%s\n\n[red]WARNING: Erigon currently requires 16 GB of RAM to run smoothly. We do not recommend it for your system.", originalDescription) + } } return originalDescription @@ -673,6 +684,7 @@ func (cfg *RocketPoolConfig) GetSubconfigs() map[string]config.Config { "nethermind": cfg.Nethermind, "besu": cfg.Besu, "reth": cfg.Reth, + "erigon": cfg.Erigon, "externalExecution": cfg.ExternalExecution, "consensusCommon": cfg.ConsensusCommon, "lighthouse": cfg.Lighthouse, @@ -748,6 +760,8 @@ func (cfg *RocketPoolConfig) GetEventLogInterval() (int, error) { return cfg.Nethermind.EventLogInterval, nil case config.ExecutionClient_Reth: return cfg.Reth.EventLogInterval, nil + case config.ExecutionClient_Erigon: + return cfg.Erigon.EventLogInterval, nil default: return 0, fmt.Errorf("can't get event log interval of unknown execution client [%v]", client) } @@ -1320,6 +1334,8 @@ func (cfg *RocketPoolConfig) GetECContainerTag() (string, error) { return cfg.Besu.ContainerTag.Value.(string), nil case config.ExecutionClient_Reth: return cfg.Reth.ContainerTag.Value.(string), nil + case config.ExecutionClient_Erigon: + return cfg.Erigon.ContainerTag.Value.(string), nil } return "", fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) @@ -1341,6 +1357,8 @@ func (cfg *RocketPoolConfig) GetECStopSignal() (string, error) { return besuStopSignal, nil case config.ExecutionClient_Reth: return rethStopSignal, nil + case config.ExecutionClient_Erigon: + return erigonStopSignal, nil } return "", fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) @@ -1379,6 +1397,8 @@ func (cfg *RocketPoolConfig) GetECMaxPeers() (uint16, error) { return cfg.Besu.MaxPeers.Value.(uint16), nil case config.ExecutionClient_Reth: return cfg.Reth.MaxPeers.Value.(uint16), nil + case config.ExecutionClient_Erigon: + return cfg.Erigon.MaxPeers.Value.(uint16), nil } return 0, fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) @@ -1399,6 +1419,8 @@ func (cfg *RocketPoolConfig) GetECAdditionalFlags() (string, error) { return cfg.Besu.AdditionalFlags.Value.(string), nil case config.ExecutionClient_Reth: return cfg.Reth.AdditionalFlags.Value.(string), nil + case config.ExecutionClient_Erigon: + return cfg.Erigon.AdditionalFlags.Value.(string), nil } return "", fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) diff --git a/shared/services/rocketpool/assets/install/scripts/start-ec.sh b/shared/services/rocketpool/assets/install/scripts/start-ec.sh index 2d75e99d1..d21027fd8 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-ec.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-ec.sh @@ -37,16 +37,19 @@ if [ "$NETWORK" = "mainnet" ]; then RP_NETHERMIND_NETWORK="mainnet" BESU_NETWORK="--network=mainnet" RETH_NETWORK="--chain mainnet" + ERIGON_NETWORK="--chain mainnet" elif [ "$NETWORK" = "devnet" ]; then GETH_NETWORK="--hoodi" RP_NETHERMIND_NETWORK="hoodi" BESU_NETWORK="--network=hoodi" RETH_NETWORK="--chain hoodi" + ERIGON_NETWORK="--chain hoodi" elif [ "$NETWORK" = "testnet" ]; then GETH_NETWORK="--hoodi" RP_NETHERMIND_NETWORK="hoodi" BESU_NETWORK="--network=hoodi" RETH_NETWORK="--chain hoodi" + ERIGON_NETWORK="--chain hoodi" else echo "Unknown network [$NETWORK]" exit 1 @@ -494,3 +497,101 @@ if [ "$CLIENT" = "reth" ]; then fi fi + +# Erigon startup +if [ "$CLIENT" = "erigon" ]; then + + # Performance tuning for ARM systems + UNAME_VAL=$(uname -m) + if [ "$UNAME_VAL" = "arm64" ] || [ "$UNAME_VAL" = "aarch64" ]; then + if command -v taskset >/dev/null 2>&1 && command -v ionice >/dev/null 2>&1; then + define_perf_prefix + else + echo "taskset/ionice not available; skipping ARM performance tuning" + fi + fi + + # Create the JWT secret + # Use -s so a zero-byte leftover is repaired on restart. + if [ ! -s "/secrets/jwtsecret" ]; then + echo -n "$(head -c 32 /dev/urandom | od -A n -t x1 | tr -d '[:space:]')" > /secrets/jwtsecret + fi + + # Erigon prunes continuously from --prune.mode; there is no offline prune step. + if [ -f "/ethclient/prune.lock" ]; then + echo "Erigon pruning is applied at startup; skipping offline prune" + echo "Changing prune mode requires a resync (rocketpool service resync-eth1)" + rm -f /ethclient/prune.lock + fi + + CMD="$PERF_PREFIX /usr/local/bin/erigon $ERIGON_NETWORK \ + --datadir /ethclient/erigon \ + --externalcl \ + --mcp.disable \ + --http \ + --http.addr 0.0.0.0 \ + --http.port ${EC_HTTP_PORT:-8545} \ + --http.api eth,net,web3 \ + --http.corsdomain=* \ + --http.vhosts=* \ + --ws \ + --ws.port ${EC_WS_PORT:-8546} \ + --authrpc.addr 0.0.0.0 \ + --authrpc.port ${EC_ENGINE_PORT:-8551} \ + --authrpc.vhosts=* \ + --authrpc.jwtsecret /secrets/jwtsecret \ + --rpc.returndata.limit 1000000 \ + $EC_ADDITIONAL_FLAGS" + + if [ "$NETWORK" = "devnet" ]; then + CMD="$CMD --bootnodes $BOOTNODE_ENODE_LIST" + fi + + if [ ! -z "$EXTERNAL_IP" ]; then + CMD="$CMD --nat extip:$EXTERNAL_IP" + fi + + if [ ! -z "$EC_SUGGESTED_BLOCK_GAS_LIMIT" ]; then + CMD="$CMD --miner.gaslimit $EC_SUGGESTED_BLOCK_GAS_LIMIT" + fi + + if [ "$EC_PRUNING_MODE" = "archive" ]; then + CMD="$CMD --prune.mode=archive" + fi + + if [ "$EC_PRUNING_MODE" = "fullNode" ]; then + CMD="$CMD --prune.mode=blocks" + fi + + if [ "$EC_PRUNING_MODE" = "historyExpiry" ]; then + echo "Erigon does not support pre-merge-only history expiry; keeping all remaining block history with full-mode state pruning" + CMD="$CMD --prune.mode=full --persist.receipts=false --prune.distance.blocks=18446744073709551615" + fi + + if [ "$EC_PRUNING_MODE" = "rollingHistoryExpiry" ]; then + CMD="$CMD --prune.mode=full --persist.receipts=false --prune.distance=2628000 --prune.distance.blocks=2628000" + fi + + if [ ! -z "$ETHSTATS_LABEL" ] && [ ! -z "$ETHSTATS_LOGIN" ]; then + CMD="$CMD --ethstats $ETHSTATS_LABEL:$ETHSTATS_LOGIN" + fi + + if [ ! -z "$EC_MAX_PEERS" ]; then + CMD="$CMD --maxpeers $EC_MAX_PEERS" + fi + + if [ "$ENABLE_METRICS" = "true" ]; then + CMD="$CMD --metrics --metrics.addr 0.0.0.0 --metrics.port $EC_METRICS_PORT" + fi + + if [ ! -z "$EC_P2P_PORT" ]; then + CMD="$CMD --port $EC_P2P_PORT" + fi + + if [ ! -z "$ERIGON_TORRENT_PORT" ]; then + CMD="$CMD --torrent.port $ERIGON_TORRENT_PORT" + fi + + exec ${CMD} + +fi diff --git a/shared/services/rocketpool/assets/install/templates/eth1.tmpl b/shared/services/rocketpool/assets/install/templates/eth1.tmpl index da2e7178d..c1476193a 100644 --- a/shared/services/rocketpool/assets/install/templates/eth1.tmpl +++ b/shared/services/rocketpool/assets/install/templates/eth1.tmpl @@ -14,7 +14,8 @@ services: stop_signal: {{.GetECStopSignal}} stop_grace_period: 15m {{- $p2p := (or .ExecutionCommon.P2pPort.Value "30303")}} - ports: [ "{{$p2p}}:{{$p2p}}/udp", "{{$p2p}}:{{$p2p}}/tcp"{{.GetECOpenAPIPorts}} ] + {{- $torrent := (or .Erigon.TorrentPort.Value "42069")}} + ports: [ "{{$p2p}}:{{$p2p}}/udp", "{{$p2p}}:{{$p2p}}/tcp"{{if eq .ExecutionClient.String "erigon"}}, "{{$torrent}}:{{$torrent}}/udp", "{{$torrent}}:{{$torrent}}/tcp"{{end}}{{.GetECOpenAPIPorts}} ] volumes: - eth1clientdata:/ethclient - {{.RocketPoolDirectory}}/scripts:/setup:ro @@ -55,6 +56,8 @@ services: - GETH_EVM_TIMEOUT={{.Geth.EvmTimeout}} {{- else if eq .ExecutionClient.String "reth"}} - RETH_MAX_INBOUND_PEERS={{.Reth.MaxInboundPeers}} + {{- else if eq .ExecutionClient.String "erigon"}} + - ERIGON_TORRENT_PORT={{.Erigon.TorrentPort}} {{- end}} entrypoint: sh command: "/setup/start-ec.sh" diff --git a/shared/types/config/types.go b/shared/types/config/types.go index 3259ba279..da13bbd5a 100644 --- a/shared/types/config/types.go +++ b/shared/types/config/types.go @@ -77,6 +77,7 @@ const ( ExecutionClient_Nethermind ExecutionClient = "nethermind" ExecutionClient_Besu ExecutionClient = "besu" ExecutionClient_Reth ExecutionClient = "reth" + ExecutionClient_Erigon ExecutionClient = "erigon" ) // Enum to describe the Consensus client options