diff --git a/cmd/cel-key/node_types.go b/cmd/cel-key/node_types.go index 76cc690af7..a70889a263 100644 --- a/cmd/cel-key/node_types.go +++ b/cmd/cel-key/node_types.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" - nodecmd "github.com/celestiaorg/celestia-node/cmd" + "github.com/celestiaorg/celestia-node/nodebuilder" "github.com/celestiaorg/celestia-node/nodebuilder/p2p" ) @@ -55,7 +55,7 @@ func ParseDirectoryFlags(cmd *cobra.Command) error { } switch nodeType { case "bridge", "full", "light": - path, err := nodecmd.DefaultNodeStorePath(nodeType, network) + path, err := nodebuilder.DefaultNodeStorePath(nodeType, network) if err != nil { return err } diff --git a/cmd/flags_node.go b/cmd/flags_node.go index ef5af26580..bc86cb4a27 100644 --- a/cmd/flags_node.go +++ b/cmd/flags_node.go @@ -3,9 +3,7 @@ package cmd import ( "context" "fmt" - "os" "path/filepath" - "strings" "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" @@ -44,7 +42,7 @@ func ParseNodeFlags(ctx context.Context, cmd *cobra.Command, network p2p.Network if store == "" { tp := NodeType(ctx) var err error - store, err = DefaultNodeStorePath(tp.String(), network.String()) + store, err = nodebuilder.DefaultNodeStorePath(tp.String(), network.String()) if err != nil { return ctx, err } @@ -74,27 +72,3 @@ func ParseNodeFlags(ctx context.Context, cmd *cobra.Command, network p2p.Network } return ctx, nil } - -// DefaultNodeStorePath constructs the default node store path using the given -// node type and network. -func DefaultNodeStorePath(tp string, network string) (string, error) { - home := os.Getenv("CELESTIA_HOME") - - if home == "" { - var err error - home, err = os.UserHomeDir() - if err != nil { - return "", err - } - } - if network == p2p.Mainnet.String() { - return fmt.Sprintf("%s/.celestia-%s", home, strings.ToLower(tp)), nil - } - // only include network name in path for testnets and custom networks - return fmt.Sprintf( - "%s/.celestia-%s-%s", - home, - strings.ToLower(tp), - strings.ToLower(network), - ), nil -} diff --git a/cmd/rpc.go b/cmd/rpc.go index 1935069229..2a6c267d54 100644 --- a/cmd/rpc.go +++ b/cmd/rpc.go @@ -4,20 +4,17 @@ import ( "context" "errors" "fmt" + "path/filepath" "github.com/spf13/cobra" flag "github.com/spf13/pflag" rpc "github.com/celestiaorg/celestia-node/api/rpc/client" "github.com/celestiaorg/celestia-node/api/rpc/perms" + "github.com/celestiaorg/celestia-node/nodebuilder" nodemod "github.com/celestiaorg/celestia-node/nodebuilder/node" ) -const ( - // defaultRPCAddress is a default address to dial to - defaultRPCAddress = "http://localhost:26658" -) - var ( requestURL string authTokenFlag string @@ -29,7 +26,7 @@ func RPCFlags() *flag.FlagSet { fset.StringVar( &requestURL, "url", - defaultRPCAddress, + "", // will try to load value from Config, which defines its own default url "Request URL", ) @@ -47,16 +44,33 @@ func RPCFlags() *flag.FlagSet { func InitClient(cmd *cobra.Command, _ []string) error { if authTokenFlag == "" { - storePath := "" - if !cmd.Flag(nodeStoreFlag).Changed { - return errors.New("cant get the access to the auth token: token/node-store flag was not specified") + rootErrMsg := "cant access the auth token" + + storePath, err := getStorePath(cmd) + if err != nil { + return fmt.Errorf("%s: %v", rootErrMsg, err) } - storePath = cmd.Flag(nodeStoreFlag).Value.String() - token, err := getToken(storePath) + + cfg, err := nodebuilder.LoadConfig(filepath.Join(storePath, "config.toml")) if err != nil { - return fmt.Errorf("cant get the access to the auth token: %v", err) + return fmt.Errorf("%s: root directory was not specified: %v", rootErrMsg, err) + } + + if requestURL == "" { + requestURL = cfg.RPC.RequestURL() + } + + // only get token if auth is not skipped + if cfg.RPC.SkipAuth { + authTokenFlag = "skip" // arbitrary value required + } else { + token, err := getToken(storePath) + if err != nil { + return fmt.Errorf("%s: %v", rootErrMsg, err) + } + + authTokenFlag = token } - authTokenFlag = token } client, err := rpc.NewClient(cmd.Context(), requestURL, authTokenFlag) @@ -69,6 +83,21 @@ func InitClient(cmd *cobra.Command, _ []string) error { return nil } +func getStorePath(cmd *cobra.Command) (string, error) { + // if node store flag is set, use it + if cmd.Flag(nodeStoreFlag).Changed { + return cmd.Flag(nodeStoreFlag).Value.String(), nil + } + + // try to detect a running node + path, err := nodebuilder.DiscoverOpened() + if err != nil { + return "", fmt.Errorf("token/node-store flag was not specified: %w", err) + } + + return path, nil +} + func getToken(path string) (string, error) { if path == "" { return "", errors.New("root directory was not specified") diff --git a/nodebuilder/node/type.go b/nodebuilder/node/type.go index 2f09b26503..a86d802af1 100644 --- a/nodebuilder/node/type.go +++ b/nodebuilder/node/type.go @@ -57,3 +57,11 @@ var stringToType = map[string]Type{ "Light": Light, "Full": Full, } + +// orderedTypes is a slice of all valid types in order of priority. +var orderedTypes = []Type{Bridge, Full, Light} + +// GetTypes returns a list of all known types in order of priority. +func GetTypes() []Type { + return append([]Type(nil), orderedTypes...) +} diff --git a/nodebuilder/p2p/flags.go b/nodebuilder/p2p/flags.go index 8e7c0f8bc0..7c2fc7bfad 100644 --- a/nodebuilder/p2p/flags.go +++ b/nodebuilder/p2p/flags.go @@ -35,7 +35,7 @@ Peers must bidirectionally point to each other. (Format: multiformats.io/multiad DefaultNetwork.String(), fmt.Sprintf("The name of the network to connect to, e.g. %s. Must be passed on "+ "both init and start to take effect. Assumes mainnet (%s) unless otherwise specified.", - listProvidedNetworks(), + listAvailableNetworks(), DefaultNetwork.String()), ) @@ -74,7 +74,7 @@ func ParseNetwork(cmd *cobra.Command) (Network, error) { parsed := cmd.Flag(networkFlag).Value.String() switch parsed { case "": - return "", fmt.Errorf("no network provided, allowed values: %s", listProvidedNetworks()) + return "", fmt.Errorf("no network provided, allowed values: %s", listAvailableNetworks()) case DefaultNetwork.String(): return DefaultNetwork, nil @@ -83,7 +83,7 @@ func ParseNetwork(cmd *cobra.Command) (Network, error) { if net, err := Network(parsed).Validate(); err == nil { return net, nil } - return "", fmt.Errorf("invalid network specified: %s, allowed values: %s", parsed, listProvidedNetworks()) + return "", fmt.Errorf("invalid network specified: %s, allowed values: %s", parsed, listAvailableNetworks()) } } @@ -104,7 +104,7 @@ func parseNetworkFromEnv() (Network, error) { } netID := params[0] network = Network(netID) - networksList[network] = struct{}{} + addCustomNetwork(network) // check if genesis hash provided and register it if exists if len(params) >= 2 { genHash := params[1] diff --git a/nodebuilder/p2p/network.go b/nodebuilder/p2p/network.go index 53893eff7c..eb44169ee6 100644 --- a/nodebuilder/p2p/network.go +++ b/nodebuilder/p2p/network.go @@ -2,6 +2,7 @@ package p2p import ( "errors" + "strings" "time" "github.com/libp2p/go-libp2p/core/peer" @@ -68,16 +69,31 @@ var networkAliases = map[string]Network{ "private": Private, } -// listProvidedNetworks provides a string listing all known long-standing networks for things like -// command hints. -func listProvidedNetworks() string { - var networks string - for net := range networksList { - // "private" network isn't really a choosable option, so skip +// orderedNetworks is a list of all known networks in order of priority. +var orderedNetworks = []Network{Mainnet, Mocha, Arabica, Private} + +// GetOrderedNetworks provides a list of all known networks in order of priority. +func GetNetworks() []Network { + return append([]Network(nil), orderedNetworks...) +} + +// listAvailableNetworks provides a string listing all known long-standing networks for things +// like CLI hints. +func listAvailableNetworks() string { + var networks []string + for _, net := range orderedNetworks { + // "private" networks are configured via env vars, so skip if net != Private { - networks += string(net) + ", " + networks = append(networks, net.String()) } } - // chop off trailing ", " - return networks[:len(networks)-2] + + return strings.Join(networks, ", ") +} + +// addCustomNetwork adds a custom network to the list of known networks. +func addCustomNetwork(network Network) { + networksList[network] = struct{}{} + networkAliases[network.String()] = network + orderedNetworks = append(orderedNetworks, network) } diff --git a/nodebuilder/rpc/config.go b/nodebuilder/rpc/config.go index d6031082a8..63893683ad 100644 --- a/nodebuilder/rpc/config.go +++ b/nodebuilder/rpc/config.go @@ -3,6 +3,7 @@ package rpc import ( "fmt" "strconv" + "strings" "github.com/celestiaorg/celestia-node/libs/utils" ) @@ -22,6 +23,16 @@ func DefaultConfig() Config { } } +func (cfg *Config) RequestURL() string { + if strings.HasPrefix(cfg.Address, "://") { + parts := strings.Split(cfg.Address, "://") + return fmt.Sprintf("%s://%s:%s", parts[0], parts[1], cfg.Port) + } + + // Default to HTTP if no protocol is specified + return fmt.Sprintf("http://%s:%s", cfg.Address, cfg.Port) +} + func (cfg *Config) Validate() error { sanitizedAddress, err := utils.ValidateAddr(cfg.Address) if err != nil { diff --git a/nodebuilder/store.go b/nodebuilder/store.go index ba6c19eaa2..b9ff165a96 100644 --- a/nodebuilder/store.go +++ b/nodebuilder/store.go @@ -3,8 +3,10 @@ package nodebuilder import ( "errors" "fmt" + "os" "path/filepath" "runtime" + "strings" "sync" "time" @@ -16,6 +18,8 @@ import ( "github.com/mitchellh/go-homedir" "github.com/celestiaorg/celestia-node/libs/keystore" + nodemod "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/nodebuilder/p2p" "github.com/celestiaorg/celestia-node/share" ) @@ -24,6 +28,8 @@ var ( ErrOpened = errors.New("node: store is in use") // ErrNotInited is thrown on attempt to open Store without initialization. ErrNotInited = errors.New("node: store is not initialized") + // ErrNoOpenStore is thrown when no opened Store is found, indicating that no node is running. + ErrNoOpenStore = errors.New("no opened Node Store found (no node is running)") ) // Store encapsulates storage for the Node. Basically, it is the Store of all Stores. @@ -150,6 +156,73 @@ type fsStore struct { dirLock *flock.Flock // protects directory } +// DiscoverOpened finds a path of an opened Node Store and returns its path. +// If multiple nodes are running, it only returns the path of the first found node. +// Network is favored over node type. +// +// Network preference order: Mainnet, Mocha, Arabica, Private, Custom +// Type preference order: Bridge, Full, Light +func DiscoverOpened() (string, error) { + defaultNetwork := p2p.GetNetworks() + nodeTypes := nodemod.GetTypes() + + for _, n := range defaultNetwork { + for _, tp := range nodeTypes { + path, err := DefaultNodeStorePath(tp.String(), n.String()) + if err != nil { + return "", err + } + + ok, _ := IsOpened(path) + if ok { + return path, nil + } + } + } + + return "", ErrNoOpenStore +} + +// DefaultNodeStorePath constructs the default node store path using the given +// node type and network. +var DefaultNodeStorePath = func(tp string, network string) (string, error) { + home := os.Getenv("CELESTIA_HOME") + + if home == "" { + var err error + home, err = os.UserHomeDir() + if err != nil { + return "", err + } + } + if network == p2p.Mainnet.String() { + return fmt.Sprintf("%s/.celestia-%s", home, strings.ToLower(tp)), nil + } + // only include network name in path for testnets and custom networks + return fmt.Sprintf( + "%s/.celestia-%s-%s", + home, + strings.ToLower(tp), + strings.ToLower(network), + ), nil +} + +// IsOpened checks if the Store is opened in a directory by checking its file lock. +func IsOpened(path string) (bool, error) { + flk := flock.New(lockPath(path)) + ok, err := flk.TryLock() + if err != nil { + return false, fmt.Errorf("locking file: %w", err) + } + + err = flk.Unlock() + if err != nil { + return false, fmt.Errorf("unlocking file: %w", err) + } + + return !ok, nil +} + func storePath(path string) (string, error) { return homedir.Expand(filepath.Clean(path)) } diff --git a/nodebuilder/store_test.go b/nodebuilder/store_test.go index 51bd89c5a7..4193d8b79b 100644 --- a/nodebuilder/store_test.go +++ b/nodebuilder/store_test.go @@ -4,10 +4,12 @@ package nodebuilder import ( "context" + "fmt" "strconv" "testing" "time" + "github.com/gofrs/flock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,6 +19,7 @@ import ( "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/nodebuilder/p2p" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/eds/edstest" @@ -160,6 +163,106 @@ func TestStoreRestart(t *testing.T) { } } +func TestDiscoverOpened(t *testing.T) { + t.Run("single open store", func(t *testing.T) { + _, dir := initAndOpenStore(t, node.Full) + + mockDefaultNodeStorePath := func(t string, n string) (string, error) { + return dir, nil + } + DefaultNodeStorePath = mockDefaultNodeStorePath + + path, err := DiscoverOpened() + require.NoError(t, err) + require.Equal(t, dir, path) + }) + + t.Run("multiple open nodes by preference order", func(t *testing.T) { + networks := []p2p.Network{p2p.Mainnet, p2p.Mocha, p2p.Arabica, p2p.Private} + nodeTypes := []node.Type{node.Bridge, node.Full, node.Light} + + // Store opened stores in a map (network + node -> dir/store) + dirMap := make(map[string]string) + storeMap := make(map[string]Store) + for _, network := range networks { + for _, tp := range nodeTypes { + store, dir := initAndOpenStore(t, tp) + key := network.String() + "_" + tp.String() + dirMap[key] = dir + storeMap[key] = store + } + } + + mockDefaultNodeStorePath := func(tp string, n string) (string, error) { + key := n + "_" + tp + if dir, ok := dirMap[key]; ok { + return dir, nil + } + return "", fmt.Errorf("no store for %s_%s", n, tp) + } + DefaultNodeStorePath = mockDefaultNodeStorePath + + // Discover opened stores in preference order + for _, network := range networks { + for _, tp := range nodeTypes { + path, err := DiscoverOpened() + require.NoError(t, err) + key := network.String() + "_" + tp.String() + require.Equal(t, dirMap[key], path) + + // close the store to discover the next one + storeMap[key].Close() + } + } + }) + + t.Run("no opened store", func(t *testing.T) { + dir := t.TempDir() + mockDefaultNodeStorePath := func(t string, n string) (string, error) { + return dir, nil + } + DefaultNodeStorePath = mockDefaultNodeStorePath + + path, err := DiscoverOpened() + assert.ErrorIs(t, err, ErrNoOpenStore) + assert.Empty(t, path) + }) +} + +func TestIsOpened(t *testing.T) { + dir := t.TempDir() + + // Case 1: non-existent node store + ok, err := IsOpened(dir) + require.NoError(t, err) + require.False(t, ok) + + // Case 2: initialized node store, not locked + err = Init(*DefaultConfig(node.Full), dir, node.Full) + require.NoError(t, err) + ok, err = IsOpened(dir) + require.NoError(t, err) + require.False(t, ok) + + // Case 3: initialized node store, locked + flk := flock.New(lockPath(dir)) + _, err = flk.TryLock() + require.NoError(t, err) + defer flk.Unlock() //nolint:errcheck + ok, err = IsOpened(dir) + require.NoError(t, err) + require.True(t, ok) +} + +func initAndOpenStore(t *testing.T, tp node.Type) (store Store, dir string) { + dir = t.TempDir() + err := Init(*DefaultConfig(tp), dir, tp) + require.NoError(t, err) + store, err = OpenStore(dir, nil) + require.NoError(t, err) + return store, dir +} + type store struct { s Store edsStore *eds.Store