Repository: celestiaorg/celestia-node
Pull request #81 (2021-09-23 08:47:50)

core: initialization and Repository
## Context
The penultimate piece for #30. It introduces initialization flow for the embedded Core node along with repository to access Core' stored data. Basically, it implements some of the important functionality for future `init` command.

## Others
* Implements custom serialization for `core.Config`(rationale is in the code)
* Starts own collection of utilities
* Provides FS, InMem and Mock repositories


## Refs
Depends on #71 and #74
Moves towards done #30


Wondertan: Rebased
Wondertan: @liamsi, need one more approval from @renaynay or @mattdf 
Wondertan: To avoid confusion, I meant an approval not for this PR, but for #71 to rebase this one on master.

Patch:
diff --git a/core/config.go b/core/config.go
index f2362149..311fa156 100644
--- a/core/config.go
+++ b/core/config.go
@@ -4,13 +4,20 @@ import (
 	"os"
 	"testing"
 
+	"github.com/BurntSushi/toml"
+
 	"github.com/celestiaorg/celestia-core/config"
 )
 
+// Config is an alias for Core config.
+// It has to be used throughout celestia-node codebase to simplify future config changes and etc.
 type Config = config.Config
 
+// DefaultConfig returns a modified default config of Core node.
 func DefaultConfig() *Config {
-	return config.DefaultConfig()
+	cfg := config.DefaultConfig()
+	updateDefaults(cfg)
+	return cfg
 }
 
 func TestConfig(t *testing.T) *Config {
@@ -20,3 +27,38 @@ func TestConfig(t *testing.T) *Config {
 	})
 	return cfg
 }
+
+// LoadConfig loads the config from the file under the given 'path'.
+// NOTE: Unfortunately, Core(Tendermint) does not provide us with convenient function to load the Config,
+// so we have to do our own. Furthermore, we have to make own SaveConfig, as the one the Core provides has
+// incompatibility issues with our toml parser version.
+func LoadConfig(path string) (*Config, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	var cfg config.Config
+	_, err = toml.DecodeReader(f, &cfg)
+	return &cfg, err
+}
+
+// SaveConfig saves config in a file under the given 'path'.
+func SaveConfig(path string, cfg *Config) error {
+	f, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	return toml.NewEncoder(f).Encode(cfg)
+}
+
+// change default paths to remove unneeded 'config' sub dir
+func updateDefaults(cfg *Config) {
+	cfg.Genesis = "genesis.json"
+	cfg.PrivValidatorKey = "priv_validator_key.json"
+	cfg.PrivValidatorState = "priv_validator_state.json"
+	cfg.NodeKey = "p2p_key.json"
+	cfg.P2P.AddrBook = "addr_book.json"
+}
diff --git a/core/init.go b/core/init.go
new file mode 100644
index 00000000..e35abc18
--- /dev/null
+++ b/core/init.go
@@ -0,0 +1,107 @@
+package core
+
+import (
+	"fmt"
+	"path/filepath"
+
+	tmrand "github.com/celestiaorg/celestia-core/libs/rand"
+	"github.com/celestiaorg/celestia-core/p2p"
+	"github.com/celestiaorg/celestia-core/privval"
+	"github.com/celestiaorg/celestia-core/types"
+	tmtime "github.com/celestiaorg/celestia-core/types/time"
+
+	"github.com/celestiaorg/celestia-node/libs/utils"
+)
+
+// defaultValKeyType is default crypto algo for consensus in core
+const defaultValKeyType = types.ABCIPubKeyTypeSecp256k1
+
+// Init initializes the Core repository under the given 'path'.
+// It checks and creates the default if not exist:
+// * Config
+// * Private validator key
+// * P2P comms private key
+// * Stub genesis doc file
+func Init(path string) (err error) {
+	log.Info("Initializing repository over '%s'", path)
+	// 1 - ensure config
+	cfg := DefaultConfig()
+	cfg.SetRoot(path)
+	cfgPath := configPath(path)
+	if !utils.Exists(cfgPath) {
+		err = SaveConfig(cfgPath, cfg)
+		if err != nil {
+			return fmt.Errorf("core: can't write config: %w", err)
+		}
+	}
+	// 2 - ensure private validator key
+	var pv *privval.FilePV
+	keyPath := cfg.PrivValidatorKeyFile()
+	if utils.Exists(keyPath) {
+		pv = privval.LoadFilePV(keyPath, cfg.PrivValidatorStateFile())
+	} else {
+		pv, err = privval.GenFilePV(keyPath, cfg.PrivValidatorStateFile(), defaultValKeyType)
+		if err != nil {
+			return
+		}
+		pv.Save()
+	}
+	// 3 - ensure private p2p key
+	_, err = p2p.LoadOrGenNodeKey(cfg.NodeKeyFile())
+	if err != nil {
+		return
+	}
+	// 4 - ensure genesis
+	genPath := cfg.GenesisFile()
+	if !utils.Exists(genPath) {
+		log.Warn("Genesis file is not found - generating a stub")
+		pubKey, err := pv.GetPubKey()
+		if err != nil {
+			return fmt.Errorf("can't get pubkey: %w", err)
+		}
+
+		params := types.DefaultConsensusParams()
+		params.Validator.PubKeyTypes = []string{defaultValKeyType}
+		genDoc := types.GenesisDoc{
+			ChainID:         fmt.Sprintf("localnet-%v", tmrand.Str(6)),
+			GenesisTime:     tmtime.Now(),
+			ConsensusParams: params,
+			Validators: []types.GenesisValidator{{
+				Address: pubKey.Address(),
+				PubKey:  pubKey,
+				Power:   10,
+			}},
+		}
+
+		return genDoc.SaveAs(genPath)
+	}
+
+	return nil
+}
+
+// IsInit checks whether the Core is initialized under the given 'path'.
+// It requires Config, Private Validator Key, Private P2P Key and genesis doc.
+func IsInit(path string) bool {
+	cfgPath := configPath(path)
+	if !utils.Exists(cfgPath) {
+		return false
+	}
+
+	cfg, err := LoadConfig(cfgPath)
+	if err != nil {
+		log.Errorf("loading config: %s", err)
+		return false
+	}
+
+	if !utils.Exists(cfg.PrivValidatorKeyFile()) ||
+		!utils.Exists(cfg.NodeKeyFile()) ||
+		!utils.Exists(cfg.GenesisFile()) {
+		return false
+	}
+
+	return true
+}
+
+func configPath(base string) string {
+	return filepath.Join(base, "config.toml")
+}
diff --git a/core/repo.go b/core/repo.go
new file mode 100644
index 00000000..94b95965
--- /dev/null
+++ b/core/repo.go
@@ -0,0 +1,44 @@
+package core
+
+import (
+	"errors"
+
+	"github.com/celestiaorg/celestia-core/config"
+)
+
+// ErrNotInited is used to signal when core is intended to be started without being initialized.
+var ErrNotInited = errors.New("core: repository is not initialized")
+
+// TODO(@Wondertan):
+//  * This should expose GenesisDoc and others. We can add ad-hoc
+//  * Ideally, add private keys to Keystore to unify access pattern.
+type Repository interface {
+	// Config loads a Config.
+	Config() (*Config, error)
+	// PutConfig saves given config.
+	PutConfig(*Config) error
+}
+
+// Open instantiates FS Repository from a given 'path'.
+func Open(path string) (Repository, error) {
+	if !IsInit(path) {
+		return nil, ErrNotInited
+	}
+
+	return &fsRepository{path}, nil
+}
+
+func (f *fsRepository) Config() (*Config, error) {
+	return LoadConfig(configPath(f.path))
+}
+
+func (f *fsRepository) PutConfig(cfg *Config) error {
+	cfg.SetRoot(f.path) // not allowed to change root in runtime
+	updateDefaults(cfg) // ensure stability over path fields
+	config.WriteConfigFile(configPath(f.path), cfg)
+	return nil
+}
+
+type fsRepository struct {
+	path string
+}
diff --git a/core/repo_mem.go b/core/repo_mem.go
new file mode 100644
index 00000000..54664389
--- /dev/null
+++ b/core/repo_mem.go
@@ -0,0 +1,29 @@
+package core
+
+import "sync"
+
+// NewMemRepository creates in memory implementation of Repository
+func NewMemRepository() Repository {
+	return &memRepository{}
+}
+
+type memRepository struct {
+	cfg  *Config
+	lock sync.Mutex
+}
+
+func (m *memRepository) Config() (*Config, error) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	return m.cfg, nil
+}
+
+func (m *memRepository) PutConfig(cfg *Config) error {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	m.cfg = cfg
+	return nil
+}
+
+// not to accidentally forget to update mem implementation when Repository will be extended.
+var _ = (Repository)(&memRepository{})
diff --git a/libs/utils/fs.go b/libs/utils/fs.go
new file mode 100644
index 00000000..d67e9a1e
--- /dev/null
+++ b/libs/utils/fs.go
@@ -0,0 +1,9 @@
+package utils
+
+import "os"
+
+// Exists checks whether file or directory exists under the given 'path' on the system.
+func Exists(path string) bool {
+	_, err := os.Stat(path)
+	return !os.IsNotExist(err)
+}


Test patch:
diff --git a/core/init_test.go b/core/init_test.go
new file mode 100644
index 00000000..199223a6
--- /dev/null
+++ b/core/init_test.go
@@ -0,0 +1,16 @@
+package core
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIsInit(t *testing.T) {
+	dir := t.TempDir()
+	err := Init(dir)
+	require.NoError(t, err)
+	ok := IsInit(dir)
+	assert.True(t, ok)
+}
diff --git a/core/mock_test.go b/core/mock_test.go
index 0bf9764e..217349d9 100644
--- a/core/mock_test.go
+++ b/core/mock_test.go
@@ -1,11 +1,19 @@
 package core
 
 import (
+	"testing"
+
 	"github.com/celestiaorg/celestia-core/abci/example/kvstore"
 	"github.com/celestiaorg/celestia-core/node"
 	rpctest "github.com/celestiaorg/celestia-core/rpc/test"
 )
 
+func MockRepo(t *testing.T) Repository {
+	repo := NewMemRepository()
+	repo.PutConfig(TestConfig(t)) //nolint: errcheck
+	return repo
+}
+
 // StartMockNode starts a mock Core node background process and returns it.
 func StartMockNode() *node.Node {
 	app := kvstore.NewApplication()