diff --git a/.gitignore b/.gitignore index 221e318e1134480da924f070bef662ab4e5e9ff0..12be4236c891d5077c99a4a556c9813fc1b86284 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ target *.swp debug .idea +.vscode diff --git a/client/cmd/eris-client.go b/client/cmd/eris-client.go index 57e8d3ba4b8f7da97c2dc4d8adae4711c02c1a15..f073141998de883a7134df05199485f30b5e53e0 100644 --- a/client/cmd/eris-client.go +++ b/client/cmd/eris-client.go @@ -23,8 +23,6 @@ import ( "github.com/spf13/cobra" - log "github.com/eris-ltd/eris-logger" - "github.com/eris-ltd/eris-db/definitions" "github.com/eris-ltd/eris-db/version" ) @@ -41,14 +39,6 @@ Made with <3 by Eris Industries. Complete documentation is available at https://monax.io/docs/documentation ` + "\nVERSION:\n " + version.VERSION, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - log.SetLevel(log.WarnLevel) - if clientDo.Verbose { - log.SetLevel(log.InfoLevel) - } else if clientDo.Debug { - log.SetLevel(log.DebugLevel) - } - }, Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, } @@ -70,11 +60,12 @@ func AddGlobalFlags() { } func AddClientCommands() { - buildTransactionCommand() - ErisClientCmd.AddCommand(TransactionCmd) + ErisClientCmd.AddCommand(buildTransactionCommand()) + ErisClientCmd.AddCommand(buildStatusCommand()) + + buildGenesisGenCommand() + ErisClientCmd.AddCommand(GenesisGenCmd) - buildStatusCommand() - ErisClientCmd.AddCommand(StatusCmd) } //------------------------------------------------------------------------------ diff --git a/client/cmd/genesis.go b/client/cmd/genesis.go new file mode 100644 index 0000000000000000000000000000000000000000..ad8d01216b3fdcbc1304c7e8787581e533a49403 --- /dev/null +++ b/client/cmd/genesis.go @@ -0,0 +1,39 @@ +package commands + +import ( + "fmt" + + "github.com/eris-ltd/eris-db/genesis" + + "github.com/spf13/cobra" +) + +// TODO refactor these vars into a struct? +var ( + AccountsPathFlag string + ValidatorsPathFlag string +) + +var GenesisGenCmd = &cobra.Command{ + Use: "make-genesis", + Short: "eris-client make-genesis creates a genesis.json with known inputs", + Long: "eris-client make-genesis creates a genesis.json with known inputs", + + Run: func(cmd *cobra.Command, args []string) { + // TODO refactor to not panic + genesisFile, err := genesis.GenerateKnown(args[0], AccountsPathFlag, ValidatorsPathFlag) + if err != nil { + panic(err) + } + fmt.Println(genesisFile) // may want to save somewhere instead + }, +} + +func buildGenesisGenCommand() { + addGenesisPersistentFlags() +} + +func addGenesisPersistentFlags() { + GenesisGenCmd.Flags().StringVarP(&AccountsPathFlag, "accounts", "", "", "path to accounts.csv with the following params: (pubkey, starting balance, name, permissions, setbit") + GenesisGenCmd.Flags().StringVarP(&ValidatorsPathFlag, "validators", "", "", "path to validators.csv with the following params: (pubkey, starting balance, name, permissions, setbit") +} diff --git a/client/cmd/status.go b/client/cmd/status.go index 5b29803d848fba681b028bcc0cae3cf969853968..08807956dc1012a49aea2b49e1a50c8ac975e974 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -20,24 +20,23 @@ import ( "github.com/spf13/cobra" "github.com/eris-ltd/eris-db/client/methods" + "github.com/eris-ltd/eris-db/util" ) -var StatusCmd = &cobra.Command{ - Use: "status", - Short: "eris-client status returns the current status from a chain.", - Long: `eris-client status returns the current status from a chain. +func buildStatusCommand() *cobra.Command { + statusCmd := &cobra.Command{ + Use: "status", + Short: "eris-client status returns the current status from a chain.", + Long: `eris-client status returns the current status from a chain. `, - Run: func(cmd *cobra.Command, args []string) { - methods.Status(clientDo) - }, -} - -func buildStatusCommand() { - addStatusPersistentFlags() -} - -func addStatusPersistentFlags() { - StatusCmd.PersistentFlags().StringVarP(&clientDo.NodeAddrFlag, "node-addr", "", defaultNodeRpcAddress(), "set the eris-db node rpc server address (default respects $ERIS_CLIENT_NODE_ADDRESS)") + Run: func(cmd *cobra.Command, args []string) { + err := methods.Status(clientDo) + if err != nil { + util.Fatalf("Could not get status: %s", err) + } + }, + } + statusCmd.PersistentFlags().StringVarP(&clientDo.NodeAddrFlag, "node-addr", "", defaultNodeRpcAddress(), "set the eris-db node rpc server address (default respects $ERIS_CLIENT_NODE_ADDRESS)") // TransactionCmd.PersistentFlags().StringVarP(&clientDo.PubkeyFlag, "pubkey", "", defaultPublicKey(), "specify the public key to sign with (defaults to $ERIS_CLIENT_PUBLIC_KEY)") // TransactionCmd.PersistentFlags().StringVarP(&clientDo.AddrFlag, "addr", "", defaultAddress(), "specify the account address (for which the public key can be found at eris-keys) (default respects $ERIS_CLIENT_ADDRESS)") // TransactionCmd.PersistentFlags().StringVarP(&clientDo.ChainidFlag, "chain-id", "", defaultChainId(), "specify the chainID (default respects $CHAIN_ID)") @@ -46,4 +45,6 @@ func addStatusPersistentFlags() { // // TransactionCmd.PersistentFlags().BoolVarP(&clientDo.SignFlag, "sign", "s", false, "sign the transaction using the eris-keys daemon") // TransactionCmd.PersistentFlags().BoolVarP(&clientDo.BroadcastFlag, "broadcast", "b", true, "broadcast the transaction to the blockchain") // TransactionCmd.PersistentFlags().BoolVarP(&clientDo.WaitFlag, "wait", "w", false, "wait for the transaction to be committed in a block") + + return statusCmd } diff --git a/client/cmd/transaction.go b/client/cmd/transaction.go index 87a3e10c5ee99aeb84a97062280d98c4047d0016..a527577481c7fec56dc87ac25383cbfef6b7f3f7 100644 --- a/client/cmd/transaction.go +++ b/client/cmd/transaction.go @@ -17,37 +17,36 @@ package commands import ( - "os" "strings" "github.com/spf13/cobra" - log "github.com/eris-ltd/eris-logger" - - "github.com/eris-ltd/eris-db/client/transaction" + "github.com/eris-ltd/eris-db/client/methods" + "github.com/eris-ltd/eris-db/util" ) -var TransactionCmd = &cobra.Command{ - Use: "tx", - Short: "eris-client tx formulates and signs a transaction to a chain", - Long: `eris-client tx formulates and signs a transaction to a chain. -`, - Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, -} - -func buildTransactionCommand() { +func buildTransactionCommand() *cobra.Command { // Transaction command has subcommands send, name, call, bond, // unbond, rebond, permissions. Dupeout transaction is not accessible through the command line. + transactionCmd := &cobra.Command{ + Use: "tx", + Short: "eris-client tx formulates and signs a transaction to a chain", + Long: "eris-client tx formulates and signs a transaction to a chain.", + Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, + } - addTransactionPersistentFlags() + addTransactionPersistentFlags(transactionCmd) // SendTx - var sendCmd = &cobra.Command{ + sendCmd := &cobra.Command{ Use: "send", Short: "eris-client tx send --amt <amt> --to <addr>", Long: "eris-client tx send --amt <amt> --to <addr>", Run: func(cmd *cobra.Command, args []string) { - transaction.Send(clientDo) + err := methods.Send(clientDo) + if err != nil { + util.Fatalf("Could not complete send: %s", err) + } }, PreRun: assertParameters, } @@ -55,7 +54,7 @@ func buildTransactionCommand() { sendCmd.Flags().StringVarP(&clientDo.ToFlag, "to", "t", "", "specify an address to send to") // NameTx - var nameCmd = &cobra.Command{ + nameCmd := &cobra.Command{ Use: "name", Short: "eris-client tx name --amt <amt> --name <name> --data <data>", Long: "eris-client tx name --amt <amt> --name <name> --data <data>", @@ -71,12 +70,15 @@ func buildTransactionCommand() { nameCmd.Flags().StringVarP(&clientDo.FeeFlag, "fee", "f", "", "specify the fee to send") // CallTx - var callCmd = &cobra.Command{ + callCmd := &cobra.Command{ Use: "call", Short: "eris-client tx call --amt <amt> --fee <fee> --gas <gas> --to <contract addr> --data <data>", Long: "eris-client tx call --amt <amt> --fee <fee> --gas <gas> --to <contract addr> --data <data>", Run: func(cmd *cobra.Command, args []string) { - transaction.Call(clientDo) + err := methods.Call(clientDo) + if err != nil { + util.Fatalf("Could not complete call: %s", err) + } }, PreRun: assertParameters, } @@ -87,7 +89,7 @@ func buildTransactionCommand() { callCmd.Flags().StringVarP(&clientDo.GasFlag, "gas", "g", "", "specify the gas limit for a CallTx") // BondTx - var bondCmd = &cobra.Command{ + bondCmd := &cobra.Command{ Use: "bond", Short: "eris-client tx bond --pubkey <pubkey> --amt <amt> --unbond-to <address>", Long: "eris-client tx bond --pubkey <pubkey> --amt <amt> --unbond-to <address>", @@ -100,7 +102,7 @@ func buildTransactionCommand() { bondCmd.Flags().StringVarP(&clientDo.UnbondtoFlag, "to", "t", "", "specify an address to unbond to") // UnbondTx - var unbondCmd = &cobra.Command{ + unbondCmd := &cobra.Command{ Use: "unbond", Short: "eris-client tx unbond --addr <address> --height <block_height>", Long: "eris-client tx unbond --addr <address> --height <block_height>", @@ -126,7 +128,7 @@ func buildTransactionCommand() { rebondCmd.Flags().StringVarP(&clientDo.HeightFlag, "height", "n", "", "specify a height to unbond at") // PermissionsTx - var permissionsCmd = &cobra.Command{ + permissionsCmd := &cobra.Command{ Use: "permission", Short: "eris-client tx perm <function name> <args ...>", Long: "eris-client tx perm <function name> <args ...>", @@ -136,20 +138,21 @@ func buildTransactionCommand() { PreRun: assertParameters, } - TransactionCmd.AddCommand(sendCmd, nameCmd, callCmd, bondCmd, unbondCmd, rebondCmd, permissionsCmd) + transactionCmd.AddCommand(sendCmd, nameCmd, callCmd, bondCmd, unbondCmd, rebondCmd, permissionsCmd) + return transactionCmd } -func addTransactionPersistentFlags() { - TransactionCmd.PersistentFlags().StringVarP(&clientDo.SignAddrFlag, "sign-addr", "", defaultKeyDaemonAddress(), "set eris-keys daemon address (default respects $ERIS_CLIENT_SIGN_ADDRESS)") - TransactionCmd.PersistentFlags().StringVarP(&clientDo.NodeAddrFlag, "node-addr", "", defaultNodeRpcAddress(), "set the eris-db node rpc server address (default respects $ERIS_CLIENT_NODE_ADDRESS)") - TransactionCmd.PersistentFlags().StringVarP(&clientDo.PubkeyFlag, "pubkey", "", defaultPublicKey(), "specify the public key to sign with (defaults to $ERIS_CLIENT_PUBLIC_KEY)") - TransactionCmd.PersistentFlags().StringVarP(&clientDo.AddrFlag, "addr", "", defaultAddress(), "specify the account address (for which the public key can be found at eris-keys) (default respects $ERIS_CLIENT_ADDRESS)") - TransactionCmd.PersistentFlags().StringVarP(&clientDo.ChainidFlag, "chain-id", "", defaultChainId(), "specify the chainID (default respects $CHAIN_ID)") - TransactionCmd.PersistentFlags().StringVarP(&clientDo.NonceFlag, "nonce", "", "", "specify the nonce to use for the transaction (should equal the sender account's nonce + 1)") - - // TransactionCmd.PersistentFlags().BoolVarP(&clientDo.SignFlag, "sign", "s", false, "sign the transaction using the eris-keys daemon") - TransactionCmd.PersistentFlags().BoolVarP(&clientDo.BroadcastFlag, "broadcast", "b", true, "broadcast the transaction to the blockchain") - TransactionCmd.PersistentFlags().BoolVarP(&clientDo.WaitFlag, "wait", "w", true, "wait for the transaction to be committed in a block") +func addTransactionPersistentFlags(transactionCmd *cobra.Command) { + transactionCmd.PersistentFlags().StringVarP(&clientDo.SignAddrFlag, "sign-addr", "", defaultKeyDaemonAddress(), "set eris-keys daemon address (default respects $ERIS_CLIENT_SIGN_ADDRESS)") + transactionCmd.PersistentFlags().StringVarP(&clientDo.NodeAddrFlag, "node-addr", "", defaultNodeRpcAddress(), "set the eris-db node rpc server address (default respects $ERIS_CLIENT_NODE_ADDRESS)") + transactionCmd.PersistentFlags().StringVarP(&clientDo.PubkeyFlag, "pubkey", "", defaultPublicKey(), "specify the public key to sign with (defaults to $ERIS_CLIENT_PUBLIC_KEY)") + transactionCmd.PersistentFlags().StringVarP(&clientDo.AddrFlag, "addr", "", defaultAddress(), "specify the account address (for which the public key can be found at eris-keys) (default respects $ERIS_CLIENT_ADDRESS)") + transactionCmd.PersistentFlags().StringVarP(&clientDo.ChainidFlag, "chain-id", "", defaultChainId(), "specify the chainID (default respects $CHAIN_ID)") + transactionCmd.PersistentFlags().StringVarP(&clientDo.NonceFlag, "nonce", "", "", "specify the nonce to use for the transaction (should equal the sender account's nonce + 1)") + + // transactionCmd.PersistentFlags().BoolVarP(&clientDo.SignFlag, "sign", "s", false, "sign the transaction using the eris-keys daemon") + transactionCmd.PersistentFlags().BoolVarP(&clientDo.BroadcastFlag, "broadcast", "b", true, "broadcast the transaction to the blockchain") + transactionCmd.PersistentFlags().BoolVarP(&clientDo.WaitFlag, "wait", "w", true, "wait for the transaction to be committed in a block") } //------------------------------------------------------------------------------ @@ -180,26 +183,21 @@ func defaultAddress() string { func assertParameters(cmd *cobra.Command, args []string) { if clientDo.ChainidFlag == "" { - log.Fatal(`Please provide a chain id either through the flag --chain-id or environment variable $CHAIN_ID.`) - os.Exit(1) + util.Fatalf(`Please provide a chain id either through the flag --chain-id or environment variable $CHAIN_ID.`) } if !strings.HasPrefix(clientDo.NodeAddrFlag, "tcp://") && !strings.HasPrefix(clientDo.NodeAddrFlag, "unix://") { // TODO: [ben] go-rpc will deprecate reformatting; also it is bad practice to auto-correct for this; - log.Warn(`Please use fully formed listening address for the node, including the tcp:// or unix:// prefix.`) + // TODO: [Silas] I've made this fatal, but I'm inclined to define the default as tcp:// and normalise as with http + // below + util.Fatalf(`Please use fully formed listening address for the node, including the tcp:// or unix:// prefix.`) } if !strings.HasPrefix(clientDo.SignAddrFlag, "http://") { // NOTE: [ben] we preserve the auto-correction here as it is a simple http request-response to the key server. + // TODO: [Silas] we don't have logging here to log that we've done this. I'm inclined to either urls without a scheme + // and be quiet about it, or to make non-compliance fatal clientDo.SignAddrFlag = "http://" + clientDo.SignAddrFlag - log.WithFields(log.Fields{ - "signing address": clientDo.SignAddrFlag, - }).Warn(`Please use fully formed listening address for the key server; adding http:// prefix`) } - log.WithFields(log.Fields{ - "signing address": clientDo.SignAddrFlag, - "node address": clientDo.NodeAddrFlag, - "chain id": clientDo.ChainidFlag, - }).Debug("Asserted parameters") } diff --git a/client/methods/call.go b/client/methods/call.go new file mode 100644 index 0000000000000000000000000000000000000000..1bdcbdfeceea4f9c41663c244d76cddf392a6591 --- /dev/null +++ b/client/methods/call.go @@ -0,0 +1,55 @@ +// Copyright 2015, 2016 Eris Industries (UK) Ltd. +// This file is part of Eris-RT + +// Eris-RT is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Eris-RT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. + +package methods + +import ( + "fmt" + + "github.com/eris-ltd/eris-db/client" + "github.com/eris-ltd/eris-db/client/rpc" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/keys" +) + +func Call(do *definitions.ClientDo) error { + // construct two clients to call out to keys server and + // blockchain node. + logger, err := loggerFromClientDo(do, "Call") + if err != nil { + return fmt.Errorf("Could not generate logging config from ClientDo: %s", err) + } + erisKeyClient := keys.NewErisKeyClient(do.SignAddrFlag, logger) + erisNodeClient := client.NewErisNodeClient(do.NodeAddrFlag, logger) + // form the call transaction + callTransaction, err := rpc.Call(erisNodeClient, erisKeyClient, + do.PubkeyFlag, do.AddrFlag, do.ToFlag, do.AmtFlag, do.NonceFlag, + do.GasFlag, do.FeeFlag, do.DataFlag) + if err != nil { + return fmt.Errorf("Failed on forming Call Transaction: %s", err) + } + // TODO: [ben] we carry over the sign bool, but always set it to true, + // as we move away from and deprecate the api that allows sending unsigned + // transactions and relying on (our) receiving node to sign it. + txResult, err := rpc.SignAndBroadcast(do.ChainidFlag, erisNodeClient, erisKeyClient, + callTransaction, true, do.BroadcastFlag, do.WaitFlag) + + if err != nil { + return fmt.Errorf("Failed on signing (and broadcasting) transaction: %s", err) + } + unpackSignAndBroadcast(txResult, logger) + return nil +} diff --git a/client/methods/helpers.go b/client/methods/helpers.go new file mode 100644 index 0000000000000000000000000000000000000000..13c7138d26f1a9ddcc15a7e38fe505b6df43acef --- /dev/null +++ b/client/methods/helpers.go @@ -0,0 +1,56 @@ +// Copyright 2015, 2016 Eris Industries (UK) Ltd. +// This file is part of Eris-RT + +// Eris-RT is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Eris-RT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. + +package methods + +import ( + "github.com/eris-ltd/eris-db/client/rpc" + "github.com/eris-ltd/eris-db/core" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/lifecycle" + "github.com/eris-ltd/eris-db/logging/loggers" +) + +func unpackSignAndBroadcast(result *rpc.TxResult, logger loggers.InfoTraceLogger) { + if result == nil { + // if we don't provide --sign or --broadcast + return + } + + logger = logger.With("transaction hash", result.Hash) + + if result.Address != nil { + logger = logger.With("Contract Address", result.Address) + } + + if result.Return != nil { + logger = logger.With("Block Hash", result.BlockHash, + "Return Value", result.Return, + "Exception", result.Exception, + ) + } + + logging.InfoMsg(logger, "SignAndBroadcast result") +} + +func loggerFromClientDo(do *definitions.ClientDo, scope string) (loggers.InfoTraceLogger, error) { + lc, err := core.LoadLoggingConfigFromClientDo(do) + if err != nil { + return nil, err + } + return logging.WithScope(lifecycle.NewLoggerFromLoggingConfig(lc), scope), nil +} diff --git a/client/methods/send.go b/client/methods/send.go new file mode 100644 index 0000000000000000000000000000000000000000..65c2db37e5584c6e11205097aa0d3c01c45343bb --- /dev/null +++ b/client/methods/send.go @@ -0,0 +1,53 @@ +// Copyright 2015, 2016 Eris Industries (UK) Ltd. +// This file is part of Eris-RT + +// Eris-RT is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Eris-RT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. + +package methods + +import ( + "fmt" + + "github.com/eris-ltd/eris-db/client" + "github.com/eris-ltd/eris-db/client/rpc" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/keys" +) + +func Send(do *definitions.ClientDo) error { + // construct two clients to call out to keys server and + // blockchain node. + logger, err := loggerFromClientDo(do, "Send") + if err != nil { + return fmt.Errorf("Could not generate logging config from ClientDo: %s", err) + } + erisKeyClient := keys.NewErisKeyClient(do.SignAddrFlag, logger) + erisNodeClient := client.NewErisNodeClient(do.NodeAddrFlag, logger) + // form the send transaction + sendTransaction, err := rpc.Send(erisNodeClient, erisKeyClient, + do.PubkeyFlag, do.AddrFlag, do.ToFlag, do.AmtFlag, do.NonceFlag) + if err != nil { + fmt.Errorf("Failed on forming Send Transaction: %s", err) + } + // TODO: [ben] we carry over the sign bool, but always set it to true, + // as we move away from and deprecate the api that allows sending unsigned + // transactions and relying on (our) receiving node to sign it. + txResult, err := rpc.SignAndBroadcast(do.ChainidFlag, erisNodeClient, erisKeyClient, + sendTransaction, true, do.BroadcastFlag, do.WaitFlag) + if err != nil { + return fmt.Errorf("Failed on signing (and broadcasting) transaction: %s", err) + } + unpackSignAndBroadcast(txResult, logger) + return nil +} diff --git a/client/methods/status.go b/client/methods/status.go index ba2470e7645ba0759445daed1042211c8f184401..dd5bf72523bc9ba5277fadf10fb6e9bf2c1d7209 100644 --- a/client/methods/status.go +++ b/client/methods/status.go @@ -19,35 +19,35 @@ package methods import ( "fmt" - log "github.com/eris-ltd/eris-logger" - "github.com/eris-ltd/eris-db/client" "github.com/eris-ltd/eris-db/definitions" ) -func Status(do *definitions.ClientDo) { - erisNodeClient := client.NewErisNodeClient(do.NodeAddrFlag) +func Status(do *definitions.ClientDo) error { + logger, err := loggerFromClientDo(do, "Status") + if err != nil { + return fmt.Errorf("Could not generate logging config from ClientDo: %s", err) + } + erisNodeClient := client.NewErisNodeClient(do.NodeAddrFlag, logger) genesisHash, validatorPublicKey, latestBlockHash, latestBlockHeight, latestBlockTime, err := erisNodeClient.Status() if err != nil { - log.Errorf("Error requesting status from chain at (%s): %s", do.NodeAddrFlag, err) - return + return fmt.Errorf("Error requesting status from chain at (%s): %s", do.NodeAddrFlag, err) } chainName, chainId, genesisHashfromChainId, err := erisNodeClient.ChainId() if err != nil { - log.Errorf("Error requesting chainId from chain at (%s): %s", do.NodeAddrFlag, err) - return + return fmt.Errorf("Error requesting chainId from chain at (%s): %s", do.NodeAddrFlag, err) } - log.WithFields(log.Fields{ - "chain": do.NodeAddrFlag, - "genesisHash": fmt.Sprintf("%X", genesisHash), - "chainName": chainName, - "chainId": chainId, - "genesisHash from chainId": fmt.Sprintf("%X", genesisHashfromChainId), - "validator public key": fmt.Sprintf("%X", validatorPublicKey), - "latest block hash": fmt.Sprintf("%X", latestBlockHash), - "latest block height": latestBlockHeight, - "latest block time": latestBlockTime, - }).Info("status") + logger.Info("chain", do.NodeAddrFlag, + "genesisHash", fmt.Sprintf("%X", genesisHash), + "chainName", chainName, + "chainId", chainId, + "genesisHash from chainId", fmt.Sprintf("%X", genesisHashfromChainId), + "validator public key", fmt.Sprintf("%X", validatorPublicKey), + "latest block hash", fmt.Sprintf("%X", latestBlockHash), + "latest block height", latestBlockHeight, + "latest block time", latestBlockTime, + ) + return nil } diff --git a/client/mock/client_mock.go b/client/mock/client_mock.go index 12cdd76cf9b15f9575898224889a1528eb8a58b7..adcde24bd7ba41b53b2f21feedb62189da477a92 100644 --- a/client/mock/client_mock.go +++ b/client/mock/client_mock.go @@ -23,6 +23,7 @@ import ( . "github.com/eris-ltd/eris-db/client" consensus_types "github.com/eris-ltd/eris-db/consensus/types" core_types "github.com/eris-ltd/eris-db/core/types" + "github.com/eris-ltd/eris-db/logging/loggers" "github.com/eris-ltd/eris-db/txs" ) @@ -117,3 +118,7 @@ func (mock *MockNodeClient) GetName(name string) (owner []byte, data string, exp func (mock *MockNodeClient) ListValidators() (blockHeight int, bondedValidators, unbondingValidators []consensus_types.Validator, err error) { return 0, nil, nil, nil } + +func (mock *MockNodeClient) Logger() loggers.InfoTraceLogger { + return loggers.NewNoopInfoTraceLogger() +} diff --git a/client/client.go b/client/node_client.go similarity index 84% rename from client/client.go rename to client/node_client.go index 34275c8b2deacc6a10d18d18a496297dd3b44430..4502d77cca860fc0708f8ec14248502a515843ac 100644 --- a/client/client.go +++ b/client/node_client.go @@ -21,18 +21,16 @@ import ( // "strings" "github.com/tendermint/go-rpc/client" - // Note [ben]: this is included to silence the logger from tendermint/go-rpc/client - // see func init() - tendermint_log "github.com/tendermint/log15" - - log "github.com/eris-ltd/eris-logger" acc "github.com/eris-ltd/eris-db/account" consensus_types "github.com/eris-ltd/eris-db/consensus/types" core_types "github.com/eris-ltd/eris-db/core/types" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" tendermint_client "github.com/eris-ltd/eris-db/rpc/tendermint/client" tendermint_types "github.com/eris-ltd/eris-db/rpc/tendermint/core/types" "github.com/eris-ltd/eris-db/txs" + tmLog15 "github.com/tendermint/log15" ) type NodeClient interface { @@ -48,6 +46,9 @@ type NodeClient interface { DumpStorage(address []byte) (storage *core_types.Storage, err error) GetName(name string) (owner []byte, data string, expirationBlock int, err error) ListValidators() (blockHeight int, bondedValidators, unbondingValidators []consensus_types.Validator, err error) + + // Logging context for this NodeClient + Logger() loggers.InfoTraceLogger } type NodeWebsocketClient interface { @@ -58,29 +59,30 @@ type NodeWebsocketClient interface { Close() } -// NOTE [ben] Compiler check to ensure ErisNodeClient successfully implements +// NOTE [ben] Compiler check to ensure erisNodeClient successfully implements // eris-db/client.NodeClient -var _ NodeClient = (*ErisNodeClient)(nil) +var _ NodeClient = (*erisNodeClient)(nil) // Eris-Client is a simple struct exposing the client rpc methods - -type ErisNodeClient struct { +type erisNodeClient struct { broadcastRPC string + logger loggers.InfoTraceLogger } // ErisKeyClient.New returns a new eris-keys client for provided rpc location // Eris-keys connects over http request-responses -func NewErisNodeClient(rpcString string) *ErisNodeClient { - return &ErisNodeClient{ +func NewErisNodeClient(rpcString string, logger loggers.InfoTraceLogger) *erisNodeClient { + return &erisNodeClient{ broadcastRPC: rpcString, + logger: logging.WithScope(logger, "ErisNodeClient"), } } // Note [Ben]: This is a hack to silence Tendermint logger from tendermint/go-rpc // it needs to be initialised before go-rpc, hence it's placement here. func init() { - h := tendermint_log.LvlFilterHandler(tendermint_log.LvlWarn, tendermint_log.StdoutHandler) - tendermint_log.Root().SetHandler(h) + h := tmLog15.LvlFilterHandler(tmLog15.LvlWarn, tmLog15.StdoutHandler) + tmLog15.Root().SetHandler(h) } //------------------------------------------------------------------------------------ @@ -88,7 +90,7 @@ func init() { // NOTE: [ben] Eris Client first continues from tendermint rpc, but will have handshake to negotiate // protocol version for moving towards rpc/v1 -func (erisNodeClient *ErisNodeClient) Broadcast(tx txs.Tx) (*txs.Receipt, error) { +func (erisNodeClient *erisNodeClient) Broadcast(tx txs.Tx) (*txs.Receipt, error) { client := rpcclient.NewClientURI(erisNodeClient.broadcastRPC) receipt, err := tendermint_client.BroadcastTx(client, tx) if err != nil { @@ -97,7 +99,7 @@ func (erisNodeClient *ErisNodeClient) Broadcast(tx txs.Tx) (*txs.Receipt, error) return &receipt, nil } -func (erisNodeClient *ErisNodeClient) DeriveWebsocketClient() (nodeWsClient NodeWebsocketClient, err error) { +func (erisNodeClient *erisNodeClient) DeriveWebsocketClient() (nodeWsClient NodeWebsocketClient, err error) { var wsAddr string // TODO: clean up this inherited mess on dealing with the address prefixes. nodeAddr := erisNodeClient.broadcastRPC @@ -115,16 +117,17 @@ func (erisNodeClient *ErisNodeClient) DeriveWebsocketClient() (nodeWsClient Node // } // wsAddr = "ws://" + wsAddr wsAddr = nodeAddr - log.WithFields(log.Fields{ - "websocket address": wsAddr, - "endpoint": "/websocket", - }).Debug("Subscribing to websocket address") + logging.TraceMsg(erisNodeClient.logger, "Subscribing to websocket address", + "websocket address", wsAddr, + "endpoint", "/websocket", + ) wsClient := rpcclient.NewWSClient(wsAddr, "/websocket") if _, err = wsClient.Start(); err != nil { return nil, err } - derivedErisNodeWebsocketClient := &ErisNodeWebsocketClient{ + derivedErisNodeWebsocketClient := &erisNodeWebsocketClient{ tendermintWebsocket: wsClient, + logger: logging.WithScope(erisNodeClient.logger, "ErisNodeWebsocketClient"), } return derivedErisNodeWebsocketClient, nil } @@ -134,7 +137,7 @@ func (erisNodeClient *ErisNodeClient) DeriveWebsocketClient() (nodeWsClient Node // Status returns the ChainId (GenesisHash), validator's PublicKey, latest block hash // the block height and the latest block time. -func (erisNodeClient *ErisNodeClient) Status() (GenesisHash []byte, ValidatorPublicKey []byte, LatestBlockHash []byte, LatestBlockHeight int, LatestBlockTime int64, err error) { +func (erisNodeClient *erisNodeClient) Status() (GenesisHash []byte, ValidatorPublicKey []byte, LatestBlockHash []byte, LatestBlockHeight int, LatestBlockTime int64, err error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) res, err := tendermint_client.Status(client) if err != nil { @@ -152,7 +155,7 @@ func (erisNodeClient *ErisNodeClient) Status() (GenesisHash []byte, ValidatorPub return } -func (erisNodeClient *ErisNodeClient) ChainId() (ChainName, ChainId string, GenesisHash []byte, err error) { +func (erisNodeClient *erisNodeClient) ChainId() (ChainName, ChainId string, GenesisHash []byte, err error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) chainIdResult, err := tendermint_client.ChainId(client) if err != nil { @@ -170,7 +173,7 @@ func (erisNodeClient *ErisNodeClient) ChainId() (ChainName, ChainId string, Gene // QueryContract executes the contract code at address with the given data // NOTE: there is no check on the caller; -func (erisNodeClient *ErisNodeClient) QueryContract(callerAddress, calleeAddress, data []byte) (ret []byte, gasUsed int64, err error) { +func (erisNodeClient *erisNodeClient) QueryContract(callerAddress, calleeAddress, data []byte) (ret []byte, gasUsed int64, err error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) callResult, err := tendermint_client.Call(client, callerAddress, calleeAddress, data) if err != nil { @@ -182,7 +185,7 @@ func (erisNodeClient *ErisNodeClient) QueryContract(callerAddress, calleeAddress } // QueryContractCode executes the contract code at address with the given data but with provided code -func (erisNodeClient *ErisNodeClient) QueryContractCode(address, code, data []byte) (ret []byte, gasUsed int64, err error) { +func (erisNodeClient *erisNodeClient) QueryContractCode(address, code, data []byte) (ret []byte, gasUsed int64, err error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) // TODO: [ben] Call and CallCode have an inconsistent signature; it makes sense for both to only // have a single address that is the contract to query. @@ -196,7 +199,7 @@ func (erisNodeClient *ErisNodeClient) QueryContractCode(address, code, data []by } // GetAccount returns a copy of the account -func (erisNodeClient *ErisNodeClient) GetAccount(address []byte) (*acc.Account, error) { +func (erisNodeClient *erisNodeClient) GetAccount(address []byte) (*acc.Account, error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) account, err := tendermint_client.GetAccount(client, address) if err != nil { @@ -213,7 +216,7 @@ func (erisNodeClient *ErisNodeClient) GetAccount(address []byte) (*acc.Account, } // DumpStorage returns the full storage for an account. -func (erisNodeClient *ErisNodeClient) DumpStorage(address []byte) (storage *core_types.Storage, err error) { +func (erisNodeClient *erisNodeClient) DumpStorage(address []byte) (storage *core_types.Storage, err error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) resultStorage, err := tendermint_client.DumpStorage(client, address) if err != nil { @@ -231,7 +234,7 @@ func (erisNodeClient *ErisNodeClient) DumpStorage(address []byte) (storage *core //-------------------------------------------------------------------------------------------- // Name registry -func (erisNodeClient *ErisNodeClient) GetName(name string) (owner []byte, data string, expirationBlock int, err error) { +func (erisNodeClient *erisNodeClient) GetName(name string) (owner []byte, data string, expirationBlock int, err error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) entryResult, err := tendermint_client.GetName(client, name) if err != nil { @@ -248,7 +251,7 @@ func (erisNodeClient *ErisNodeClient) GetName(name string) (owner []byte, data s //-------------------------------------------------------------------------------------------- -func (erisNodeClient *ErisNodeClient) ListValidators() (blockHeight int, +func (erisNodeClient *erisNodeClient) ListValidators() (blockHeight int, bondedValidators []consensus_types.Validator, unbondingValidators []consensus_types.Validator, err error) { client := rpcclient.NewClientJSONRPC(erisNodeClient.broadcastRPC) validatorsResult, err := tendermint_client.ListValidators(client) @@ -263,3 +266,7 @@ func (erisNodeClient *ErisNodeClient) ListValidators() (blockHeight int, unbondingValidators = validatorsResult.UnbondingValidators return } + +func (erisNodeClient *erisNodeClient) Logger() loggers.InfoTraceLogger { + return erisNodeClient.logger +} diff --git a/client/core/transaction_factory.go b/client/rpc/client.go similarity index 93% rename from client/core/transaction_factory.go rename to client/rpc/client.go index 569e1d8a2cd3fb5131ca42f9c96d4e6a5633a613..77ed13b528cf10853ab790088a456e777ecf910c 100644 --- a/client/core/transaction_factory.go +++ b/client/rpc/client.go @@ -14,27 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. -package core +package rpc import ( "encoding/hex" "fmt" "strconv" - log "github.com/eris-ltd/eris-logger" - ptypes "github.com/eris-ltd/eris-db/permission/types" - "github.com/eris-ltd/eris-db/account" "github.com/eris-ltd/eris-db/client" "github.com/eris-ltd/eris-db/keys" "github.com/eris-ltd/eris-db/txs" ) -var ( - MaxCommitWaitTimeSeconds = 20 -) - //------------------------------------------------------------------------------------ // core functions with string args. // validates strings and forms transaction @@ -272,16 +265,14 @@ type TxResult struct { } // Preserve -func SignAndBroadcast(chainID string, nodeClient client.NodeClient, keyClient keys.KeyClient, tx txs.Tx, sign, broadcast, wait bool) (txResult *TxResult, err error) { +func SignAndBroadcast(chainID string, nodeClient client.NodeClient, keyClient keys.KeyClient, tx txs.Tx, sign, + broadcast, wait bool) (txResult *TxResult, err error) { var inputAddr []byte if sign { inputAddr, tx, err = signTx(keyClient, chainID, tx) if err != nil { return nil, err } - log.WithFields(log.Fields{ - "transaction": string(account.SignBytes(chainID, tx)), - }).Debug("Signed transaction") } if broadcast { @@ -300,23 +291,19 @@ func SignAndBroadcast(chainID string, nodeClient client.NodeClient, keyClient ke // if broadcast threw an error, just return return } - log.Debug("Waiting for transaction to be confirmed.") confirmation := <-confirmationChannel if confirmation.Error != nil { - log.Errorf("Encountered error waiting for event: %s\n", confirmation.Error) - err = confirmation.Error + err = fmt.Errorf("Encountered error waiting for event: %s", confirmation.Error) return } if confirmation.Exception != nil { - log.Errorf("Encountered Exception from chain: %s\n", confirmation.Exception) - err = confirmation.Exception + err = fmt.Errorf("Encountered Exception from chain: %s", confirmation.Exception) return } txResult.BlockHash = confirmation.BlockHash txResult.Exception = "" eventDataTx, ok := confirmation.Event.(*txs.EventDataTx) if !ok { - log.Errorf("Received wrong event type.") err = fmt.Errorf("Received wrong event type.") return } diff --git a/client/core/transaction_factory_test.go b/client/rpc/client_test.go similarity index 91% rename from client/core/transaction_factory_test.go rename to client/rpc/client_test.go index 4544655e1d045332e07678a772b09a1c267b155b..6541bd3f9f3b92b29a3c1c231669afde4350a0a2 100644 --- a/client/core/transaction_factory_test.go +++ b/client/rpc/client_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. -package core +package rpc import ( "fmt" @@ -26,19 +26,19 @@ import ( mockkeys "github.com/eris-ltd/eris-db/keys/mock" ) -func TestTransactionFactory(t *testing.T) { +func Test(t *testing.T) { mockKeyClient := mockkeys.NewMockKeyClient() mockNodeClient := mockclient.NewMockNodeClient() - testTransactionFactorySend(t, mockNodeClient, mockKeyClient) - testTransactionFactoryCall(t, mockNodeClient, mockKeyClient) - testTransactionFactoryName(t, mockNodeClient, mockKeyClient) - testTransactionFactoryPermissions(t, mockNodeClient, mockKeyClient) + testSend(t, mockNodeClient, mockKeyClient) + testCall(t, mockNodeClient, mockKeyClient) + testName(t, mockNodeClient, mockKeyClient) + testPermissions(t, mockNodeClient, mockKeyClient) // t.Run("BondTransaction", ) // t.Run("UnbondTransaction", ) // t.Run("RebondTransaction", ) } -func testTransactionFactorySend(t *testing.T, +func testSend(t *testing.T, nodeClient *mockclient.MockNodeClient, keyClient *mockkeys.MockKeyClient) { // generate an ED25519 key and ripemd160 address @@ -66,7 +66,7 @@ func testTransactionFactorySend(t *testing.T, // TODO: test content of Transaction } -func testTransactionFactoryCall(t *testing.T, +func testCall(t *testing.T, nodeClient *mockclient.MockNodeClient, keyClient *mockkeys.MockKeyClient) { // generate an ED25519 key and ripemd160 address @@ -99,7 +99,7 @@ func testTransactionFactoryCall(t *testing.T, // TODO: test content of Transaction } -func testTransactionFactoryName(t *testing.T, +func testName(t *testing.T, nodeClient *mockclient.MockNodeClient, keyClient *mockkeys.MockKeyClient) { // generate an ED25519 key and ripemd160 address @@ -130,7 +130,7 @@ func testTransactionFactoryName(t *testing.T, // TODO: test content of Transaction } -func testTransactionFactoryPermissions(t *testing.T, +func testPermissions(t *testing.T, nodeClient *mockclient.MockNodeClient, keyClient *mockkeys.MockKeyClient) { // generate an ED25519 key and ripemd160 address diff --git a/client/core/transaction_factory_util.go b/client/rpc/client_util.go similarity index 92% rename from client/core/transaction_factory_util.go rename to client/rpc/client_util.go index 3ad64a6ba36ad88ad5c11b6a0d70584285239095..df8ffe3f0fe77cd927014794878b6ada6806f421 100644 --- a/client/core/transaction_factory_util.go +++ b/client/rpc/client_util.go @@ -14,20 +14,19 @@ // You should have received a copy of the GNU General Public License // along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. -package core +package rpc import ( "encoding/hex" "fmt" "strconv" - log "github.com/eris-ltd/eris-logger" - "github.com/tendermint/go-crypto" acc "github.com/eris-ltd/eris-db/account" "github.com/eris-ltd/eris-db/client" "github.com/eris-ltd/eris-db/keys" + "github.com/eris-ltd/eris-db/logging" ptypes "github.com/eris-ltd/eris-db/permission/types" "github.com/eris-ltd/eris-db/txs" ) @@ -101,10 +100,10 @@ func checkCommon(nodeClient client.NodeClient, keyClient keys.KeyClient, pubkey, return } else if pubkey != "" { if addr != "" { - log.WithFields(log.Fields{ - "public key": pubkey, - "address": addr, - }).Info("you have specified both a pubkey and an address. the pubkey takes precedent") + logging.InfoMsg(nodeClient.Logger(), "Both a public key and an address have been specified. The public key takes precedent.", + "public_key", pubkey, + "address", addr, + ) } pubKeyBytes, err = hex.DecodeString(pubkey) if err != nil { @@ -151,10 +150,10 @@ func checkCommon(nodeClient client.NodeClient, keyClient keys.KeyClient, pubkey, return pub, amt, nonce, err2 } nonce = int64(account.Sequence) + 1 - log.WithFields(log.Fields{ - "nonce": nonce, - "account address": fmt.Sprintf("%X", addrBytes), - }).Debug("Fetch nonce from node") + logging.TraceMsg(nodeClient.Logger(), "Fetch nonce from node", + "nonce", nonce, + "account address", addrBytes, + ) } else { nonce, err = strconv.ParseInt(nonceS, 10, 64) if err != nil { diff --git a/client/transaction/transaction.go b/client/transaction/transaction.go deleted file mode 100644 index e9840d244cacff3bb8b88d90d6dd23bf4eb00974..0000000000000000000000000000000000000000 --- a/client/transaction/transaction.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2015, 2016 Eris Industries (UK) Ltd. -// This file is part of Eris-RT - -// Eris-RT is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Eris-RT is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. - -package transaction - -import ( - "fmt" - "os" - - log "github.com/eris-ltd/eris-logger" - - "github.com/eris-ltd/eris-db/client" - "github.com/eris-ltd/eris-db/client/core" - "github.com/eris-ltd/eris-db/definitions" - "github.com/eris-ltd/eris-db/keys" -) - -func Send(do *definitions.ClientDo) { - // construct two clients to call out to keys server and - // blockchain node. - erisKeyClient := keys.NewErisKeyClient(do.SignAddrFlag) - erisNodeClient := client.NewErisNodeClient(do.NodeAddrFlag) - // form the send transaction - sendTransaction, err := core.Send(erisNodeClient, erisKeyClient, - do.PubkeyFlag, do.AddrFlag, do.ToFlag, do.AmtFlag, do.NonceFlag) - if err != nil { - log.Fatalf("Failed on forming Send Transaction: %s", err) - return - } - // TODO: [ben] we carry over the sign bool, but always set it to true, - // as we move away from and deprecate the api that allows sending unsigned - // transactions and relying on (our) receiving node to sign it. - unpackSignAndBroadcast( - core.SignAndBroadcast(do.ChainidFlag, erisNodeClient, - erisKeyClient, sendTransaction, true, do.BroadcastFlag, do.WaitFlag)) -} - -func Call(do *definitions.ClientDo) { - // construct two clients to call out to keys server and - // blockchain node. - erisKeyClient := keys.NewErisKeyClient(do.SignAddrFlag) - erisNodeClient := client.NewErisNodeClient(do.NodeAddrFlag) - // form the call transaction - callTransaction, err := core.Call(erisNodeClient, erisKeyClient, - do.PubkeyFlag, do.AddrFlag, do.ToFlag, do.AmtFlag, do.NonceFlag, - do.GasFlag, do.FeeFlag, do.DataFlag) - if err != nil { - log.Fatalf("Failed on forming Call Transaction: %s", err) - return - } - // TODO: [ben] we carry over the sign bool, but always set it to true, - // as we move away from and deprecate the api that allows sending unsigned - // transactions and relying on (our) receiving node to sign it. - unpackSignAndBroadcast( - core.SignAndBroadcast(do.ChainidFlag, erisNodeClient, - erisKeyClient, callTransaction, true, do.BroadcastFlag, do.WaitFlag)) -} - -//---------------------------------------------------------------------- -// Helper functions - -func unpackSignAndBroadcast(result *core.TxResult, err error) { - if err != nil { - log.Fatalf("Failed on signing (and broadcasting) transaction: %s", err) - os.Exit(1) - } - if result == nil { - // if we don't provide --sign or --broadcast - return - } - printResult := log.Fields{ - "transaction hash": fmt.Sprintf("%X", result.Hash), - } - if result.Address != nil { - printResult["Contract Address"] = fmt.Sprintf("%X", result.Address) - } - if result.Return != nil { - printResult["Block Hash"] = fmt.Sprintf("%X", result.BlockHash) - printResult["Return Value"] = fmt.Sprintf("%X", result.Return) - printResult["Exception"] = fmt.Sprintf("%s", result.Exception) - } - log.WithFields(printResult).Warn("Result") -} diff --git a/client/websocket_client.go b/client/websocket_client.go index 42f1bf37d384f999cb5d913cbf741124e71f1736..429958aad169eab380d832f83a7b70e868ebde08 100644 --- a/client/websocket_client.go +++ b/client/websocket_client.go @@ -24,8 +24,8 @@ import ( "github.com/tendermint/go-rpc/client" "github.com/tendermint/go-wire" - log "github.com/eris-ltd/eris-logger" - + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" ctypes "github.com/eris-ltd/eris-db/rpc/tendermint/core/types" "github.com/eris-ltd/eris-db/txs" ) @@ -41,29 +41,30 @@ type Confirmation struct { Error error } -// NOTE [ben] Compiler check to ensure ErisNodeClient successfully implements +// NOTE [ben] Compiler check to ensure erisNodeClient successfully implements // eris-db/client.NodeClient -var _ NodeWebsocketClient = (*ErisNodeWebsocketClient)(nil) +var _ NodeWebsocketClient = (*erisNodeWebsocketClient)(nil) -type ErisNodeWebsocketClient struct { +type erisNodeWebsocketClient struct { // TODO: assert no memory leak on closing with open websocket tendermintWebsocket *rpcclient.WSClient + logger loggers.InfoTraceLogger } // Subscribe to an eventid -func (erisNodeWebsocketClient *ErisNodeWebsocketClient) Subscribe(eventid string) error { +func (erisNodeWebsocketClient *erisNodeWebsocketClient) Subscribe(eventid string) error { // TODO we can in the background listen to the subscription id and remember it to ease unsubscribing later. return erisNodeWebsocketClient.tendermintWebsocket.Subscribe(eventid) } // Unsubscribe from an eventid -func (erisNodeWebsocketClient *ErisNodeWebsocketClient) Unsubscribe(subscriptionId string) error { +func (erisNodeWebsocketClient *erisNodeWebsocketClient) Unsubscribe(subscriptionId string) error { return erisNodeWebsocketClient.tendermintWebsocket.Unsubscribe(subscriptionId) } // Returns a channel that will receive a confirmation with a result or the exception that // has been confirmed; or an error is returned and the confirmation channel is nil. -func (erisNodeWebsocketClient *ErisNodeWebsocketClient) WaitForConfirmation(tx txs.Tx, chainId string, inputAddr []byte) (chan Confirmation, error) { +func (erisNodeWebsocketClient *erisNodeWebsocketClient) WaitForConfirmation(tx txs.Tx, chainId string, inputAddr []byte) (chan Confirmation, error) { // check no errors are reported on the websocket if err := erisNodeWebsocketClient.assertNoErrors(); err != nil { return nil, err @@ -88,7 +89,8 @@ func (erisNodeWebsocketClient *ErisNodeWebsocketClient) WaitForConfirmation(tx t result := new(ctypes.ErisDBResult) if wire.ReadJSONPtr(result, resultBytes, &err); err != nil { // keep calm and carry on - log.Errorf("[eris-client] Failed to unmarshal json bytes for websocket event: %s", err) + logging.InfoMsg(erisNodeWebsocketClient.logger, "Failed to unmarshal json bytes for websocket event", + "error", err) continue } @@ -97,36 +99,41 @@ func (erisNodeWebsocketClient *ErisNodeWebsocketClient) WaitForConfirmation(tx t // Received confirmation of subscription to event streams // TODO: collect subscription IDs, push into channel and on completion // unsubscribe - log.Infof("[eris-client] recceived confirmation for event (%s) with subscription id (%s).", - subscription.Event, subscription.SubscriptionId) + logging.InfoMsg(erisNodeWebsocketClient.logger, "Received confirmation for event", + "event", subscription.Event, + "subscription_id", subscription.SubscriptionId) continue } event, ok := (*result).(*ctypes.ResultEvent) if !ok { // keep calm and carry on - log.Warnf("[eris-client] Failed to cast to ResultEvent for websocket event: %s", *result) + logging.InfoMsg(erisNodeWebsocketClient.logger, "Failed to cast to ResultEvent for websocket event", + "event", event.Event) continue } blockData, ok := event.Data.(txs.EventDataNewBlock) if ok { latestBlockHash = blockData.Block.Hash() - log.WithFields(log.Fields{ - "new block": blockData.Block, - "latest hash": latestBlockHash, - }).Debug("Registered new block") + logging.TraceMsg(erisNodeWebsocketClient.logger, "Registered new block", + "block", blockData.Block, + "latest_block_hash", latestBlockHash, + ) continue } // we don't accept events unless they came after a new block (ie. in) if latestBlockHash == nil { - log.Infof("[eris-client] no first block has been registered, so ignoring event: %s", event.Event) + logging.InfoMsg(erisNodeWebsocketClient.logger, "First block has not been registered so ignoring event", + "event", event.Event) continue } if event.Event != eid { - log.Warnf("[eris-client] received unsolicited event! Got %s, expected %s\n", event.Event, eid) + logging.InfoMsg(erisNodeWebsocketClient.logger, "Received unsolicited event", + "event_received", event.Event, + "event_expected", eid) continue } @@ -143,10 +150,9 @@ func (erisNodeWebsocketClient *ErisNodeWebsocketClient) WaitForConfirmation(tx t } if !bytes.Equal(txs.TxHash(chainId, data.Tx), txs.TxHash(chainId, tx)) { - log.WithFields(log.Fields{ + logging.TraceMsg(erisNodeWebsocketClient.logger, "Received different event", // TODO: consider re-implementing TxID again, or other more clear debug - "received transaction event": txs.TxHash(chainId, data.Tx), - }).Debug("Received different event") + "received transaction event", txs.TxHash(chainId, data.Tx)) continue } @@ -188,13 +194,13 @@ func (erisNodeWebsocketClient *ErisNodeWebsocketClient) WaitForConfirmation(tx t return confirmationChannel, nil } -func (erisNodeWebsocketClient *ErisNodeWebsocketClient) Close() { +func (erisNodeWebsocketClient *erisNodeWebsocketClient) Close() { if erisNodeWebsocketClient.tendermintWebsocket != nil { erisNodeWebsocketClient.tendermintWebsocket.Stop() } } -func (erisNodeWebsocketClient *ErisNodeWebsocketClient) assertNoErrors() error { +func (erisNodeWebsocketClient *erisNodeWebsocketClient) assertNoErrors() error { if erisNodeWebsocketClient.tendermintWebsocket != nil { select { case err := <-erisNodeWebsocketClient.tendermintWebsocket.ErrorsCh: diff --git a/cmd/eris-db.go b/cmd/eris-db.go index 605b12060b4f669c249dd4a04b05937f6b568262..5f66acdca4d5d50304bb864ddedd2e054a1a30fe 100644 --- a/cmd/eris-db.go +++ b/cmd/eris-db.go @@ -21,17 +21,12 @@ import ( "strconv" "strings" - cobra "github.com/spf13/cobra" + "github.com/spf13/cobra" - log "github.com/eris-ltd/eris-logger" - - definitions "github.com/eris-ltd/eris-db/definitions" - version "github.com/eris-ltd/eris-db/version" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/version" ) -// Global Do struct -var do *definitions.Do - var ErisDbCmd = &cobra.Command{ Use: "eris-db", Short: "Eris-DB is the server side of the eris chain.", @@ -39,42 +34,30 @@ var ErisDbCmd = &cobra.Command{ a modular consensus engine and application manager to run a chain to suit your needs. -Made with <3 by Eris Industries. +Made with <3 by Monax Industries. Complete documentation is available at https://monax.io/docs/documentation ` + "\nVERSION:\n " + version.VERSION, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - - log.SetLevel(log.WarnLevel) - if do.Verbose { - log.SetLevel(log.InfoLevel) - } else if do.Debug { - log.SetLevel(log.DebugLevel) - } - }, Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, } func Execute() { - InitErisDbCli() - AddGlobalFlags() - AddCommands() + do := definitions.NewDo() + AddGlobalFlags(do) + AddCommands(do) ErisDbCmd.Execute() } -func InitErisDbCli() { - // initialise an empty Do struct for command execution - do = definitions.NewDo() -} - -func AddGlobalFlags() { - ErisDbCmd.PersistentFlags().BoolVarP(&do.Verbose, "verbose", "v", defaultVerbose(), "verbose output; more output than no output flags; less output than debug level; default respects $ERIS_DB_VERBOSE") - ErisDbCmd.PersistentFlags().BoolVarP(&do.Debug, "debug", "d", defaultDebug(), "debug level output; the most output available for eris-db; if it is too chatty use verbose flag; default respects $ERIS_DB_DEBUG") +func AddGlobalFlags(do *definitions.Do) { + ErisDbCmd.PersistentFlags().BoolVarP(&do.Verbose, "verbose", "v", + defaultVerbose(), + "verbose output; more output than no output flags; less output than debug level; default respects $ERIS_DB_VERBOSE") + ErisDbCmd.PersistentFlags().BoolVarP(&do.Debug, "debug", "d", defaultDebug(), + "debug level output; the most output available for eris-db; if it is too chatty use verbose flag; default respects $ERIS_DB_DEBUG") } -func AddCommands() { - buildServeCommand() - ErisDbCmd.AddCommand(ServeCmd) +func AddCommands(do *definitions.Do) { + ErisDbCmd.AddCommand(buildServeCommand(do)) } //------------------------------------------------------------------------------ diff --git a/cmd/serve.go b/cmd/serve.go index eccf3388ab0c14bc167c8211ce3a9e82208a39f4..29319b1fd815498dae5ac82659211afacc8280c8 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -17,19 +17,19 @@ package commands import ( + "fmt" "os" "os/signal" "path" "syscall" - cobra "github.com/spf13/cobra" - - log "github.com/eris-ltd/eris-logger" - - "fmt" + "github.com/spf13/cobra" - core "github.com/eris-ltd/eris-db/core" - util "github.com/eris-ltd/eris-db/util" + "github.com/eris-ltd/eris-db/core" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/lifecycle" + "github.com/eris-ltd/eris-db/util" ) const ( @@ -41,148 +41,157 @@ var DefaultConfigFilename = fmt.Sprintf("%s.%s", DefaultConfigBasename, DefaultConfigType) -var ServeCmd = &cobra.Command{ - Use: "serve", - Short: "Eris-DB serve starts an eris-db node with client API enabled by default.", - Long: `Eris-DB serve starts an eris-db node with client API enabled by default. +// build the serve subcommand +func buildServeCommand(do *definitions.Do) *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "Eris-DB serve starts an eris-db node with client API enabled by default.", + Long: `Eris-DB serve starts an eris-db node with client API enabled by default. The Eris-DB node is modularly configured for the consensus engine and application manager. The client API can be disabled.`, - Example: fmt.Sprintf(`$ eris-db serve -- will start the Eris-DB node based on the configuration file "%s" in the current working directory + Example: fmt.Sprintf(`$ eris-db serve -- will start the Eris-DB node based on the configuration file "%s" in the current working directory $ eris-db serve --work-dir <path-to-working-directory> -- will start the Eris-DB node based on the configuration file "%s" in the provided working directory $ eris-db serve --chain-id <CHAIN_ID> -- will overrule the configuration entry assert_chain_id`, - DefaultConfigFilename, DefaultConfigFilename), - PreRun: func(cmd *cobra.Command, args []string) { - // if WorkDir was not set by a flag or by $ERIS_DB_WORKDIR - // NOTE [ben]: we can consider an `Explicit` flag that eliminates - // the use of any assumptions while starting Eris-DB - if do.WorkDir == "" { - if currentDirectory, err := os.Getwd(); err != nil { - log.Fatalf("No directory provided and failed to get current working directory: %v", err) + DefaultConfigFilename, DefaultConfigFilename), + PreRun: func(cmd *cobra.Command, args []string) { + // if WorkDir was not set by a flag or by $ERIS_DB_WORKDIR + // NOTE [ben]: we can consider an `Explicit` flag that eliminates + // the use of any assumptions while starting Eris-DB + if do.WorkDir == "" { + if currentDirectory, err := os.Getwd(); err != nil { + panic(fmt.Sprintf("No directory provided and failed to get current "+ + "working directory: %v", err)) + os.Exit(1) + } else { + do.WorkDir = currentDirectory + } + } + if !util.IsDir(do.WorkDir) { + panic(fmt.Sprintf("Provided working directory %s is not a directory", + do.WorkDir)) os.Exit(1) - } else { - do.WorkDir = currentDirectory } - } - if !util.IsDir(do.WorkDir) { - log.Fatalf("Provided working directory %s is not a directory", do.WorkDir) - } - }, - Run: Serve, -} - -// build the serve subcommand -func buildServeCommand() { - addServeFlags() + }, + Run: ServeRunner(do), + } + addServeFlags(do, cmd) + return cmd } -func addServeFlags() { - ServeCmd.PersistentFlags().StringVarP(&do.ChainId, "chain-id", "c", +func addServeFlags(do *definitions.Do, serveCmd *cobra.Command) { + serveCmd.PersistentFlags().StringVarP(&do.ChainId, "chain-id", "c", defaultChainId(), "specify the chain id to use for assertion against the genesis file or the existing state. If omitted, and no id is set in $CHAIN_ID, then assert_chain_id is used from the configuration file.") - ServeCmd.PersistentFlags().StringVarP(&do.WorkDir, "work-dir", "w", + serveCmd.PersistentFlags().StringVarP(&do.WorkDir, "work-dir", "w", defaultWorkDir(), "specify the working directory for the chain to run. If omitted, and no path set in $ERIS_DB_WORKDIR, the current working directory is taken.") - ServeCmd.PersistentFlags().StringVarP(&do.DataDir, "data-dir", "", + serveCmd.PersistentFlags().StringVarP(&do.DataDir, "data-dir", "", defaultDataDir(), "specify the data directory. If omitted and not set in $ERIS_DB_DATADIR, <working_directory>/data is taken.") - ServeCmd.PersistentFlags().BoolVarP(&do.DisableRpc, "disable-rpc", "", + serveCmd.PersistentFlags().BoolVarP(&do.DisableRpc, "disable-rpc", "", defaultDisableRpc(), "indicate for the RPC to be disabled. If omitted the RPC is enabled by default, unless (deprecated) $ERISDB_API is set to false.") } //------------------------------------------------------------------------------ // functions - -// serve() prepares the environment and sets up the core for Eris_DB to run. -// After the setup succeeds, serve() starts the core and halts for core to -// terminate. -func Serve(cmd *cobra.Command, args []string) { - // load configuration from a single location to avoid a wrong configuration - // file is loaded. - err := do.ReadConfig(do.WorkDir, DefaultConfigBasename, DefaultConfigType) - if err != nil { - log.WithFields(log.Fields{ - "directory": do.WorkDir, - "file": DefaultConfigFilename, - }).Fatalf("Fatal error reading configuration") - os.Exit(1) - } - // if do.ChainId is not yet set, load chain_id for assertion from configuration file - if do.ChainId == "" { - if do.ChainId = do.Config.GetString("chain.assert_chain_id"); do.ChainId == "" { - log.Fatalf("Failed to read non-empty string for ChainId from config.") - os.Exit(1) - } - } +func NewCoreFromDo(do *definitions.Do) (*core.Core, error) { // load the genesis file path do.GenesisFile = path.Join(do.WorkDir, do.Config.GetString("chain.genesis_file")) + if do.Config.GetString("chain.genesis_file") == "" { - log.Fatalf("Failed to read non-empty string for genesis file from config.") - os.Exit(1) + return nil, fmt.Errorf("The config value chain.genesis_file is empty, " + + "but should be set to the location of the genesis.json file.") } // Ensure data directory is set and accessible if err := do.InitialiseDataDirectory(); err != nil { - log.Fatalf("Failed to initialise data directory (%s): %v", do.DataDir, err) - os.Exit(1) + return nil, fmt.Errorf("Failed to initialise data directory (%s): %v", do.DataDir, err) } - log.WithFields(log.Fields{ - "chainId": do.ChainId, - "workingDirectory": do.WorkDir, - "dataDirectory": do.DataDir, - "genesisFile": do.GenesisFile, - }).Info("Eris-DB serve configuring") - consensusConfig, err := core.LoadConsensusModuleConfig(do) + loggerConfig, err := core.LoadLoggingConfigFromDo(do) if err != nil { - log.Fatalf("Failed to load consensus module configuration: %s.", err) - os.Exit(1) + return nil, fmt.Errorf("Failed to load logging config: %s", err) } - managerConfig, err := core.LoadApplicationManagerModuleConfig(do) + // Create a root logger to pass through to dependencies + logger := logging.WithScope(lifecycle.NewLoggerFromLoggingConfig(loggerConfig), "Serve") + // Capture all logging from tendermint/tendermint and tendermint/go-* + // dependencies + lifecycle.CaptureTendermintLog15Output(logger) + // And from stdlib go log + lifecycle.CaptureStdlibLogOutput(logger) + + // if do.ChainId is not yet set, load chain_id for assertion from configuration file + + if do.ChainId == "" { + if do.ChainId = do.Config.GetString("chain.assert_chain_id"); do.ChainId == "" { + return nil, fmt.Errorf("The config chain.assert_chain_id is empty, " + + "but should be set to the chain_id of the chain we are trying to run.") + } + } + + logging.Msg(logger, "Loading configuration for serve command", + "chainId", do.ChainId, + "workingDirectory", do.WorkDir, + "dataDirectory", do.DataDir, + "genesisFile", do.GenesisFile) + + consensusConfig, err := core.LoadConsensusModuleConfig(do) if err != nil { - log.Fatalf("Failed to load application manager module configuration: %s.", err) - os.Exit(1) + return nil, fmt.Errorf("Failed to load consensus module configuration: %s.", err) } - log.WithFields(log.Fields{ - "consensusModule": consensusConfig.Version, - "applicationManager": managerConfig.Version, - }).Debug("Modules configured") - newCore, err := core.NewCore(do.ChainId, consensusConfig, managerConfig) + managerConfig, err := core.LoadApplicationManagerModuleConfig(do) if err != nil { - log.Fatalf("Failed to load core: %s", err) + return nil, fmt.Errorf("Failed to load application manager module configuration: %s.", err) } - if !do.DisableRpc { - serverConfig, err := core.LoadServerConfig(do) - if err != nil { - log.Fatalf("Failed to load server configuration: %s.", err) - os.Exit(1) - } + logging.Msg(logger, "Modules configured", + "consensusModule", consensusConfig.Version, + "applicationManager", managerConfig.Version) + + return core.NewCore(do.ChainId, consensusConfig, managerConfig, logger) +} - serverProcess, err := newCore.NewGatewayV0(serverConfig) +// ServeRunner() returns a command runner that prepares the environment and sets +// up the core for Eris-DB to run. After the setup succeeds, it starts the core +// and waits for the core to terminate. +func ServeRunner(do *definitions.Do) func(*cobra.Command, []string) { + return func(cmd *cobra.Command, args []string) { + // load configuration from a single location to avoid a wrong configuration + // file is loaded. + err := do.ReadConfig(do.WorkDir, DefaultConfigBasename, DefaultConfigType) if err != nil { - log.Fatalf("Failed to load servers: %s.", err) - os.Exit(1) + util.Fatalf("Fatal error reading configuration from %s/%s", do.WorkDir, + DefaultConfigFilename) } - err = serverProcess.Start() + + newCore, err := NewCoreFromDo(do) + if err != nil { - log.Fatalf("Failed to start servers: %s.", err) - os.Exit(1) + util.Fatalf("Failed to load core: %s", err) } - _, err = newCore.NewGatewayTendermint(serverConfig) - if err != nil { - log.Fatalf("Failed to start Tendermint gateway") + + if !do.DisableRpc { + serverConfig, err := core.LoadServerConfig(do) + if err != nil { + util.Fatalf("Failed to load server configuration: %s.", err) + } + serverProcess, err := newCore.NewGatewayV0(serverConfig) + if err != nil { + util.Fatalf("Failed to load servers: %s.", err) + } + err = serverProcess.Start() + if err != nil { + util.Fatalf("Failed to start servers: %s.", err) + } + _, err = newCore.NewGatewayTendermint(serverConfig) + if err != nil { + util.Fatalf("Failed to start Tendermint gateway") + } + <-serverProcess.StopEventChannel() + } else { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + fmt.Fprintf(os.Stderr, "Received %s signal. Marmots out.", <-signals) } - <-serverProcess.StopEventChannel() - } else { - signals := make(chan os.Signal, 1) - done := make(chan bool, 1) - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) - go func() { - signal := <-signals - // TODO: [ben] clean up core; in a manner consistent with enabled rpc - log.Fatalf("Received %s signal. Marmots out.", signal) - done <- true - }() - <-done } } diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000000000000000000000000000000000000..32dc2c320950ee81bd0780cae6499baabd9c8824 --- /dev/null +++ b/config/config.go @@ -0,0 +1,187 @@ +// Copyright 2015, 2016 Monax Industries (UK) Ltd. +// This file is part of Eris-RT + +// Eris-RT is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Eris-RT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. + +package config + +import ( + "bytes" + "fmt" + "text/template" +) + +type ConfigServiceGeneral struct { + ChainImageName string + UseDataContainer bool + ExportedPorts string + ContainerEntrypoint string +} + +// TODO: [ben] increase the configurability upon need +type ConfigChainGeneral struct { + AssertChainId string + ErisdbMajorVersion uint8 + ErisdbMinorVersion uint8 + GenesisRelativePath string +} + +type ConfigChainModule struct { + Name string + MajorVersion uint8 + MinorVersion uint8 + ModuleRelativeRoot string +} + +type ConfigTendermint struct { + Moniker string + Seeds string + FastSync bool +} + +var serviceGeneralTemplate *template.Template +var chainGeneralTemplate *template.Template +var chainConsensusTemplate *template.Template +var chainApplicationManagerTemplate *template.Template +var tendermintTemplate *template.Template + +func init() { + var err error + if serviceGeneralTemplate, err = template.New("serviceGeneral").Parse(sectionServiceGeneral); err != nil { + panic(err) + } + if chainGeneralTemplate, err = template.New("chainGeneral").Parse(sectionChainGeneral); err != nil { + panic(err) + } + if chainConsensusTemplate, err = template.New("chainConsensus").Parse(sectionChainConsensus); err != nil { + panic(err) + } + if chainApplicationManagerTemplate, err = template.New("chainApplicationManager").Parse(sectionChainApplicationManager); err != nil { + panic(err) + } + if tendermintTemplate, err = template.New("tendermint").Parse(sectionTendermint); err != nil { + panic(err) + } +} + +// NOTE: [ben] for 0.12.0-rc3 we only have a single configuration path +// with Tendermint in-process as the consensus engine and ErisMint +// in-process as the application manager, so we hard-code the few +// parameters that are already templated. +// Let's learn to walk before we can run. +func GetConfigurationFileBytes(chainId, moniker, seeds string, chainImageName string, + useDataContainer bool, exportedPortsString, containerEntrypoint string) ([]byte, error) { + + erisdbService := &ConfigServiceGeneral{ + ChainImageName: chainImageName, + UseDataContainer: useDataContainer, + ExportedPorts: exportedPortsString, + ContainerEntrypoint: containerEntrypoint, + } + erisdbChain := &ConfigChainGeneral{ + AssertChainId: chainId, + ErisdbMajorVersion: uint8(0), + ErisdbMinorVersion: uint8(12), + GenesisRelativePath: "genesis.json", + } + chainConsensusModule := &ConfigChainModule{ + Name: "tendermint", + MajorVersion: uint8(0), + MinorVersion: uint8(6), + ModuleRelativeRoot: "tendermint", + } + chainApplicationManagerModule := &ConfigChainModule{ + Name: "erismint", + MajorVersion: uint8(0), + MinorVersion: uint8(12), + ModuleRelativeRoot: "erismint", + } + tendermintModule := &ConfigTendermint{ + Moniker: moniker, + Seeds: seeds, + FastSync: false, + } + + // NOTE: [ben] according to StackOverflow appending strings with copy is + // more efficient than bytes.WriteString, but for readability and because + // this is not performance critical code we opt for bytes, which is + // still more efficient than + concatentation operator. + var buffer bytes.Buffer + + // write copyright header + buffer.WriteString(headerCopyright) + + // write section [service] + if err := serviceGeneralTemplate.Execute(&buffer, erisdbService); err != nil { + return nil, fmt.Errorf("Failed to write template service general for %s: %s", + chainId, err) + } + // write section for service dependencies; this is currently a static section + // with a fixed dependency on eris-keys + buffer.WriteString(sectionServiceDependencies) + + // write section [chain] + if err := chainGeneralTemplate.Execute(&buffer, erisdbChain); err != nil { + return nil, fmt.Errorf("Failed to write template chain general for %s: %s", + chainId, err) + } + + // write separator chain consensus + buffer.WriteString(separatorChainConsensus) + // write section [chain.consensus] + if err := chainConsensusTemplate.Execute(&buffer, chainConsensusModule); err != nil { + return nil, fmt.Errorf("Failed to write template chain consensus for %s: %s", + chainId, err) + } + + // write separator chain application manager + buffer.WriteString(separatorChainApplicationManager) + // write section [chain.consensus] + if err := chainApplicationManagerTemplate.Execute(&buffer, + chainApplicationManagerModule); err != nil { + return nil, fmt.Errorf("Failed to write template chain application manager for %s: %s", + chainId, err) + } + + // write separator servers + buffer.WriteString(separatorServerConfiguration) + // TODO: [ben] upon necessity replace this with template too + // write static section servers + buffer.WriteString(sectionServers) + + // write separator modules + buffer.WriteString(separatorModules) + + // write section module Tendermint + if err := tendermintTemplate.Execute(&buffer, tendermintModule); err != nil { + return nil, fmt.Errorf("Failed to write template tendermint for %s, moniker %s: %s", + chainId, moniker, err) + } + + // write static section erismint + buffer.WriteString(sectionErisMint) + + return buffer.Bytes(), nil +} + +func GetExampleConfigFileBytes() ([]byte, error) { + return GetConfigurationFileBytes( + "simplechain", + "delectable_marmot", + "192.168.168.255", + "db:latest", + true, + "46657", + "eris-db") +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e01346e846133f931b825009eb627aa6616f4b3b --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,37 @@ +// Copyright 2015, 2016 Monax Industries (UK) Ltd. +// This file is part of Eris-RT + +// Eris-RT is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Eris-RT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. + +package config + +import ( + "bytes" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +// Since the logic for generating configuration files (in eris-cm) is split from +// the logic for consuming them +func TestGeneratedConfigIsUsable(t *testing.T) { + bs, err := GetExampleConfigFileBytes() + assert.NoError(t, err, "Should be able to create example config") + buf := bytes.NewBuffer(bs) + conf := viper.New() + viper.SetConfigType("toml") + err = conf.ReadConfig(buf) + assert.NoError(t, err, "Should be able to read example config into Viper") +} diff --git a/config/dump_config_test.go b/config/dump_config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4ea3dd0598fa3b78cdffeae44e95c4d8d35212a5 --- /dev/null +++ b/config/dump_config_test.go @@ -0,0 +1,20 @@ +// +build dumpconfig + +// Space above matters +package config + +import ( + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" +) + +// This is a little convenience for getting a config file dump. Just run: +// go test -tags dumpconfig ./config +// This pseudo test won't run unless the dumpconfig tag is +func TestDumpConfig(t *testing.T) { + bs, err := GetExampleConfigFileBytes() + assert.NoError(t, err, "Should be able to create example config") + ioutil.WriteFile("config_dump.toml", bs, 0644) +} diff --git a/config/templates.go b/config/templates.go new file mode 100644 index 0000000000000000000000000000000000000000..cead77d92b7f5c40988c88de97a92f0ed9a4d093 --- /dev/null +++ b/config/templates.go @@ -0,0 +1,318 @@ +// Copyright 2015, 2016 Monax Industries (UK) Ltd. +// This file is part of Eris-RT + +// Eris-RT is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Eris-RT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. + +package config + +const headerCopyright = `# Copyright 2015, 2016 Eris Industries (UK) Ltd. +# This file is part of Eris-RT +# Eris-RT is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Eris-RT is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Eris-RT. If not, see <http://www.gnu.org/licenses/>. + +# This is a TOML configuration for Eris-DB chains generated by Eris-CM + +` + +const sectionServiceGeneral = `[service] +# NOTE: this section is read by Eris tooling, and ignored by eris-db. +# Image specifies the image name eris-cli needs to pull +# for running the chain. +image = "{{.ChainImageName}}" +# Define whether eris-cli needs to attach the data container +# for the chain. +data_container = {{.UseDataContainer}} +# Specify a list of ports that need to be exported on the container. +ports = {{.ExportedPorts}} +{{ if ne .ContainerEntrypoint "" }}# Entrypoint points to the default action to execute +# in the chain container. +entry_point = "{{.ContainerEntrypoint}}"{{ end }} + +` + +const sectionServiceDependencies = `[dependencies] +# NOTE: this section is read by Eris tooling, and ignored by eris-db. +# Eris-db expects these services to be available; eric-cli tooling will +# automatically set these services up for you. +# Services to boot with/required by the chain +services = [ "keys" ] + +` + +const sectionChainGeneral = `[chain] + +# ChainId is a human-readable name to identify the chain. +# This must correspond to the chain_id defined in the genesis file +# and the assertion here provides a safe-guard on misconfiguring chains. +assert_chain_id = "{{.AssertChainId}}" +# semantic major and minor version +major_version = {{.ErisdbMajorVersion}} +minor_version = {{.ErisdbMinorVersion}} +# genesis file, relative path is to eris-db working directory +genesis_file = "{{.GenesisRelativePath}}" + +` + +const separatorChainConsensus = ` +################################################################################ +## +## consensus +## +################################################################################ + +` + +const sectionChainConsensus = ` [chain.consensus] + # consensus defines the module to use for consensus and + # this will define the peer-to-peer consensus network; + # accepted values are ("noops", "tmsp",) "tendermint" + name = "{{.Name}}" + # version is the major and minor semantic version; + # the version will be asserted on + major_version = {{.MajorVersion}} + minor_version = {{.MinorVersion}} + # relative path to consensus' module root folder + relative_root = "{{.ModuleRelativeRoot}}" + + ` + +const separatorChainApplicationManager = ` +################################################################################ +## +## application manager +## +################################################################################ + +` + +const sectionChainApplicationManager = ` [chain.manager] + # application manager name defines the module to use for handling + # the transactions. Supported names are "erismint" + name = "{{.Name}}" + # version is the major and minor semantic version; + # the version will be asserted on + major_version = {{.MajorVersion}} + minor_version = {{.MinorVersion}} + # relative path to application manager root folder + relative_root = "{{.ModuleRelativeRoot}}" + + ` + +const separatorServerConfiguration = ` +################################################################################ +################################################################################ +## +## Server configurations +## +################################################################################ +################################################################################ + +` + +// TODO: [ben] map entries to structure defined in eris-db +const sectionServers = `[servers] + + [servers.bind] + address = "" + port = 1337 + + [servers.tls] + tls = false + cert_path = "" + key_path = "" + + [servers.cors] + enable = false + allow_origins = [] + allow_credentials = false + allow_methods = [] + allow_headers = [] + expose_headers = [] + max_age = 0 + + [servers.http] + json_rpc_endpoint = "/rpc" + + [servers.websocket] + endpoint = "/socketrpc" + max_sessions = 50 + read_buffer_size = 4096 + write_buffer_size = 4096 + + [servers.tendermint] + # Multiple listeners can be separated with a comma + rpc_local_address = "0.0.0.0:46657" + endpoint = "/websocket" + + [servers.logging] + console_log_level = "info" + file_log_level = "warn" + log_file = "" + + ` + +const separatorModules = ` +################################################################################ +################################################################################ +## +## Module configurations - dynamically loaded based on chain configuration +## +################################################################################ +################################################################################ + +` + +// TODO: [ben] make configurable +const sectionTmsp = ` +################################################################################ +## +## Tendermint Socket Protocol (TMSP) +## version 0.6.0 +## +## TMSP expects a tendermint consensus process to run and connect to Eris-DB +## +################################################################################ + +[tmsp] +# listener address for accepting tendermint socket protocol connections +listener = "tcp://0.0.0.0:46658" + +` + +// TODO: [ben] minimal fields have been made configurable; expand where needed +const sectionTendermint = ` +################################################################################ +## +## Tendermint +## version 0.6.0 +## +## in-process execution of Tendermint consensus engine +## +################################################################################ + +[tendermint] +# private validator file is used by tendermint to keep the status +# of the private validator, but also (currently) holds the private key +# for the private vaildator to sign with. This private key needs to be moved +# out and directly managed by eris-keys +# This file needs to be in the root directory +private_validator_file = "priv_validator.json" + + # Tendermint requires additional configuration parameters. + # Eris-DB's tendermint consensus module will load [tendermint.configuration] + # as the configuration for Tendermint. + # Eris-DB will respect the configurations set in this file where applicable, + # but reserves the option to override or block conflicting settings. + [tendermint.configuration] + # moniker is the name of the node on the tendermint p2p network + moniker = "{{.Moniker}}" + # seeds lists the peers tendermint can connect to join the network + seeds = "{{.Seeds}}" + # fast_sync allows a tendermint node to catch up faster when joining + # the network. + # NOTE: Tendermint has reported potential issues with fast_sync enabled. + # The recommended setting is for keeping it disabled. + fast_sync = {{.FastSync}} + # database backend to use for Tendermint. Supported "leveldb" and "memdb". + db_backend = "leveldb" + # logging level. Supported "error" < "warn" < "notice" < "info" < "debug" + log_level = "info" + # node local address + node_laddr = "0.0.0.0:46656" + # rpc local address + # NOTE: value is ignored when run in-process as RPC is + # handled by [servers.tendermint] + rpc_laddr = "0.0.0.0:46657" + # proxy application address - used for tmsp connections, + # and this port should not be exposed for in-process Tendermint + proxy_app = "tcp://127.0.0.1:46658" + + # Extended Tendermint configuration settings + # for reference to Tendermint see https://github.com/tendermint/tendermint/blob/master/config/tendermint/config.go + + # genesis_file = "./data/tendermint/genesis.json" + # skip_upnp = false + # addrbook_file = "./data/tendermint/addrbook.json" + # priv_validator_file = "./data/tendermint/priv_validator.json" + # db_dir = "./data/tendermint/data" + # prof_laddr = "" + # revision_file = "./data/tendermint/revision" + # cswal = "./data/tendermint/data/cswal" + # cswal_light = false + + # block_size = 10000 + # disable_data_hash = false + # timeout_propose = 3000 + # timeout_propose_delta = 500 + # timeout_prevote = 1000 + # timeout_prevote_delta = 500 + # timeout_precommit = 1000 + # timeout_precommit_delta = 500 + # timeout_commit = 1000 + # mempool_recheck = true + # mempool_recheck_empty = true + # mempool_broadcast = true + + [tendermint.configuration.p2p] + # Switch config keys + dial_timeout_seconds = 3 + handshake_timeout_seconds = 20 + max_num_peers = 20 + authenticated_encryption = true + + # MConnection config keys + send_rate = 512000 + recv_rate = 512000 + + # Fuzz params + fuzz_enable = false # use the fuzz wrapped conn + fuzz_active = false # toggle fuzzing + fuzz_mode = "drop" # eg. drop, delay + fuzz_max_delay_milliseconds = 3000 + fuzz_prob_drop_rw = 0.2 + fuzz_prob_drop_conn = 0.00 + fuzz_prob_sleep = 0.00 + +` + +const sectionErisMint = ` +################################################################################ +## +## Eris-Mint +## version 0.12.0 +## +## The original Ethereum virtual machine with IAVL merkle trees +## and tendermint/go-wire encoding +## +################################################################################ + +[erismint] +# Database backend to use for ErisMint state database. +# Supported "leveldb" and "memdb". +db_backend = "leveldb" +# tendermint host address needs to correspond to tendermints configuration +# of the rpc local address +tendermint_host = "0.0.0.0:46657" + +` diff --git a/config/viper.go b/config/viper.go new file mode 100644 index 0000000000000000000000000000000000000000..cb59f540c3548177a1bbb23a356ed28c61583aff --- /dev/null +++ b/config/viper.go @@ -0,0 +1,30 @@ +package config + +import ( + "fmt" + + "github.com/spf13/viper" +) + +// Safely get the subtree from a viper config, returning an error if it could not +// be obtained for any reason. +func ViperSubConfig(conf *viper.Viper, configSubtreePath string) (subConfig *viper.Viper, err error) { + // Viper internally panics if `moduleName` contains an unallowed + // character (eg, a dash). + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("Viper panicked trying to read config subtree: %s", + configSubtreePath) + } + }() + if !conf.IsSet(configSubtreePath) { + return nil, fmt.Errorf("Failed to read config subtree: %s", + configSubtreePath) + } + subConfig = conf.Sub(configSubtreePath) + if subConfig == nil { + return nil, fmt.Errorf("Failed to read config subtree: %s", + configSubtreePath) + } + return subConfig, err +} diff --git a/consensus/consensus.go b/consensus/consensus.go index f0795a5dde853f3ec6ee1aa046241aba5c25d250..2f4132d6e4e5aa89975b3100d913f86d4af72e7b 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -28,13 +28,13 @@ func LoadConsensusEngineInPipe(moduleConfig *config.ModuleConfig, pipe definitions.Pipe) error { switch moduleConfig.Name { case "tendermint": - tendermint, err := tendermint.NewTendermint(moduleConfig, - pipe.GetApplication()) + tmint, err := tendermint.NewTendermint(moduleConfig, pipe.GetApplication(), + pipe.Logger().With()) if err != nil { return fmt.Errorf("Failed to load Tendermint node: %v", err) } - err = pipe.SetConsensusEngine(tendermint) + err = pipe.SetConsensusEngine(tmint) if err != nil { return fmt.Errorf("Failed to load Tendermint in pipe as "+ "ConsensusEngine: %v", err) @@ -42,7 +42,7 @@ func LoadConsensusEngineInPipe(moduleConfig *config.ModuleConfig, // For Tendermint we have a coupled Blockchain and ConsensusEngine // implementation, so load it at the same time as ConsensusEngine - err = pipe.SetBlockchain(tendermint) + err = pipe.SetBlockchain(tmint) if err != nil { return fmt.Errorf("Failed to load Tendermint in pipe as "+ "Blockchain: %v", err) diff --git a/consensus/tendermint/config.go b/consensus/tendermint/config.go index 1bbb74bd6aa35be73c28c7bb75adea040cfddfbb..094d7979e429ae16ec2bd3ff39e59e926895a77d 100644 --- a/consensus/tendermint/config.go +++ b/consensus/tendermint/config.go @@ -23,10 +23,10 @@ import ( "path" "time" - viper "github.com/spf13/viper" + "github.com/spf13/viper" tendermintConfig "github.com/tendermint/go-config" - config "github.com/eris-ltd/eris-db/config" + "github.com/eris-ltd/eris-db/config" ) // NOTE [ben] Compiler check to ensure TendermintConfig successfully implements @@ -147,7 +147,8 @@ func (tmintConfig *TendermintConfig) GetMapString(key string) map[string]string func (tmintConfig *TendermintConfig) GetConfig(key string) tendermintConfig.Config { // TODO: [ben] log out a warning as this indicates a potentially breaking code // change from Tendermints side - if !tmintConfig.subTree.IsSet(key) { + subTree, _ := config.ViperSubConfig(tmintConfig.subTree, key) + if subTree == nil { return &TendermintConfig{ subTree: viper.New(), } diff --git a/consensus/tendermint/tendermint.go b/consensus/tendermint/tendermint.go index 237fb4a0bd9b3da558fa73a7014b0f14546db661..5adc08e1a2619723123088901bea595f75a300eb 100644 --- a/consensus/tendermint/tendermint.go +++ b/consensus/tendermint/tendermint.go @@ -34,13 +34,14 @@ import ( tmsp_types "github.com/tendermint/tmsp/types" edb_event "github.com/eris-ltd/eris-db/event" - log "github.com/eris-ltd/eris-logger" config "github.com/eris-ltd/eris-db/config" manager_types "github.com/eris-ltd/eris-db/manager/types" // files "github.com/eris-ltd/eris-db/files" blockchain_types "github.com/eris-ltd/eris-db/blockchain/types" consensus_types "github.com/eris-ltd/eris-db/consensus/types" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" "github.com/eris-ltd/eris-db/txs" "github.com/tendermint/go-wire" ) @@ -49,6 +50,7 @@ type Tendermint struct { tmintNode *node.Node tmintConfig *TendermintConfig chainId string + logger loggers.InfoTraceLogger } // Compiler checks to ensure Tendermint successfully implements @@ -57,7 +59,8 @@ var _ consensus_types.ConsensusEngine = (*Tendermint)(nil) var _ blockchain_types.Blockchain = (*Tendermint)(nil) func NewTendermint(moduleConfig *config.ModuleConfig, - application manager_types.Application) (*Tendermint, error) { + application manager_types.Application, + logger loggers.InfoTraceLogger) (*Tendermint, error) { // re-assert proper configuration for module if moduleConfig.Version != GetTendermintVersion().GetMinorVersionString() { return nil, fmt.Errorf("Version string %s did not match %s", @@ -71,10 +74,10 @@ func NewTendermint(moduleConfig *config.ModuleConfig, if !moduleConfig.Config.IsSet("configuration") { return nil, fmt.Errorf("Failed to extract Tendermint configuration subtree.") } - tendermintConfigViper := moduleConfig.Config.Sub("configuration") + tendermintConfigViper, err := config.ViperSubConfig(moduleConfig.Config, "configuration") if tendermintConfigViper == nil { return nil, - fmt.Errorf("Failed to extract Tendermint configuration subtree.") + fmt.Errorf("Failed to extract Tendermint configuration subtree: %s", err) } // wrap a copy of the viper config in a tendermint/go-config interface tmintConfig := GetTendermintConfig(tendermintConfigViper) @@ -92,18 +95,19 @@ func NewTendermint(moduleConfig *config.ModuleConfig, tmintConfig.AssertTendermintConsistency(moduleConfig, privateValidatorFilePath) chainId := tmintConfig.GetString("chain_id") - log.WithFields(log.Fields{ - "chainId": chainId, - "genesisFile": tmintConfig.GetString("genesis_file"), - "nodeLocalAddress": tmintConfig.GetString("node_laddr"), - "moniker": tmintConfig.GetString("moniker"), - "seeds": tmintConfig.GetString("seeds"), - "fastSync": tmintConfig.GetBool("fast_sync"), - "rpcLocalAddress": tmintConfig.GetString("rpc_laddr"), - "databaseDirectory": tmintConfig.GetString("db_dir"), - "privateValidatorFile": tmintConfig.GetString("priv_validator_file"), - "privValFile": moduleConfig.Config.GetString("private_validator_file"), - }).Debug("Loaded Tendermint sub-configuration") + + logging.TraceMsg(logger, "Loaded Tendermint sub-configuration", + "chainId", chainId, + "genesisFile", tmintConfig.GetString("genesis_file"), + "nodeLocalAddress", tmintConfig.GetString("node_laddr"), + "moniker", tmintConfig.GetString("moniker"), + "seeds", tmintConfig.GetString("seeds"), + "fastSync", tmintConfig.GetBool("fast_sync"), + "rpcLocalAddress", tmintConfig.GetString("rpc_laddr"), + "databaseDirectory", tmintConfig.GetString("db_dir"), + "privateValidatorFile", tmintConfig.GetString("priv_validator_file"), + "privValFile", moduleConfig.Config.GetString("private_validator_file")) + // TODO: [ben] do not "or Generate Validator keys", rather fail directly // TODO: [ben] implement the signer for Private validator over eris-keys // TODO: [ben] copy from rootDir to tendermint workingDir; @@ -116,8 +120,8 @@ func NewTendermint(moduleConfig *config.ModuleConfig, // not running the tendermint RPC as it could lead to unexpected behaviour, // not least if we accidentally try to run it on the same address as our own if tmintConfig.GetString("rpc_laddr") != "" { - log.Warnf("Force disabling Tendermint's native RPC, which had been set to "+ - "run on '%s' in the Tendermint config.", tmintConfig.GetString("rpc_laddr")) + logging.InfoMsg(logger, "Force disabling Tendermint's native RPC", + "provided_rpc_laddr", tmintConfig.GetString("rpc_laddr")) tmintConfig.Set("rpc_laddr", "") } @@ -136,26 +140,25 @@ func NewTendermint(moduleConfig *config.ModuleConfig, newNode.Stop() return nil, fmt.Errorf("Failed to start Tendermint consensus node: %v", err) } - log.WithFields(log.Fields{ - "nodeAddress": tmintConfig.GetString("node_laddr"), - "transportProtocol": "tcp", - "upnp": !tmintConfig.GetBool("skip_upnp"), - "moniker": tmintConfig.GetString("moniker"), - }).Info("Tendermint consensus node started") + logging.InfoMsg(logger, "Tendermint consensus node started", + "nodeAddress", tmintConfig.GetString("node_laddr"), + "transportProtocol", "tcp", + "upnp", !tmintConfig.GetBool("skip_upnp"), + "moniker", tmintConfig.GetString("moniker")) // If seedNode is provided by config, dial out. if tmintConfig.GetString("seeds") != "" { seeds := strings.Split(tmintConfig.GetString("seeds"), ",") newNode.DialSeeds(seeds) - log.WithFields(log.Fields{ - "seeds": seeds, - }).Debug("Tendermint node called seeds") + logging.TraceMsg(logger, "Tendermint node called seeds", + "seeds", seeds) } return &Tendermint{ tmintNode: newNode, tmintConfig: tmintConfig, chainId: chainId, + logger: logger, }, nil } @@ -228,7 +231,7 @@ func (tendermint *Tendermint) PublicValidatorKey() crypto.PubKey { } func (tendermint *Tendermint) Events() edb_event.EventEmitter { - return edb_event.NewEvents(tendermint.tmintNode.EventSwitch()) + return edb_event.NewEvents(tendermint.tmintNode.EventSwitch(), tendermint.logger) } func (tendermint *Tendermint) BroadcastTransaction(transaction []byte, diff --git a/core/config.go b/core/config.go index a2a82dbd1e07e649962f8ee37e2f61b48ceb25aa..5a10047e276bd1db6a06d717e6f141242e340026 100644 --- a/core/config.go +++ b/core/config.go @@ -24,13 +24,14 @@ import ( "os" "path" - config "github.com/eris-ltd/eris-db/config" - consensus "github.com/eris-ltd/eris-db/consensus" - definitions "github.com/eris-ltd/eris-db/definitions" - manager "github.com/eris-ltd/eris-db/manager" - server "github.com/eris-ltd/eris-db/server" - util "github.com/eris-ltd/eris-db/util" - version "github.com/eris-ltd/eris-db/version" + "github.com/eris-ltd/eris-db/config" + "github.com/eris-ltd/eris-db/consensus" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/manager" + "github.com/eris-ltd/eris-db/server" + "github.com/eris-ltd/eris-db/util" + "github.com/eris-ltd/eris-db/version" "github.com/spf13/viper" ) @@ -75,17 +76,14 @@ func LoadModuleConfig(conf *viper.Viper, rootWorkDir, rootDataDir, fmt.Errorf("Failed to create module data directory %s.", dataDir) } // load configuration subtree for module - // TODO: [ben] Viper internally panics if `moduleName` contains an unallowed - // character (eg, a dash). Either this needs to be wrapped in a go-routine - // and recovered from or a PR to viper is needed to address this bug. if !conf.IsSet(moduleName) { return nil, fmt.Errorf("Failed to read configuration section for %s", moduleName) } - subConfig := conf.Sub(moduleName) + subConfig, err := config.ViperSubConfig(conf, moduleName) if subConfig == nil { - return nil, - fmt.Errorf("Failed to read configuration section for %s.", moduleName) + return nil, fmt.Errorf("Failed to read configuration section for %s: %s", + moduleName, err) } return &config.ModuleConfig{ @@ -104,19 +102,29 @@ func LoadModuleConfig(conf *viper.Viper, rootWorkDir, rootDataDir, // LoadServerModuleConfig wraps specifically for the servers run by core func LoadServerConfig(do *definitions.Do) (*server.ServerConfig, error) { // load configuration subtree for servers - if !do.Config.IsSet("servers") { - return nil, fmt.Errorf("Failed to read configuration section for servers") - } - subConfig := do.Config.Sub("servers") - if subConfig == nil { - return nil, - fmt.Errorf("Failed to read configuration section for servers") + subConfig, err := config.ViperSubConfig(do.Config, "servers") + if err != nil { + return nil, err } serverConfig, err := server.ReadServerConfig(subConfig) + if err != nil { + return nil, err + } serverConfig.ChainId = do.ChainId return serverConfig, err } +func LoadLoggingConfigFromDo(do *definitions.Do) (*logging.LoggingConfig, error) { + //subConfig, err := SubConfig(conf, "logging") + loggingConfig := &logging.LoggingConfig{} + return loggingConfig, nil +} + +func LoadLoggingConfigFromClientDo(do *definitions.ClientDo) (*logging.LoggingConfig, error) { + loggingConfig := &logging.LoggingConfig{} + return loggingConfig, nil +} + //------------------------------------------------------------------------------ // Helper functions diff --git a/core/core.go b/core/core.go index 03f56b56a47927b09d8b2bdc9e5a2302da1fcd84..6c58d1a3312eba6c17cf06c121be1bbb201d48eb 100644 --- a/core/core.go +++ b/core/core.go @@ -22,18 +22,19 @@ import ( // TODO: [ben] swap out go-events with eris-db/event (currently unused) events "github.com/tendermint/go-events" - log "github.com/eris-ltd/eris-logger" - - config "github.com/eris-ltd/eris-db/config" - consensus "github.com/eris-ltd/eris-db/consensus" - definitions "github.com/eris-ltd/eris-db/definitions" - event "github.com/eris-ltd/eris-db/event" - manager "github.com/eris-ltd/eris-db/manager" + "github.com/eris-ltd/eris-db/config" + "github.com/eris-ltd/eris-db/consensus" + "github.com/eris-ltd/eris-db/definitions" + "github.com/eris-ltd/eris-db/event" + "github.com/eris-ltd/eris-db/manager" // rpc_v0 is carried over from Eris-DBv0.11 and before on port 1337 rpc_v0 "github.com/eris-ltd/eris-db/rpc/v0" // rpc_tendermint is carried over from Eris-DBv0.11 and before on port 46657 + + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" rpc_tendermint "github.com/eris-ltd/eris-db/rpc/tendermint/core" - server "github.com/eris-ltd/eris-db/server" + "github.com/eris-ltd/eris-db/server" ) // Core is the high-level structure @@ -44,33 +45,30 @@ type Core struct { tendermintPipe definitions.TendermintPipe } -func NewCore(chainId string, consensusConfig *config.ModuleConfig, - managerConfig *config.ModuleConfig) (*Core, error) { +func NewCore(chainId string, + consensusConfig *config.ModuleConfig, + managerConfig *config.ModuleConfig, + logger loggers.InfoTraceLogger) (*Core, error) { // start new event switch, TODO: [ben] replace with eris-db/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, + pipe, err := manager.NewApplicationPipe(managerConfig, evsw, logger, consensusConfig.Version) if err != nil { return nil, fmt.Errorf("Failed to load application pipe: %v", err) } - log.Debug("Loaded pipe with application manager") + 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 { - log.Warn(fmt.Sprintf("Tendermint gateway not supported by %s", - managerConfig.Version)) - return &Core{ - chainId: chainId, - evsw: evsw, - pipe: pipe, - tendermintPipe: nil, - }, nil + logging.TraceMsg(logger, "Tendermint gateway not supported by manager", + "manager-version", managerConfig.Version) } return &Core{ chainId: chainId, diff --git a/definitions/pipe.go b/definitions/pipe.go index f15c60df5cbe4aceda0a9421ed824c35bb0b3ea3..a32a4cdf24cd43e63b95f0defcfb95ecbc311057 100644 --- a/definitions/pipe.go +++ b/definitions/pipe.go @@ -31,6 +31,7 @@ import ( core_types "github.com/eris-ltd/eris-db/core/types" types "github.com/eris-ltd/eris-db/core/types" event "github.com/eris-ltd/eris-db/event" + "github.com/eris-ltd/eris-db/logging/loggers" manager_types "github.com/eris-ltd/eris-db/manager/types" "github.com/eris-ltd/eris-db/txs" ) @@ -43,6 +44,7 @@ type Pipe interface { Transactor() Transactor // Hash of Genesis state GenesisHash() []byte + Logger() loggers.InfoTraceLogger // NOTE: [ben] added to Pipe interface on 0.12 refactor GetApplication() manager_types.Application SetConsensusEngine(consensusEngine consensus_types.ConsensusEngine) error diff --git a/docs/generator.go b/docs/generator.go index fb430fc067be744340d07c486593563e994a75fd..c3aa23876050cdac4db9d63feb5fa8b82e0655e8 100644 --- a/docs/generator.go +++ b/docs/generator.go @@ -7,10 +7,11 @@ import ( "strings" "text/template" - "github.com/eris-ltd/common/go/docs" commands "github.com/eris-ltd/eris-db/cmd" + docs "github.com/eris-ltd/eris-db/docs/generator" clientCommands "github.com/eris-ltd/eris-db/client/cmd" + "github.com/eris-ltd/eris-db/definitions" "github.com/eris-ltd/eris-db/version" "github.com/spf13/cobra" ) @@ -115,9 +116,9 @@ func AddClientToDB(dbCmd, clientCmd *cobra.Command) error { func main() { // Repository maintainers should populate the top level command object. erisDbCommand := commands.ErisDbCmd - commands.InitErisDbCli() - commands.AddCommands() - commands.AddGlobalFlags() + do := definitions.NewDo() + commands.AddGlobalFlags(do) + commands.AddCommands(do) erisClientCommand := clientCommands.ErisClientCmd clientCommands.InitErisClientInit() diff --git a/docs/generator/generator.go b/docs/generator/generator.go new file mode 100644 index 0000000000000000000000000000000000000000..fee2e3d5cfbb524b89b2b61b95fba9b6cd813d43 --- /dev/null +++ b/docs/generator/generator.go @@ -0,0 +1,333 @@ +package generator + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "text/template" +) + +const FrontMatter = `--- + +layout: single +type: docs +title: "Documentation | {{ .Description }} | {{ $name }}" + +---` + +type Entry struct { + Title string + Template *template.Template + Specifications []*Entry + Examples []*Entry + Description string + FileName string + CmdEntryPoint string + URL string + BaseURL string +} + +func GenerateFileName(dir, s string) string { + return (dir + strings.Replace(strings.ToLower(s), " ", "_", -1) + ".md") +} + +func GenerateTitleFromFileName(file string) string { + file = strings.Replace(file, "_", " ", -1) + file = strings.Replace(file, "-", " ", -1) + return strings.Title(strings.Replace(file, ".md", "", 1)) +} + +func GenerateFileNameFromGlob(dir, s string) string { + return (dir + strings.Replace(filepath.Base(s), " ", "_", -1)) +} + +func GenerateURLFromFileName(s string) string { + s = strings.Replace(s, "./", "/", 1) + return strings.Replace(s, ".md", "/", -1) +} + +func GenerateCommandsTemplate() (*template.Template, error) { + handle_link := func(s string) string { + return (strings.Replace(s, ".md", "/", -1)) + } + + handle_file := func(s1, s2 string) string { + return strings.Replace((s1 + " " + s2 + ".md"), " ", "_", -1) + } + + funcMap := template.FuncMap{ + "title": strings.Title, + "replace": strings.Replace, + "chomp": strings.TrimSpace, + "handle_file": handle_file, + "handle_link": handle_link, + } + + var templateText = `{{- $name := .Command.CommandPath -}}` + FrontMatter + ` + +# {{ $name }} + +{{ title .Command.Short }} + +{{ if .Command.Runnable }}## Usage + +` + "```bash\n{{ .Command.UseLine }}\n```" + `{{ end }} + +{{ if ne .Command.Long "" }}## Synopsis + +{{ .Command.Long }} +{{ end }} +{{ $flags := .Command.NonInheritedFlags }} +{{ if $flags.HasFlags }}## Options + +` + "```bash\n {{ $flags.FlagUsages | chomp }}\n```" + `{{ end }} +{{ $global_flags := .Command.InheritedFlags }} +{{ if $global_flags.HasFlags }}## Options inherited from parent commands + +` + "```bash\n {{ $global_flags.FlagUsages | chomp }}\n```" + `{{ end }} + +{{ if .Command.HasSubCommands }}# Subcommands +{{ range .Command.Commands }} +{{ if ne .Deprecated "" }} +* [{{ $name }} {{ .Name }}]({{ .BaseURL }}{{ handle_file $name .Name | handle_link }}) - {{ .Short }} +{{ end }} +{{ end }} +{{ end }} + +{{ if .Command.HasParent }}{{ $parent := .Command.Parent }}## See Also +* [{{ $parent.CommandPath }}]({{ .BaseURL }}{{ handle_file $parent.CommandPath "" | handle_link }}) - {{ $parent.Short }} +{{ end }} + +{{ if ne .Command.Example "" }}# Quick Tips + +` + "```bash\n{{ .Command.Example }}\n```" + `{{ end }} + +{{ if ne (len .Entry.Examples) 0 }}# Examples +{{ range .Entry.Examples }} +* [{{ title .Title }}]({{ .URL }}) +{{- end }} +{{ end }} + +{{ if ne (len .Entry.Specifications) 0 }}# Specifications +{{ range .Entry.Specifications }} +* [{{ title .Title }}]({{ .URL }}) +{{- end }} +{{ end }} +` + + return template.New("docGenerator").Funcs(funcMap).Parse(templateText) +} + +func GenerateEntries(dir, render_dir, description string) ([]*Entry, error) { + var entries []*Entry + + if _, err := os.Stat(render_dir); os.IsNotExist(err) { + err = os.MkdirAll(render_dir, 0755) + if err != nil { + panic(err) + } + } + + files := CollectEntries(dir) + + for _, file := range files { + this_entry, err := GenerateEntry(file, dir, render_dir, description) + if err != nil { + return nil, err + } else { + entries = append(entries, this_entry) + } + } + + return entries, nil +} + +func CollectEntries(dir string) []string { + var newFiles []string + + files, err := filepath.Glob(dir + "/*") + if err != nil { + panic(err) + } + + for _, file := range files { + f_info, err := os.Stat(file) + + if err != nil { + panic(err) + } + + if f_info.IsDir() { + newFiles = append(newFiles, CollectEntries(file)...) + } else { + if filepath.Ext(file) == ".md" { + newFiles = append(newFiles, file) + } + } + } + + return newFiles +} + +func GenerateEntry(file, dir, render_dir, description string) (*Entry, error) { + var err error + + this_entry := &Entry{ + FileName: GenerateFileNameFromGlob(render_dir, file), + Title: GenerateTitleFromFileName(filepath.Base(file)), + Description: description, + } + + this_entry.URL = GenerateURLFromFileName(this_entry.FileName) + + txt, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + // Get template from docs generator + this_entry.Template, err = GenerateEntriesTemplate(txt) + if err != nil { + return nil, err + } + + return this_entry, nil +} + +func GenerateEntriesTemplate(txt []byte) (*template.Template, error) { + handle_link := func(s string) string { + return (strings.Replace(s, ".md", "/", -1)) + } + + handle_file := func(s1 string) string { + return strings.Replace((s1 + ".md"), " ", "_", -1) + } + + insert_definition := func(file string, struc string) string { + txt, err := ioutil.ReadFile(filepath.Join("definitions", file)) + if err != nil { + panic(err) + } + finder := regexp.MustCompile(fmt.Sprintf(`(?ms:^type %s struct {.*?^})`, struc)) + return ("```go\n" + string(finder.Find(txt)) + "\n```") + } + + insert_bash_lines := func(file string, linesToRead string) string { + var lines []byte + var line []byte + var start int + var stop int + + fileInfo, err := os.Open(filepath.Join("docs", "tests", file)) + if err != nil { + panic(err) + } + defer fileInfo.Close() + + start, err = strconv.Atoi(strings.Split(linesToRead, "-")[0]) + if strings.Contains(linesToRead, "-") { + stop, err = strconv.Atoi(strings.Split(linesToRead, "-")[1]) + } else { + stop = start + } + if err != nil { + panic(err) + } + + r := bufio.NewReader(fileInfo) + for i := 1; ; i++ { + line, err = r.ReadBytes('\n') + if err != nil { + break + } + if i >= start && i <= stop { + lines = append(lines, line...) + } + } + if err != io.EOF { + panic(err) + } + + return ("```bash\n" + string(lines) + "```") + } + + insert_file := func(file string) string { + file = filepath.Join("docs", "tests", file) + ext := filepath.Ext(file) + switch ext { + case ".sol": + ext = ".javascript" + case ".yml": + ext = ".yaml" + } + + ext = strings.Replace(ext, ".", "", 1) + + txtB, err := ioutil.ReadFile(file) + if err != nil { + panic(err) + } + + txt := string(txtB) + if !strings.HasSuffix(txt, "\n") { + txt = txt + "\n" + } + + return ("```" + ext + "\n" + txt + "```") // TODO: add auto-curl text + } + + funcMap := template.FuncMap{ + "title": strings.Title, + "replace": strings.Replace, + "chomp": strings.TrimSpace, + "handle_file": handle_file, + "handle_link": handle_link, + "insert_definition": insert_definition, + "insert_bash_lines": insert_bash_lines, + "insert_file": insert_file, + } + + var templateText = `{{- $name := .Title -}}` + FrontMatter + ` + +` + string(txt) + ` + +## Commands + +* [{{ .CmdEntryPoint }}]({{ .BaseURL }}{{ handle_file .CmdEntryPoint | handle_link }}) + +{{ if ne (len .Examples) 0 }}# Examples +{{ range .Examples }} +* [{{ title .Title }}]({{ .URL }}) +{{- end }} +{{ end }} + +{{ if ne (len .Specifications) 0 }}# Specifications +{{ range .Specifications }} +* [{{ title .Title }}]({{ .URL }}) +{{- end }} +{{ end }} +` + + return template.New("entryGenerator").Funcs(funcMap).Parse(templateText) +} + +func RenderEntry(this_entry *Entry) error { + out_file, err := os.Create(this_entry.FileName) + if err != nil { + return err + } + defer out_file.Close() + + err = this_entry.Template.Execute(out_file, this_entry) + if err != nil { + return err + } + + return nil +} diff --git a/event/event_cache.go b/event/event_cache.go index 08db30116ea20225cb27875fb79fd103b0165070..7b6bc356cdf528a390730daa1ac0cabcb2d5749e 100644 --- a/event/event_cache.go +++ b/event/event_cache.go @@ -2,9 +2,10 @@ package event import ( "fmt" - "github.com/eris-ltd/eris-db/txs" "sync" "time" + + "github.com/eris-ltd/eris-db/txs" ) var ( diff --git a/event/events.go b/event/events.go index d5c37d8e724259036a661cc4758d9030c322aba1..ec790a9ed8b1d120a46ecf1642bec840756fcbdd 100644 --- a/event/events.go +++ b/event/events.go @@ -23,8 +23,9 @@ import ( "fmt" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" "github.com/eris-ltd/eris-db/txs" - log "github.com/eris-ltd/eris-logger" go_events "github.com/tendermint/go-events" tm_types "github.com/tendermint/tendermint/types" ) @@ -43,8 +44,8 @@ type EventEmitter interface { Unsubscribe(subId string) error } -func NewEvents(eventSwitch *go_events.EventSwitch) *events { - return &events{eventSwitch} +func NewEvents(eventSwitch *go_events.EventSwitch, logger loggers.InfoTraceLogger) *events { + return &events{eventSwitch: eventSwitch, logger: logging.WithScope(logger, "Events")} } // Provides an EventEmitter that wraps many underlying EventEmitters as a @@ -57,27 +58,28 @@ func Multiplex(events ...EventEmitter) *multiplexedEvents { // The events struct has methods for working with events. type events struct { eventSwitch *go_events.EventSwitch + logger loggers.InfoTraceLogger } // Subscribe to an event. -func (this *events) Subscribe(subId, event string, +func (evts *events) Subscribe(subId, event string, callback func(txs.EventData)) error { cb := func(evt go_events.EventData) { eventData, err := mapToOurEventData(evt) if err != nil { - log.WithError(err). - WithFields(log.Fields{"event": event}). - Error("Failed to map go-events EventData to our EventData") + logging.InfoMsg(evts.logger, "Failed to map go-events EventData to our EventData", + "error", err, + "event", event) } callback(eventData) } - this.eventSwitch.AddListenerForEvent(subId, event, cb) + evts.eventSwitch.AddListenerForEvent(subId, event, cb) return nil } // Un-subscribe from an event. -func (this *events) Unsubscribe(subId string) error { - this.eventSwitch.RemoveListener(subId) +func (evts *events) Unsubscribe(subId string) error { + evts.eventSwitch.RemoveListener(subId) return nil } diff --git a/genesis/gen_test.go b/genesis/gen_test.go new file mode 100644 index 0000000000000000000000000000000000000000..30d5f9c511df7efbd372c6a44814b1c0d64070c8 --- /dev/null +++ b/genesis/gen_test.go @@ -0,0 +1,162 @@ +package genesis + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +// set the chain ID +var chainID string = "genesis-file-maker-test" + +var genesisFileExpected string = `{ + "genesis_time": "0001-01-01T00:00:00.000Z", + "chain_id": "genesis-file-maker-test", + "params": null, + "accounts": [ + { + "address": "74417C1BEFB3938B71B22B202050A4C6591FFCF6", + "amount": 9999999999, + "name": "genesis-file-maker-test_developer_000", + "permissions": { + "base": { + "perms": 14430, + "set": 16383 + }, + "roles": [] + } + }, + { + "address": "0C9DAEA4046491A661FCE0B41B0CAA2AD3415268", + "amount": 99999999999999, + "name": "genesis-file-maker-test_full_000", + "permissions": { + "base": { + "perms": 16383, + "set": 16383 + }, + "roles": [] + } + }, + { + "address": "E1BD50A1B90A15861F5CF0F182D291F556B21A86", + "amount": 9999999999, + "name": "genesis-file-maker-test_participant_000", + "permissions": { + "base": { + "perms": 2118, + "set": 16383 + }, + "roles": [] + } + }, + { + "address": "A6C8E2DE652DB8ADB4036293DC21F8FE389D77C2", + "amount": 9999999999, + "name": "genesis-file-maker-test_root_000", + "permissions": { + "base": { + "perms": 16383, + "set": 16383 + }, + "roles": [] + } + }, + { + "address": "E96CB7910001320B6F1E2266A8431D5E98FF0183", + "amount": 9999999999, + "name": "genesis-file-maker-test_validator_000", + "permissions": { + "base": { + "perms": 32, + "set": 16383 + }, + "roles": [] + } + } + ], + "validators": [ + { + "pub_key": [ + 1, + "238E1A77CC7CDCD13F4D77841F1FE4A46A77DB691EC89718CD0D4CB3409F61D2" + ], + "amount": 9999999999, + "name": "genesis-file-maker-test_full_000", + "unbond_to": [ + { + "address": "0C9DAEA4046491A661FCE0B41B0CAA2AD3415268", + "amount": 9999999999 + } + ] + }, + { + "pub_key": [ + 1, + "7F53D78C526F96C87ACBD0D2B9DB2E9FC176981623D26B1DB1CF59748EE9F4CF" + ], + "amount": 9999999998, + "name": "genesis-file-maker-test_validator_000", + "unbond_to": [ + { + "address": "E96CB7910001320B6F1E2266A8431D5E98FF0183", + "amount": 9999999998 + } + ] + } + ] +}` + +var accountsCSV string = `F0BD5CE45D306D61C9AB73CE5268C2B59D52CAF7127EF0E3B65523302254350A,9999999999,genesis-file-maker-test_developer_000,14430,16383 +238E1A77CC7CDCD13F4D77841F1FE4A46A77DB691EC89718CD0D4CB3409F61D2,99999999999999,genesis-file-maker-test_full_000,16383,16383 +E37A655E560D53721C9BB06BA742398323504DFE2EB2C67E71F8D16E71E0471B,9999999999,genesis-file-maker-test_participant_000,2118,16383 +EC0E38CC8308EC9E720EE839242A7BC5C781D1F852E962FAC5A8E0599CE5B224,9999999999,genesis-file-maker-test_root_000,16383,16383 +7F53D78C526F96C87ACBD0D2B9DB2E9FC176981623D26B1DB1CF59748EE9F4CF,9999999999,genesis-file-maker-test_validator_000,32,16383` + +var validatorsCSV string = `238E1A77CC7CDCD13F4D77841F1FE4A46A77DB691EC89718CD0D4CB3409F61D2,9999999999,genesis-file-maker-test_full_000,16383,16383 +7F53D78C526F96C87ACBD0D2B9DB2E9FC176981623D26B1DB1CF59748EE9F4CF,9999999998,genesis-file-maker-test_validator_000,32,16383` + +func TestKnownCSV(t *testing.T) { + // make temp dir + dir, err := ioutil.TempDir(os.TempDir(), "genesis-file-maker-test") + if err != nil { + t.Fatal(err) + } + + defer func() { + //cleanup + os.RemoveAll(dir) + if err != nil { + t.Fatal(err) + } + + }() + + // set the filepaths to be written to + accountsCSVpath := filepath.Join(dir, "accounts.csv") + validatorsCSVpath := filepath.Join(dir, "validators.csv") + + // write the accounts.csv + if err := ioutil.WriteFile(accountsCSVpath, []byte(accountsCSV), 0600); err != nil { + t.Fatal(err) + } + + // write the validators.csv + if err := ioutil.WriteFile(validatorsCSVpath, []byte(validatorsCSV), 0600); err != nil { + t.Fatal(err) + } + + // create the genesis file + genesisFileWritten, err := GenerateKnown(chainID, accountsCSVpath, validatorsCSVpath) + if err != nil { + t.Fatal(err) + } + + // compare + if !bytes.Equal([]byte(genesisFileExpected), []byte(genesisFileWritten)) { + t.Fatalf("Bad genesis file: got (%s), expected (%s)", genesisFileWritten, genesisFileExpected) + } + +} diff --git a/genesis/make_genesis_file.go b/genesis/make_genesis_file.go new file mode 100644 index 0000000000000000000000000000000000000000..c4324366ccf4f1c6195684ce32c59592ddc37712 --- /dev/null +++ b/genesis/make_genesis_file.go @@ -0,0 +1,199 @@ +package genesis + +import ( + "bytes" + "encoding/csv" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strconv" + + ptypes "github.com/eris-ltd/eris-db/permission/types" + "github.com/eris-ltd/eris-db/util" + + "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" +) + +//------------------------------------------------------------------------------------ +// core functions + +func GenerateKnown(chainID, accountsPathCSV, validatorsPathCSV string) (string, error) { + var genDoc *GenesisDoc + + // TODO [eb] eliminate reading priv_val ... [zr] where? + if accountsPathCSV == "" || validatorsPathCSV == "" { + return "", fmt.Errorf("both accounts.csv and validators.csv is required") + } + + pubkeys, amts, names, perms, setbits, err := parseCsv(validatorsPathCSV) + if err != nil { + return "", err + } + + pubkeysA, amtsA, namesA, permsA, setbitsA, err := parseCsv(accountsPathCSV) + if err != nil { + return "", err + } + + genDoc = newGenDoc(chainID, len(pubkeys), len(pubkeysA)) + for i, pk := range pubkeys { + genDocAddValidator(genDoc, pk, amts[i], names[i], perms[i], setbits[i], i) + } + for i, pk := range pubkeysA { + genDocAddAccount(genDoc, pk, amtsA[i], namesA[i], permsA[i], setbitsA[i], i) + } + + buf, buf2, n := new(bytes.Buffer), new(bytes.Buffer), new(int) + wire.WriteJSON(genDoc, buf, n, &err) + if err != nil { + return "", err + } + if err := json.Indent(buf2, buf.Bytes(), "", "\t"); err != nil { + return "", err + } + + return buf2.String(), nil +} + +//----------------------------------------------------------------------------- +// gendoc convenience functions + +func newGenDoc(chainID string, nVal, nAcc int) *GenesisDoc { + genDoc := GenesisDoc{ + ChainID: chainID, + // GenesisTime: time.Now(), + } + genDoc.Accounts = make([]GenesisAccount, nAcc) + genDoc.Validators = make([]GenesisValidator, nVal) + return &genDoc +} + +func genDocAddAccount(genDoc *GenesisDoc, pubKey crypto.PubKeyEd25519, amt int64, name string, perm, setbit ptypes.PermFlag, index int) { + addr := pubKey.Address() + acc := GenesisAccount{ + Address: addr, + Amount: amt, + Name: name, + Permissions: &ptypes.AccountPermissions{ + Base: ptypes.BasePermissions{ + Perms: perm, + SetBit: setbit, + }, + }, + } + if index < 0 { + genDoc.Accounts = append(genDoc.Accounts, acc) + } else { + genDoc.Accounts[index] = acc + } +} + +func genDocAddValidator(genDoc *GenesisDoc, pubKey crypto.PubKeyEd25519, amt int64, name string, perm, setbit ptypes.PermFlag, index int) { + addr := pubKey.Address() + genDoc.Validators[index] = GenesisValidator{ + PubKey: pubKey, + Amount: amt, + Name: name, + UnbondTo: []BasicAccount{ + { + Address: addr, + Amount: amt, + }, + }, + } + // [zr] why no index < 0 like in genDocAddAccount? +} + +//----------------------------------------------------------------------------- +// util functions + +// convert hex strings to ed25519 pubkeys +func pubKeyStringsToPubKeys(pubkeys []string) ([]crypto.PubKeyEd25519, error) { + pubKeys := make([]crypto.PubKeyEd25519, len(pubkeys)) + for i, k := range pubkeys { + pubBytes, err := hex.DecodeString(k) + if err != nil { + return pubKeys, err + } + copy(pubKeys[i][:], pubBytes) + } + return pubKeys, nil +} + +// empty is over written +func ifExistsElse(list []string, index int, defaultValue string) string { + if len(list) > index { + if list[index] != "" { + return list[index] + } + } + return defaultValue +} + +// takes a csv in the following format: pubkey, starting balance, name, permissions, setbit +func parseCsv(filePath string) (pubKeys []crypto.PubKeyEd25519, amts []int64, names []string, perms, setbits []ptypes.PermFlag, err error) { + + csvFile, err := os.Open(filePath) + if err != nil { + util.Fatalf("Couldn't open file: %s: %v", filePath, err) + } + defer csvFile.Close() + + r := csv.NewReader(csvFile) + //r.FieldsPerRecord = # of records expected + params, err := r.ReadAll() + if err != nil { + util.Fatalf("Couldn't read file: %v", err) + + } + + pubkeys := make([]string, len(params)) + amtS := make([]string, len(params)) + names = make([]string, len(params)) + permsS := make([]string, len(params)) + setbitS := make([]string, len(params)) + for i, each := range params { + pubkeys[i] = each[0] + amtS[i] = ifExistsElse(each, 1, "1000") + names[i] = ifExistsElse(each, 2, "") + permsS[i] = ifExistsElse(each, 3, fmt.Sprintf("%d", ptypes.DefaultPermFlags)) + setbitS[i] = ifExistsElse(each, 4, permsS[i]) + } + + //TODO convert int to uint64, see issue #25 + perms = make([]ptypes.PermFlag, len(permsS)) + for i, perm := range permsS { + pflag, err := strconv.Atoi(perm) + if err != nil { + util.Fatalf("Permissions (%v) must be an integer", perm) + } + perms[i] = ptypes.PermFlag(pflag) + } + setbits = make([]ptypes.PermFlag, len(setbitS)) + for i, setbit := range setbitS { + setbitsFlag, err := strconv.Atoi(setbit) + if err != nil { + util.Fatalf("SetBits (%v) must be an integer", setbit) + } + setbits[i] = ptypes.PermFlag(setbitsFlag) + } + + // convert amts to ints + amts = make([]int64, len(amtS)) + for i, a := range amtS { + if amts[i], err = strconv.ParseInt(a, 10, 64); err != nil { + err = fmt.Errorf("Invalid amount: %v", err) + return + } + } + + // convert pubkey hex strings to struct + pubKeys, err = pubKeyStringsToPubKeys(pubkeys) + if err != nil { + return + } + + return pubKeys, amts, names, perms, setbits, nil +} diff --git a/genesis/types.go b/genesis/types.go new file mode 100644 index 0000000000000000000000000000000000000000..d03e6197645b67ab9f306a9d71106a2ab0d7b338 --- /dev/null +++ b/genesis/types.go @@ -0,0 +1,67 @@ +package genesis + +import ( + "fmt" + "os" + "time" + + ptypes "github.com/eris-ltd/eris-db/permission/types" + "github.com/tendermint/go-crypto" + "github.com/tendermint/go-wire" +) + +//------------------------------------------------------------ +// we store the GenesisDoc in the db under this key + +var GenDocKey = []byte("GenDocKey") + +//------------------------------------------------------------ +// core types for a genesis definition + +type BasicAccount struct { + Address []byte `json:"address"` + Amount int64 `json:"amount"` +} + +type GenesisAccount struct { + Address []byte `json:"address"` + Amount int64 `json:"amount"` + Name string `json:"name"` + Permissions *ptypes.AccountPermissions `json:"permissions"` +} + +type GenesisValidator struct { + PubKey crypto.PubKey `json:"pub_key"` + Amount int64 `json:"amount"` + Name string `json:"name"` + UnbondTo []BasicAccount `json:"unbond_to"` +} + +type GenesisParams struct { + GlobalPermissions *ptypes.AccountPermissions `json:"global_permissions"` +} + +//------------------------------------------------------------ +// GenesisDoc is stored in the state database + +type GenesisDoc struct { + GenesisTime time.Time `json:"genesis_time"` + ChainID string `json:"chain_id"` + Params *GenesisParams `json:"params"` + Accounts []GenesisAccount `json:"accounts"` + Validators []GenesisValidator `json:"validators"` +} + +//------------------------------------------------------------ +// Make genesis state from file + +func GenesisDocFromJSON(jsonBlob []byte) (genState *GenesisDoc) { + var err error + wire.ReadJSONPtr(&genState, jsonBlob, &err) + if err != nil { + fmt.Printf("Couldn't read GenesisDoc: %v", err) + // TODO: on error return error, not exit + os.Exit(1) + } + return +} diff --git a/glide.lock b/glide.lock index 76c7e0b2c0dc6b668890e5388cc0820b9d4b430c..62620c5ded26d6461e28801b4c5ce280fdfcb974 100644 --- a/glide.lock +++ b/glide.lock @@ -23,15 +23,8 @@ imports: version: f0aeabca5a127c4078abb8c8d64298b147264b55 - name: github.com/davecgh/go-spew version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d -- name: github.com/eris-ltd/common - version: 8ca15f5455104403db4202c995e2f6e161654c02 - subpackages: - - go/docs - - go/common - name: github.com/eris-ltd/eris-keys version: 114ebc77443db9a153692233294e48bc7e184215 -- name: github.com/eris-ltd/eris-logger - version: ea48a395d6ecc0eccc67a26da9fc7a6106fabb84 - name: github.com/fsnotify/fsnotify version: 30411dbcefb7a1da7e84f75530ad3abe4011b4f8 - name: github.com/gin-gonic/gin @@ -215,4 +208,26 @@ imports: version: ecde8c8f16df93a994dda8936c8f60f0c26c28ab - name: gopkg.in/yaml.v2 version: a83829b6f1293c91addabc89d0571c246397bbf4 +- name: github.com/go-kit/kit + version: f66b0e13579bfc5a48b9e2a94b1209c107ea1f41 + subpackages: + - log +- name: github.com/eapache/channels + version: 47238d5aae8c0fefd518ef2bee46290909cf8263 +- name: github.com/eapache/queue + version: 44cc805cf13205b55f69e14bcb69867d1ae92f98 +- name: github.com/go-logfmt/logfmt + version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 +- name: github.com/go-stack/stack + version: 100eb0c0a9c5b306ca2fb4f165df21d80ada4b82 +- name: github.com/Sirupsen/logrus + version: d26492970760ca5d33129d2d799e34be5c4782eb +- name: github.com/inconshreveable/log15 + version: 46a701a619de90c65a78c04d1a58bf02585e9701 + subpackages: + - term +- name: github.com/streadway/simpleuuid + version: 6617b501e485b77e61b98cd533aefff9e258b5a7 +- name: github.com/Masterminds/glide + version: 84607742b10f492430762d038e954236bbaf23f7 devImports: [] diff --git a/glide.yaml b/glide.yaml index ca8e52b5c438cf55e2cb5a85ca74617f8d53c485..d832bc2213309b201c8c52bda69fbb9e87ef52f5 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,6 +1,5 @@ package: github.com/eris-ltd/eris-db import: -- package: github.com/eris-ltd/eris-logger - package: github.com/eris-ltd/eris-keys - package: github.com/spf13/cobra - package: github.com/spf13/viper @@ -15,5 +14,20 @@ import: - ripemd160 - package: gopkg.in/fatih/set.v0 - package: gopkg.in/tylerb/graceful.v1 -- package: golang.org/x/net/http2 -- package: github.com/eris-ltd/common +- package: golang.org/x/net + subpackages: + - http2 +- package: github.com/go-kit/kit + version: ^0.3.0 +- package: github.com/eapache/channels + version: ~1.1.0 +- package: github.com/go-logfmt/logfmt + version: ^0.3.0 +- package: github.com/go-stack/stack + version: ^1.5.2 +- package: github.com/inconshreveable/log15 +- package: github.com/Sirupsen/logrus + version: ^0.11.0 +- package: github.com/streadway/simpleuuid +- package: github.com/Masterminds/glide + version: ~0.12.3 \ No newline at end of file diff --git a/hell b/hell new file mode 100644 index 0000000000000000000000000000000000000000..392907b3a74800038d60cc899e866c97f0afc28d --- /dev/null +++ b/hell @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go run ./util/hell/cmd/hell/main.go "$@" \ No newline at end of file diff --git a/keys/key_client.go b/keys/key_client.go index e841f50abd71f68ce4bdd9fc838acf764e5f3b1f..2431bc643500016bc5859d91c8a48955d451dbf2 100644 --- a/keys/key_client.go +++ b/keys/key_client.go @@ -19,6 +19,9 @@ package keys import ( "encoding/hex" "fmt" + + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" ) type KeyClient interface { @@ -29,31 +32,33 @@ type KeyClient interface { PublicKey(address []byte) (publicKey []byte, err error) } -// NOTE [ben] Compiler check to ensure ErisKeyClient successfully implements +// NOTE [ben] Compiler check to ensure erisKeyClient successfully implements // eris-db/keys.KeyClient -var _ KeyClient = (*ErisKeyClient)(nil) +var _ KeyClient = (*erisKeyClient)(nil) -type ErisKeyClient struct { +type erisKeyClient struct { rpcString string + logger loggers.InfoTraceLogger } -// ErisKeyClient.New returns a new eris-keys client for provided rpc location +// erisKeyClient.New returns a new eris-keys client for provided rpc location // Eris-keys connects over http request-responses -func NewErisKeyClient(rpcString string) *ErisKeyClient { - return &ErisKeyClient{ +func NewErisKeyClient(rpcString string, logger loggers.InfoTraceLogger) *erisKeyClient { + return &erisKeyClient{ rpcString: rpcString, + logger: logging.WithScope(logger, "ErisKeysClient"), } } // Eris-keys client Sign requests the signature from ErisKeysClient over rpc for the given // bytes to be signed and the address to sign them with. -func (erisKeys *ErisKeyClient) Sign(signBytesString string, signAddress []byte) (signature []byte, err error) { +func (erisKeys *erisKeyClient) Sign(signBytesString string, signAddress []byte) (signature []byte, err error) { args := map[string]string{ "msg": signBytesString, "hash": signBytesString, // TODO:[ben] backwards compatibility "addr": fmt.Sprintf("%X", signAddress), } - sigS, err := RequestResponse(erisKeys.rpcString, "sign", args) + sigS, err := RequestResponse(erisKeys.rpcString, "sign", args, erisKeys.logger) if err != nil { return } @@ -66,11 +71,11 @@ func (erisKeys *ErisKeyClient) Sign(signBytesString string, signAddress []byte) // Eris-keys client PublicKey requests the public key associated with an address from // the eris-keys server. -func (erisKeys *ErisKeyClient) PublicKey(address []byte) (publicKey []byte, err error) { +func (erisKeys *erisKeyClient) PublicKey(address []byte) (publicKey []byte, err error) { args := map[string]string{ "addr": fmt.Sprintf("%X", address), } - pubS, err := RequestResponse(erisKeys.rpcString, "pub", args) + pubS, err := RequestResponse(erisKeys.rpcString, "pub", args, erisKeys.logger) if err != nil { return } diff --git a/keys/key_client_util.go b/keys/key_client_util.go index e9e5f4783d51ce2df4e590cea008535ed5f1a4d7..5fd1cbce41f55d1348584aeb29d55dacf7fcc1d8 100644 --- a/keys/key_client_util.go +++ b/keys/key_client_util.go @@ -26,7 +26,8 @@ import ( "io/ioutil" "net/http" - log "github.com/eris-ltd/eris-logger" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" ) // Eris-Keys server connects over http request-response structures @@ -36,17 +37,17 @@ type HTTPResponse struct { Error string } -func RequestResponse(addr, method string, args map[string]string) (string, error) { - b, err := json.Marshal(args) +func RequestResponse(addr, method string, args map[string]string, logger loggers.InfoTraceLogger) (string, error) { + body, err := json.Marshal(args) if err != nil { return "", err } endpoint := fmt.Sprintf("%s/%s", addr, method) - log.WithFields(log.Fields{ - "key server endpoint": endpoint, - "request body": string(b), - }).Debugf("Eris-client: Sending request body to key server") - req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(b)) + logging.TraceMsg(logger, "Sending request to key server", + "key_server_endpoint", endpoint, + "request_body", string(body), + ) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(body)) if err != nil { return "", err } @@ -58,11 +59,11 @@ func RequestResponse(addr, method string, args map[string]string) (string, error if errS != "" { return "", fmt.Errorf("Error (string) calling eris-keys at %s: %s", endpoint, errS) } - log.WithFields(log.Fields{ - "endpoint": endpoint, - "request body": string(b), - "response": res, - }).Debugf("Received response from key server") + logging.TraceMsg(logger, "Received response from key server", + "endpoint", endpoint, + "request body", string(body), + "response", res, + ) return res, nil } diff --git a/logging/adapters/logrus/logrus.go b/logging/adapters/logrus/logrus.go new file mode 100644 index 0000000000000000000000000000000000000000..fe974768f69179c01c1d3a5b4b6334ba7d6ce7d2 --- /dev/null +++ b/logging/adapters/logrus/logrus.go @@ -0,0 +1,22 @@ +package adapters + +import ( + "github.com/Sirupsen/logrus" + kitlog "github.com/go-kit/kit/log" +) + +type logrusLogger struct { + logger logrus.Logger +} + +var _ kitlog.Logger = (*logrusLogger)(nil) + +func NewLogrusLogger(logger logrus.Logger) *logrusLogger { + return &logrusLogger{ + logger: logger, + } +} + +func (ll *logrusLogger) Log(keyvals ...interface{}) error { + return nil +} diff --git a/logging/adapters/stdlib/capture.go b/logging/adapters/stdlib/capture.go new file mode 100644 index 0000000000000000000000000000000000000000..b5fadacc9d87cd3f71a8a603d29040f8dc4d8921 --- /dev/null +++ b/logging/adapters/stdlib/capture.go @@ -0,0 +1,26 @@ +package stdlib + +import ( + "io" + "log" + + "github.com/eris-ltd/eris-db/logging/loggers" + kitlog "github.com/go-kit/kit/log" +) + +func Capture(stdLibLogger log.Logger, + logger loggers.InfoTraceLogger) io.Writer { + adapter := newAdapter(logger) + stdLibLogger.SetOutput(adapter) + return adapter +} + +func CaptureRootLogger(logger loggers.InfoTraceLogger) io.Writer { + adapter := newAdapter(logger) + log.SetOutput(adapter) + return adapter +} + +func newAdapter(logger loggers.InfoTraceLogger) io.Writer { + return kitlog.NewStdlibAdapter(logger) +} diff --git a/logging/adapters/tendermint_log15/capture.go b/logging/adapters/tendermint_log15/capture.go new file mode 100644 index 0000000000000000000000000000000000000000..afc5d29eedcc61aacbbe8318775da50e74be0468 --- /dev/null +++ b/logging/adapters/tendermint_log15/capture.go @@ -0,0 +1,47 @@ +package adapters + +import ( + "github.com/eris-ltd/eris-db/logging/loggers" + kitlog "github.com/go-kit/kit/log" + "github.com/tendermint/log15" +) + +type infoTraceLoggerAsLog15Handler struct { + logger loggers.InfoTraceLogger +} + +var _ log15.Handler = (*infoTraceLoggerAsLog15Handler)(nil) + +type log15HandlerAsKitLogger struct { + handler log15.Handler +} + +var _ kitlog.Logger = (*log15HandlerAsKitLogger)(nil) + +func (l *log15HandlerAsKitLogger) Log(keyvals ...interface{}) error { + record := LogLineToRecord(keyvals...) + return l.handler.Log(record) +} + +func (h *infoTraceLoggerAsLog15Handler) Log(record *log15.Record) error { + if record.Lvl < log15.LvlDebug { + // Send to Critical, Warning, Error, and Info to the Info channel + h.logger.Info(RecordToLogLine(record)...) + } else { + // Send to Debug to the Trace channel + h.logger.Trace(RecordToLogLine(record)...) + } + return nil +} + +func Log15HandlerAsKitLogger(handler log15.Handler) kitlog.Logger { + return &log15HandlerAsKitLogger{ + handler: handler, + } +} + +func InfoTraceLoggerAsLog15Handler(logger loggers.InfoTraceLogger) log15.Handler { + return &infoTraceLoggerAsLog15Handler{ + logger: logger, + } +} diff --git a/logging/adapters/tendermint_log15/convert.go b/logging/adapters/tendermint_log15/convert.go new file mode 100644 index 0000000000000000000000000000000000000000..f7be8420866a67a987afb39f84b2e93cea849018 --- /dev/null +++ b/logging/adapters/tendermint_log15/convert.go @@ -0,0 +1,70 @@ +package adapters + +import ( + "time" + + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" + . "github.com/eris-ltd/eris-db/util/slice" + "github.com/go-stack/stack" + "github.com/tendermint/log15" +) + +// Convert a go-kit log line (i.e. keyvals... interface{}) into a log15 record +// This allows us to use log15 output handlers +func LogLineToRecord(keyvals ...interface{}) *log15.Record { + vals, ctx := structure.ValuesAndContext(keyvals, structure.TimeKey, + structure.MessageKey, structure.CallerKey, structure.LevelKey) + + // Mapping of log line to Record is on a best effort basis + theTime, _ := vals[structure.TimeKey].(time.Time) + call, _ := vals[structure.CallerKey].(stack.Call) + level, _ := vals[structure.LevelKey].(string) + message, _ := vals[structure.MessageKey].(string) + + return &log15.Record{ + Time: theTime, + Lvl: Log15LvlFromString(level), + Msg: message, + Call: call, + Ctx: ctx, + KeyNames: log15.RecordKeyNames{ + Time: structure.TimeKey, + Msg: structure.MessageKey, + Lvl: structure.LevelKey, + }} +} + +// Convert a log15 record to a go-kit log line (i.e. keyvals... interface{}) +// This allows us to capture output from dependencies using log15 +func RecordToLogLine(record *log15.Record) []interface{} { + return Concat( + Slice( + structure.TimeKey, record.Time, + structure.CallerKey, record.Call, + structure.LevelKey, record.Lvl.String(), + ), + record.Ctx, + Slice( + structure.MessageKey, record.Msg, + )) +} + +// Collapse our weak notion of leveling and log15's into a log15.Lvl +func Log15LvlFromString(level string) log15.Lvl { + if level == "" { + return log15.LvlDebug + } + switch level { + case loggers.InfoLevelName: + return log15.LvlInfo + case loggers.TraceLevelName: + return log15.LvlDebug + default: + lvl, err := log15.LvlFromString(level) + if err == nil { + return lvl + } + return log15.LvlDebug + } +} diff --git a/logging/config.go b/logging/config.go new file mode 100644 index 0000000000000000000000000000000000000000..b43b1b5545ba47586cb3e61a01589bac4de564c0 --- /dev/null +++ b/logging/config.go @@ -0,0 +1,11 @@ +package logging + +type ( + SinkConfig struct { + Channels []string + } + + LoggingConfig struct { + Sinks []SinkConfig + } +) diff --git a/logging/convention.go b/logging/convention.go new file mode 100644 index 0000000000000000000000000000000000000000..0f5d3372c13bb9903ea1db88ef24035bfc33a9b5 --- /dev/null +++ b/logging/convention.go @@ -0,0 +1,58 @@ +package logging + +import ( + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" + "github.com/eris-ltd/eris-db/util/slice" + kitlog "github.com/go-kit/kit/log" +) + +// Helper functions for InfoTraceLoggers, sort of extension methods to loggers +// to centralise and establish logging conventions on top of in with the base +// logging interface + +// Record structured Info log line with a message and conventional keys +func InfoMsgVals(logger loggers.InfoTraceLogger, message string, vals ...interface{}) { + MsgVals(kitlog.LoggerFunc(logger.Info), message, vals...) +} + +// Record structured Trace log line with a message and conventional keys +func TraceMsgVals(logger loggers.InfoTraceLogger, message string, vals ...interface{}) { + MsgVals(kitlog.LoggerFunc(logger.Trace), message, vals...) +} + +// Record structured Info log line with a message +func InfoMsg(logger loggers.InfoTraceLogger, message string, keyvals ...interface{}) { + Msg(kitlog.LoggerFunc(logger.Info), message, keyvals...) +} + +// Record structured Trace log line with a message +func TraceMsg(logger loggers.InfoTraceLogger, message string, keyvals ...interface{}) { + Msg(kitlog.LoggerFunc(logger.Trace), message, keyvals...) +} + +// Establish or extend the scope of this logger by appending scopeName to the Scope vector. +// Like With the logging scope is append only but can be used to provide parenthetical scopes by hanging on to the +// parent scope and using once the scope has been exited. The scope mechanism does is agnostic to the type of scope +// so can be used to identify certain segments of the call stack, a lexical scope, or any other nested scope. +func WithScope(logger loggers.InfoTraceLogger, scopeName string) loggers.InfoTraceLogger { + // InfoTraceLogger will collapse successive (ScopeKey, scopeName) pairs into a vector in the order which they appear + return logger.With(structure.ScopeKey, scopeName) +} + +// Record a structured log line with a message +func Msg(logger kitlog.Logger, message string, keyvals ...interface{}) error { + prepended := slice.CopyPrepend(keyvals, structure.MessageKey, message) + return logger.Log(prepended...) +} + +// Record a structured log line with a message and conventional keys +func MsgVals(logger kitlog.Logger, message string, vals ...interface{}) error { + keyvals := make([]interface{}, len(vals)*2) + for i := 0; i < len(vals); i++ { + kv := i * 2 + keyvals[kv] = structure.KeyFromValue(vals[i]) + keyvals[kv+1] = vals[i] + } + return Msg(logger, message, keyvals) +} diff --git a/logging/lifecycle/lifecycle.go b/logging/lifecycle/lifecycle.go new file mode 100644 index 0000000000000000000000000000000000000000..ce0faef40ace4decfb9f71a09126d3d22ce4e41d --- /dev/null +++ b/logging/lifecycle/lifecycle.go @@ -0,0 +1,58 @@ +package lifecycle + +// No package in ./logging/... should depend on lifecycle +import ( + "os" + + "time" + + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/adapters/stdlib" + tmLog15adapter "github.com/eris-ltd/eris-db/logging/adapters/tendermint_log15" + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" + + kitlog "github.com/go-kit/kit/log" + "github.com/streadway/simpleuuid" + tmLog15 "github.com/tendermint/log15" +) + +// Lifecycle provides a canonical source for eris loggers. Components should use the functions here +// to set up their root logger and capture any other logging output. + +// Obtain a logger from a LoggingConfig +func NewLoggerFromLoggingConfig(LoggingConfig *logging.LoggingConfig) loggers.InfoTraceLogger { + return NewStdErrLogger() +} + +func NewStdErrLogger() loggers.InfoTraceLogger { + logger := tmLog15adapter.Log15HandlerAsKitLogger( + tmLog15.StreamHandler(os.Stderr, tmLog15.TerminalFormat())) + return NewLogger(logger, logger) +} + +// Provided a standard eris logger that outputs to the supplied underlying info and trace +// loggers +func NewLogger(infoLogger, traceLogger kitlog.Logger) loggers.InfoTraceLogger { + infoTraceLogger := loggers.NewInfoTraceLogger( + loggers.ErisFormatLogger(infoLogger), + loggers.ErisFormatLogger(traceLogger)) + // Create a random ID based on start time + uuid, _ := simpleuuid.NewTime(time.Now()) + var runId string + if uuid != nil { + runId = uuid.String() + } + return logging.WithMetadata(infoTraceLogger.With(structure.RunId, runId)) +} + +func CaptureTendermintLog15Output(infoTraceLogger loggers.InfoTraceLogger) { + tmLog15.Root().SetHandler( + tmLog15adapter.InfoTraceLoggerAsLog15Handler(infoTraceLogger. + With(structure.CapturedLoggingSourceKey, "tendermint_log15"))) +} + +func CaptureStdlibLogOutput(infoTraceLogger loggers.InfoTraceLogger) { + stdlib.CaptureRootLogger(infoTraceLogger. + With(structure.CapturedLoggingSourceKey, "stdlib_log")) +} diff --git a/logging/loggers/channel_logger.go b/logging/loggers/channel_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..20e824ef804deaf9de64266d94c1ef8338d47344 --- /dev/null +++ b/logging/loggers/channel_logger.go @@ -0,0 +1,73 @@ +package loggers + +import ( + "github.com/eapache/channels" + kitlog "github.com/go-kit/kit/log" +) + +const ( + LoggingRingBufferCap channels.BufferCap = 100 +) + +type ChannelLogger struct { + ch channels.Channel +} + +var _ kitlog.Logger = (*ChannelLogger)(nil) + +// Creates a Logger that uses a uses a non-blocking channel. +// +// We would like calls to Log to never block so we use a channel implementation +// that is non-blocking on writes and is able to be so by using a finite ring +// buffer. +func newChannelLogger() *ChannelLogger { + return &ChannelLogger{ + ch: channels.NewRingChannel(LoggingRingBufferCap), + } +} + +func (cl *ChannelLogger) Log(keyvals ...interface{}) error { + cl.ch.In() <- keyvals + // We don't have a way to pass on any logging errors, but that's okay: Log is + // a maximal interface and the error return type is only there for special + // cases. + return nil +} + +// Read a log line by waiting until one is available and returning it +func (cl *ChannelLogger) WaitReadLogLine() []interface{} { + log := <-cl.ch.Out() + // We are passing slices of interfaces down this channel (go-kit log's Log + // interface type), a panic is the right thing to do if this type assertion + // fails. + return log.([]interface{}) +} + +// Tries to read a log line from the channel buffer or returns nil if none is +// immediately available +func (cl *ChannelLogger) ReadLogLine() []interface{} { + select { + case log := <-cl.ch.Out(): + // See WaitReadLogLine + return log.([]interface{}) + default: + return nil + } +} + +// Enters an infinite loop that will drain any log lines from the passed logger. +// +// Exits if the channel is closed. +func (cl *ChannelLogger) DrainChannelToLogger(logger kitlog.Logger) { + for cl.ch.Out() != nil { + logger.Log(cl.WaitReadLogLine()...) + } +} + +// Wraps an underlying Logger baseLogger to provide a Logger that is +// is non-blocking on calls to Log. +func NonBlockingLogger(logger kitlog.Logger) *ChannelLogger { + cl := newChannelLogger() + go cl.DrainChannelToLogger(logger) + return cl +} diff --git a/logging/loggers/channel_logger_test.go b/logging/loggers/channel_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..14b50fa0edc621761e7f9e78df2c304478a4a31e --- /dev/null +++ b/logging/loggers/channel_logger_test.go @@ -0,0 +1,33 @@ +package loggers + +import ( + "testing" + + "fmt" + + "github.com/stretchr/testify/assert" +) + +func TestChannelLogger(t *testing.T) { + cl := newChannelLogger() + + // Push a larger number of log messages than will fit into ring buffer + for i := 0; i < int(LoggingRingBufferCap)+10; i++ { + cl.Log("log line", i) + } + + // Observe that oldest 10 messages are overwritten (so first message is 10) + for i := 0; i < int(LoggingRingBufferCap); i++ { + ll := cl.WaitReadLogLine() + assert.Equal(t, 10+i, ll[1]) + } + + assert.Nil(t, cl.ReadLogLine(), "Since we have drained the buffer there "+ + "should be no more log lines.") +} + +func TestBlether(t *testing.T) { + var bs []byte + ext := append(bs) + fmt.Println(ext) +} diff --git a/logging/loggers/eris_format_logger.go b/logging/loggers/eris_format_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..20cdd8d653ad1ef09076c24e547d7f0f89e58297 --- /dev/null +++ b/logging/loggers/eris_format_logger.go @@ -0,0 +1,39 @@ +package loggers + +import ( + "fmt" + + "github.com/eris-ltd/eris-db/logging/structure" + + kitlog "github.com/go-kit/kit/log" +) + +// Logger that implements some formatting conventions for eris-db and eris-client +// This is intended for applying consistent value formatting before the final 'output' logger; +// we should avoid prematurely formatting values here if it is useful to let the output logger +// decide how it wants to display values. Ideal candidates for 'early' formatting here are types that +// we control and generic output loggers are unlikely to know about. +type erisFormatLogger struct { + logger kitlog.Logger +} + +var _ kitlog.Logger = &erisFormatLogger{} + +func (efl *erisFormatLogger) Log(keyvals ...interface{}) error { + return efl.logger.Log(structure.MapKeyValues(keyvals, erisFormatKeyValueMapper)...) +} + +func erisFormatKeyValueMapper(key, value interface{}) (interface{}, interface{}) { + switch key { + default: + switch v := value.(type) { + case []byte: + return key, fmt.Sprintf("%X", v) + } + } + return key, value +} + +func ErisFormatLogger(logger kitlog.Logger) *erisFormatLogger { + return &erisFormatLogger{logger: logger} +} diff --git a/logging/loggers/info_trace_logger.go b/logging/loggers/info_trace_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..6cbdcee6bd9f0aeb2d7965465835f304226eb42a --- /dev/null +++ b/logging/loggers/info_trace_logger.go @@ -0,0 +1,127 @@ +package loggers + +import ( + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" +) + +const ( + InfoChannelName = "Info" + TraceChannelName = "Trace" + + InfoLevelName = InfoChannelName + TraceLevelName = TraceChannelName +) + +type infoTraceLogger struct { + infoLogger *kitlog.Context + traceLogger *kitlog.Context +} + +// InfoTraceLogger maintains two independent concurrently-safe channels of +// logging. The idea behind the independence is that you can ignore one channel +// with no performance penalty. For more fine grained filtering or aggregation +// the Info and Trace loggers can be decorated loggers that perform arbitrary +// filtering/routing/aggregation on log messages. +type InfoTraceLogger interface { + // Send a log message to the default channel + kitlog.Logger + + // Send an log message to the Info channel, formed of a sequence of key value + // pairs. Info messages should be operationally interesting to a human who is + // monitoring the logs. But not necessarily a human who is trying to + // understand or debug the system. Any handled errors or warnings should be + // sent to the Info channel (where you may wish to tag them with a suitable + // key-value pair to categorise them as such). + Info(keyvals ...interface{}) error + + // Send an log message to the Trace channel, formed of a sequence of key-value + // pairs. Trace messages can be used for any state change in the system that + // may be of interest to a machine consumer or a human who is trying to debug + // the system or trying to understand the system in detail. If the messages + // are very point-like and contain little structure, consider using a metric + // instead. + Trace(keyvals ...interface{}) error + + // A logging context (see go-kit log's Context). Takes a sequence key values + // via With or WithPrefix and ensures future calls to log will have those + // contextual values appended to the call to an underlying logger. + // Values can be dynamic by passing an instance of the kitlog.Valuer interface + // This provides an interface version of the kitlog.Context struct to be used + // For implementations that wrap a kitlog.Context. In addition it makes no + // assumption about the name or signature of the logging method(s). + // See InfoTraceLogger + + // Establish a context by appending contextual key-values to any existing + // contextual values + With(keyvals ...interface{}) InfoTraceLogger + + // Establish a context by prepending contextual key-values to any existing + // contextual values + WithPrefix(keyvals ...interface{}) InfoTraceLogger +} + +// Interface assertions +var _ InfoTraceLogger = (*infoTraceLogger)(nil) +var _ kitlog.Logger = (InfoTraceLogger)(nil) + +func NewInfoTraceLogger(infoLogger, traceLogger kitlog.Logger) InfoTraceLogger { + // We will never halt the progress of a log emitter. If log output takes too + // long will start dropping log lines by using a ring buffer. + // We also guard against any concurrency bugs in underlying loggers by feeding + // them from a single channel + logger := kitlog.NewContext(NonBlockingLogger(VectorValuedLogger( + MultipleChannelLogger( + map[string]kitlog.Logger{ + InfoChannelName: infoLogger, + TraceChannelName: traceLogger, + })))) + return &infoTraceLogger{ + infoLogger: logger.With( + structure.ChannelKey, InfoChannelName, + structure.LevelKey, InfoLevelName, + ), + traceLogger: logger.With( + structure.ChannelKey, TraceChannelName, + structure.LevelKey, TraceLevelName, + ), + } +} + +func NewNoopInfoTraceLogger() InfoTraceLogger { + noopLogger := kitlog.NewNopLogger() + return NewInfoTraceLogger(noopLogger, noopLogger) +} + +func (l *infoTraceLogger) With(keyvals ...interface{}) InfoTraceLogger { + return &infoTraceLogger{ + infoLogger: l.infoLogger.With(keyvals...), + traceLogger: l.traceLogger.With(keyvals...), + } +} + +func (l *infoTraceLogger) WithPrefix(keyvals ...interface{}) InfoTraceLogger { + return &infoTraceLogger{ + infoLogger: l.infoLogger.WithPrefix(keyvals...), + traceLogger: l.traceLogger.WithPrefix(keyvals...), + } +} + +func (l *infoTraceLogger) Info(keyvals ...interface{}) error { + // We send Info and Trace log lines down the same pipe to keep them ordered + return l.infoLogger.Log(keyvals...) +} + +func (l *infoTraceLogger) Trace(keyvals ...interface{}) error { + return l.traceLogger.Log(keyvals...) +} + +// If logged to as a plain kitlog logger presume the message is for Trace +// This favours keeping Info reasonably quiet. Note that an InfoTraceLogger +// aware adapter can make its own choices, but we tend to thing of logs from +// dependencies as less interesting than logs generated by us or specifically +// routed by us. +func (l *infoTraceLogger) Log(keyvals ...interface{}) error { + l.Trace(keyvals...) + return nil +} diff --git a/logging/loggers/info_trace_logger_test.go b/logging/loggers/info_trace_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..505f2606badbeb178f9046982a6d130030225456 --- /dev/null +++ b/logging/loggers/info_trace_logger_test.go @@ -0,0 +1,14 @@ +package loggers + +import ( + "os" + "testing" + + kitlog "github.com/go-kit/kit/log" +) + +func TestLogger(t *testing.T) { + stderrLogger := kitlog.NewLogfmtLogger(os.Stderr) + logger := NewInfoTraceLogger(stderrLogger, stderrLogger) + logger.Trace("hello", "barry") +} diff --git a/logging/loggers/logging_test.go b/logging/loggers/logging_test.go new file mode 100644 index 0000000000000000000000000000000000000000..78166fc91ca74989abeb7e08daf793dce10595a2 --- /dev/null +++ b/logging/loggers/logging_test.go @@ -0,0 +1,21 @@ +package loggers + +import "errors" + +type testLogger struct { + logLines [][]interface{} + err error +} + +func newErrorLogger(errMessage string) *testLogger { + return &testLogger{err: errors.New(errMessage)} +} + +func newTestLogger() *testLogger { + return &testLogger{} +} + +func (tl *testLogger) Log(keyvals ...interface{}) error { + tl.logLines = append(tl.logLines, keyvals) + return tl.err +} diff --git a/logging/loggers/multiple_channel_logger.go b/logging/loggers/multiple_channel_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..47ee96065ae4d8b04a3130f5903bc192ea341aed --- /dev/null +++ b/logging/loggers/multiple_channel_logger.go @@ -0,0 +1,37 @@ +package loggers + +import ( + "fmt" + + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" +) + +// This represents a 'SELECT ONE' type logger. When logged to it will search +// for the ChannelKey field, look that up in its map and send the log line there +// Otherwise logging is a noop (but an error will be returned - which is optional) +type MultipleChannelLogger map[string]kitlog.Logger + +var _ kitlog.Logger = MultipleChannelLogger(nil) + +// Like go-kit log's Log method only logs a message to the specified channelName +// which must be a member of this MultipleChannelLogger +func (mcl MultipleChannelLogger) Log(keyvals ...interface{}) error { + channel := structure.Value(keyvals, structure.ChannelKey) + if channel == nil { + return fmt.Errorf("MultipleChannelLogger could not select channel because"+ + " '%s' was not set in log message", structure.ChannelKey) + } + channelName, ok := channel.(string) + if !ok { + return fmt.Errorf("MultipleChannelLogger could not select channel because"+ + " channel was set to non-string value %v", channel) + } + logger := mcl[channelName] + if logger == nil { + return fmt.Errorf("Could not log to channel '%s', since it is not "+ + "registered with this MultipleChannelLogger (the underlying logger may "+ + "have been nil when passed to NewMultipleChannelLogger)", channelName) + } + return logger.Log(keyvals...) +} diff --git a/logging/loggers/multiple_channel_logger_test.go b/logging/loggers/multiple_channel_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..db8f5013a13c636af96c2791d0ee5e00a38695fc --- /dev/null +++ b/logging/loggers/multiple_channel_logger_test.go @@ -0,0 +1,28 @@ +package loggers + +import ( + "runtime" + "testing" + "time" + + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" + "github.com/stretchr/testify/assert" +) + +func TestMultipleChannelLogger(t *testing.T) { + boringLogger, interestingLogger := newTestLogger(), newTestLogger() + mcl := kitlog.NewContext(MultipleChannelLogger(map[string]kitlog.Logger{ + "Boring": boringLogger, + "Interesting": interestingLogger, + })) + err := mcl.With("time", kitlog.Valuer(func() interface{} { return "aa" })). + Log(structure.ChannelKey, "Boring", "foo", "bar") + assert.NoError(t, err, "Should log without an error") + // Wait for channel to drain + time.Sleep(time.Second) + runtime.Gosched() + assert.Equal(t, []interface{}{"time", "aa", structure.ChannelKey, "Boring", + "foo", "bar"}, + boringLogger.logLines[0]) +} diff --git a/logging/loggers/multiple_output_logger.go b/logging/loggers/multiple_output_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..9f3cb5326aa1a98d0f3fcfad6462051965749064 --- /dev/null +++ b/logging/loggers/multiple_output_logger.go @@ -0,0 +1,50 @@ +package loggers + +import ( + "strings" + + kitlog "github.com/go-kit/kit/log" +) + +// This represents an 'AND' type logger. When logged to it will log to each of +// the loggers in the slice. +type MultipleOutputLogger []kitlog.Logger + +var _ kitlog.Logger = MultipleOutputLogger(nil) + +func (mol MultipleOutputLogger) Log(keyvals ...interface{}) error { + var errs []error + for _, logger := range mol { + err := logger.Log(keyvals...) + if err != nil { + errs = append(errs, err) + } + } + return combineErrors(errs) +} + +// Creates a logger that forks log messages to each of its outputLoggers +func NewMultipleOutputLogger(outputLoggers ...kitlog.Logger) kitlog.Logger { + return MultipleOutputLogger(outputLoggers) +} + +type multipleErrors []error + +func combineErrors(errs []error) error { + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + return multipleErrors(errs) + } +} + +func (errs multipleErrors) Error() string { + var errStrings []string + for _, err := range errs { + errStrings = append(errStrings, err.Error()) + } + return strings.Join(errStrings, ";") +} diff --git a/logging/loggers/multiple_output_logger_test.go b/logging/loggers/multiple_output_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..786c60064947d3475c60275f45140ce39edde199 --- /dev/null +++ b/logging/loggers/multiple_output_logger_test.go @@ -0,0 +1,18 @@ +package loggers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewMultipleOutputLogger(t *testing.T) { + a, b := newErrorLogger("error a"), newErrorLogger("error b") + mol := NewMultipleOutputLogger(a, b) + logLine := []interface{}{"msg", "hello"} + err := mol.Log(logLine...) + expected := [][]interface{}{logLine} + assert.Equal(t, expected, a.logLines) + assert.Equal(t, expected, b.logLines) + assert.IsType(t, multipleErrors{}, err) +} diff --git a/logging/loggers/vector_valued_logger.go b/logging/loggers/vector_valued_logger.go new file mode 100644 index 0000000000000000000000000000000000000000..b8963db13f61a9e02fdf5273010d9f4910d51009 --- /dev/null +++ b/logging/loggers/vector_valued_logger.go @@ -0,0 +1,21 @@ +package loggers + +import ( + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" +) + +// Treat duplicate key-values as consecutive entries in a vector-valued lookup +type vectorValuedLogger struct { + logger kitlog.Logger +} + +var _ kitlog.Logger = &vectorValuedLogger{} + +func (vvl *vectorValuedLogger) Log(keyvals ...interface{}) error { + return vvl.logger.Log(structure.Vectorise(keyvals)...) +} + +func VectorValuedLogger(logger kitlog.Logger) *vectorValuedLogger { + return &vectorValuedLogger{logger: logger} +} diff --git a/logging/loggers/vector_valued_logger_test.go b/logging/loggers/vector_valued_logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d6bc54ebdcfb76d0a7b3f20606828d458e6a5d13 --- /dev/null +++ b/logging/loggers/vector_valued_logger_test.go @@ -0,0 +1,17 @@ +package loggers + +import ( + "testing" + + . "github.com/eris-ltd/eris-db/util/slice" + "github.com/stretchr/testify/assert" +) + +func TestVectorValuedLogger(t *testing.T) { + logger := newTestLogger() + vvl := VectorValuedLogger(logger) + vvl.Log("foo", "bar", "seen", 1, "seen", 3, "seen", 2) + + assert.Equal(t, Slice("foo", "bar", "seen", Slice(1, 3, 2)), + logger.logLines[0]) +} diff --git a/logging/metadata.go b/logging/metadata.go new file mode 100644 index 0000000000000000000000000000000000000000..d645699902e9a6b9a334390a8824c774aac5a4e6 --- /dev/null +++ b/logging/metadata.go @@ -0,0 +1,33 @@ +package logging + +import ( + "time" + + "github.com/eris-ltd/eris-db/logging/loggers" + "github.com/eris-ltd/eris-db/logging/structure" + kitlog "github.com/go-kit/kit/log" + "github.com/go-stack/stack" +) + +const ( + // To get the Caller information correct on the log, we need to count the + // number of calls from a log call in the code to the time it hits a kitlog + // context: [log call site (5), Info/Trace (4), MultipleChannelLogger.Log (3), + // kitlog.Context.Log (2), kitlog.bindValues (1) (binding occurs), + // kitlog.Caller (0), stack.caller] + infoTraceLoggerCallDepth = 5 +) + +var defaultTimestampUTCValuer kitlog.Valuer = func() interface{} { + return time.Now() +} + +func WithMetadata(infoTraceLogger loggers.InfoTraceLogger) loggers.InfoTraceLogger { + return infoTraceLogger.With(structure.TimeKey, defaultTimestampUTCValuer, + structure.CallerKey, kitlog.Caller(infoTraceLoggerCallDepth), + "trace", TraceValuer()) +} + +func TraceValuer() kitlog.Valuer { + return func() interface{} { return stack.Trace() } +} diff --git a/logging/structure/structure.go b/logging/structure/structure.go new file mode 100644 index 0000000000000000000000000000000000000000..a104e8c9dad8cc5ec7dc67880557e5593f071111 --- /dev/null +++ b/logging/structure/structure.go @@ -0,0 +1,146 @@ +package structure + +import ( + "reflect" + + . "github.com/eris-ltd/eris-db/util/slice" +) + +const ( + // Log time (time.Time) + TimeKey = "time" + // Call site for log invocation (go-stack.Call) + CallerKey = "caller" + // Level name (string) + LevelKey = "level" + // Channel name in a vector channel logging context + ChannelKey = "channel" + // Log message (string) + MessageKey = "message" + // Captured logging source (like tendermint_log15, stdlib_log) + CapturedLoggingSourceKey = "captured_logging_source" + // Top-level component (choose one) name + ComponentKey = "component" + // Vector-valued scope + ScopeKey = "scope" + // Globally unique identifier persisting while a single instance (root process) + // of this program/service is running + RunId = "run_id" +) + +// Pull the specified values from a structured log line into a map. +// Assumes keys are single-valued. +// Returns a map of the key-values from the requested keys and +// the unmatched remainder keyvals as context as a slice of key-values. +func ValuesAndContext(keyvals []interface{}, + keys ...interface{}) (map[interface{}]interface{}, []interface{}) { + vals := make(map[interface{}]interface{}, len(keys)) + context := make([]interface{}, len(keyvals)) + copy(context, keyvals) + deletions := 0 + // We can't really do better than a linear scan of both lists here. N is small + // so screw the asymptotics. + // Guard against odd-length list + for i := 0; i < 2*(len(keyvals)/2); i += 2 { + for k := 0; k < len(keys); k++ { + if keyvals[i] == keys[k] { + // Pull the matching key-value pair into vals to return + vals[keys[k]] = keyvals[i+1] + // Delete the key once it's found + keys = DeleteAt(keys, k) + // And remove the key-value pair from context + context = Delete(context, i-deletions, 2) + // Keep a track of how much we've shrunk the context to offset next + // deletion + deletions += 2 + break + } + } + } + return vals, context +} + +// Stateful index that tracks the location of a possible vector value +type vectorValueindex struct { + // Location of the value belonging to a key in output slice + valueIndex int + // Whether or not the value is currently a vector + vector bool +} + +// 'Vectorises' values associated with repeated string keys member by collapsing many values into a single vector value. +// The result is a copy of keyvals where the first occurrence of each matching key and its first value are replaced by +// that key and all of its values in a single slice. +func Vectorise(keyvals []interface{}, vectorKeys ...string) []interface{} { + // We rely on working against a single backing array, so we use a capacity that is the maximum possible size of the + // slice after vectorising (in the case there are no duplicate keys and this is a no-op) + outputKeyvals := make([]interface{}, 0, len(keyvals)) + // Track the location and vector status of the values in the output + valueIndices := make(map[string]*vectorValueindex, len(vectorKeys)) + elided := 0 + for i := 0; i < 2*(len(keyvals)/2); i += 2 { + key := keyvals[i] + val := keyvals[i+1] + + // Only attempt to vectorise string keys + if k, ok := key.(string); ok { + if valueIndices[k] == nil { + // Record that this key has been seen once + valueIndices[k] = &vectorValueindex{ + valueIndex: i + 1 - elided, + } + // Copy the key-value to output with the single value + outputKeyvals = append(outputKeyvals, key, val) + } else { + // We have seen this key before + vi := valueIndices[k] + if !vi.vector { + // This must be the only second occurrence of the key so now vectorise the value + outputKeyvals[vi.valueIndex] = []interface{}{outputKeyvals[vi.valueIndex]} + vi.vector = true + } + // Grow the vector value + outputKeyvals[vi.valueIndex] = append(outputKeyvals[vi.valueIndex].([]interface{}), val) + // We are now running two more elements behind the input keyvals because we have absorbed this key-value pair + elided += 2 + } + } else { + // Just copy the key-value to the output for non-string keys + outputKeyvals = append(outputKeyvals, key, val) + } + } + return outputKeyvals +} + +// Return a single value corresponding to key in keyvals +func Value(keyvals []interface{}, key interface{}) interface{} { + for i := 0; i < 2*(len(keyvals)/2); i += 2 { + if keyvals[i] == key { + return keyvals[i+1] + } + } + return nil +} + +// Obtain a canonical key from a value. Useful for structured logging where the +// type of value alone may be sufficient to determine its key. Providing this +// function centralises any convention over type names +func KeyFromValue(val interface{}) string { + switch val.(type) { + case string: + return "text" + default: + return reflect.TypeOf(val).Name() + } +} + +// Maps key values pairs with a function (key, value) -> (new key, new value) +func MapKeyValues(keyvals []interface{}, fn func(interface{}, interface{}) (interface{}, interface{})) []interface{} { + mappedKeyvals := make([]interface{}, len(keyvals)) + for i := 0; i < 2*(len(keyvals)/2); i += 2 { + key := keyvals[i] + val := keyvals[i+1] + mappedKeyvals[i], mappedKeyvals[i+1] = fn(key, val) + } + return mappedKeyvals +} diff --git a/logging/structure/structure_test.go b/logging/structure/structure_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fde15ff4ae247a02166d91fe7850328c01417e33 --- /dev/null +++ b/logging/structure/structure_test.go @@ -0,0 +1,36 @@ +package structure + +import ( + "testing" + + . "github.com/eris-ltd/eris-db/util/slice" + "github.com/stretchr/testify/assert" +) + +func TestValuesAndContext(t *testing.T) { + keyvals := Slice("hello", 1, "dog", 2, "fish", 3, "fork", 5) + vals, ctx := ValuesAndContext(keyvals, "hello", "fish") + assert.Equal(t, map[interface{}]interface{}{"hello": 1, "fish": 3}, vals) + assert.Equal(t, Slice("dog", 2, "fork", 5), ctx) +} + +func TestVectorise(t *testing.T) { + kvs := Slice( + "scope", "lawnmower", + "hub", "budub", + "occupation", "fish brewer", + "scope", "hose pipe", + "flub", "dub", + "scope", "rake", + "flub", "brub", + ) + + kvsVector := Vectorise(kvs, "occupation", "scope") + assert.Equal(t, Slice( + "scope", Slice("lawnmower", "hose pipe", "rake"), + "hub", "budub", + "occupation", "fish brewer", + "flub", Slice("dub", "brub"), + ), + kvsVector) +} diff --git a/logging/terminal.go b/logging/terminal.go new file mode 100644 index 0000000000000000000000000000000000000000..e8eea7d8107484f1f5bd79a745f042e77fa49e8f --- /dev/null +++ b/logging/terminal.go @@ -0,0 +1,29 @@ +package logging + +import ( + "github.com/eris-ltd/eris-db/logging/structure" + "github.com/go-kit/kit/log/term" +) + +func Colors(keyvals ...interface{}) term.FgBgColor { + for i := 0; i < len(keyvals)-1; i += 2 { + if keyvals[i] != structure.LevelKey { + continue + } + switch keyvals[i+1] { + case "debug": + return term.FgBgColor{Fg: term.DarkGray} + case "info": + return term.FgBgColor{Fg: term.Gray} + case "warn": + return term.FgBgColor{Fg: term.Yellow} + case "error": + return term.FgBgColor{Fg: term.Red} + case "crit": + return term.FgBgColor{Fg: term.Gray, Bg: term.DarkRed} + default: + return term.FgBgColor{} + } + } + return term.FgBgColor{} +} diff --git a/manager/eris-mint/eris-mint.go b/manager/eris-mint/eris-mint.go index 2c08757c1a2b6539992a51bd1eb22c6043b1ea04..90f9af757bd1548845b1e304e7953a12289f6948 100644 --- a/manager/eris-mint/eris-mint.go +++ b/manager/eris-mint/eris-mint.go @@ -25,7 +25,8 @@ import ( wire "github.com/tendermint/go-wire" tmsp "github.com/tendermint/tmsp/types" - log "github.com/eris-ltd/eris-logger" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" sm "github.com/eris-ltd/eris-db/manager/eris-mint/state" manager_types "github.com/eris-ltd/eris-db/manager/types" @@ -47,7 +48,8 @@ type ErisMint struct { evc *tendermint_events.EventCache evsw *tendermint_events.EventSwitch - nTxs int // count txs in a block + nTxs int // count txs in a block + logger loggers.InfoTraceLogger } // NOTE [ben] Compiler check to ensure ErisMint successfully implements @@ -71,13 +73,14 @@ func (app *ErisMint) GetCheckCache() *sm.BlockCache { return app.checkCache } -func NewErisMint(s *sm.State, evsw *tendermint_events.EventSwitch) *ErisMint { +func NewErisMint(s *sm.State, evsw *tendermint_events.EventSwitch, logger loggers.InfoTraceLogger) *ErisMint { return &ErisMint{ state: s, cache: sm.NewBlockCache(s), checkCache: sm.NewBlockCache(s), evc: tendermint_events.NewEventCache(evsw), evsw: evsw, + logger: logging.WithScope(logger, "ErisMint"), } } @@ -145,18 +148,17 @@ func (app *ErisMint) Commit() (res tmsp.Result) { defer app.mtx.Unlock() app.state.LastBlockHeight += 1 - log.WithFields(log.Fields{ - "blockheight": app.state.LastBlockHeight, - }).Info("Commit block") + logging.InfoMsg(app.logger, "Committing block", + "last_block_height", app.state.LastBlockHeight) // sync the AppendTx cache app.cache.Sync() // Refresh the checkCache with the latest commited state - log.WithFields(log.Fields{ - "txs": app.nTxs, - }).Info("Reset checkCache") + logging.InfoMsg(app.logger, "Resetting checkCache", + "txs", app.nTxs) app.checkCache = sm.NewBlockCache(app.state) + app.nTxs = 0 // save state to disk diff --git a/manager/eris-mint/pipe.go b/manager/eris-mint/pipe.go index dc9b64875319a7fb50c6fb6c269778f2d9272d5a..014de66207a1c2b0f27de336016d15291f9c8de6 100644 --- a/manager/eris-mint/pipe.go +++ b/manager/eris-mint/pipe.go @@ -28,8 +28,6 @@ import ( tm_types "github.com/tendermint/tendermint/types" tmsp_types "github.com/tendermint/tmsp/types" - log "github.com/eris-ltd/eris-logger" - "github.com/eris-ltd/eris-db/account" blockchain_types "github.com/eris-ltd/eris-db/blockchain/types" imath "github.com/eris-ltd/eris-db/common/math/integral" @@ -38,9 +36,11 @@ import ( core_types "github.com/eris-ltd/eris-db/core/types" "github.com/eris-ltd/eris-db/definitions" edb_event "github.com/eris-ltd/eris-db/event" + genesis "github.com/eris-ltd/eris-db/genesis" + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" vm "github.com/eris-ltd/eris-db/manager/eris-mint/evm" "github.com/eris-ltd/eris-db/manager/eris-mint/state" - state_types "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" manager_types "github.com/eris-ltd/eris-db/manager/types" rpc_tm_types "github.com/eris-ltd/eris-db/rpc/tendermint/core/types" "github.com/eris-ltd/eris-db/txs" @@ -57,8 +57,9 @@ type erisMintPipe struct { namereg *namereg transactor *transactor // Genesis cache - genesisDoc *state_types.GenesisDoc + genesisDoc *genesis.GenesisDoc genesisState *state.State + logger loggers.InfoTraceLogger } // NOTE [ben] Compiler check to ensure erisMintPipe successfully implements @@ -70,7 +71,8 @@ var _ definitions.Pipe = (*erisMintPipe)(nil) var _ definitions.TendermintPipe = (*erisMintPipe)(nil) func NewErisMintPipe(moduleConfig *config.ModuleConfig, - eventSwitch *go_events.EventSwitch) (*erisMintPipe, error) { + eventSwitch *go_events.EventSwitch, + logger loggers.InfoTraceLogger) (*erisMintPipe, error) { startedState, genesisDoc, err := startState(moduleConfig.DataDir, moduleConfig.Config.GetString("db_backend"), moduleConfig.GenesisFile, @@ -78,17 +80,17 @@ func NewErisMintPipe(moduleConfig *config.ModuleConfig, if err != nil { return nil, fmt.Errorf("Failed to start state: %v", err) } + logger = logging.WithScope(logger, "ErisMintPipe") // assert ChainId matches genesis ChainId - log.WithFields(log.Fields{ - "chainId": startedState.ChainID, - "lastBlockHeight": startedState.LastBlockHeight, - "lastBlockHash": startedState.LastBlockHash, - }).Debug("Loaded state") + logging.InfoMsg(logger, "Loaded state", + "chainId", startedState.ChainID, + "lastBlockHeight", startedState.LastBlockHeight, + "lastBlockHash", startedState.LastBlockHash) // start the application - erisMint := NewErisMint(startedState, eventSwitch) + erisMint := NewErisMint(startedState, eventSwitch, logger) // initialise the components of the pipe - events := edb_event.NewEvents(eventSwitch) + events := edb_event.NewEvents(eventSwitch, logger) accounts := newAccounts(erisMint) namereg := newNameReg(erisMint) @@ -108,6 +110,7 @@ func NewErisMintPipe(moduleConfig *config.ModuleConfig, // authority - this is a sort of dependency injection pattern consensusEngine: nil, blockchain: nil, + logger: logger, } // NOTE: [Silas] @@ -139,7 +142,7 @@ func NewErisMintPipe(moduleConfig *config.ModuleConfig, // If no state can be loaded, the JSON genesis file will be loaded into the // state database as the zero state. func startState(dataDir, backend, genesisFile, chainId string) (*state.State, - *state_types.GenesisDoc, error) { + *genesis.GenesisDoc, error) { // avoid Tendermints PanicSanity and return a clean error if backend != db.DBBackendMemDB && backend != db.DBBackendLevelDB { @@ -149,18 +152,18 @@ func startState(dataDir, backend, genesisFile, chainId string) (*state.State, stateDB := db.NewDB("erismint", backend, dataDir) newState := state.LoadState(stateDB) - var genesisDoc *state_types.GenesisDoc + var genesisDoc *genesis.GenesisDoc if newState == nil { genesisDoc, newState = state.MakeGenesisStateFromFile(stateDB, genesisFile) newState.Save() buf, n, err := new(bytes.Buffer), new(int), new(error) wire.WriteJSON(genesisDoc, buf, n, err) - stateDB.Set(state_types.GenDocKey, buf.Bytes()) + stateDB.Set(genesis.GenDocKey, buf.Bytes()) if *err != nil { return nil, nil, fmt.Errorf("Unable to write genesisDoc to db: %v", err) } } else { - loadedGenesisDocBytes := stateDB.Get(state_types.GenDocKey) + loadedGenesisDocBytes := stateDB.Get(genesis.GenDocKey) err := new(error) wire.ReadJSONPtr(&genesisDoc, loadedGenesisDocBytes, err) if *err != nil { @@ -168,12 +171,8 @@ func startState(dataDir, backend, genesisFile, chainId string) (*state.State, } // assert loaded genesis doc has the same chainId as the provided chainId if genesisDoc.ChainID != chainId { - log.WithFields(log.Fields{ - "chainId from loaded genesis": genesisDoc.ChainID, - "chainId from configuration": chainId, - }).Warn("Conflicting chainIds") - // return nil, nil, fmt.Errorf("ChainId (%s) loaded from genesis document in existing database does not match configuration chainId (%s).", - // genesisDoc.ChainID, chainId) + return nil, nil, fmt.Errorf("ChainId (%s) loaded from genesis document in existing database does not match"+ + " configuration chainId (%s).", genesisDoc.ChainID, chainId) } } @@ -183,6 +182,10 @@ func startState(dataDir, backend, genesisFile, chainId string) (*state.State, //------------------------------------------------------------------------------ // Implement definitions.Pipe for erisMintPipe +func (pipe *erisMintPipe) Logger() loggers.InfoTraceLogger { + return pipe.logger +} + func (pipe *erisMintPipe) Accounts() definitions.Accounts { return pipe.accounts } @@ -253,11 +256,9 @@ func (pipe *erisMintPipe) Subscribe(event string, subscriptionId, err := edb_event.GenerateSubId() if err != nil { return nil, err + logging.InfoMsg(pipe.logger, "Subscribing to event", + "event", event, "subscriptionId", subscriptionId) } - - log.WithFields(log.Fields{"event": event, "subscriptionId": subscriptionId}). - Info("Subscribing to event") - pipe.consensusAndManagerEvents().Subscribe(subscriptionId, event, func(eventData txs.EventData) { result := rpc_tm_types.ErisDBResult(&rpc_tm_types.ResultEvent{event, @@ -272,8 +273,8 @@ func (pipe *erisMintPipe) Subscribe(event string, } func (pipe *erisMintPipe) Unsubscribe(subscriptionId string) (*rpc_tm_types.ResultUnsubscribe, error) { - log.WithFields(log.Fields{"subscriptionId": subscriptionId}). - Info("Unsubscribing from event") + logging.InfoMsg(pipe.logger, "Unsubscribing from event", + "subscriptionId", subscriptionId) pipe.consensusAndManagerEvents().Unsubscribe(subscriptionId) return &rpc_tm_types.ResultUnsubscribe{SubscriptionId: subscriptionId}, nil } @@ -351,13 +352,8 @@ func (pipe *erisMintPipe) Genesis() (*rpc_tm_types.ResultGenesis, error) { func (pipe *erisMintPipe) GetAccount(address []byte) (*rpc_tm_types.ResultGetAccount, error) { cache := pipe.erisMint.GetCheckCache() - // cache := mempoolReactor.Mempool.GetCache() account := cache.GetAccount(address) - if account == nil { - log.Warn("Nil Account") - return &rpc_tm_types.ResultGetAccount{nil}, nil - } - return &rpc_tm_types.ResultGetAccount{account}, nil + return &rpc_tm_types.ResultGetAccount{Account: account}, nil } func (pipe *erisMintPipe) ListAccounts() (*rpc_tm_types.ResultListAccounts, error) { @@ -589,10 +585,11 @@ func (pipe *erisMintPipe) BroadcastTxSync(tx txs.Tx) (*rpc_tm_types.ResultBroadc case tmsp_types.CodeType_InternalError: return resultBroadCastTx, fmt.Errorf(resultBroadCastTx.Log) default: - log.WithFields(log.Fields{ - "application": GetErisMintVersion().GetVersionString(), - "TMSP_code_type": responseCheckTx.Code, - }).Warn("Unknown error returned from Tendermint CheckTx on BroadcastTxSync") + logging.InfoMsg(pipe.logger, "Unknown error returned from Tendermint CheckTx on BroadcastTxSync", + "application", GetErisMintVersion().GetVersionString(), + "TMSP_code_type", responseCheckTx.Code, + "TMSP_log", responseCheckTx.Log, + ) return resultBroadCastTx, fmt.Errorf("Unknown error returned: " + responseCheckTx.Log) } } diff --git a/manager/eris-mint/state/execution.go b/manager/eris-mint/state/execution.go index ceefbbd8368a2491e1a743f87b823fd40558935e..9d9fd85bd3f85fc9d0cd2c50402b86a3376d98cf 100644 --- a/manager/eris-mint/state/execution.go +++ b/manager/eris-mint/state/execution.go @@ -203,6 +203,12 @@ func getOrMakeOutputs(state AccountGetter, accounts map[string]*acm.Account, out return accounts, nil } +// Since all ethereum accounts implicitly exist we sometimes lazily create an Account object to represent them +// only when needed. Sometimes we need to create an unknown Account knowing only its address (which is expected to +// be a deterministic hash of its associated public key) and not its public key. When we eventually receive a +// transaction acting on behalf of that account we will be given a public key that we can check matches the address. +// If it does then we will associate the public key with the stub account already registered in the system once and +// for all time. func checkInputPubKey(acc *acm.Account, in *txs.TxInput) error { if acc.PubKey == nil { if in.PubKey == nil { diff --git a/manager/eris-mint/state/genesis_test.go b/manager/eris-mint/state/genesis_test.go index 73e7f92f4d4ad04f135f1270f6173dc5cae61832..3bc2fdf005fc50686e698ef061a1340def5e8619 100644 --- a/manager/eris-mint/state/genesis_test.go +++ b/manager/eris-mint/state/genesis_test.go @@ -9,7 +9,7 @@ import ( "time" acm "github.com/eris-ltd/eris-db/account" - . "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" + genesis "github.com/eris-ltd/eris-db/genesis" ptypes "github.com/eris-ltd/eris-db/permission/types" . "github.com/tendermint/go-common" @@ -60,7 +60,7 @@ var g1 = fmt.Sprintf(` `, chain_id, addr1, amt1, accName, perms, setbit, roles1[0], roles1[1]) func TestGenesisReadable(t *testing.T) { - genDoc := GenesisDocFromJSON([]byte(g1)) + genDoc := genesis.GenesisDocFromJSON([]byte(g1)) if genDoc.ChainID != chain_id { t.Fatalf("Incorrect chain id. Got %d, expected %d\n", genDoc.ChainID, chain_id) } @@ -82,7 +82,7 @@ func TestGenesisReadable(t *testing.T) { } func TestGenesisMakeState(t *testing.T) { - genDoc := GenesisDocFromJSON([]byte(g1)) + genDoc := genesis.GenesisDocFromJSON([]byte(g1)) db := tdb.NewMemDB() st := MakeGenesisState(db, genDoc) acc := st.GetAccount(addr1) @@ -118,27 +118,27 @@ func RandAccount(randBalance bool, minBalance int64) (*acm.Account, *acm.PrivAcc return acc, privAccount } -func RandGenesisDoc(numAccounts int, randBalance bool, minBalance int64, numValidators int, randBonded bool, minBonded int64) (*GenesisDoc, []*acm.PrivAccount, []*types.PrivValidator) { - accounts := make([]GenesisAccount, numAccounts) +func RandGenesisDoc(numAccounts int, randBalance bool, minBalance int64, numValidators int, randBonded bool, minBonded int64) (*genesis.GenesisDoc, []*acm.PrivAccount, []*types.PrivValidator) { + accounts := make([]genesis.GenesisAccount, numAccounts) privAccounts := make([]*acm.PrivAccount, numAccounts) defaultPerms := ptypes.DefaultAccountPermissions for i := 0; i < numAccounts; i++ { account, privAccount := RandAccount(randBalance, minBalance) - accounts[i] = GenesisAccount{ + accounts[i] = genesis.GenesisAccount{ Address: account.Address, Amount: account.Balance, Permissions: &defaultPerms, // This will get copied into each state.Account. } privAccounts[i] = privAccount } - validators := make([]GenesisValidator, numValidators) + validators := make([]genesis.GenesisValidator, numValidators) privValidators := make([]*types.PrivValidator, numValidators) for i := 0; i < numValidators; i++ { valInfo, privVal := types.RandValidator(randBonded, minBonded) - validators[i] = GenesisValidator{ + validators[i] = genesis.GenesisValidator{ PubKey: valInfo.PubKey, Amount: valInfo.VotingPower, - UnbondTo: []BasicAccount{ + UnbondTo: []genesis.BasicAccount{ { Address: valInfo.PubKey.Address(), Amount: valInfo.VotingPower, @@ -148,7 +148,7 @@ func RandGenesisDoc(numAccounts int, randBalance bool, minBalance int64, numVali privValidators[i] = privVal } sort.Sort(types.PrivValidatorsByAddress(privValidators)) - return &GenesisDoc{ + return &genesis.GenesisDoc{ GenesisTime: time.Now(), ChainID: "tendermint_test", Accounts: accounts, diff --git a/manager/eris-mint/state/permissions_test.go b/manager/eris-mint/state/permissions_test.go index 523a5ffc8b2a75a13749c8ba924d4c6ded2e824a..8632313647c5ef8a0d03a1f6716c661f8b75274c 100644 --- a/manager/eris-mint/state/permissions_test.go +++ b/manager/eris-mint/state/permissions_test.go @@ -9,8 +9,8 @@ import ( "time" acm "github.com/eris-ltd/eris-db/account" + genesis "github.com/eris-ltd/eris-db/genesis" "github.com/eris-ltd/eris-db/manager/eris-mint/evm" - . "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" ptypes "github.com/eris-ltd/eris-db/permission/types" "github.com/eris-ltd/eris-db/txs" @@ -111,30 +111,30 @@ var ( PermsAllFalse = ptypes.ZeroAccountPermissions ) -func newBaseGenDoc(globalPerm, accountPerm ptypes.AccountPermissions) GenesisDoc { - genAccounts := []GenesisAccount{} +func newBaseGenDoc(globalPerm, accountPerm ptypes.AccountPermissions) genesis.GenesisDoc { + genAccounts := []genesis.GenesisAccount{} for _, u := range user[:5] { accountPermCopy := accountPerm // Create new instance for custom overridability. - genAccounts = append(genAccounts, GenesisAccount{ + genAccounts = append(genAccounts, genesis.GenesisAccount{ Address: u.Address, Amount: 1000000, Permissions: &accountPermCopy, }) } - return GenesisDoc{ + return genesis.GenesisDoc{ GenesisTime: time.Now(), ChainID: chainID, - Params: &GenesisParams{ + Params: &genesis.GenesisParams{ GlobalPermissions: &globalPerm, }, Accounts: genAccounts, - Validators: []GenesisValidator{ - GenesisValidator{ + Validators: []genesis.GenesisValidator{ + genesis.GenesisValidator{ PubKey: user[0].PubKey.(crypto.PubKeyEd25519), Amount: 10, - UnbondTo: []BasicAccount{ - BasicAccount{ + UnbondTo: []genesis.BasicAccount{ + genesis.BasicAccount{ Address: user[0].Address, }, }, diff --git a/manager/eris-mint/state/state.go b/manager/eris-mint/state/state.go index a36bc8ec2f607eaa505fcf8b1b7ad66c04a44033..082971acde2828e6534f04efff6cba38dd048db2 100644 --- a/manager/eris-mint/state/state.go +++ b/manager/eris-mint/state/state.go @@ -8,17 +8,17 @@ import ( "time" acm "github.com/eris-ltd/eris-db/account" - . "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" + genesis "github.com/eris-ltd/eris-db/genesis" ptypes "github.com/eris-ltd/eris-db/permission/types" "github.com/eris-ltd/eris-db/txs" - . "github.com/tendermint/go-common" dbm "github.com/tendermint/go-db" "github.com/tendermint/go-events" "github.com/tendermint/go-merkle" "github.com/tendermint/go-wire" core_types "github.com/eris-ltd/eris-db/core/types" + "github.com/eris-ltd/eris-db/util" "github.com/tendermint/tendermint/types" ) @@ -77,7 +77,7 @@ func LoadState(db dbm.DB) *State { s.nameReg.Load(nameRegHash) if *err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED - Exit(Fmt("Data has been corrupted or its spec has changed: %v\n", *err)) + util.Fatalf("Data has been corrupted or its spec has changed: %v\n", *err) } // TODO: ensure that buf is completely read. } @@ -101,7 +101,10 @@ func (s *State) Save() { //wire.WriteByteSlice(s.validatorInfos.Hash(), buf, n, err) wire.WriteByteSlice(s.nameReg.Hash(), buf, n, err) if *err != nil { - PanicCrisis(*err) + // TODO: [Silas] Do something better than this, really serialising ought to + // be error-free + util.Fatalf("Could not serialise state in order to save the state, "+ + "cannot continue, error: %s", *err) } s.DB.Set(stateKey, buf.Bytes()) } @@ -153,9 +156,9 @@ func (s *State) ComputeBlockStateHash(block *types.Block) error { } */ -func (s *State) GetGenesisDoc() (*GenesisDoc, error) { - var genesisDoc *GenesisDoc - loadedGenesisDocBytes := s.DB.Get(GenDocKey) +func (s *State) GetGenesisDoc() (*genesis.GenesisDoc, error) { + var genesisDoc *genesis.GenesisDoc + loadedGenesisDocBytes := s.DB.Get(genesis.GenDocKey) err := new(error) wire.ReadJSONPtr(&genesisDoc, loadedGenesisDocBytes, err) if *err != nil { @@ -398,18 +401,18 @@ func (s *State) SetFireable(evc events.Fireable) { //----------------------------------------------------------------------------- // Genesis -func MakeGenesisStateFromFile(db dbm.DB, genDocFile string) (*GenesisDoc, *State) { +func MakeGenesisStateFromFile(db dbm.DB, genDocFile string) (*genesis.GenesisDoc, *State) { jsonBlob, err := ioutil.ReadFile(genDocFile) if err != nil { - Exit(Fmt("Couldn't read GenesisDoc file: %v", err)) + util.Fatalf("Couldn't read GenesisDoc file: %v", err) } - genDoc := GenesisDocFromJSON(jsonBlob) + genDoc := genesis.GenesisDocFromJSON(jsonBlob) return genDoc, MakeGenesisState(db, genDoc) } -func MakeGenesisState(db dbm.DB, genDoc *GenesisDoc) *State { +func MakeGenesisState(db dbm.DB, genDoc *genesis.GenesisDoc) *State { if len(genDoc.Validators) == 0 { - Exit(Fmt("The genesis file has no validators")) + util.Fatalf("The genesis file has no validators") } if genDoc.GenesisTime.IsZero() { diff --git a/manager/manager.go b/manager/manager.go index 406648cd3ef36bc56857a55689c3b233870e633a..6f71aefa92dfe857cc4bf8d65b9c3cb22fae6eac 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -21,12 +21,13 @@ import ( events "github.com/tendermint/go-events" - log "github.com/eris-ltd/eris-logger" - config "github.com/eris-ltd/eris-db/config" definitions "github.com/eris-ltd/eris-db/definitions" erismint "github.com/eris-ltd/eris-db/manager/eris-mint" // types "github.com/eris-ltd/eris-db/manager/types" + + "github.com/eris-ltd/eris-db/logging" + "github.com/eris-ltd/eris-db/logging/loggers" ) // NewApplicationPipe returns an initialised Pipe interface @@ -35,18 +36,18 @@ import ( // of an application. It is feasible this will be insufficient to support // different types of applications later down the line. func NewApplicationPipe(moduleConfig *config.ModuleConfig, - evsw *events.EventSwitch, consensusMinorVersion string) (definitions.Pipe, + evsw *events.EventSwitch, logger loggers.InfoTraceLogger, + consensusMinorVersion string) (definitions.Pipe, error) { switch moduleConfig.Name { case "erismint": if err := erismint.AssertCompatibleConsensus(consensusMinorVersion); err != nil { return nil, err } - log.WithFields(log.Fields{ - "compatibleConsensus": consensusMinorVersion, - "erisMintVersion": erismint.GetErisMintVersion().GetVersionString(), - }).Debug("Loading ErisMint") - return erismint.NewErisMintPipe(moduleConfig, evsw) + logging.InfoMsg(logger, "Loading ErisMint", + "compatibleConsensus", consensusMinorVersion, + "erisMintVersion", erismint.GetErisMintVersion().GetVersionString()) + return erismint.NewErisMintPipe(moduleConfig, evsw, logger) } return nil, fmt.Errorf("Failed to return Pipe for %s", moduleConfig.Name) } diff --git a/rpc/tendermint/client/client_test.go b/rpc/tendermint/client/client_test.go index 2580508d828b88b457cc683f78a21c145aee5e9b..3fe36f9795c6fe4fd68b6935ba33bf73f690b2b4 100644 --- a/rpc/tendermint/client/client_test.go +++ b/rpc/tendermint/client/client_test.go @@ -1,8 +1,9 @@ package client import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestMapsAndValues(t *testing.T) { @@ -30,5 +31,4 @@ func TestMapsAndValues(t *testing.T) { _, _, err = mapAndValues("Foo", 4, 4, "Bar") assert.Error(t, err, "Should be an error to provide non-string keys") - } diff --git a/rpc/tendermint/core/types/responses.go b/rpc/tendermint/core/types/responses.go index c6494d919f3868507d562eeb3c0f0c30aa831f2e..98b72fbbd200d120c46c834a02d5cc237ff6b5a4 100644 --- a/rpc/tendermint/core/types/responses.go +++ b/rpc/tendermint/core/types/responses.go @@ -3,7 +3,7 @@ package types import ( acm "github.com/eris-ltd/eris-db/account" core_types "github.com/eris-ltd/eris-db/core/types" - stypes "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" + genesis "github.com/eris-ltd/eris-db/genesis" "github.com/eris-ltd/eris-db/txs" tendermint_types "github.com/tendermint/tendermint/types" @@ -126,7 +126,7 @@ type ResultGetName struct { } type ResultGenesis struct { - Genesis *stypes.GenesisDoc `json:"genesis"` + Genesis *genesis.GenesisDoc `json:"genesis"` } type ResultSignTx struct { diff --git a/rpc/tendermint/test/common.go b/rpc/tendermint/test/common.go index 76909182df98801eab3e08db5b5c4bbfd03617cf..a060833c5091b00156e855c70a276753115d925f 100644 --- a/rpc/tendermint/test/common.go +++ b/rpc/tendermint/test/common.go @@ -6,6 +6,7 @@ package test import ( "fmt" + vm "github.com/eris-ltd/eris-db/manager/eris-mint/evm" rpc_core "github.com/eris-ltd/eris-db/rpc/tendermint/core" "github.com/eris-ltd/eris-db/test/fixtures" ) @@ -17,6 +18,7 @@ func TestWrapper(runner func() int) int { defer ffs.RemoveAll() + vm.SetDebug(true) err := initGlobalVariables(ffs) if err != nil { diff --git a/rpc/tendermint/test/shared.go b/rpc/tendermint/test/shared.go index d211bd5d11c6fd7c078ce12665366f0a48d0a134..8c01b6a2f024d402cbea6dc1e7cea5f54dbf7f7e 100644 --- a/rpc/tendermint/test/shared.go +++ b/rpc/tendermint/test/shared.go @@ -21,6 +21,7 @@ import ( "path" + "github.com/eris-ltd/eris-db/logging/lifecycle" state_types "github.com/eris-ltd/eris-db/manager/eris-mint/state/types" "github.com/spf13/viper" tm_common "github.com/tendermint/go-common" @@ -83,7 +84,14 @@ func initGlobalVariables(ffs *fixtures.FileFixtures) error { // Set up priv_validator.json before we start tendermint (otherwise it will // create its own one. saveNewPriv() - testCore, err = core.NewCore("testCore", consensusConfig, managerConfig) + logger := lifecycle.NewStdErrLogger() + // To spill tendermint logs on the floor: + // lifecycle.CaptureTendermintLog15Output(loggers.NewNoopInfoTraceLogger()) + lifecycle.CaptureTendermintLog15Output(logger) + lifecycle.CaptureStdlibLogOutput(logger) + + testCore, err = core.NewCore("testCore", consensusConfig, managerConfig, + logger) if err != nil { return err } diff --git a/rpc/v0/json_service.go b/rpc/v0/json_service.go index 4c43f533432fdd1e2da6c1dc5f46233ede862069..ab4ce33f60b0be23622fc83b594d5005d48745f9 100644 --- a/rpc/v0/json_service.go +++ b/rpc/v0/json_service.go @@ -6,8 +6,6 @@ import ( "github.com/gin-gonic/gin" - log "github.com/eris-ltd/eris-logger" - definitions "github.com/eris-ltd/eris-db/definitions" event "github.com/eris-ltd/eris-db/event" rpc "github.com/eris-ltd/eris-db/rpc" @@ -125,10 +123,8 @@ func (this *ErisDbJsonService) writeError(msg, id string, code int, w http.Respo // Helper for writing responses. func (this *ErisDbJsonService) writeResponse(id string, result interface{}, w http.ResponseWriter) { - log.Debug("Result: %v\n", result) response := rpc.NewRPCResponse(id, result) err := this.codec.Encode(response, w) - log.Debug("Response: %v\n", response) if err != nil { this.writeError("Internal error: "+err.Error(), id, rpc.INTERNAL_ERROR, w) return diff --git a/rpc/v0/wsService.go b/rpc/v0/wsService.go index 35705ed39c179ded02944c931580f04e57d51d96..c7e89ff5b5950ea0900471ed971dd5f26c6c23f8 100644 --- a/rpc/v0/wsService.go +++ b/rpc/v0/wsService.go @@ -4,8 +4,6 @@ import ( "encoding/json" "fmt" - log "github.com/eris-ltd/eris-logger" - definitions "github.com/eris-ltd/eris-db/definitions" "github.com/eris-ltd/eris-db/event" rpc "github.com/eris-ltd/eris-db/rpc" @@ -36,7 +34,6 @@ func NewErisDbWsService(codec rpc.Codec, // Process a request. func (this *ErisDbWsService) Process(msg []byte, session *server.WSSession) { - log.Debug("REQUEST: %s\n", string(msg)) // Create new request object and unmarshal. req := &rpc.RPCRequest{} errU := json.Unmarshal(msg, req) @@ -84,7 +81,6 @@ func (this *ErisDbWsService) writeResponse(id string, result interface{}, session *server.WSSession) error { response := rpc.NewRPCResponse(id, result) bts, err := this.codec.EncodeBytes(response) - log.Debug("RESPONSE: %v\n", response) if err != nil { this.writeError("Internal error: "+err.Error(), id, rpc.INTERNAL_ERROR, session) return err diff --git a/test/mock/pipe.go b/test/mock/pipe.go index e279ad18253e2fc755f9948d8ec5108d89588a41..ecd245ec87e2c9934e360ae72869210e134ee820 100644 --- a/test/mock/pipe.go +++ b/test/mock/pipe.go @@ -14,6 +14,7 @@ import ( td "github.com/eris-ltd/eris-db/test/testdata/testdata" "github.com/eris-ltd/eris-db/txs" + "github.com/eris-ltd/eris-db/logging/loggers" "github.com/tendermint/go-crypto" "github.com/tendermint/go-p2p" mintTypes "github.com/tendermint/tendermint/types" @@ -29,24 +30,20 @@ type MockPipe struct { events event.EventEmitter namereg definitions.NameReg transactor definitions.Transactor + logger loggers.InfoTraceLogger } // Create a new mock tendermint pipe. func NewMockPipe(td *td.TestData) definitions.Pipe { - accounts := &accounts{td} - blockchain := &blockchain{td} - consensusEngine := &consensusEngine{td} - eventer := &eventer{td} - namereg := &namereg{td} - transactor := &transactor{td} return &MockPipe{ - td, - accounts, - blockchain, - consensusEngine, - eventer, - namereg, - transactor, + testData: td, + accounts: &accounts{td}, + blockchain: &blockchain{td}, + consensusEngine: &consensusEngine{td}, + events: &eventer{td}, + namereg: &namereg{td}, + transactor: &transactor{td}, + logger: loggers.NewNoopInfoTraceLogger(), } } @@ -75,6 +72,10 @@ func (pipe *MockPipe) Transactor() definitions.Transactor { return pipe.transactor } +func (pipe *MockPipe) Logger() loggers.InfoTraceLogger { + return pipe.logger +} + func (pipe *MockPipe) GetApplication() manager_types.Application { // TODO: [ben] mock application return nil diff --git a/txs/log.go b/txs/log.go deleted file mode 100644 index b967a58d0ef7d4701fc15f5ab3dfee1f046b15f5..0000000000000000000000000000000000000000 --- a/txs/log.go +++ /dev/null @@ -1,7 +0,0 @@ -package txs - -import ( - "github.com/tendermint/go-logger" -) - -var log = logger.New("module", "types") diff --git a/util/hell/README.md b/util/hell/README.md new file mode 100644 index 0000000000000000000000000000000000000000..715cb5522ca5a98143a45edf7e9ec49517b6778d --- /dev/null +++ b/util/hell/README.md @@ -0,0 +1,11 @@ +> Hell is other people's packages + +While we wait for working package management in go we need a way to make +maintaining the glide.lock by hand less painful. + +To interactively add a package run from the root: + +```bash +go run ./util/hell/cmd/hell/main.go get --interactive github.com/tendermint/tendermint +``` + diff --git a/util/hell/cmd/hell/main.go b/util/hell/cmd/hell/main.go new file mode 100644 index 0000000000000000000000000000000000000000..6736e496f29cb335239c8553117f13df355f588d --- /dev/null +++ b/util/hell/cmd/hell/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/eris-ltd/eris-db/util/hell" + + "github.com/Masterminds/glide/action" + "github.com/Masterminds/glide/cache" + "github.com/Masterminds/glide/cfg" + "github.com/Masterminds/glide/msg" + "github.com/Masterminds/glide/path" + "github.com/Masterminds/glide/repo" + "github.com/Masterminds/glide/util" + "github.com/spf13/cobra" +) + +func main() { + hellCmd := &cobra.Command{ + Use: "hell", + Short: "Hell makes the most of it being warm", + Long: "", + Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, + } + + // Lock merge command + var baseGlideLockFile, depGlideLockFile string + lockMergeCmd := &cobra.Command{ + Use: "lock-merge", + Short: "Merge glide.lock files together", + Long: "This command merges two glide.lock files into a single one by copying all dependencies " + + "from a base glide.lock and an override glide.lock to an output glide.lock with dependencies " + + "from override taking precedence over those from base.", + Run: func(cmd *cobra.Command, args []string) { + baseLockFile, err := cfg.ReadLockFile(baseGlideLockFile) + if err != nil { + fmt.Printf("Could not read file: %s\n", err) + os.Exit(1) + } + overrideLockFile, err := cfg.ReadLockFile(depGlideLockFile) + if err != nil { + fmt.Printf("Could not read file: %s\n", err) + os.Exit(1) + } + mergedLockFile, err := hell.MergeGlideLockFiles(baseLockFile, overrideLockFile) + if err != nil { + fmt.Printf("Could not merge lock files: %s\n", err) + os.Exit(1) + } + mergedBytes, err := mergedLockFile.Marshal() + if err != nil { + fmt.Printf("Could not marshal lock file: %s\n", err) + os.Exit(1) + } + os.Stdout.Write(mergedBytes) + }, + } + lockMergeCmd.PersistentFlags().StringVarP(&baseGlideLockFile, "base", "b", "", "base lock file") + lockMergeCmd.PersistentFlags().StringVarP(&depGlideLockFile, "override", "o", "", "override lock file") + + // Lock update + interactive := false + getTransitiveCmd := &cobra.Command{ + Use: "get", + Short: "gets a remote dependency to this project along with its transtive dependencies.", + Long: "Gets a remote dependency and its transitive dependencies by adding the remote " + + "depednency to this project's glide.yaml and merging the remote dependency's " + + "glide.lock into this project's glide.lock", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + msg.Die("%s requires a single argument of the remote dependency\n", cmd.Name()) + } + rootPackage, _ := util.NormalizeName(args[0]) + // Add dependency to glide + installer := repo.NewInstaller() + action.Get(args, installer, false, true, false, !interactive, false) + // Now hunt down the repo cache + dep := action.EnsureConfig().Imports.Get(rootPackage) + + key, err := cache.Key(dep.Remote()) + if err != nil { + msg.Die("%s requires a single argument of the remote dependency\n", cmd.Name()) + } + cacheDir := filepath.Join(cache.Location(), "src", key) + repos, err := dep.GetRepo(cacheDir) + if err != nil { + msg.Die("Could not get repo: %s", err) + } + version, err := repos.Version() + if err != nil { + msg.Die("Could not get version: %s", err) + } + dep.Pin = version + lockPath := filepath.Join(".", path.LockFile) + baseLockFile, err := cfg.ReadLockFile(lockPath) + if err != nil { + msg.Die("Could not read base lock file: %s", err) + } + overrideLockFile := &cfg.Lockfile{} + if path.HasLock(cacheDir) { + msg.Info("Found dependency lock file so merging into project lock file") + overrideLockFile, err = cfg.ReadLockFile(filepath.Join(cacheDir, path.LockFile)) + if err != nil { + msg.Die("Could not read dependency lock file: %s", err) + } + } + // Add the package to glide lock too! + overrideLockFile.Imports = append(overrideLockFile.Imports, cfg.LockFromDependency(dep)) + + mergedLockFile, err := hell.MergeGlideLockFiles(baseLockFile, overrideLockFile) + fmt.Printf("%#v\n", mergedLockFile.Imports) + if err != nil { + msg.Die("Could not merge lock files: %s\n", err) + } + err = mergedLockFile.WriteFile(lockPath) + if err != nil { + msg.Die("Could not write merged lock file: %s", err) + } + + action.Install(installer, false) + }, + } + + getTransitiveCmd.PersistentFlags().BoolVarP(&interactive, "interactive", "i", false, + "set dependency verion interactively") + + hellCmd.AddCommand(lockMergeCmd) + hellCmd.AddCommand(getTransitiveCmd) + lockMergeCmd.Execute() +} diff --git a/util/hell/merge.go b/util/hell/merge.go new file mode 100644 index 0000000000000000000000000000000000000000..915001f398d7fa229df015f4d03e13f2a87ade5a --- /dev/null +++ b/util/hell/merge.go @@ -0,0 +1,87 @@ +package hell + +import ( + "crypto/sha256" + "fmt" + "sort" + + "github.com/Masterminds/glide/cfg" +) + +// Merges two glide lock files together, letting dependencies from 'base' be overwritten +// by those from 'override'. Returns the resultant glide lock file bytes +func MergeGlideLockFiles(baseLockFile, overrideLockFile *cfg.Lockfile) (*cfg.Lockfile, error) { + imports := make(map[string]*cfg.Lock, len(baseLockFile.Imports)) + devImports := make(map[string]*cfg.Lock, len(baseLockFile.DevImports)) + // Copy the base dependencies into a map + for _, lock := range baseLockFile.Imports { + imports[lock.Name] = lock + } + for _, lock := range baseLockFile.DevImports { + devImports[lock.Name] = lock + } + // Override base dependencies and add any extra ones + for _, lock := range overrideLockFile.Imports { + imports[lock.Name] = mergeLocks(imports[lock.Name], lock) + } + for _, lock := range overrideLockFile.DevImports { + devImports[lock.Name] = mergeLocks(imports[lock.Name], lock) + } + + deps := make([]*cfg.Dependency, 0, len(imports)) + devDeps := make([]*cfg.Dependency, 0, len(devImports)) + + // Flatten to Dependencies + for _, lock := range imports { + deps = append(deps, pinnedDependencyFromLock(lock)) + } + + for _, lock := range devImports { + devDeps = append(devDeps, pinnedDependencyFromLock(lock)) + } + + hasher := sha256.New() + hasher.Write(([]byte)(baseLockFile.Hash)) + hasher.Write(([]byte)(overrideLockFile.Hash)) + + return cfg.NewLockfile(deps, devDeps, fmt.Sprintf("%x", hasher.Sum(nil))) +} + +func mergeLocks(baseLock, overrideLock *cfg.Lock) *cfg.Lock { + lock := overrideLock.Clone() + if baseLock == nil { + return lock + } + + // Merge and dedupe subpackages + subpackages := make([]string, 0, len(lock.Subpackages)+len(baseLock.Subpackages)) + for _, sp := range lock.Subpackages { + subpackages = append(subpackages, sp) + } + for _, sp := range baseLock.Subpackages { + subpackages = append(subpackages, sp) + } + + sort.Stable(sort.StringSlice(subpackages)) + + dedupeSubpackages := make([]string, 0, len(subpackages)) + + lastSp := "" + elided := 0 + for _, sp := range subpackages { + if lastSp == sp { + elided++ + } else { + dedupeSubpackages = append(dedupeSubpackages, sp) + lastSp = sp + } + } + lock.Subpackages = dedupeSubpackages[:len(dedupeSubpackages)-elided] + return lock +} + +func pinnedDependencyFromLock(lock *cfg.Lock) *cfg.Dependency { + dep := cfg.DependencyFromLock(lock) + dep.Pin = lock.Version + return dep +} diff --git a/util/hell/merge_test.go b/util/hell/merge_test.go new file mode 100644 index 0000000000000000000000000000000000000000..df44045a7d6ab111c441bdaa8d6d96e6fcd58517 --- /dev/null +++ b/util/hell/merge_test.go @@ -0,0 +1,71 @@ +package hell + +import ( + "strings" + "testing" + + "github.com/Masterminds/glide/cfg" + "github.com/stretchr/testify/assert" +) + +const baseLockYml = `imports: +- name: github.com/gogo/protobuf + version: 82d16f734d6d871204a3feb1a73cb220cc92574c +- name: github.com/tendermint/tendermint + version: aaea0c5d2e3ecfbf29f2608f9d43649ec7f07f50 + subpackages: + - node + - proxy + - types + - version + - consensus + - rpc/core/types + - blockchain + - mempool + - rpc/core + - state +` +const overrideLockYml = `imports: +- name: github.com/tendermint/tendermint + version: 764091dfbb035f1b28da4b067526e04c6a849966 + subpackages: + - benchmarks + - proxy + - types + - version +` +const expectedLockYml = `imports: +- name: github.com/gogo/protobuf + version: 82d16f734d6d871204a3feb1a73cb220cc92574c +- name: github.com/tendermint/tendermint + version: 764091dfbb035f1b28da4b067526e04c6a849966 + subpackages: + - benchmarks + - blockchain + - consensus + - mempool + - node + - proxy + - rpc/core + - rpc/core/types +testImports: [] +` + +func TestMergeGlideLockFiles(t *testing.T) { + baseLockFile, err := cfg.LockfileFromYaml(([]byte)(baseLockYml)) + assert.NoError(t, err, "Lockfile should parse") + + overrideLockFile, err := cfg.LockfileFromYaml(([]byte)(overrideLockYml)) + assert.NoError(t, err, "Lockfile should parse") + + mergedLockFile, err := MergeGlideLockFiles(baseLockFile, overrideLockFile) + assert.NoError(t, err, "Lockfiles should merge") + + mergedYmlBytes, err := mergedLockFile.Marshal() + assert.NoError(t, err, "Lockfile should marshal") + + ymlLines := strings.Split(string(mergedYmlBytes), "\n") + // Drop the updated and hash lines + actualYml := strings.Join(ymlLines[2:], "\n") + assert.Equal(t, expectedLockYml, actualYml) +} diff --git a/util/os.go b/util/os.go new file mode 100644 index 0000000000000000000000000000000000000000..cc682918d88264505a5b48ab0d8819ff612d5395 --- /dev/null +++ b/util/os.go @@ -0,0 +1,12 @@ +package util + +import ( + "fmt" + "os" +) + +// Prints an error message to stderr and exits with status code 1 +func Fatalf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) + os.Exit(1) +} diff --git a/util/slice/slice.go b/util/slice/slice.go new file mode 100644 index 0000000000000000000000000000000000000000..2c51efd21f89c3478a185d623e54dcc9d4ea3064 --- /dev/null +++ b/util/slice/slice.go @@ -0,0 +1,91 @@ +package slice + +func Slice(elements ...interface{}) []interface{} { + return elements +} + +func EmptySlice() []interface{} { + return []interface{}{} +} + +// Like append but on the interface{} type and always to a fresh backing array +// so can be used safely with slices over arrays you did not create. +func CopyAppend(slice []interface{}, elements ...interface{}) []interface{} { + sliceLength := len(slice) + newSlice := make([]interface{}, sliceLength+len(elements)) + for i, e := range slice { + newSlice[i] = e + } + for i, e := range elements { + newSlice[sliceLength+i] = e + } + return newSlice +} + +// Prepend elements to slice in the order they appear +func CopyPrepend(slice []interface{}, elements ...interface{}) []interface{} { + elementsLength := len(elements) + newSlice := make([]interface{}, len(slice)+elementsLength) + for i, e := range elements { + newSlice[i] = e + } + for i, e := range slice { + newSlice[elementsLength+i] = e + } + return newSlice +} + +// Concatenate slices into a single slice +func Concat(slices ...[]interface{}) []interface{} { + offset := 0 + for _, slice := range slices { + offset += len(slice) + } + concat := make([]interface{}, offset) + offset = 0 + for _, slice := range slices { + for i, e := range slice { + concat[offset+i] = e + } + offset += len(slice) + } + return concat +} + +// Deletes n elements starting with the ith from a slice by splicing. +// Beware uses append so the underlying backing array will be modified! +func Delete(slice []interface{}, i int, n int) []interface{} { + return append(slice[:i], slice[i+n:]...) +} + +// Delete an element at a specific index and return the contracted list +func DeleteAt(slice []interface{}, i int) []interface{} { + return Delete(slice, i, 1) +} + +// Flatten a slice by a list by splicing any elements of the list that are +// themselves lists into the slice elements to the list in place of slice itself +func Flatten(slice []interface{}) []interface{} { + return DeepFlatten(slice, 1) +} + +// Recursively flattens a list by splicing any sub-lists into their parent until +// depth is reached. If a negative number is passed for depth then it continues +// until no elements of the returned list are lists +func DeepFlatten(slice []interface{}, depth int) []interface{} { + if depth == 0 { + return slice + } + returnSlice := []interface{}{} + + for _, element := range slice { + if s, ok := element.([]interface{}); ok { + returnSlice = append(returnSlice, DeepFlatten(s, depth-1)...) + } else { + returnSlice = append(returnSlice, element) + } + + } + + return returnSlice +} diff --git a/util/slice/slice_test.go b/util/slice/slice_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b3d1c9b6a530b1fe050427b3ba41ae8f9f23c388 --- /dev/null +++ b/util/slice/slice_test.go @@ -0,0 +1,43 @@ +package slice + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCopyAppend(t *testing.T) { + assert.Equal(t, Slice(1, "two", "three", 4), + CopyAppend(Slice(1, "two"), "three", 4)) + assert.Equal(t, EmptySlice(), CopyAppend(nil)) + assert.Equal(t, Slice(1), CopyAppend(nil, 1)) + assert.Equal(t, Slice(1), CopyAppend(Slice(1))) +} + +func TestCopyPrepend(t *testing.T) { + assert.Equal(t, Slice("three", 4, 1, "two"), + CopyPrepend(Slice(1, "two"), "three", 4)) + assert.Equal(t, EmptySlice(), CopyPrepend(nil)) + assert.Equal(t, Slice(1), CopyPrepend(nil, 1)) + assert.Equal(t, Slice(1), CopyPrepend(Slice(1))) +} + +func TestConcat(t *testing.T) { + assert.Equal(t, Slice(1, 2, 3, 4, 5), Concat(Slice(1, 2, 3, 4, 5))) + assert.Equal(t, Slice(1, 2, 3, 4, 5), Concat(Slice(1, 2, 3), Slice(4, 5))) + assert.Equal(t, Slice(1, 2, 3, 4, 5), Concat(Slice(1), Slice(2, 3), Slice(4, 5))) + assert.Equal(t, EmptySlice(), Concat(nil)) + assert.Equal(t, Slice(1), Concat(nil, Slice(), Slice(1))) + assert.Equal(t, Slice(1), Concat(Slice(1), Slice(), nil)) +} + +func TestDelete(t *testing.T) { + assert.Equal(t, Slice(1, 2, 4, 5), Delete(Slice(1, 2, 3, 4, 5), 2, 1)) +} + +func TestDeepFlatten(t *testing.T) { + assert.Equal(t, Flatten(Slice(Slice(1, 2), 3, 4)), Slice(1, 2, 3, 4)) + nestedSlice := Slice(Slice(1, Slice(Slice(2))), Slice(3, 4)) + assert.Equal(t, DeepFlatten(nestedSlice, -1), Slice(1, 2, 3, 4)) + assert.Equal(t, DeepFlatten(nestedSlice, 2), Slice(1, Slice(2), 3, 4)) +}