Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions rocketpool-cli/service/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
60 changes: 60 additions & 0 deletions rocketpool-cli/service/db-details.go
Original file line number Diff line number Diff line change
@@ -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
}
89 changes: 89 additions & 0 deletions rocketpool-cli/service/db-details_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
37 changes: 27 additions & 10 deletions rocketpool-cli/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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.")
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions shared/services/config/nethermind-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down Expand Up @@ -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},
Expand All @@ -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},
Expand All @@ -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},
Expand All @@ -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},
Expand Down
2 changes: 1 addition & 1 deletion shared/services/rocketpool/assets/install/mainnet.env
Original file line number Diff line number Diff line change
Expand Up @@ -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@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
Expand Down
24 changes: 24 additions & 0 deletions shared/services/rocketpool/assets/install/scripts/nethermind-db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# 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}

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
Loading
Loading