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
38 changes: 38 additions & 0 deletions pkg/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ func InitKeyring(cfg config.KeyringConfig) (sdkkeyring.Keyring, error) {
if err != nil {
return nil, err
}
// Validate non-interactive passphrase sources early so we can emit
// clear errors for misconfigured environment variables or files.
if err := validatePassphraseConfig(cfg, backend); err != nil {
return nil, err
}
// Use the directory as-is, it should already be resolved by the config
dir := cfg.Dir

Expand Down Expand Up @@ -112,6 +117,39 @@ func selectPassphrase(cfg config.KeyringConfig) string {
return ""
}

// validatePassphraseConfig ensures that configured non-interactive passphrase
// sources are usable. It does not enforce that a passphrase is provided at all
// (interactive mode is still allowed); it only catches obvious misconfigurations
// such as an empty environment variable or unreadable/empty file.
func validatePassphraseConfig(cfg config.KeyringConfig, backend string) error {
backend = strings.ToLower(backend)
// The "test" backend never uses a passphrase.
if backend == "test" {
return nil
}

// If an environment variable is configured, it must be set and non-empty.
if cfg.PassEnv != "" {
val, ok := os.LookupEnv(cfg.PassEnv)
if !ok || strings.TrimSpace(val) == "" {
return fmt.Errorf("keyring passphrase environment variable %q is not set or is empty", cfg.PassEnv)
}
}

// If a passphrase file is configured, it must be readable and non-empty.
if cfg.PassFile != "" {
b, err := os.ReadFile(cfg.PassFile)
if err != nil {
return fmt.Errorf("failed to read keyring passphrase file %q: %w", cfg.PassFile, err)
}
if strings.TrimSpace(string(b)) == "" {
return fmt.Errorf("keyring passphrase file %q is empty", cfg.PassFile)
}
}

return nil
}

func normaliseBackend(b string) (string, error) {
switch strings.ToLower(b) {
case "file":
Expand Down
19 changes: 18 additions & 1 deletion sn-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,22 @@ Auto-update checks run every 10 minutes when enabled.
- `--lumera-grpc` - gRPC endpoint
- `--chain-id` - Chain identifier

### Keyring configuration details

All keyring-related flags are forwarded directly to `supernode init` and configure the underlying Cosmos SDK keyring used by SuperNode:

- `--keyring-backend` controls where and how keys are stored:
- `file` – encrypted keyring files under `<BASE_DIR>/.supernode/keys/keyring-file` (recommended for servers).
- `os` – use the operating system’s keyring (where supported).
- `test` – in-memory, unencrypted test keys (development only).
- Passphrase options configure how the keyring unlocks keys in non-interactive mode:
- `--keyring-passphrase` – passphrase provided directly on the command line (only for testing; avoid in production).
- `--keyring-passphrase-env` – name of an environment variable containing the passphrase. The variable must be set and non-empty; otherwise `supernode start` will fail with a clear error.
- `--keyring-passphrase-file` – path to a file containing the passphrase. The file must be readable and non-empty.
- If none of the passphrase flags are provided, the keyring will prompt interactively when needed.

For more background and examples, see the `supernode init` section in the top-level `README.md`, which documents these flags in the context of SuperNode itself.

## Commands

- `init` - Initialize sn-manager and SuperNode
Expand Down Expand Up @@ -327,7 +343,8 @@ The auto-updater follows stable-only, same-major update rules and defers updates

Mechanics and notes:
- Stable-only: auto-updater targets latest stable GitHub release (non-draft, non-prerelease).
- Same-major only: SuperNode and sn-manager auto-update only when the latest is the same major version (the number before the first dot). Example: 1.7 → 1.8 = allowed; 1.x → 2.0 = manual.
- Same-major only (periodic checks): during regular background checks, SuperNode and sn-manager auto-update only when the latest is the same major version (the number before the first dot). Example: 1.7 → 1.8 = allowed; 1.x → 2.0 = manual.
- Startup sync: when `sn-manager start` runs, it performs a one-time forced sync to the latest stable release for both sn-manager and SuperNode. This startup sync may update across major versions and does not wait for the gateway to be idle; failures are logged but do not block startup.
- Gateway-aware: updates are applied only when the gateway reports no running tasks; otherwise they are deferred.
- Gateway errors: repeated check failures over a 5-minute window request a clean SuperNode restart (no version change) to recover.
- Combined tarball: when updating, sn-manager downloads a single tarball once, then updates itself first (if eligible), then installs/activates the new SuperNode version.
Expand Down
7 changes: 5 additions & 2 deletions sn-manager/cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ func checkInitialized() error {
homeDir := config.GetManagerHome()
configPath := filepath.Join(homeDir, "config.yml")

if _, err := os.Stat(configPath); os.IsNotExist(err) {
return fmt.Errorf("not initialized. Run: sn-manager init")
if _, err := os.Stat(configPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("not initialized. Run: sn-manager init")
}
return fmt.Errorf("failed to check manager config: %w", err)
}

return nil
Expand Down
14 changes: 13 additions & 1 deletion sn-manager/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ func promptForManagerConfig(flags *initFlags) error {
}

func runInit(cmd *cobra.Command, args []string) error {
// If the user explicitly asks for help on this subcommand, show usage
// without performing any initialization or forwarding flags to supernode.
for _, a := range args {
if a == "--help" || a == "-h" {
return cmd.Help()
}
}

// Parse flags
flags := parseInitFlags(args)

Expand Down Expand Up @@ -224,7 +232,11 @@ func runInit(cmd *cobra.Command, args []string) error {
fmt.Println("\nStep 3: Initializing SuperNode...")

// Check if SuperNode is already initialized
supernodeConfigPath := filepath.Join(os.Getenv("HOME"), ".supernode", "config.yml")
userHome, _ := os.UserHomeDir()
if userHome == "" {
userHome = os.Getenv("HOME")
}
supernodeConfigPath := filepath.Join(userHome, ".supernode", "config.yml")
if _, err := os.Stat(supernodeConfigPath); err == nil {
fmt.Println("✓ SuperNode already initialized, skipping initialization")
} else {
Expand Down
115 changes: 60 additions & 55 deletions sn-manager/cmd/supernode_start.go
Original file line number Diff line number Diff line change
@@ -1,71 +1,76 @@
package cmd

import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"

"github.com/spf13/cobra"
"github.com/spf13/cobra"
)

var supernodeStartCmd = &cobra.Command{
Use: "start",
Short: "Start the managed SuperNode",
Long: `Signal the running sn-manager to start SuperNode. Requires sn-manager service to be running.`,
RunE: runSupernodeStart,
Use: "start",
Short: "Start the managed SuperNode",
Long: `Signal the running sn-manager to start SuperNode. Requires sn-manager service to be running.`,
RunE: runSupernodeStart,
}

func runSupernodeStart(cmd *cobra.Command, args []string) error {
// Ensure manager is initialized
if err := checkInitialized(); err != nil {
return err
}
// Ensure manager is initialized
if err := checkInitialized(); err != nil {
return err
}

home := getHomeDir()
home := getHomeDir()

// Check if sn-manager (service) is running via manager PID file
managerPidPath := filepath.Join(home, managerPIDFile)
data, err := os.ReadFile(managerPidPath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("sn-manager is not running. Start it with: sn-manager start")
return nil
}
return fmt.Errorf("failed to read sn-manager PID: %w", err)
}
pidStr := strings.TrimSpace(string(data))
pid, _ := strconv.Atoi(pidStr)
proc, alive := getProcessIfAlive(pid)
if !alive {
// Stale PID file, clean up and instruct user
_ = os.Remove(managerPidPath)
fmt.Println("sn-manager is not running. Start it with: sn-manager start")
return nil
}
// Best-effort ping
_ = proc.Signal(syscall.Signal(0))
// Check if sn-manager (service) is running via manager PID file
managerPidPath := filepath.Join(home, managerPIDFile)
data, err := os.ReadFile(managerPidPath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("sn-manager is not running. Start it with: sn-manager start")
return nil
}
return fmt.Errorf("failed to read sn-manager PID: %w", err)
}
pidStr := strings.TrimSpace(string(data))
pid, err := strconv.Atoi(pidStr)
if err != nil || pid <= 0 {
// Invalid PID contents; treat as stale and ask user to restart manager
_ = os.Remove(managerPidPath)
fmt.Println("sn-manager is not running. Start it with: sn-manager start")
return nil
}
proc, alive := getProcessIfAlive(pid)
if !alive {
// Stale PID file, clean up and instruct user
_ = os.Remove(managerPidPath)
fmt.Println("sn-manager is not running. Start it with: sn-manager start")
return nil
}
// Best-effort ping
_ = proc.Signal(syscall.Signal(0))

// Remove stop marker to allow the manager to start SuperNode
stopMarker := filepath.Join(home, stopMarkerFile)
if err := os.Remove(stopMarker); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to clear stop marker: %w", err)
}
// Remove stop marker to allow the manager to start SuperNode
stopMarker := filepath.Join(home, stopMarkerFile)
if err := os.Remove(stopMarker); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to clear stop marker: %w", err)
}

// If SuperNode already running, just inform
pidPath := filepath.Join(home, supernodePIDFile)
if p, err := readPIDFromFile(pidPath); err == nil {
if _, ok := getProcessIfAlive(p); ok {
fmt.Println("SuperNode is already running")
return nil
}
// stale pid file
_ = os.Remove(pidPath)
}
// If SuperNode already running, just inform
pidPath := filepath.Join(home, supernodePIDFile)
if p, err := readPIDFromFile(pidPath); err == nil {
if _, ok := getProcessIfAlive(p); ok {
fmt.Println("SuperNode is already running")
return nil
}
// stale pid file
_ = os.Remove(pidPath)
}

fmt.Println("Request sent. The running sn-manager will start SuperNode shortly.")
return nil
fmt.Println("Request sent. The running sn-manager will start SuperNode shortly.")
return nil
}

26 changes: 13 additions & 13 deletions sn-manager/go.mod
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
module github.com/LumeraProtocol/supernode/v2/sn-manager

go 1.24.1
go 1.25.1

require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/LumeraProtocol/supernode/v2 v2.0.0-00010101000000-000000000000
github.com/spf13/cobra v1.8.1
github.com/spf13/cobra v1.10.1
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.10 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/grpc v1.76.0 // indirect
)

replace github.com/LumeraProtocol/supernode/v2 => ../
Loading