Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CLI] Consistent config/flag parsing & common helpers #891

Merged
merged 10 commits into from
Jul 26, 2023
155 changes: 25 additions & 130 deletions app/client/cli/debug.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cli

import (
"errors"
"fmt"
"os"

"github.com/manifoldco/promptui"
Expand All @@ -11,10 +9,7 @@ import (

"github.com/pokt-network/pocket/app/client/cli/helpers"
"github.com/pokt-network/pocket/logger"
"github.com/pokt-network/pocket/p2p/providers/peerstore_provider"
typesP2P "github.com/pokt-network/pocket/p2p/types"
"github.com/pokt-network/pocket/shared/messaging"
"github.com/pokt-network/pocket/shared/modules"
)

// TECHDEBT: Lowercase variables / constants that do not need to be exported.
Expand All @@ -28,26 +23,20 @@ const (
PromptSendBlockRequest string = "BlockRequest (broadcast)"
)

var (
items = []string{
PromptPrintNodeState,
PromptTriggerNextView,
PromptTogglePacemakerMode,
PromptResetToGenesis,
PromptShowLatestBlockInStore,
PromptSendMetadataRequest,
PromptSendBlockRequest,
}
)
var items = []string{
PromptPrintNodeState,
PromptTriggerNextView,
PromptTogglePacemakerMode,
PromptResetToGenesis,
PromptShowLatestBlockInStore,
PromptSendMetadataRequest,
PromptSendBlockRequest,
}

func init() {
dbgUI := newDebugUICommand()
dbgUI.AddCommand(newDebugUISubCommands()...)
rootCmd.AddCommand(dbgUI)

dbg := newDebugCommand()
dbg.AddCommand(debugCommands()...)
rootCmd.AddCommand(dbg)
}

// newDebugUISubCommands builds out the list of debug subcommands by matching the
Expand All @@ -60,7 +49,7 @@ func newDebugUISubCommands() []*cobra.Command {
commands[idx] = &cobra.Command{
Use: promptItem,
PersistentPreRunE: helpers.P2PDependenciesPreRunE,
Run: func(cmd *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, _ []string) {
handleSelect(cmd, cmd.Use)
},
ValidArgs: items,
Expand All @@ -81,56 +70,7 @@ func newDebugUICommand() *cobra.Command {
}
}

// newDebugCommand returns the cobra CLI for the Debug command.
func newDebugCommand() *cobra.Command {
return &cobra.Command{
Use: "Debug",
Aliases: []string{"d"},
Short: "Debug utility for rapid development",
Args: cobra.MaximumNArgs(1),
PersistentPreRunE: helpers.P2PDependenciesPreRunE,
}
}

func debugCommands() []*cobra.Command {
cmds := []*cobra.Command{
{
Use: "TriggerView",
Aliases: []string{"next", "trigger", "view"},
Short: "Trigger the next view in consensus",
Long: "Sends a message to all visible nodes on the network to start the next view (height/step/round) in consensus",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
m := &messaging.DebugMessage{
Action: messaging.DebugMessageAction_DEBUG_CONSENSUS_TRIGGER_NEXT_VIEW,
Type: messaging.DebugMessageRoutingType_DEBUG_MESSAGE_TYPE_BROADCAST,
Message: nil,
}
broadcastDebugMessage(cmd, m)
return nil
},
},
{
Use: "TogglePacemakerMode",
Short: "Toggle the pacemaker",
Long: "Toggle the consensus pacemaker either on or off so the chain progresses on its own or loses liveness",
Aliases: []string{"togglePaceMaker"},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
m := &messaging.DebugMessage{
Action: messaging.DebugMessageAction_DEBUG_CONSENSUS_TOGGLE_PACE_MAKER_MODE,
Type: messaging.DebugMessageRoutingType_DEBUG_MESSAGE_TYPE_BROADCAST,
Message: nil,
}
broadcastDebugMessage(cmd, m)
return nil
},
},
}
return cmds
}

func runDebug(cmd *cobra.Command, args []string) (err error) {
func runDebug(cmd *cobra.Command, _ []string) (err error) {
for {
if selection, err := promptGetInput(); err == nil {
handleSelect(cmd, selection)
Expand Down Expand Up @@ -218,32 +158,20 @@ func handleSelect(cmd *cobra.Command, selection string) {
}
}

// Broadcast to the entire validator set
// Broadcast to the entire network.
func broadcastDebugMessage(cmd *cobra.Command, debugMsg *messaging.DebugMessage) {
anyProto, err := anypb.New(debugMsg)
if err != nil {
logger.Global.Fatal().Err(err).Msg("Failed to create Any proto")
}

// TODO(olshansky): Once we implement the cleanup layer in RainTree, we'll be able to use
// broadcast. The reason it cannot be done right now is because this client is not in the
// address book of the actual validator nodes, so `validator1` never receives the message.
// p2pMod.Broadcast(anyProto)

pstore, err := fetchPeerstore(cmd)
bus, err := helpers.GetBusFromCmd(cmd)
if err != nil {
logger.Global.Fatal().Err(err).Msg("Unable to retrieve the pstore")
logger.Global.Fatal().Err(err).Msg("Failed to retrieve bus from command")
}
for _, val := range pstore.GetPeerList() {
addr := val.GetAddress()
if err != nil {
logger.Global.Fatal().Err(err).Msg("Failed to convert validator address into pocketCrypto.Address")
}
if err := helpers.P2PMod.Send(addr, anyProto); err != nil {
logger.Global.Error().Err(err).Msg("Failed to send debug message")
}
if err := bus.GetP2PModule().Broadcast(anyProto); err != nil {
logger.Global.Error().Err(err).Msg("Failed to broadcast debug message")
}

}

// Send to just a single (i.e. first) validator in the set
Expand All @@ -253,62 +181,29 @@ func sendDebugMessage(cmd *cobra.Command, debugMsg *messaging.DebugMessage) {
logger.Global.Error().Err(err).Msg("Failed to create Any proto")
}

pstore, err := fetchPeerstore(cmd)
pstore, err := helpers.FetchPeerstore(cmd)
if err != nil {
logger.Global.Fatal().Err(err).Msg("Unable to retrieve the pstore")
}

var validatorAddress []byte
if pstore.Size() == 0 {
logger.Global.Fatal().Msg("No validators found")
}

// if the message needs to be broadcast, it'll be handled by the business logic of the message handler
validatorAddress = pstore.GetPeerList()[0].GetAddress()
//
// TODO(#936): The statement above is false. Using `#Send()` will only
// be unicast with no opportunity for further propagation.
firstStakedActorAddress := pstore.GetPeerList()[0].GetAddress()
if err != nil {
logger.Global.Fatal().Err(err).Msg("Failed to convert validator address into pocketCrypto.Address")
}

if err := helpers.P2PMod.Send(validatorAddress, anyProto); err != nil {
logger.Global.Error().Err(err).Msg("Failed to send debug message")
}
}

// fetchPeerstore retrieves the providers from the CLI context and uses them to retrieve the address book for the current height
func fetchPeerstore(cmd *cobra.Command) (typesP2P.Peerstore, error) {
bus, ok := helpers.GetValueFromCLIContext[modules.Bus](cmd, helpers.BusCLICtxKey)
if !ok || bus == nil {
return nil, errors.New("retrieving bus from CLI context")
}
// TECHDEBT(#810, #811): use `bus.GetPeerstoreProvider()` after peerstore provider
// is retrievable as a proper submodule
pstoreProvider, err := bus.GetModulesRegistry().GetModule(peerstore_provider.PeerstoreProviderSubmoduleName)
if err != nil {
return nil, errors.New("retrieving peerstore provider")
}
currentHeightProvider := bus.GetCurrentHeightProvider()

height := currentHeightProvider.CurrentHeight()
pstore, err := pstoreProvider.(peerstore_provider.PeerstoreProvider).GetStakedPeerstoreAtHeight(height)
if err != nil {
return nil, fmt.Errorf("retrieving peerstore at height %d", height)
}
// Inform the client's main P2P that a the blockchain is at a new height so it can, if needed, update its view of the validator set
err = sendConsensusNewHeightEventToP2PModule(height, bus)
bus, err := helpers.GetBusFromCmd(cmd)
if err != nil {
return nil, errors.New("sending consensus new height event")
logger.Global.Fatal().Err(err).Msg("Failed to retrieve bus from command")
}
return pstore, nil
}

// sendConsensusNewHeightEventToP2PModule mimicks the consensus module sending a ConsensusNewHeightEvent to the p2p module
// This is necessary because the debug client is not a validator and has no consensus module but it has to update the peerstore
// depending on the changes in the validator set.
// TODO(#613): Make the debug client mimic a full node.
func sendConsensusNewHeightEventToP2PModule(height uint64, bus modules.Bus) error {
newHeightEvent, err := messaging.PackMessage(&messaging.ConsensusNewHeightEvent{Height: height})
if err != nil {
logger.Global.Fatal().Err(err).Msg("Failed to pack consensus new height event")
if err := bus.GetP2PModule().Send(firstStakedActorAddress, anyProto); err != nil {
logger.Global.Error().Err(err).Msg("Failed to send debug message")
}
return bus.GetP2PModule().HandleEvent(newHeightEvent.Content)
}
54 changes: 48 additions & 6 deletions app/client/cli/helpers/common.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,56 @@
package helpers

import (
"errors"
"fmt"

"github.com/spf13/cobra"

"github.com/pokt-network/pocket/logger"
"github.com/pokt-network/pocket/p2p/providers/peerstore_provider"
"github.com/pokt-network/pocket/p2p/types"
"github.com/pokt-network/pocket/runtime"
"github.com/pokt-network/pocket/shared/messaging"
"github.com/pokt-network/pocket/shared/modules"
)

var (
// TECHDEBT: Accept reading this from `Datadir` and/or as a flag.
genesisPath = runtime.GetEnv("GENESIS_PATH", "build/config/genesis.json")
// TECHDEBT: Accept reading this from `Datadir` and/or as a flag.
var genesisPath = runtime.GetEnv("GENESIS_PATH", "build/config/genesis.json")

// P2PMod is initialized in order to broadcast a message to the local network
P2PMod modules.P2PModule
)
// FetchPeerstore retrieves the providers from the CLI context and uses them to retrieve the address book for the current height
func FetchPeerstore(cmd *cobra.Command) (types.Peerstore, error) {
bus, err := GetBusFromCmd(cmd)
if err != nil {
return nil, err
}
// TECHDEBT(#811): use `bus.GetPeerstoreProvider()` after peerstore provider
// is retrievable as a proper submodule
pstoreProvider, err := bus.GetModulesRegistry().GetModule(peerstore_provider.PeerstoreProviderSubmoduleName)
if err != nil {
return nil, errors.New("retrieving peerstore provider")
}
currentHeightProvider := bus.GetCurrentHeightProvider()
height := currentHeightProvider.CurrentHeight()
pstore, err := pstoreProvider.(peerstore_provider.PeerstoreProvider).GetStakedPeerstoreAtHeight(height)
if err != nil {
return nil, fmt.Errorf("retrieving peerstore at height %d", height)
}
// Inform the client's main P2P that a the blockchain is at a new height so it can, if needed, update its view of the validator set
if err := sendConsensusNewHeightEventToP2PModule(height, bus); err != nil {
return nil, errors.New("sending consensus new height event")
}
return pstore, nil
}

// sendConsensusNewHeightEventToP2PModule mimicks the consensus module sending a ConsensusNewHeightEvent to the p2p module
// This is necessary because the debug client is not a validator and has no consensus module but it has to update the peerstore
// depending on the changes in the validator set.
// TODO(#613): Make the debug client mimic a full node.
// TECHDEBT: This may no longer be required (https://github.com/pokt-network/pocket/pull/891/files#r1262710098)
func sendConsensusNewHeightEventToP2PModule(height uint64, bus modules.Bus) error {
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
h5law marked this conversation as resolved.
Show resolved Hide resolved
newHeightEvent, err := messaging.PackMessage(&messaging.ConsensusNewHeightEvent{Height: height})
if err != nil {
logger.Global.Fatal().Err(err).Msg("Failed to pack consensus new height event")
}
return bus.GetP2PModule().HandleEvent(newHeightEvent.Content)
}
14 changes: 14 additions & 0 deletions app/client/cli/helpers/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ package helpers

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/pokt-network/pocket/shared/modules"
)

const BusCLICtxKey cliContextKey = "bus"

var ErrCxtFromBus = fmt.Errorf("could not get context from bus")

// NOTE: this is required by the linter, otherwise a simple string constant would have been enough
type cliContextKey string

Expand All @@ -19,3 +24,12 @@ func GetValueFromCLIContext[T any](cmd *cobra.Command, key cliContextKey) (T, bo
value, ok := cmd.Context().Value(key).(T)
return value, ok
}

func GetBusFromCmd(cmd *cobra.Command) (modules.Bus, error) {
bus, ok := GetValueFromCLIContext[modules.Bus](cmd, BusCLICtxKey)
if !ok {
return nil, ErrCxtFromBus
}

return bus, nil
}
20 changes: 18 additions & 2 deletions app/client/cli/helpers/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,27 @@ import (
"github.com/pokt-network/pocket/shared/modules"
)

// debugPrivKey is used in the generation of a runtime config to provide a private key to the P2P and Consensus modules
// this is not a private key used for sending transactions, but is used for the purposes of broadcasting messages etc.
// this must be done as the CLI does not take a node configuration file and still requires a Private Key for modules
const debugPrivKey = "09fc8ee114e678e665d09179acb9a30060f680df44ba06b51434ee47940a8613be19b2b886e743eb1ff7880968d6ce1a46350315e569243e747a227ee8faec3d"

// P2PDependenciesPreRunE initializes peerstore & current height providers, and a
// p2p module which consumes them. Everything is registered to the bus.
func P2PDependenciesPreRunE(cmd *cobra.Command, _ []string) error {
// TECHDEBT: this was being used for backwards compatibility with LocalNet and need to re-evaluate if its still necessary
flags.ConfigPath = runtime.GetEnv("CONFIG_PATH", "build/config/config.validator1.json")
configs.ParseConfig(flags.ConfigPath)

// set final `remote_cli_url` value; order of precedence: flag > env var > config > default
flags.RemoteCLIURL = viper.GetString("remote_cli_url")
Olshansk marked this conversation as resolved.
Show resolved Hide resolved

// By this time, the config path should be set.
// This is only being called for viper related side effects
// TECHDEBT(#907): refactor and improve how viper is used to parse configs throughout the codebase
_ = configs.ParseConfig(flags.ConfigPath)
// set final `remote_cli_url` value; order of precedence: flag > env var > config > default
flags.RemoteCLIURL = viper.GetString("remote_cli_url")

// By this time, the config path should be set.
// This is only being called for viper related side effects
Expand All @@ -32,7 +48,7 @@ func P2PDependenciesPreRunE(cmd *cobra.Command, _ []string) error {
runtimeMgr := runtime.NewManagerFromFiles(
flags.ConfigPath, genesisPath,
runtime.WithClientDebugMode(),
runtime.WithRandomPK(),
runtime.WithPK(debugPrivKey),
)

bus := runtimeMgr.GetBus()
Expand Down Expand Up @@ -79,7 +95,7 @@ func setupAndStartP2PModule(rm runtime.Manager) {
}

var ok bool
P2PMod, ok = mod.(modules.P2PModule)
P2PMod, ok := mod.(modules.P2PModule)
if !ok {
logger.Global.Fatal().Msgf("unexpected P2P module type: %T", mod)
}
Expand Down
1 change: 1 addition & 0 deletions runtime/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func WithRandomPK() func(*Manager) {
return WithPK(privateKey.String())
}

// TECHDEBT(#750): separate consensus and P2P (identity vs communication) keys.
func WithPK(pk string) func(*Manager) {
return func(b *Manager) {
if b.config.Consensus == nil {
Expand Down
Loading