Skip to content

Commit

Permalink
feat(cmd/rpc): Automatically detect running node for RPC requests (#3246
Browse files Browse the repository at this point in the history
)

Closes #2859.

TL;DR Previously, the node.store flag had to be specified manually for each RPC request. This commit introduces automatic detection of the running node.

Assumptions:
- presence of lock indicates a running node
- specific order of networks (mainnet, mocha, arabica, private, custom) and type (bridge, full, light)
- 1 network will only have 1 running node type. multiple nodes of same network, same type are disallowed (receive `Error: node: store is in use`).
- auth token, other flags still retain prev behavior and have priority
- address, port read in from config
- skipAuth skips auth for trusted setups
- aligns with Unix daemon conventions
- non-default node store & cel-key still require config flags

**Sample Test Cases**
1. Node store set
```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY= --node.store=$NODE_STORE
{
  "result": {
    "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAAEJpDCBNOWAP3dM=",
    "data": "0x676d",
    "share_version": 0,
    "commitment": "0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY=",
    "index": 23
  }
}
```

2. Node store not set, but flag specified
```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY= --node.store=
Error: cant access the auth token: root directory was not specified: unable to load in config: open config.toml: no such file or directory
```

3. No node store flag specified, yay
```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY=
{ // valid result, same as 1. }
```

4. Multiple networks running, will go to mainnet before mocha
```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY=
{
  "result": "RPC client error: sendRequest failed: http status 401 Unauthorized unmarshaling response: EOF"
}
```

5. Multiple networks running, will go to mocha before arabica
 ```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY=
{
   "result": "header: given height is from the future: networkHeight: 802095, requestedHeight: 1318129"
}
```

6.  Run node with rpc config Address: 0.0.0.1 and Port: 25231 -- accurately directs request
```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY=
{
  "result": "RPC client error: sendRequest failed: Post \"http://0.0.0.1:25231\": dial tcp 0.0.0.1:25231: connect: no route to host"
}
```

7. Run node with --rpc.skip_auth=true and config with SkipAuth: true (prints log in rpc/server [here](https://github.com/celestiaorg/celestia-node/blob/main/api/rpc/server.go#L57))
```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY=
{ // valid result, same as 1. }
```

8. Run node with --rpc.skip_auth=true and config with SkipAuth: false (server skips auth, prints log in rpc/server [here](https://github.com/celestiaorg/celestia-node/blob/main/api/rpc/server.go#L57))
```
❯ celestia blob get 1318129 0x42690c204d39600fddd3 0MFhYKQUi2BU+U1jxPzG7QY2BVV1lb3kiU+zAK7nUiY=
{ // valid result, same as 1. }
```

Co-authored-by: Samwise Gamgee <samwisegamgee369@proton.me>
  • Loading branch information
mastergaurang94 and samwisegamgee369 authored Apr 17, 2024
1 parent ad04298 commit 29678cb
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 55 deletions.
4 changes: 2 additions & 2 deletions cmd/cel-key/node_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand Down
28 changes: 1 addition & 27 deletions cmd/flags_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
55 changes: 42 additions & 13 deletions cmd/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
)

Expand All @@ -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)
Expand All @@ -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")
Expand Down
8 changes: 8 additions & 0 deletions nodebuilder/node/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
8 changes: 4 additions & 4 deletions nodebuilder/p2p/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
)

Expand Down Expand Up @@ -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
Expand All @@ -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())
}
}

Expand All @@ -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]
Expand Down
34 changes: 25 additions & 9 deletions nodebuilder/p2p/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package p2p

import (
"errors"
"strings"
"time"

"github.com/libp2p/go-libp2p/core/peer"
Expand Down Expand Up @@ -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)
}
11 changes: 11 additions & 0 deletions nodebuilder/rpc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rpc
import (
"fmt"
"strconv"
"strings"

"github.com/celestiaorg/celestia-node/libs/utils"
)
Expand All @@ -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 {
Expand Down
73 changes: 73 additions & 0 deletions nodebuilder/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package nodebuilder
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"

Expand All @@ -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"
)

Expand All @@ -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.
Expand Down Expand Up @@ -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))
}
Expand Down
Loading

0 comments on commit 29678cb

Please sign in to comment.