From fbd52cb3e61876f3459ac180412d2d066ae14cfd Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:13:21 -0300 Subject: [PATCH 1/5] Checking NM DB version --- rocketpool-cli/service/service.go | 37 ++++++++++++++----- shared/services/config/nethermind-config.go | 10 +++-- .../rocketpool/assets/install/mainnet.env | 2 +- .../assets/install/scripts/nethermind-db.sh | 25 +++++++++++++ .../assets/install/scripts/start-ec.sh | 32 ++++++++++++---- .../assets/install/templates/eth1.tmpl | 1 + shared/services/rocketpool/client.go | 18 +++++++++ 7 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 shared/services/rocketpool/assets/install/scripts/nethermind-db.sh diff --git a/rocketpool-cli/service/service.go b/rocketpool-cli/service/service.go index 18a355fc5..1c7bd3799 100644 --- a/rocketpool-cli/service/service.go +++ b/rocketpool-cli/service/service.go @@ -38,7 +38,8 @@ const ( clientDataVolumeName string = "/ethclient" dataFolderVolumeName string = "/.rocketpool/data" - PruneFreeSpaceRequired uint64 = 50 * 1024 * 1024 * 1024 + PruneFreeSpaceRequired uint64 = 50 * 1024 * 1024 * 1024 + // TODO(Hegota): Remove the free-space requirement for Patricia state pruning. NethermindPruneFreeSpaceRequired uint64 = 250 * 1024 * 1024 * 1024 clearLine string = "\033[2K" @@ -934,6 +935,7 @@ func pruneExecutionClient(yes bool) error { } if cfg.IsNativeMode { fmt.Println("You are using Native Mode.\nThe Smart Node cannot prune your Execution client for you, you'll have to do it manually.") + return nil } selectedEc := cfg.ExecutionClient.Value.(cfgtypes.ExecutionClient) @@ -945,6 +947,30 @@ func pruneExecutionClient(yes bool) error { pruningMode, _ := cfg.ExecutionCommon.PruningMode.Value.(cfgtypes.Mode) + // Get the execution container before checking whether it supports pruning. + prefix, err := rp.GetContainerPrefix() + if err != nil { + return fmt.Errorf("Error getting container prefix: %w", err) + } + executionContainerName := prefix + ExecutionContainerSuffix + + // TODO(Hegota): Remove Nethermind's Patricia state pruning path and free-space requirement. + if selectedEc == cfgtypes.ExecutionClient_Nethermind { + layout, err := rp.NethermindDBLayout(executionContainerName) + if err != nil { + return err + } + if layout == "flat" { + fmt.Println("Nethermind is using FlatDB, which does not need or support manual state pruning.") + fmt.Println("History expiry is managed separately by your configured pruning mode.") + return nil + } + if layout == "none" { + fmt.Println("Nethermind has no persisted state database to prune yet.") + return nil + } + } + // Besu rolling expiry is applied continuously at startup; there is no offline prune step. if selectedEc == cfgtypes.ExecutionClient_Besu && pruningMode == cfgtypes.PruningMode_RollingHistoryExpiry { fmt.Println("Besu rolling history expiry is applied while the client is running.") @@ -989,21 +1015,12 @@ func pruneExecutionClient(yes bool) error { } fmt.Println() - // Get the container prefix - prefix, err := rp.GetContainerPrefix() - if err != nil { - return fmt.Errorf("Error getting container prefix: %w", err) - } - // Prompt for confirmation if prompt.Declined(yes, "Are you sure you want to prune your main execution client?") { fmt.Println("Cancelled.") return nil } - // Get the execution container name - executionContainerName := prefix + ExecutionContainerSuffix - // Check for enough free space volumePath, err := rp.GetClientVolumeSource(executionContainerName, clientDataVolumeName) if err != nil { diff --git a/shared/services/config/nethermind-config.go b/shared/services/config/nethermind-config.go index 1bf6bd8d2..fb71f39dc 100644 --- a/shared/services/config/nethermind-config.go +++ b/shared/services/config/nethermind-config.go @@ -33,6 +33,8 @@ type NethermindConfig struct { // Max number of P2P peers to connect to MaxPeers config.Parameter `yaml:"maxPeers,omitempty"` + // TODO(Hegota): Remove these four Patricia-only state pruning parameters, + // their defaults/GetParameters entries, and their eth1.tmpl environment variables. // Nethermind's memory for in-memory pruning PruneMemSize config.Parameter `yaml:"pruneMemSize,omitempty"` @@ -100,7 +102,7 @@ func NewNethermindConfig(cfg *RocketPoolConfig) *NethermindConfig { PruneMemSize: config.Parameter{ ID: "pruneMemSize", Name: "In-Memory Pruning Cache Size", - Description: "The amount of RAM (in MB) you want to dedicate to Nethermind for its in-memory pruning system. Higher values mean less writes to your SSD and slower overall database growth.\n\n Leave it blank to use the client's default.", + Description: "Only used with Nethermind's legacy Patricia database; ignored with FlatDB.\n\nThe amount of RAM (in MB) you want to dedicate to Nethermind for its in-memory pruning system. Higher values mean less writes to your SSD and slower overall database growth.\n\n Leave it blank to use the client's default.", Type: config.ParameterType_String, Default: map[config.Network]interface{}{config.Network_All: ""}, AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, @@ -111,7 +113,7 @@ func NewNethermindConfig(cfg *RocketPoolConfig) *NethermindConfig { FullPruneMemoryBudget: config.Parameter{ ID: "fullPruneMemoryBudget", Name: "Full Prune Memory Budget Size", - Description: "The amount of RAM (in MB) you want to dedicate to Nethermind for its full pruning system. Higher values mean less writes to your SSD and faster pruning times.\n\n Leave blank to use the client's default.", + Description: "Only used with Nethermind's legacy Patricia database; ignored with FlatDB.\n\nThe amount of RAM (in MB) you want to dedicate to Nethermind for its full pruning system. Higher values mean less writes to your SSD and faster pruning times.\n\n Leave blank to use the client's default.", Type: config.ParameterType_String, Default: map[config.Network]interface{}{config.Network_All: ""}, AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, @@ -122,7 +124,7 @@ func NewNethermindConfig(cfg *RocketPoolConfig) *NethermindConfig { FullPruningThresholdMb: config.Parameter{ ID: "fullPruningThresholdMb", Name: "Prune threshold (MB)", - Description: "When the volume free space (in MB) hits this level, Nethermind will automatically start full pruning to reclaim disk space.", + Description: "Only used with Nethermind's legacy Patricia database; ignored with FlatDB.\n\nWhen the volume free space (in MB) hits this level, Nethermind will automatically start full pruning to reclaim disk space.", Type: config.ParameterType_Uint, Default: nethermindPruneThresholdDefaults(cfg.networks), AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, @@ -133,7 +135,7 @@ func NewNethermindConfig(cfg *RocketPoolConfig) *NethermindConfig { FullPruningMaxDegreeOfParallelism: config.Parameter{ ID: "fullPruningMaxDegreeOfParallelism", Name: "Full pruning parallelism", - Description: "This option will be used to determine the number of threads allocated to concurrently by Nethermind to prune data.", + Description: "Only used with Nethermind's legacy Patricia database; ignored with FlatDB.\n\nThis option will be used to determine the number of threads allocated to concurrently by Nethermind to prune data.", Type: config.ParameterType_Int, Default: map[config.Network]interface{}{config.Network_All: int64(0)}, AffectsContainers: []config.ContainerID{config.ContainerID_Eth1}, diff --git a/shared/services/rocketpool/assets/install/mainnet.env b/shared/services/rocketpool/assets/install/mainnet.env index f44a0e4f6..00aa7a7f2 100644 --- a/shared/services/rocketpool/assets/install/mainnet.env +++ b/shared/services/rocketpool/assets/install/mainnet.env @@ -7,7 +7,7 @@ RP_IMAGE_SMARTNODE=rocketpool/smartnode:v1.24.2@sha256:0e94df463f2fb461e16f9b45e8eaae38caa639e714167d6f28b2c8f6570bc7a5 RP_IMAGE_GETH=ethereum/client-go:v1.17.5@sha256:523d3ba26623a619e912019068dc2784f02934070ac46bdae4d5b9df0d917814 -RP_IMAGE_NETHERMIND=nethermind/nethermind:1.39.3@sha256:1b6b01419de4ff75ed3d61995904bccc2fdcc2865fee6dae07d88c14a0758e40 +RP_IMAGE_NETHERMIND=nethermind/nethermind:2.0.0-rc2@sha256:98c6d4bf61c3904fe10bb8a40520bf13cccc228b2859b9e99a2bc34a76dbdf10 RP_IMAGE_BESU=hyperledger/besu:26.8.1@sha256:6f3f21ce533383fcc8db3bce02252b59d5a9e776b72b5a1c8ecd2db011600042 RP_IMAGE_RETH=ghcr.io/paradigmxyz/reth:v2.6.0@sha256:8ce703acf113b2a20705b6e76adebed20f74ba8591bc7c9407203b2968aca70d RP_IMAGE_ERIGON=erigontech/erigon:v3.6.1@sha256:8cd3fbcbb35d8b16768225ff7494a0672b7b06c9bf13d466850aabb920ac0de7 diff --git a/shared/services/rocketpool/assets/install/scripts/nethermind-db.sh b/shared/services/rocketpool/assets/install/scripts/nethermind-db.sh new file mode 100644 index 000000000..9b2bfec9b --- /dev/null +++ b/shared/services/rocketpool/assets/install/scripts/nethermind-db.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# TODO(Hegota): Remove this detector when Smart Node drops Patricia state pruning. +# Shared by start-ec.sh and prune-eth1. Inspect persisted state, not just the flat +# directory: Nethermind 2.0 also creates empty flat column families on Patricia. +# Based on https://github.com/ethstaker/eth-docker/pull/2819. +DB_ROOT=${1:-/ethclient/nethermind/nethermind_db} + +if [ ! -d "$DB_ROOT" ]; then + echo none + exit 0 +fi + +FLAT_FILES=$(find "$DB_ROOT" -mindepth 3 -maxdepth 3 -path '*/flat/*' -name '*.sst' -print -quit) || exit 1 +if [ -n "$FLAT_FILES" ]; then + echo flat + exit 0 +fi + +# Full pruning can put Patricia state files in numbered subdirectories. +STATE_FILES=$(find "$DB_ROOT" -path '*/state/*' -name '*.sst' -print -quit) || exit 1 +if [ -n "$STATE_FILES" ]; then + echo patricia +else + echo none +fi diff --git a/shared/services/rocketpool/assets/install/scripts/start-ec.sh b/shared/services/rocketpool/assets/install/scripts/start-ec.sh index fd4b6fd47..3d5628022 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-ec.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-ec.sh @@ -195,6 +195,21 @@ if [ "$CLIENT" = "nethermind" ]; then RP_NETHERMIND_NETWORK="${RP_NETHERMIND_NETWORK}_archive" fi + # TODO(Hegota): Remove Patricia detection and the state pruning flags below. + NETHERMIND_DB=$(sh /setup/nethermind-db.sh) || exit 1 + # A fresh/resynced database uses FlatDB in v2.0 unless explicitly opted out. + if [ "$NETHERMIND_DB" = "none" ]; then + if printf '%s\n' "$EC_ADDITIONAL_FLAGS" | grep -Eiq -- '(^|[[:space:]])--(flatdb\.enabled|flatdb-enabled)(=|[[:space:]]+)false([[:space:]]|$)'; then + NETHERMIND_DB=patricia + elif ! printf '%s\n' "$EC_ADDITIONAL_FLAGS" | grep -Eiq -- '(^|[[:space:]])--(flatdb\.enabled|flatdb-enabled)(=|[[:space:]]+)true([[:space:]]|$)'; then + # Preserve fresh-sync auto pruning with the still-supported v1 images. + NETHERMIND_VERSION=$("$NETHERMIND_BINARY" --version) || exit 1 + if printf '%s\n' "$NETHERMIND_VERSION" | grep -Eq '(^|/|[[:space:]])v?1\.[0-9]'; then + NETHERMIND_DB=patricia + fi + fi + fi + CMD="$PERF_PREFIX $NETHERMIND_BINARY \ --config $RP_NETHERMIND_NETWORK \ --data-dir /ethclient/nethermind \ @@ -208,8 +223,10 @@ if [ "$CLIENT" = "nethermind" ]; then --JsonRpc.JwtSecretFile=/secrets/jwtsecret \ $EC_ADDITIONAL_FLAGS" - if [ "$EC_PRUNING_MODE" != "archive" ]; then - CMD="$CMD --Pruning.FullPruningTrigger=VolumeFreeSpace \ + # TODO(Hegota): Drop automatic state pruning; FlatDB does not use it. + if [ "$NETHERMIND_DB" = "patricia" ] && [ "$EC_PRUNING_MODE" != "archive" ]; then + CMD="$CMD --Pruning.Mode=Hybrid \ + --Pruning.FullPruningTrigger=VolumeFreeSpace \ --Pruning.FullPruningThresholdMb=$RP_NETHERMIND_FULL_PRUNING_THRESHOLD_MB \ --Pruning.FullPruningCompletionBehavior AlwaysShutdown \ --Pruning.FullPruningMaxDegreeOfParallelism=$RP_NETHERMIND_FULL_PRUNING_MAX_DEGREE_PARALLELISM" @@ -221,23 +238,23 @@ if [ "$CLIENT" = "nethermind" ]; then if [ "$EC_PRUNING_MODE" = "archive" ]; then CMD="$CMD --Sync.DownloadBodiesInFastSync=false --Sync.DownloadReceiptsInFastSync=false --Sync.FastSync=false --Sync.SnapSync=false --Sync.FastBlocks=false --Sync.PivotNumber=0" - CMD="$CMD --Pruning.Mode=None --Receipt.TxLookupLimit=0" + CMD="$CMD --Receipt.TxLookupLimit=0" + if [ "$NETHERMIND_DB" = "patricia" ]; then + CMD="$CMD --Pruning.Mode=None" + fi fi if [ "$EC_PRUNING_MODE" = "fullNode" ]; then CMD="$CMD --Sync.AncientBodiesBarrier=0 --Sync.AncientReceiptsBarrier=0" - CMD="$CMD --Pruning.Mode=Hybrid" fi if [ "$EC_PRUNING_MODE" = "historyExpiry" ]; then CMD="$CMD --Sync.AncientBodiesBarrier=15537394 --Sync.AncientReceiptsBarrier=15537394" CMD="$CMD --History.Pruning=UseAncientBarriers" - CMD="$CMD --Pruning.Mode=Hybrid" fi if [ "$EC_PRUNING_MODE" = "rollingHistoryExpiry" ]; then CMD="$CMD --History.Pruning=Rolling" - CMD="$CMD --Pruning.Mode=Hybrid" fi # Add optional supplemental primary JSON-RPC modules @@ -272,7 +289,8 @@ if [ "$CLIENT" = "nethermind" ]; then CMD="$CMD --Network.DiscoveryPort $EC_P2P_PORT --Network.P2PPort $EC_P2P_PORT" fi - if [ "$EC_PRUNING_MODE" != "archive" ]; then + # TODO(Hegota): Drop the Patricia state pruning memory settings. + if [ "$NETHERMIND_DB" = "patricia" ] && [ "$EC_PRUNING_MODE" != "archive" ]; then if [ ! -z "$RP_NETHERMIND_PRUNE_MEM_SIZE" ]; then CMD="$CMD --Pruning.CacheMb $RP_NETHERMIND_PRUNE_MEM_SIZE" fi diff --git a/shared/services/rocketpool/assets/install/templates/eth1.tmpl b/shared/services/rocketpool/assets/install/templates/eth1.tmpl index eaa120e18..427f695be 100644 --- a/shared/services/rocketpool/assets/install/templates/eth1.tmpl +++ b/shared/services/rocketpool/assets/install/templates/eth1.tmpl @@ -56,6 +56,7 @@ services: - BESU_JVM_HEAP_SIZE={{.Besu.JvmHeapSize}} {{- else if eq .ExecutionClient.String "nethermind"}} - EC_CACHE_SIZE={{.Nethermind.CacheSize}} + {{- /* TODO(Hegota): Remove the four Patricia state pruning environment variables below. */}} - RP_NETHERMIND_PRUNE_MEM_SIZE={{.Nethermind.PruneMemSize}} - RP_NETHERMIND_ADDITIONAL_MODULES={{.Nethermind.AdditionalModules}} - RP_NETHERMIND_ADDITIONAL_URLS={{.Nethermind.AdditionalUrls}} diff --git a/shared/services/rocketpool/client.go b/shared/services/rocketpool/client.go index 8bf2a3675..98f2e10d6 100644 --- a/shared/services/rocketpool/client.go +++ b/shared/services/rocketpool/client.go @@ -952,6 +952,24 @@ func (c *Client) TouchEthclientMarker(container, volume, marker string) error { return nil } +// NethermindDBLayout inspects the execution container's persisted state using the +// same detector as client startup. Errors must not be treated as Patricia. +// TODO(Hegota): Remove this check and RunNethermindPruneStarter when dropping Patricia pruning. +func (c *Client) NethermindDBLayout(executionContainerName string) (string, error) { + cmd := fmt.Sprintf("docker exec %s sh /setup/nethermind-db.sh", shellescape.Quote(executionContainerName)) + output, err := c.readOutput(cmd) + if err != nil { + return "", fmt.Errorf("could not inspect Nethermind state database (the execution container must be running): %w", err) + } + layout := strings.TrimSpace(string(output)) + switch layout { + case "patricia", "flat", "none": + return layout, nil + default: + return "", fmt.Errorf("unexpected Nethermind database layout: %q", layout) + } +} + // Curls the Nethermind admin URL to trigger pruning func (c *Client) RunNethermindPruneStarter(executionContainerName string) error { retryCount := 5 From afcbc1c74e5498de0691eb79120a8e1fa312659c Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:52:01 -0300 Subject: [PATCH 2/5] Add get-db-details cmd --- rocketpool-cli/service/commands.go | 12 +++ rocketpool-cli/service/db-details.go | 60 +++++++++++++ rocketpool-cli/service/db-details_test.go | 89 +++++++++++++++++++ .../assets/install/scripts/nethermind-db.sh | 3 +- .../assets/install/scripts/start-ec.sh | 12 +-- shared/services/rocketpool/client.go | 2 +- shared/services/rocketpool/db-details.go | 68 ++++++++++++++ shared/services/rocketpool/db-details_test.go | 30 +++++++ 8 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 rocketpool-cli/service/db-details.go create mode 100644 rocketpool-cli/service/db-details_test.go create mode 100644 shared/services/rocketpool/db-details.go create mode 100644 shared/services/rocketpool/db-details_test.go diff --git a/rocketpool-cli/service/commands.go b/rocketpool-cli/service/commands.go index 21e40655b..7f54eff3b 100644 --- a/rocketpool-cli/service/commands.go +++ b/rocketpool-cli/service/commands.go @@ -399,6 +399,18 @@ func RegisterCommands(app *cli.Command, name string, aliases []string) { }, }, + { + Name: "get-db-details", + Usage: "View the Nethermind database layout or Geth Pebble database version", + UsageText: "rocketpool service get-db-details", + Action: func(ctx context.Context, c *cli.Command) error { + if err := cliutils.ValidateArgCount(c, 0); err != nil { + return err + } + return getDBDetails() + }, + }, + { Name: "prune-eth1", Aliases: []string{"n"}, diff --git a/rocketpool-cli/service/db-details.go b/rocketpool-cli/service/db-details.go new file mode 100644 index 000000000..e51199a20 --- /dev/null +++ b/rocketpool-cli/service/db-details.go @@ -0,0 +1,60 @@ +package service + +import ( + "fmt" + + "github.com/rocket-pool/smartnode/shared/services/rocketpool" + cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" +) + +func getDBDetails() error { + rp := rocketpool.NewClient() + defer rp.Close() + + cfg, isNew, err := rp.LoadConfig() + if err != nil { + return err + } + if isNew { + return fmt.Errorf("Settings file not found. Please run `rocketpool service config` to set up your Smart Node.") + } + if cfg.IsNativeMode || cfg.ExecutionClientMode.Value.(cfgtypes.Mode) == cfgtypes.Mode_External { + fmt.Println("Database details are only available for execution clients managed by Smart Node in Docker mode.") + return nil + } + + container := cfg.Smartnode.ProjectName.Value.(string) + ExecutionContainerSuffix + switch cfg.ExecutionClient.Value.(cfgtypes.ExecutionClient) { + case cfgtypes.ExecutionClient_Nethermind: + layout, err := rp.NethermindDBLayout(container) + if err != nil { + return err + } + switch layout { + case "flat": + fmt.Println("Nethermind is using FlatDB. Manual state pruning is not needed or supported.") + case "patricia": + fmt.Println("Nethermind is using the legacy Patricia database. State pruning is supported.") + case "none": + fmt.Println("Nethermind has no persisted state database yet.") + } + case cfgtypes.ExecutionClient_Geth: + version, err := rp.GethDBVersion(container) + if err != nil { + return err + } + switch version { + case "v1": + fmt.Println("Geth is using Pebble v1. Run `rocketpool service migrate-geth` to migrate to Pebble v2.") + case "v2": + fmt.Println("Geth is using Pebble v2. No database migration is needed.") + case "leveldb": + fmt.Println("Geth is using LevelDB. Pebble v1/v2 migration does not apply to this database.") + case "none": + fmt.Println("Geth has no database yet.") + } + default: + fmt.Println("Database details are currently supported for Nethermind and Geth only.") + } + return nil +} diff --git a/rocketpool-cli/service/db-details_test.go b/rocketpool-cli/service/db-details_test.go new file mode 100644 index 000000000..63ce9affc --- /dev/null +++ b/rocketpool-cli/service/db-details_test.go @@ -0,0 +1,89 @@ +package service + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/rocket-pool/smartnode/shared/services/config" + "github.com/rocket-pool/smartnode/shared/services/rocketpool" + cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" +) + +func TestGetDBDetails(t *testing.T) { + for _, tc := range []struct { + name, response, want string + client cfgtypes.ExecutionClient + external, wantErr bool + }{ + {name: "flat", client: cfgtypes.ExecutionClient_Nethermind, response: "flat", want: "Nethermind is using FlatDB"}, + {name: "patricia", client: cfgtypes.ExecutionClient_Nethermind, response: "patricia", want: "legacy Patricia"}, + {name: "no nethermind state", client: cfgtypes.ExecutionClient_Nethermind, response: "none", want: "no persisted state"}, + {name: "unknown nethermind layout", client: cfgtypes.ExecutionClient_Nethermind, response: "unknown", wantErr: true}, + {name: "geth v1", client: cfgtypes.ExecutionClient_Geth, response: "CURRENT\nOPTIONS-000001", want: "Geth is using Pebble v1"}, + {name: "geth v2", client: cfgtypes.ExecutionClient_Geth, response: "marker.manifest.000001.MANIFEST-000001\nmarker.format-version.000001.013", want: "Geth is using Pebble v2"}, + {name: "leveldb", client: cfgtypes.ExecutionClient_Geth, response: "CURRENT", want: "Geth is using LevelDB"}, + {name: "no geth database", client: cfgtypes.ExecutionClient_Geth, want: "Geth has no database yet"}, + {name: "container unavailable", client: cfgtypes.ExecutionClient_Geth, response: "fail", wantErr: true}, + {name: "external", client: cfgtypes.ExecutionClient_Geth, external: true, response: "fail", want: "only available for execution clients managed by Smart Node"}, + {name: "unsupported", client: cfgtypes.ExecutionClient_Besu, response: "fail", want: "Nethermind and Geth only"}, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + cfg, err := config.NewRocketPoolConfig(dir, false) + if err != nil { + t.Fatal(err) + } + cfg.IsCLI = true + cfg.Smartnode.DataPath.Value = filepath.Join(dir, "data") + cfg.ExecutionClient.Value = tc.client + cfg.ExecutionClientMode.Value = cfgtypes.Mode_Local + if tc.external { + cfg.ExecutionClientMode.Value = cfgtypes.Mode_External + } + if err := cfg.Save(dir, rocketpool.SettingsFile); err != nil { + t.Fatal(err) + } + oldDefaults := rocketpool.Defaults + rocketpool.Defaults.ConfigPath = dir + t.Cleanup(func() { rocketpool.Defaults = oldDefaults }) + // Exercise the Docker command boundary without a real execution node. + script := `#!/bin/sh +[ "$1" = exec ] && [ "$2" = rocketpool_eth1 ] && [ "$3" = sh ] || exit 2 +if [ "$4" = -c ]; then + [ "$#" = 5 ] || exit 2 + case "$5" in *'/ethclient/geth/geth/chaindata'*) ;; *) exit 2 ;; esac +else + [ "$#" = 4 ] && [ "$4" = /setup/nethermind-db.sh ] || exit 2 +fi +[ "$TEST_DB_RESPONSE" != fail ] || exit 1 +printf '%s\n' "$TEST_DB_RESPONSE" +` + if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", dir+":"+os.Getenv("PATH")) + t.Setenv("TEST_DB_RESPONSE", tc.response) + reader, writer, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer reader.Close() + stdout := os.Stdout + os.Stdout = writer + defer func() { os.Stdout = stdout }() + err = getDBDetails() + writer.Close() + os.Stdout = stdout + output, readErr := io.ReadAll(reader) + if readErr != nil { + t.Fatal(readErr) + } + if (err != nil) != tc.wantErr || !strings.Contains(string(output), tc.want) { + t.Fatalf("got %q, %v; want %q, error=%v", output, err, tc.want, tc.wantErr) + } + }) + } +} diff --git a/shared/services/rocketpool/assets/install/scripts/nethermind-db.sh b/shared/services/rocketpool/assets/install/scripts/nethermind-db.sh index 9b2bfec9b..56b19861d 100644 --- a/shared/services/rocketpool/assets/install/scripts/nethermind-db.sh +++ b/shared/services/rocketpool/assets/install/scripts/nethermind-db.sh @@ -1,6 +1,5 @@ #!/bin/sh -# TODO(Hegota): Remove this detector when Smart Node drops Patricia state pruning. -# Shared by start-ec.sh and prune-eth1. Inspect persisted state, not just the flat +# Shared by start-ec.sh, prune-eth1, and get-db-details. Inspect persisted state, not just the flat # directory: Nethermind 2.0 also creates empty flat column families on Patricia. # Based on https://github.com/ethstaker/eth-docker/pull/2819. DB_ROOT=${1:-/ethclient/nethermind/nethermind_db} diff --git a/shared/services/rocketpool/assets/install/scripts/start-ec.sh b/shared/services/rocketpool/assets/install/scripts/start-ec.sh index 3d5628022..5fba6a5ae 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-ec.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-ec.sh @@ -198,16 +198,8 @@ if [ "$CLIENT" = "nethermind" ]; then # TODO(Hegota): Remove Patricia detection and the state pruning flags below. NETHERMIND_DB=$(sh /setup/nethermind-db.sh) || exit 1 # A fresh/resynced database uses FlatDB in v2.0 unless explicitly opted out. - if [ "$NETHERMIND_DB" = "none" ]; then - if printf '%s\n' "$EC_ADDITIONAL_FLAGS" | grep -Eiq -- '(^|[[:space:]])--(flatdb\.enabled|flatdb-enabled)(=|[[:space:]]+)false([[:space:]]|$)'; then - NETHERMIND_DB=patricia - elif ! printf '%s\n' "$EC_ADDITIONAL_FLAGS" | grep -Eiq -- '(^|[[:space:]])--(flatdb\.enabled|flatdb-enabled)(=|[[:space:]]+)true([[:space:]]|$)'; then - # Preserve fresh-sync auto pruning with the still-supported v1 images. - NETHERMIND_VERSION=$("$NETHERMIND_BINARY" --version) || exit 1 - if printf '%s\n' "$NETHERMIND_VERSION" | grep -Eq '(^|/|[[:space:]])v?1\.[0-9]'; then - NETHERMIND_DB=patricia - fi - fi + if [ "$NETHERMIND_DB" = "none" ] && printf '%s\n' "$EC_ADDITIONAL_FLAGS" | grep -Eiq -- '(^|[[:space:]])--(flatdb\.enabled|flatdb-enabled)(=|[[:space:]]+)false([[:space:]]|$)'; then + NETHERMIND_DB=patricia fi CMD="$PERF_PREFIX $NETHERMIND_BINARY \ diff --git a/shared/services/rocketpool/client.go b/shared/services/rocketpool/client.go index 98f2e10d6..498be34d3 100644 --- a/shared/services/rocketpool/client.go +++ b/shared/services/rocketpool/client.go @@ -954,7 +954,6 @@ func (c *Client) TouchEthclientMarker(container, volume, marker string) error { // NethermindDBLayout inspects the execution container's persisted state using the // same detector as client startup. Errors must not be treated as Patricia. -// TODO(Hegota): Remove this check and RunNethermindPruneStarter when dropping Patricia pruning. func (c *Client) NethermindDBLayout(executionContainerName string) (string, error) { cmd := fmt.Sprintf("docker exec %s sh /setup/nethermind-db.sh", shellescape.Quote(executionContainerName)) output, err := c.readOutput(cmd) @@ -971,6 +970,7 @@ func (c *Client) NethermindDBLayout(executionContainerName string) (string, erro } // Curls the Nethermind admin URL to trigger pruning +// TODO(Hegota): Remove RunNethermindPruneStarter when dropping Patricia pruning. func (c *Client) RunNethermindPruneStarter(executionContainerName string) error { retryCount := 5 retryTime := 3 * time.Second diff --git a/shared/services/rocketpool/db-details.go b/shared/services/rocketpool/db-details.go new file mode 100644 index 000000000..1caef8fec --- /dev/null +++ b/shared/services/rocketpool/db-details.go @@ -0,0 +1,68 @@ +package rocketpool + +import ( + "fmt" + "strconv" + "strings" + + "github.com/alessio/shellescape" +) + +// GethDBVersion reads metadata filenames without opening or locking the DB. +// Smart Node explicitly sets --datadir /ethclient/geth on every network. +func (c *Client) GethDBVersion(executionContainerName string) (string, error) { + const inspect = `if [ ! -d /ethclient/geth/geth/chaindata ]; then exit 0; fi; ls -1 /ethclient/geth/geth/chaindata` + output, err := c.readOutput(fmt.Sprintf("docker exec %s sh -c %s", shellescape.Quote(executionContainerName), shellescape.Quote(inspect))) + if err != nil { + return "", fmt.Errorf("could not inspect Geth database (the execution container must be running): %w", err) + } + return gethDBVersionFromFiles(string(output)) +} + +func gethDBVersionFromFiles(listing string) (string, error) { + var hasCurrent, hasManifest, hasOptions, hasFormat bool + var latestGeneration uint64 + format := uint64(1) // Pebble's legacy FormatMostCompatible has no format marker. + for _, name := range strings.Split(strings.TrimSpace(listing), "\n") { + switch { + case name == "CURRENT": + hasCurrent = true + case strings.HasPrefix(name, "marker.manifest."): + hasManifest = true + case strings.HasPrefix(name, "OPTIONS"): + hasOptions = true + case strings.HasPrefix(name, "marker.format-version."): + // Atomic markers encode marker.... + // Crashes may leave older markers behind; use the latest generation. + parts := strings.Split(name, ".") + if len(parts) != 4 { + return "", fmt.Errorf("invalid Pebble format marker %q", name) + } + generation, err := strconv.ParseUint(parts[2], 10, 64) + if err != nil { + return "", fmt.Errorf("invalid Pebble format marker %q: %w", name, err) + } + version, err := strconv.ParseUint(parts[3], 10, 64) + if err != nil || version == 0 { + return "", fmt.Errorf("invalid Pebble format version in %q", name) + } + if !hasFormat || generation > latestGeneration { + latestGeneration, format = generation, version + } + hasFormat = true + } + } + // Match Geth core/rawdb.PreexistingDatabase, including old LevelDB nodes. + if !hasManifest && !(hasCurrent && hasOptions) { + if hasCurrent { + return "leveldb", nil + } + return "none", nil + } + // Geth ethdb/pebble/version.go uses FormatFlushableIngest (13) as the + // minimum on-disk format opened with Pebble v2, including migrated DBs. + if format >= 13 { + return "v2", nil + } + return "v1", nil +} diff --git a/shared/services/rocketpool/db-details_test.go b/shared/services/rocketpool/db-details_test.go new file mode 100644 index 000000000..af2fda7b1 --- /dev/null +++ b/shared/services/rocketpool/db-details_test.go @@ -0,0 +1,30 @@ +package rocketpool + +import "testing" + +func TestGethDBVersionFromFiles(t *testing.T) { + for _, tc := range []struct { + name, files, want string + wantErr bool + }{ + {"empty", "", "none", false}, + {"leveldb", "CURRENT\nMANIFEST-000001\n", "leveldb", false}, + {"legacy pebble", "CURRENT\nOPTIONS-000001\n", "v1", false}, + {"v1 marker", "marker.manifest.000001.MANIFEST-000001\nmarker.format-version.000001.012\n", "v1", false}, + {"migrated v2", "marker.manifest.000001.MANIFEST-000001\nmarker.format-version.000002.013\n", "v2", false}, + {"newer v2", "marker.manifest.000001.MANIFEST-000001\nmarker.format-version.000002.016\n", "v2", false}, + {"stale marker", "CURRENT\nOPTIONS-000001\nmarker.format-version.000002.013\nmarker.format-version.000001.012\n", "v2", false}, + {"incomplete database", "OPTIONS-000001\n", "none", false}, + {"bad marker", "marker.format-version.bad\n", "", true}, + {"bad generation", "marker.format-version.bad.013\n", "", true}, + {"bad version", "marker.format-version.000001.bad\n", "", true}, + {"zero version", "marker.format-version.000001.000\n", "", true}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := gethDBVersionFromFiles(tc.files) + if got != tc.want || (err != nil) != tc.wantErr { + t.Fatalf("got %q, %v; want %q, error=%v", got, err, tc.want, tc.wantErr) + } + }) + } +} From 0e6a9de02f04d08f33d6f449bc8504e696c5dfce Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:01:15 -0300 Subject: [PATCH 3/5] Use FlatInTrie for machines with up to 16 GB RAM --- .../assets/install/scripts/start-ec.sh | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/shared/services/rocketpool/assets/install/scripts/start-ec.sh b/shared/services/rocketpool/assets/install/scripts/start-ec.sh index 5fba6a5ae..33df47587 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-ec.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-ec.sh @@ -195,13 +195,31 @@ if [ "$CLIENT" = "nethermind" ]; then RP_NETHERMIND_NETWORK="${RP_NETHERMIND_NETWORK}_archive" fi - # TODO(Hegota): Remove Patricia detection and the state pruning flags below. + # Detect persisted state before selecting pruning flags or a fresh FlatDB layout. NETHERMIND_DB=$(sh /setup/nethermind-db.sh) || exit 1 # A fresh/resynced database uses FlatDB in v2.0 unless explicitly opted out. if [ "$NETHERMIND_DB" = "none" ] && printf '%s\n' "$EC_ADDITIONAL_FLAGS" | grep -Eiq -- '(^|[[:space:]])--(flatdb\.enabled|flatdb-enabled)(=|[[:space:]]+)false([[:space:]]|$)'; then NETHERMIND_DB=patricia fi + # Prefer FlatInTrie for fresh FlatDB syncs on machines with up to 16 GiB RAM. + # Persisting the choice so restarts and RAM upgrades cannot change an existing database's layout. resync-eth1 + # deletes this marker together with the execution data volume. + NETHERMIND_LAYOUT_FLAGS="" + NETHERMIND_LAYOUT_MARKER=/ethclient/nethermind/.smartnode-flat-in-trie + if [ "$NETHERMIND_DB" != "patricia" ] && + [ -z "$NETHERMIND_FLATDBCONFIG_LAYOUT" ] && [ -z "$NETHERMIND_FLATDB_LAYOUT" ] && + ! printf '%s\n' "$EC_ADDITIONAL_FLAGS" | grep -Eiq -- '(^|[[:space:]])--(flatdb\.layout|flatdb-layout)(=|[[:space:]])'; then + if [ "$NETHERMIND_DB" = "none" ] && [ ! -f "$NETHERMIND_LAYOUT_MARKER" ] && + [ "$(awk '/^MemTotal:/ {print ($2 > 0 && $2 <= 16777216)}' /proc/meminfo)" = "1" ]; then + mkdir -p /ethclient/nethermind && touch "$NETHERMIND_LAYOUT_MARKER" || exit 1 + fi + if [ -f "$NETHERMIND_LAYOUT_MARKER" ]; then + NETHERMIND_LAYOUT_FLAGS="--FlatDb.Layout FlatInTrie" + echo "Using FlatInTrie, the saved Nethermind layout selected for a system with up to 16 GiB RAM." + fi + fi + CMD="$PERF_PREFIX $NETHERMIND_BINARY \ --config $RP_NETHERMIND_NETWORK \ --data-dir /ethclient/nethermind \ @@ -213,6 +231,7 @@ if [ "$CLIENT" = "nethermind" ]; then --Init.WebSocketsEnabled true \ --JsonRpc.WebSocketsPort ${EC_WS_PORT:-8546} \ --JsonRpc.JwtSecretFile=/secrets/jwtsecret \ + $NETHERMIND_LAYOUT_FLAGS \ $EC_ADDITIONAL_FLAGS" # TODO(Hegota): Drop automatic state pruning; FlatDB does not use it. From 709764bae61336f5d88320caf815840a5ffa768f Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:29:32 -0300 Subject: [PATCH 4/5] nm-v2.0 --- shared/services/rocketpool/assets/install/mainnet.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/services/rocketpool/assets/install/mainnet.env b/shared/services/rocketpool/assets/install/mainnet.env index 00aa7a7f2..40f451f97 100644 --- a/shared/services/rocketpool/assets/install/mainnet.env +++ b/shared/services/rocketpool/assets/install/mainnet.env @@ -7,7 +7,7 @@ RP_IMAGE_SMARTNODE=rocketpool/smartnode:v1.24.2@sha256:0e94df463f2fb461e16f9b45e8eaae38caa639e714167d6f28b2c8f6570bc7a5 RP_IMAGE_GETH=ethereum/client-go:v1.17.5@sha256:523d3ba26623a619e912019068dc2784f02934070ac46bdae4d5b9df0d917814 -RP_IMAGE_NETHERMIND=nethermind/nethermind:2.0.0-rc2@sha256:98c6d4bf61c3904fe10bb8a40520bf13cccc228b2859b9e99a2bc34a76dbdf10 +RP_IMAGE_NETHERMIND=nethermind/nethermind:2.0.0@sha256:8156768beb584a1e62c3dcf0e0da7be61418dc08c89a80a4f9a3a48e5d247c12 RP_IMAGE_BESU=hyperledger/besu:26.8.1@sha256:6f3f21ce533383fcc8db3bce02252b59d5a9e776b72b5a1c8ecd2db011600042 RP_IMAGE_RETH=ghcr.io/paradigmxyz/reth:v2.6.0@sha256:8ce703acf113b2a20705b6e76adebed20f74ba8591bc7c9407203b2968aca70d RP_IMAGE_ERIGON=erigontech/erigon:v3.6.1@sha256:8cd3fbcbb35d8b16768225ff7494a0672b7b06c9bf13d466850aabb920ac0de7 From 81972256e6e2e15c2f1705499a3b560099373d3f Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:45:23 -0300 Subject: [PATCH 5/5] Fix lint --- shared/services/rocketpool/db-details.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/services/rocketpool/db-details.go b/shared/services/rocketpool/db-details.go index 1caef8fec..28f6d5ee9 100644 --- a/shared/services/rocketpool/db-details.go +++ b/shared/services/rocketpool/db-details.go @@ -53,7 +53,7 @@ func gethDBVersionFromFiles(listing string) (string, error) { } } // Match Geth core/rawdb.PreexistingDatabase, including old LevelDB nodes. - if !hasManifest && !(hasCurrent && hasOptions) { + if !hasManifest && (!hasCurrent || !hasOptions) { if hasCurrent { return "leveldb", nil }