Skip to content

Commit

Permalink
Add KMS functionality (#417)
Browse files Browse the repository at this point in the history
* add a benchmark that tests the performance of KMS using SignerClient
* fix run subcommand not reading local key pair when using KMS
* fix show-validator subcommand to use the public key for KMS
* fix to use correct chainID when executing show-validator subcommand
  • Loading branch information
torao authored Jun 14, 2022
1 parent e974b31 commit f9d9fa2
Show file tree
Hide file tree
Showing 10 changed files with 545 additions and 12 deletions.
3 changes: 2 additions & 1 deletion cmd/ostracon/commands/reset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import (
"github.com/line/ostracon/privval"
)

func setupEnv(t *testing.T) {
func setupEnv(t *testing.T) string {
rootDir := t.TempDir()
viper.SetEnvPrefix("OC")
require.NoError(t, viper.BindEnv("HOME"))
require.NoError(t, os.Setenv("OC_HOME", rootDir))
return rootDir
}

func TestResetAllCmd(t *testing.T) {
Expand Down
48 changes: 40 additions & 8 deletions cmd/ostracon/commands/show_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package commands
import (
"fmt"

"github.com/line/ostracon/node"
"github.com/line/ostracon/types"
"github.com/spf13/cobra"

cfg "github.com/line/ostracon/config"
tmjson "github.com/line/ostracon/libs/json"
tmos "github.com/line/ostracon/libs/os"
"github.com/line/ostracon/privval"
Expand All @@ -15,18 +18,31 @@ var ShowValidatorCmd = &cobra.Command{
Use: "show-validator",
Aliases: []string{"show_validator"},
Short: "Show this node's validator info",
RunE: showValidator,
PreRun: deprecateSnakeCase,
RunE: func(cmd *cobra.Command, args []string) error {
return showValidator(cmd, args, config)
},
PreRun: deprecateSnakeCase,
}

func showValidator(cmd *cobra.Command, args []string) error {
keyFilePath := config.PrivValidatorKeyFile()
if !tmos.FileExists(keyFilePath) {
return fmt.Errorf("private validator file %s does not exist", keyFilePath)
func showValidator(cmd *cobra.Command, args []string, config *cfg.Config) error {
var pv types.PrivValidator
if config.PrivValidatorListenAddr != "" {
chainID, err := loadChainID(config)
if err != nil {
return err
}
pv, err = node.CreateAndStartPrivValidatorSocketClient(config.PrivValidatorListenAddr, chainID, logger)
if err != nil {
return err
}
} else {
keyFilePath := config.PrivValidatorKeyFile()
if !tmos.FileExists(keyFilePath) {
return fmt.Errorf("private validator file %s does not exist", keyFilePath)
}
pv = privval.LoadFilePV(keyFilePath, config.PrivValidatorStateFile())
}

pv := privval.LoadFilePV(keyFilePath, config.PrivValidatorStateFile())

pubKey, err := pv.GetPubKey()
if err != nil {
return fmt.Errorf("can't get pubkey: %w", err)
Expand All @@ -40,3 +56,19 @@ func showValidator(cmd *cobra.Command, args []string) error {
fmt.Println(string(bz))
return nil
}

func loadChainID(config *cfg.Config) (string, error) {
stateDB, err := node.DefaultDBProvider(&node.DBContext{ID: "state", Config: config})
if err != nil {
return "", err
}
defer func() {
var _ = stateDB.Close()
}()
genesisDocProvider := node.DefaultGenesisDocProviderFunc(config)
_, genDoc, err := node.LoadStateFromDBOrGenesisDocProvider(stateDB, genesisDocProvider)
if err != nil {
return "", err
}
return genDoc.ChainID, nil
}
191 changes: 191 additions & 0 deletions cmd/ostracon/commands/show_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package commands

import (
"bytes"
"io/ioutil"
"os"
"sync"
"testing"

"github.com/line/ostracon/types"

cfg "github.com/line/ostracon/config"
"github.com/line/ostracon/crypto"
tmjson "github.com/line/ostracon/libs/json"
tmos "github.com/line/ostracon/libs/os"
"github.com/line/ostracon/privval"
"github.com/stretchr/testify/require"
)

func TestShowValidator(t *testing.T) {
original := config
defer func() {
config = original
}()

setupEnv(t)
config = cfg.DefaultConfig()
err := RootCmd.PersistentPreRunE(RootCmd, nil)
require.NoError(t, err)
init := NewInitCmd()
err = init.RunE(init, nil)
require.NoError(t, err)
output, err := captureStdout(func() {
err = ShowValidatorCmd.RunE(ShowValidatorCmd, nil)
require.NoError(t, err)
})
require.NoError(t, err)

// output must match the locally stored priv_validator key
privKey := loadFilePVKey(t, config.PrivValidatorKeyFile())
bz, err := tmjson.Marshal(privKey.PubKey)
require.NoError(t, err)
require.Equal(t, string(bz), output)
}

func TestShowValidatorWithoutLocalKeyFile(t *testing.T) {
setupEnv(t)
config := cfg.DefaultConfig()
if tmos.FileExists(config.PrivValidatorKeyFile()) {
err := os.Remove(config.PrivValidatorKeyFile())
require.NoError(t, err)
}
err := showValidator(ShowValidatorCmd, nil, config)
require.Error(t, err)
}

func TestShowValidatorWithKMS(t *testing.T) {
dir := setupEnv(t)
cfg.EnsureRoot(dir)

original := config
defer func() {
config = original
}()

config = cfg.DefaultConfig()
config.SetRoot(dir)
err := RootCmd.PersistentPreRunE(RootCmd, nil)
require.NoError(t, err)
init := NewInitCmd()
err = init.RunE(init, nil)
require.NoError(t, err)

chainID, err := loadChainID(config)
require.NoError(t, err)

if tmos.FileExists(config.PrivValidatorKeyFile()) {
err := os.Remove(config.PrivValidatorKeyFile())
require.NoError(t, err)
}
privval.WithMockKMS(t, dir, chainID, func(addr string, privKey crypto.PrivKey) {
config.PrivValidatorListenAddr = addr
require.NoFileExists(t, config.PrivValidatorKeyFile())
output, err := captureStdout(func() {
err := showValidator(ShowValidatorCmd, nil, config)
require.NoError(t, err)
})
require.NoError(t, err)

// output must contains the KMS public key
bz, err := tmjson.Marshal(privKey.PubKey())
require.NoError(t, err)
expected := string(bz)
require.Contains(t, output, expected)
})
}

func TestShowValidatorWithInefficientKMSAddress(t *testing.T) {
dir := setupEnv(t)
cfg.EnsureRoot(dir)

original := config
defer func() {
config = original
}()

config = cfg.DefaultConfig()
config.SetRoot(dir)
err := RootCmd.PersistentPreRunE(RootCmd, nil)
require.NoError(t, err)
init := NewInitCmd()
err = init.RunE(init, nil)
require.NoError(t, err)

if tmos.FileExists(config.PrivValidatorKeyFile()) {
err := os.Remove(config.PrivValidatorKeyFile())
require.NoError(t, err)
}
config.PrivValidatorListenAddr = "127.0.0.1:inefficient"
err = showValidator(ShowValidatorCmd, nil, config)
require.Error(t, err)
}

func TestLoadChainID(t *testing.T) {
expected := "c57861"
config := cfg.ResetTestRootWithChainID("TestLoadChainID", expected)
defer func() {
var _ = os.RemoveAll(config.RootDir)
}()

require.FileExists(t, config.GenesisFile())
genDoc, err := types.GenesisDocFromFile(config.GenesisFile())
require.NoError(t, err)
require.Equal(t, expected, genDoc.ChainID)

chainID, err := loadChainID(config)
require.NoError(t, err)
require.Equal(t, expected, chainID)
}

func TestLoadChainIDWithoutStateDB(t *testing.T) {
expected := "c34091"
config := cfg.ResetTestRootWithChainID("TestLoadChainID", expected)
defer func() {
var _ = os.RemoveAll(config.RootDir)
}()

config.DBBackend = "goleveldb"
config.DBPath = "/../path with containing chars that cannot be used\\/:*?\"<>|\x00"

_, err := loadChainID(config)
require.Error(t, err)
}

func loadFilePVKey(t *testing.T, file string) privval.FilePVKey {
// output must match the locally stored priv_validator key
keyJSONBytes, err := ioutil.ReadFile(file)
require.NoError(t, err)
privKey := privval.FilePVKey{}
err = tmjson.Unmarshal(keyJSONBytes, &privKey)
require.NoError(t, err)
return privKey
}

var stdoutMutex sync.Mutex

func captureStdout(f func()) (string, error) {
r, w, err := os.Pipe()
if err != nil {
return "", err
}

stdoutMutex.Lock()
original := os.Stdout
defer func() {
stdoutMutex.Lock()
os.Stdout = original
stdoutMutex.Unlock()
}()
os.Stdout = w
stdoutMutex.Unlock()

f()
_ = w.Close()
var buffer bytes.Buffer
if _, err := buffer.ReadFrom(r); err != nil {
return "", err
}
output := buffer.String()
return output[:len(output)-1], nil
}
2 changes: 1 addition & 1 deletion cmd/ostracon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func main() {
// * Provide their own DB implementation
// can copy this file and use something other than the
// DefaultNewNode function
nodeFunc := nm.DefaultNewNode
nodeFunc := nm.NewOstraconNode

// Create & start node
rootCmd.AddCommand(cmd.NewInitCmd())
Expand Down
9 changes: 9 additions & 0 deletions config/toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ func ResetTestRootWithChainID(testName string, chainID string) *Config {
genesisFilePath := filepath.Join(rootDir, baseConfig.Genesis)
privKeyFilePath := filepath.Join(rootDir, baseConfig.PrivValidatorKey)
privStateFilePath := filepath.Join(rootDir, baseConfig.PrivValidatorState)
nodeKeyFilePath := filepath.Join(rootDir, baseConfig.NodeKey)

// Write default config file if missing.
if !tmos.FileExists(configFilePath) {
Expand All @@ -575,6 +576,7 @@ func ResetTestRootWithChainID(testName string, chainID string) *Config {
// we always overwrite the priv val
tmos.MustWriteFile(privKeyFilePath, []byte(testPrivValidatorKey), 0644)
tmos.MustWriteFile(privStateFilePath, []byte(testPrivValidatorState), 0644)
tmos.MustWriteFile(nodeKeyFilePath, []byte(testNodeKey), 0644)

config := TestConfig().SetRoot(rootDir)
return config
Expand Down Expand Up @@ -633,6 +635,13 @@ var testPrivValidatorKey = `{
}
}`

var testNodeKey = `{
"priv_key": {
"type": "ostracon/PrivKeyEd25519",
"value": "hICuZLlVwHdzz6pAQOKk07MFn3Hze1EwwTUUhEDIdti9a1cQLR5Co/lxAzeGcyPWS/LuEr7qbgHmDUJT/nxx+Q=="
}
}`

var testPrivValidatorState = `{
"height": "0",
"round": 0,
Expand Down
30 changes: 28 additions & 2 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,32 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) {
)
}

// NewOstraconNode returns an Ostracon node for more safe production environments that don't automatically generate
// critical files. This function doesn't reference local key pair in configurations using KMS.
func NewOstraconNode(config *cfg.Config, logger log.Logger) (*Node, error) {
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
if err != nil {
return nil, fmt.Errorf("failed to load node key %s: %w", config.NodeKeyFile(), err)
}

var privKey types.PrivValidator
if config.PrivValidatorListenAddr == "" {
privKey = privval.LoadFilePV(
config.PrivValidatorKeyFile(),
config.PrivValidatorStateFile())
}
return NewNode(
config,
privKey,
nodeKey,
proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()),
DefaultGenesisDocProviderFunc(config),
DefaultDBProvider,
DefaultMetricsProvider(config.Instrumentation),
logger,
)
}

// MetricsProvider returns a consensus, p2p and mempool Metrics.
type MetricsProvider func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics)

Expand Down Expand Up @@ -730,7 +756,7 @@ func NewNode(config *cfg.Config,
// external signing process.
if config.PrivValidatorListenAddr != "" {
// FIXME: we should start services inside OnStart
privValidator, err = createAndStartPrivValidatorSocketClient(config.PrivValidatorListenAddr, genDoc.ChainID, logger)
privValidator, err = CreateAndStartPrivValidatorSocketClient(config.PrivValidatorListenAddr, genDoc.ChainID, logger)
if err != nil {
return nil, fmt.Errorf("error with private validator socket client: %w", err)
}
Expand Down Expand Up @@ -1438,7 +1464,7 @@ func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) error {
return nil
}

func createAndStartPrivValidatorSocketClient(
func CreateAndStartPrivValidatorSocketClient(
listenAddr,
chainID string,
logger log.Logger,
Expand Down
19 changes: 19 additions & 0 deletions node/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ import (
tmtime "github.com/line/ostracon/types/time"
)

func TestNewOstraconNode(t *testing.T) {
config := cfg.ResetTestRootWithChainID("TestNewOstraconNode", "new_ostracon_node")
defer os.RemoveAll(config.RootDir)
require.Equal(t, config.PrivValidatorListenAddr, "")
node, err := NewOstraconNode(config, log.TestingLogger())
require.NoError(t, err)
pubKey, err := node.PrivValidator().GetPubKey()
require.NoError(t, err)
require.NotNil(t, pubKey)
}

func TestNewOstraconNode_WithoutNodeKey(t *testing.T) {
config := cfg.ResetTestRootWithChainID("TestNewOstraconNode", "new_ostracon_node_wo_node_key")
defer os.RemoveAll(config.RootDir)
_ = os.Remove(config.NodeKeyFile())
_, err := NewOstraconNode(config, log.TestingLogger())
require.Error(t, err)
}

func TestNodeStartStop(t *testing.T) {
config := cfg.ResetTestRoot("node_node_test")
defer os.RemoveAll(config.RootDir)
Expand Down
Loading

0 comments on commit f9d9fa2

Please sign in to comment.