diff --git a/config/source/source.go b/config/source/source.go new file mode 100644 index 0000000000000000000000000000000000000000..b4a31d809bb6d150eaaaa7af1b2c17522fc39d45 --- /dev/null +++ b/config/source/source.go @@ -0,0 +1,283 @@ +package source + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "reflect" + "strings" + + "github.com/BurntSushi/toml" + "github.com/cep21/xdgbasedir" + "github.com/imdario/mergo" +) + +// If passed this identifier try to read config from STDIN +const STDINFileIdentifier = "-" + +type ConfigProvider interface { + // Description of where this provider sources its config from + From() string + // Get the config values to the passed in baseConfig + Apply(baseConfig interface{}) error + // Return a copy of the provider that does nothing if skip is true + SetSkip(skip bool) ConfigProvider + // Whether to skip this provider + Skip() bool +} + +var _ ConfigProvider = &configSource{} + +type configSource struct { + from string + skip bool + apply func(baseConfig interface{}) error +} + +func NewConfigProvider(from string, skip bool, apply func(baseConfig interface{}) error) *configSource { + return &configSource{ + from: from, + skip: skip, + apply: apply, + } +} + +func (cs *configSource) From() string { + return cs.from +} + +func (cs *configSource) Apply(baseConfig interface{}) error { + return cs.apply(baseConfig) +} + +func (cs *configSource) Skip() bool { + return cs.skip +} + +// Returns a copy of the configSource with skip set as passed in +func (cs *configSource) SetSkip(skip bool) ConfigProvider { + return &configSource{ + skip: skip, + from: cs.from, + apply: cs.apply, + } +} + +// Builds a ConfigProvider by iterating over a cascade of ConfigProvider sources. Can be used +// in two distinct modes: with shortCircuit true the first successful ConfigProvider source +// is returned. With shortCircuit false sources appearing later are used to possibly override +// those appearing earlier +func Cascade(logWriter io.Writer, shortCircuit bool, providers ...ConfigProvider) *configSource { + var fromStrings []string + skip := true + for _, provider := range providers { + if !provider.Skip() { + skip = false + fromStrings = append(fromStrings, provider.From()) + } + } + fromPrefix := "each of" + if shortCircuit { + fromPrefix = "first of" + + } + return &configSource{ + skip: skip, + from: fmt.Sprintf("%s: %s", fromPrefix, strings.Join(fromStrings, " then ")), + apply: func(baseConfig interface{}) error { + if baseConfig == nil { + return fmt.Errorf("baseConfig passed to Cascade(...).Get() must not be nil") + } + for _, provider := range providers { + if !provider.Skip() { + writeLog(logWriter, fmt.Sprintf("Sourcing config from %s", provider.From())) + err := provider.Apply(baseConfig) + if err != nil { + return err + } + if shortCircuit { + return nil + } + } + } + return nil + }, + } +} + +func FirstOf(providers ...ConfigProvider) *configSource { + return Cascade(os.Stderr, true, providers...) +} + +func EachOf(providers ...ConfigProvider) *configSource { + return Cascade(os.Stderr, false, providers...) +} + +// Try to source config from provided JSON file, is skipNonExistent is true then the provider will fall-through (skip) +// when the file doesn't exist, rather than returning an error +func JSONFile(configFile string, skipNonExistent bool) *configSource { + return &configSource{ + skip: ShouldSkipFile(configFile, skipNonExistent), + from: fmt.Sprintf("JSON config file at '%s'", configFile), + apply: func(baseConfig interface{}) error { + return FromJSONFile(configFile, baseConfig) + }, + } +} + +// Try to source config from provided TOML file, is skipNonExistent is true then the provider will fall-through (skip) +// when the file doesn't exist, rather than returning an error +func TOMLFile(configFile string, skipNonExistent bool) *configSource { + return &configSource{ + skip: ShouldSkipFile(configFile, skipNonExistent), + from: fmt.Sprintf("TOML config file at '%s'", configFile), + apply: func(baseConfig interface{}) error { + return FromTOMLFile(configFile, baseConfig) + }, + } +} + +// Try to find config by using XDG base dir spec +func XDGBaseDir(configFileName string) *configSource { + skip := false + // Look for config in standard XDG specified locations + configFile, err := xdgbasedir.GetConfigFileLocation(configFileName) + if err == nil { + _, err := os.Stat(configFile) + // Skip if config file does not exist at default location + skip = os.IsNotExist(err) + } + return &configSource{ + skip: skip, + from: fmt.Sprintf("XDG base dir"), + apply: func(baseConfig interface{}) error { + if err != nil { + return err + } + return FromTOMLFile(configFile, baseConfig) + }, + } +} + +// Source from a single environment variable with config embedded in JSON +func Environment(key string) *configSource { + jsonString := os.Getenv(key) + return &configSource{ + skip: jsonString == "", + from: fmt.Sprintf("'%s' environment variable (as JSON)", key), + apply: func(baseConfig interface{}) error { + return FromJSONString(jsonString, baseConfig) + }, + } +} + +func Default(defaultConfig interface{}) *configSource { + return &configSource{ + from: "defaults", + apply: func(baseConfig interface{}) error { + return mergo.MergeWithOverwrite(baseConfig, defaultConfig) + }, + } +} + +func FromJSONFile(configFile string, conf interface{}) error { + bs, err := ReadFile(configFile) + if err != nil { + return err + } + + return FromJSONString(string(bs), conf) +} + +func FromTOMLFile(configFile string, conf interface{}) error { + bs, err := ReadFile(configFile) + if err != nil { + return err + } + + return FromTOMLString(string(bs), conf) +} + +func FromTOMLString(tomlString string, conf interface{}) error { + _, err := toml.Decode(tomlString, conf) + if err != nil { + return err + } + return nil +} + +func FromJSONString(jsonString string, conf interface{}) error { + err := json.Unmarshal(([]byte)(jsonString), conf) + if err != nil { + return err + } + return nil +} + +func TOMLString(conf interface{}) string { + buf := new(bytes.Buffer) + encoder := toml.NewEncoder(buf) + err := encoder.Encode(conf) + if err != nil { + return fmt.Sprintf("<Could not serialise config: %v>", err) + } + return buf.String() +} + +func JSONString(conf interface{}) string { + bs, err := json.MarshalIndent(conf, "", "\t") + if err != nil { + return fmt.Sprintf("<Could not serialise config: %v>", err) + } + return string(bs) +} + +func Merge(base, override interface{}) (interface{}, error) { + merged, err := DeepCopy(base) + if err != nil { + return nil, err + } + err = mergo.MergeWithOverwrite(merged, override) + if err != nil { + return nil, err + } + return merged, nil +} + +// Passed a pointer to struct creates a deep copy of the struct +func DeepCopy(conf interface{}) (interface{}, error) { + // Create a zero value + confCopy := reflect.New(reflect.TypeOf(conf).Elem()).Interface() + // Perform a merge into that value to effect the copy + err := mergo.Merge(confCopy, conf) + if err != nil { + return nil, err + } + return confCopy, nil +} + +func writeLog(writer io.Writer, msg string) { + if writer != nil { + writer.Write(([]byte)(msg)) + writer.Write(([]byte)("\n")) + } +} + +func ReadFile(file string) ([]byte, error) { + if file == STDINFileIdentifier { + return ioutil.ReadAll(os.Stdin) + } + return ioutil.ReadFile(file) +} + +func ShouldSkipFile(file string, skipNonExistent bool) bool { + skip := file == "" + if !skip && skipNonExistent { + _, err := os.Stat(file) + skip = os.IsNotExist(err) + } + return skip +} diff --git a/config/source/source_test.go b/config/source/source_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7bf062b40dd6e1713725eb74da0177ce1d2ea861 --- /dev/null +++ b/config/source/source_test.go @@ -0,0 +1,111 @@ +package source + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEnvironment(t *testing.T) { + envVar := "FISH_SPOON_ALPHA" + jsonString := JSONString(newTestConfig()) + os.Setenv(envVar, jsonString) + conf := new(animalConfig) + err := Environment(envVar).Apply(conf) + assert.NoError(t, err) + assert.Equal(t, jsonString, JSONString(conf)) +} + +func TestDeepCopy(t *testing.T) { + conf := newTestConfig() + confCopy, err := DeepCopy(conf) + require.NoError(t, err) + assert.Equal(t, conf, confCopy) +} + +func TestFile(t *testing.T) { + tomlString := TOMLString(newTestConfig()) + file := writeConfigFile(t, newTestConfig()) + defer os.Remove(file) + conf := new(animalConfig) + err := TOMLFile(file, false).Apply(conf) + assert.NoError(t, err) + assert.Equal(t, tomlString, TOMLString(conf)) +} + +func TestCascade(t *testing.T) { + envVar := "FISH_SPOON_ALPHA" + // Both fall through so baseConfig returned + conf := newTestConfig() + err := Cascade(os.Stderr, true, + Environment(envVar), + TOMLFile("", false)).Apply(conf) + assert.NoError(t, err) + assert.Equal(t, newTestConfig(), conf) + + // Env not set so falls through to file + fileConfig := newTestConfig() + file := writeConfigFile(t, fileConfig) + defer os.Remove(file) + conf = new(animalConfig) + err = Cascade(os.Stderr, true, + Environment(envVar), + TOMLFile(file, false)).Apply(conf) + assert.NoError(t, err) + assert.Equal(t, TOMLString(fileConfig), TOMLString(conf)) + + // Env set so caught by environment source + envConfig := animalConfig{ + Name: "Slug", + NumLegs: 0, + } + os.Setenv(envVar, JSONString(envConfig)) + conf = newTestConfig() + err = Cascade(os.Stderr, true, + Environment(envVar), + TOMLFile(file, false)).Apply(conf) + assert.NoError(t, err) + assert.Equal(t, TOMLString(envConfig), TOMLString(conf)) +} + +func writeConfigFile(t *testing.T, conf interface{}) string { + tomlString := TOMLString(conf) + f, err := ioutil.TempFile("", "source-test.toml") + assert.NoError(t, err) + f.Write(([]byte)(tomlString)) + f.Close() + return f.Name() +} + +// Test types + +type legConfig struct { + Leg int + Colour byte +} + +type animalConfig struct { + Name string + NumLegs int + Legs []legConfig +} + +func newTestConfig() *animalConfig { + return &animalConfig{ + Name: "Froggy!", + NumLegs: 2, + Legs: []legConfig{ + { + Leg: 1, + Colour: 034, + }, + { + Leg: 2, + Colour: 034, + }, + }, + } +} diff --git a/core/config.go b/core/config.go deleted file mode 100644 index bddb21b8491f69ed719230ceba412aa2c34aa028..0000000000000000000000000000000000000000 --- a/core/config.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2017 Monax Industries Limited -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// config.go keeps explicit structures on the runtime configuration of -// burrow and all modules. It loads these from the Viper configuration -// loaded in `definitions.Do` - -package core - -import ( - "fmt" - "os" - "path" - - "github.com/hyperledger/burrow/config" - "github.com/hyperledger/burrow/definitions" - lconfig "github.com/hyperledger/burrow/logging/config" - "github.com/hyperledger/burrow/server" - "github.com/hyperledger/burrow/util" - "github.com/spf13/viper" -) - -// LoadConsensusModuleConfig wraps specifically for the consensus module -func LoadConsensusModuleConfig(do *definitions.Do) (*config.ModuleConfig, error) { - return loadModuleConfigFromDo(do, "consensus") -} - -// LoadApplicationManagerModuleConfig wraps specifically for the application -// manager -func LoadApplicationManagerModuleConfig(do *definitions.Do) (*config.ModuleConfig, error) { - return loadModuleConfigFromDo(do, "manager") -} - -func loadModuleConfigFromDo(do *definitions.Do, module string) (*config.ModuleConfig, error) { - return LoadModuleConfig(do.Config, do.WorkDir, do.DataDir, - do.GenesisFile, do.ChainId, module) -} - -// Generic Module loader for configuration information -func LoadModuleConfig(conf *viper.Viper, rootWorkDir, rootDataDir, - genesisFile, chainId, module string) (*config.ModuleConfig, error) { - moduleName := conf.GetString("chain." + module + ".name") - // set up the directory structure for the module inside the data directory - workDir := path.Join(rootDataDir, conf.GetString("chain."+module+ - ".relative_root")) - if err := util.EnsureDir(workDir, os.ModePerm); err != nil { - return nil, - fmt.Errorf("Failed to create module root directory %s.", workDir) - } - dataDir := path.Join(workDir, "data") - if err := util.EnsureDir(dataDir, os.ModePerm); err != nil { - return nil, - fmt.Errorf("Failed to create module data directory %s.", dataDir) - } - // load configuration subtree for module - if !conf.IsSet(moduleName) { - return nil, fmt.Errorf("Failed to read configuration section for %s", - moduleName) - } - subConfig, err := config.ViperSubConfig(conf, moduleName) - if subConfig == nil { - return nil, fmt.Errorf("Failed to read configuration section for %s: %s", - moduleName, err) - } - - return &config.ModuleConfig{ - Module: module, - Name: moduleName, - WorkDir: workDir, - DataDir: dataDir, - RootDir: rootWorkDir, // burrow's working directory - ChainId: chainId, - GenesisFile: genesisFile, - Config: subConfig, - }, nil -} - -// Load the ServerConfig from commandline Do object -func LoadServerConfigFromDo(do *definitions.Do) (*server.ServerConfig, error) { - // load configuration subtree for servers - return LoadServerConfig(do.ChainId, do.Config) -} - -// Load the ServerConfig from root Viper config, fixing the ChainId -func LoadServerConfig(chainId string, rootConfig *viper.Viper) (*server.ServerConfig, error) { - subConfig, err := config.ViperSubConfig(rootConfig, "servers") - if err != nil { - return nil, err - } - serverConfig, err := server.ReadServerConfig(subConfig) - if err != nil { - return nil, err - } - serverConfig.ChainId = chainId - return serverConfig, err -} - -func LoadLoggingConfigFromDo(do *definitions.Do) (*lconfig.LoggingConfig, error) { - if !do.Config.IsSet("logging") { - return nil, nil - } - loggingConfigMap := do.Config.GetStringMap("logging") - return lconfig.LoggingConfigFromMap(loggingConfigMap) -} - -func LoadLoggingConfigFromClientDo(do *definitions.ClientDo) (*lconfig.LoggingConfig, error) { - loggingConfig := lconfig.DefaultClientLoggingConfig() - return loggingConfig, nil -} diff --git a/core/config_test.go b/core/config_test.go deleted file mode 100644 index ef552d2b0581d48538e6119a2b5615474fe05de4..0000000000000000000000000000000000000000 --- a/core/config_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package core - -import ( - "testing" - - "github.com/hyperledger/burrow/config" - "github.com/hyperledger/burrow/definitions" - lconfig "github.com/hyperledger/burrow/logging/config" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestLoadLoggingConfigFromDo(t *testing.T) { - do := new(definitions.Do) - do.Config = viper.New() - lc, err := LoadLoggingConfigFromDo(do) - assert.NoError(t, err) - assert.Nil(t, lc, "Should get nil logging config when [logging] not set") - cnf, err := config.ReadViperConfig(([]byte)(lconfig.DefaultNodeLoggingConfig().RootTOMLString())) - assert.NoError(t, err) - do.Config = cnf - lc, err = LoadLoggingConfigFromDo(do) - assert.NoError(t, err) - assert.EqualValues(t, lconfig.DefaultNodeLoggingConfig(), lc) -} diff --git a/core/core.go b/core/core.go deleted file mode 100644 index 3727a823203850afe0881b950ebb41b6a10ec253..0000000000000000000000000000000000000000 --- a/core/core.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2017 Monax Industries Limited -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package core - -import ( - "fmt" - - // TODO: [ben] swap out go-events with burrow/event (currently unused) - events "github.com/tendermint/go-events" - - "github.com/hyperledger/burrow/config" - "github.com/hyperledger/burrow/consensus" - "github.com/hyperledger/burrow/definitions" - "github.com/hyperledger/burrow/event" - "github.com/hyperledger/burrow/manager" - // rpc_v0 is carried over from burrowv0.11 and before on port 1337 - rpc_v0 "github.com/hyperledger/burrow/rpc/v0" - // rpc_tendermint is carried over from burrowv0.11 and before on port 46657 - - "github.com/hyperledger/burrow/logging" - logging_types "github.com/hyperledger/burrow/logging/types" - rpc_tendermint "github.com/hyperledger/burrow/rpc/tendermint/core" - "github.com/hyperledger/burrow/server" -) - -// Core is the high-level structure -type Core struct { - chainId string - evsw events.EventSwitch - pipe definitions.Pipe - tendermintPipe definitions.TendermintPipe - logger logging_types.InfoTraceLogger -} - -func NewCore(chainId string, - consensusConfig *config.ModuleConfig, - managerConfig *config.ModuleConfig, - logger logging_types.InfoTraceLogger) (*Core, error) { - // start new event switch, TODO: [ben] replace with burrow/event - evsw := events.NewEventSwitch() - evsw.Start() - logger = logging.WithScope(logger, "Core") - - // start a new application pipe that will load an application manager - pipe, err := manager.NewApplicationPipe(managerConfig, evsw, logger) - if err != nil { - return nil, fmt.Errorf("Failed to load application pipe: %v", err) - } - logging.TraceMsg(logger, "Loaded pipe with application manager") - // pass the consensus engine into the pipe - if e := consensus.LoadConsensusEngineInPipe(consensusConfig, pipe); e != nil { - return nil, fmt.Errorf("Failed to load consensus engine in pipe: %v", e) - } - tendermintPipe, err := pipe.GetTendermintPipe() - if err != nil { - logging.TraceMsg(logger, "Tendermint gateway not supported by manager", - "manager", managerConfig.Name) - } - return &Core{ - chainId: chainId, - evsw: evsw, - pipe: pipe, - tendermintPipe: tendermintPipe, - logger: logger, - }, nil -} - -//------------------------------------------------------------------------------ -// Explicit switch that can later be abstracted into an `Engine` definition -// where the Engine defines the explicit interaction of a specific application -// manager with a consensus engine. -// TODO: [ben] before such Engine abstraction, -// think about many-manager-to-one-consensus - -//------------------------------------------------------------------------------ -// Server functions -// NOTE: [ben] in phase 0 we exactly take over the full server architecture -// from burrow and Tendermint; This is a draft and will be overhauled. - -func (core *Core) NewGatewayV0(config *server.ServerConfig) (*server.ServeProcess, - error) { - codec := &rpc_v0.TCodec{} - eventSubscriptions := event.NewEventSubscriptions(core.pipe.Events()) - // The services. - tmwss := rpc_v0.NewBurrowWsService(codec, core.pipe) - tmjs := rpc_v0.NewBurrowJsonService(codec, core.pipe, eventSubscriptions) - // The servers. - jsonServer := rpc_v0.NewJsonRpcServer(tmjs) - restServer := rpc_v0.NewRestServer(codec, core.pipe, eventSubscriptions) - wsServer := server.NewWebSocketServer(config.WebSocket.MaxWebSocketSessions, - tmwss, core.logger) - // Create a server process. - proc, err := server.NewServeProcess(config, core.logger, jsonServer, restServer, wsServer) - if err != nil { - return nil, fmt.Errorf("Failed to load gateway: %v", err) - } - - return proc, nil -} - -func (core *Core) NewGatewayTendermint(config *server.ServerConfig) ( - *rpc_tendermint.TendermintWebsocketServer, error) { - if core.tendermintPipe == nil { - return nil, fmt.Errorf("No Tendermint pipe has been initialised for Tendermint gateway.") - } - return rpc_tendermint.NewTendermintWebsocketServer(config, - core.tendermintPipe, core.evsw) -} - -// Stop the core allowing for a graceful shutdown of component in order. -func (core *Core) Stop() bool { - return core.pipe.GetConsensusEngine().Stop() -} diff --git a/core/kernel.go b/core/kernel.go new file mode 100644 index 0000000000000000000000000000000000000000..81ddfce577ae1552775b3deadbe143d83bd72a7a --- /dev/null +++ b/core/kernel.go @@ -0,0 +1,166 @@ +// Copyright 2017 Monax Industries Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "net" + "os" + "os/signal" + "sync" + + "fmt" + + bcm "github.com/hyperledger/burrow/blockchain" + "github.com/hyperledger/burrow/consensus/tendermint" + "github.com/hyperledger/burrow/consensus/tendermint/query" + "github.com/hyperledger/burrow/event" + "github.com/hyperledger/burrow/execution" + "github.com/hyperledger/burrow/genesis" + "github.com/hyperledger/burrow/logging" + logging_types "github.com/hyperledger/burrow/logging/types" + "github.com/hyperledger/burrow/rpc" + "github.com/hyperledger/burrow/rpc/tm" + "github.com/hyperledger/burrow/txs" + tm_config "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/node" + tm_types "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/events" +) + +// Kernel is the root structure of Burrow +type Kernel struct { + eventSwitch events.EventSwitch + tmNode *node.Node + service rpc.Service + serverLaunchers []ServerLauncher + listeners []net.Listener + logger logging_types.InfoTraceLogger + shutdownNotify chan struct{} + shutdownOnce sync.Once +} + +type ServerLauncher struct { + Name string + Launch func(rpc.Service) (net.Listener, error) +} + +func NewKernel(privValidator tm_types.PrivValidator, genesisDoc *genesis.GenesisDoc, tmConf *tm_config.Config, + rpcConfig *rpc.RPCConfig, logger logging_types.InfoTraceLogger) (*Kernel, error) { + + events.NewEventSwitch().Start() + logger = logging.WithScope(logger, "NewKernel") + + stateDB := dbm.NewDB("burrow_state", dbm.GoLevelDBBackendStr, tmConf.DBDir()) + state := execution.MakeGenesisState(stateDB, genesisDoc) + state.Save() + + blockchain := bcm.NewBlockchain(genesisDoc) + evmEvents := event.NewEmitter(logger) + + tmGenesisDoc := tendermint.DeriveGenesisDoc(genesisDoc) + checker := execution.NewBatchChecker(state, tmGenesisDoc.ChainID, blockchain, logger) + committer := execution.NewBatchCommitter(state, tmGenesisDoc.ChainID, blockchain, evmEvents, logger) + tmNode, err := tendermint.NewNode(tmConf, privValidator, tmGenesisDoc, blockchain, checker, committer, logger) + if err != nil { + return nil, err + } + // Multiplex Tendermint and EVM events + eventEmitter := event.Multiplex(evmEvents, event.WrapEventSwitch(tmNode.EventSwitch(), logger)) + + txCodec := txs.NewGoWireCodec() + nameReg := execution.NewNameReg(state, blockchain) + + transactor := execution.NewTransactor(blockchain, state, eventEmitter, + tendermint.BroadcastTxAsyncFunc(tmNode, txCodec), logger) + + // TODO: consider whether we need to be more explicit about pre-commit (check cache) versus committed (state) values + // Note we pass the checker as the StateIterable to NewService which means the RPC layers will query the check + // cache state. This is in line with previous behaviour of Burrow and chiefly serves to get provide a pre-commit + // view of nonce values on the node that a client is communicating with. + // Since we don't currently execute EVM code in the checker possible conflicts are limited to account creation + // which increments the creator's account Sequence and SendTxs + service := rpc.NewService(state, eventEmitter, nameReg, blockchain, transactor, query.NewNodeView(tmNode, txCodec), + logger) + + servers := []ServerLauncher{ + { + Name: "TM", + Launch: func(service rpc.Service) (net.Listener, error) { + return tm.StartServer(service, "/websocket", rpcConfig.TM.ListenAddress, eventEmitter, logger) + }, + }, + } + + return &Kernel{ + eventSwitch: eventEmitter, + tmNode: tmNode, + service: service, + serverLaunchers: servers, + logger: logger, + shutdownNotify: make(chan struct{}), + }, nil +} + +// Boot the kernel starting Tendermint and RPC layers +func (kern *Kernel) Boot() error { + _, err := kern.tmNode.Start() + if err != nil { + return fmt.Errorf("error starting Tendermint node: %v", err) + } + for _, launcher := range kern.serverLaunchers { + listener, err := launcher.Launch(kern.service) + if err != nil { + return fmt.Errorf("error launching %s server", launcher.Name) + } + + kern.listeners = append(kern.listeners, listener) + } + go kern.supervise() + return nil +} + +// Wait for a graceful shutdown +func (kern *Kernel) WaitForShutdown() { + // Supports multiple goroutines waiting for shutdown since channel is closed + <-kern.shutdownNotify +} + +// Supervise kernel once booted +func (kern *Kernel) supervise() { + // TODO: Consider capturing kernel panics from boot and sending them here via a channel where we could + // perform disaster restarts of the kernel; rejoining the network as if we were a new node. + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt, os.Kill) + <-signals + kern.Shutdown() +} + +// Stop the kernel allowing for a graceful shutdown of components in order +func (kern *Kernel) Shutdown() (err error) { + kern.shutdownOnce.Do(func() { + logger := logging.WithScope(kern.logger, "Shutdown") + logging.InfoMsg(logger, "Attempting graceful shutdown...") + logging.InfoMsg(logger, "Shutting down listeners") + for _, listener := range kern.listeners { + err = listener.Close() + } + logging.InfoMsg(logger, "Shutting down Tendermint node") + kern.tmNode.Stop() + logging.InfoMsg(logger, "Shutdown complete") + close(kern.shutdownNotify) + }) + return +} diff --git a/core/kernel_test.go b/core/kernel_test.go new file mode 100644 index 0000000000000000000000000000000000000000..857d739924681afdbc3a83fe0fd16410f4b305f7 --- /dev/null +++ b/core/kernel_test.go @@ -0,0 +1,33 @@ +package core + +import ( + "os" + "testing" + + "github.com/hyperledger/burrow/consensus/tendermint/validator" + "github.com/hyperledger/burrow/genesis" + "github.com/hyperledger/burrow/logging/loggers" + "github.com/hyperledger/burrow/rpc" + "github.com/stretchr/testify/require" + tm_config "github.com/tendermint/tendermint/config" +) + +const testDir = "./test_scratch/kernel_test" + +func TestBootThenShutdown(t *testing.T) { + + os.RemoveAll(testDir) + os.MkdirAll(testDir, 0777) + os.Chdir(testDir) + tmConf := tm_config.DefaultConfig() + //logger, _ := lifecycle.NewStdErrLogger() + logger := loggers.NewNoopInfoTraceLogger() + genesisDoc, privateAccounts := genesis.NewDeterministicGenesis(123).GenesisDoc(1, true, 1000, 1, true, 1000) + privValidator := validator.NewPrivValidatorMemory(privateAccounts[0], privateAccounts[0]) + kern, err := NewKernel(privValidator, genesisDoc, tmConf, rpc.DefaultRPCConfig(), logger) + require.NoError(t, err) + err = kern.Boot() + require.NoError(t, err) + err = kern.Shutdown() + require.NoError(t, err) +} diff --git a/core/types/types.go b/core/types/types.go index 5b0934c164e3b90cf3245578ab0c70d6c76dbf9b..220b9e3742c547d5446bbee9036c643fc947e1f9 100644 --- a/core/types/types.go +++ b/core/types/types.go @@ -14,45 +14,19 @@ package types -// TODO: [ben] this is poorly constructed but copied over -// from burrow/burrow/pipe/types to make incremental changes and allow -// for a discussion around the proper defintion of the needed types. - import ( - // NodeInfo (drop this!) - "github.com/tendermint/tendermint/types" - - account "github.com/hyperledger/burrow/account" + tm_types "github.com/tendermint/tendermint/types" ) type ( - // *********************************** Address *********************************** - - // Accounts - AccountList struct { - Accounts []*account.Account `json:"accounts"` - } - - // A contract account storage item. - StorageItem struct { - Key []byte `json:"key"` - Value []byte `json:"value"` - } - - // Account storage - Storage struct { - StorageRoot []byte `json:"storage_root"` - StorageItems []StorageItem `json:"storage_items"` - } - // *********************************** Blockchain *********************************** // BlockchainInfo BlockchainInfo struct { - ChainId string `json:"chain_id"` - GenesisHash []byte `json:"genesis_hash"` - LatestBlockHeight int `json:"latest_block_height"` - LatestBlock *types.BlockMeta `json:"latest_block"` + ChainId string `json:"chain_id"` + GenesisHash []byte `json:"genesis_hash"` + LatestBlockHeight uint64 `json:"latest_block_height"` + LatestBlock *tm_types.BlockMeta `json:"latest_block"` } // Genesis hash @@ -62,7 +36,7 @@ type ( // Get the latest LatestBlockHeight struct { - Height int `json:"height"` + Height uint64 `json:"height"` } ChainId struct { @@ -71,78 +45,17 @@ type ( // GetBlocks Blocks struct { - MinHeight int `json:"min_height"` - MaxHeight int `json:"max_height"` - BlockMetas []*types.BlockMeta `json:"block_metas"` + MinHeight uint64 `json:"min_height"` + MaxHeight uint64 `json:"max_height"` + BlockMetas []*tm_types.BlockMeta `json:"block_metas"` } // *********************************** Consensus *********************************** // Validators ValidatorList struct { - BlockHeight int `json:"block_height"` - BondedValidators []*types.Validator `json:"bonded_validators"` - UnbondingValidators []*types.Validator `json:"unbonding_validators"` - } - - // *********************************** Events *********************************** - - // EventSubscribe - EventSub struct { - SubId string `json:"sub_id"` - } - - // EventUnsubscribe - EventUnsub struct { - Result bool `json:"result"` - } - - // EventPoll - PollResponse struct { - Events []interface{} `json:"events"` - } - - // *********************************** Network *********************************** - - ClientVersion struct { - ClientVersion string `json:"client_version"` - } - - Moniker struct { - Moniker string `json:"moniker"` - } - - Listening struct { - Listening bool `json:"listening"` - } - - Listeners struct { - Listeners []string `json:"listeners"` - } - - // *********************************** Transactions *********************************** - - // Call or CallCode - Call struct { - Return string `json:"return"` - GasUsed int64 `json:"gas_used"` - // TODO ... - } -) - -//------------------------------------------------------------------------------ -// copied in from NameReg - -type ( - NameRegEntry struct { - Name string `json:"name"` // registered name for the entry - Owner []byte `json:"owner"` // address that created the entry - Data string `json:"data"` // data to store under this name - Expires int `json:"expires"` // block at which this entry expires - } - - ResultListNames struct { - BlockHeight int `json:"block_height"` - Names []*NameRegEntry `json:"names"` + BlockHeight uint64 `json:"block_height"` + BondedValidators []*tm_types.Validator `json:"bonded_validators"` + UnbondingValidators []*tm_types.Validator `json:"unbonding_validators"` } )