diff --git a/CHANGELOG.md b/CHANGELOG.md index b1eec2f8..b7b54bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,7 +144,7 @@ IMPROVEMENTS: - Less verbose logging - Better test coverage (37% -> 49%) - Canonical SignBytes for signable types -- Write-Ahead Log for Mempool and Consensus via go-autofile +- Write-Ahead Log for Mempool and Consensus via tmlibs/autofile - Better in-process testing for the consensus reactor and byzantine faults - Better crash/restart testing for individual nodes at preset failure points, and of networks at arbitrary points - Better abstraction over timeout mechanics diff --git a/README.md b/README.md index 24b3aef2..0d7b85bb 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Yay open source! Please see our [contributing guidelines](https://tendermint.com * [ABCI](http://github.com/tendermint/abci) * [Mintnet](http://github.com/tendermint/mintnet) * [Go-Wire](http://github.com/tendermint/go-wire) -* [Go-P2P](http://github.com/tendermint/go-p2p) +* [Go-P2P](http://github.com/tendermint/tendermint/p2p) * [Go-Merkle](http://github.com/tendermint/go-merkle) ### Applications diff --git a/benchmarks/codec_test.go b/benchmarks/codec_test.go index 35dc591e..205aec40 100644 --- a/benchmarks/codec_test.go +++ b/benchmarks/codec_test.go @@ -4,18 +4,18 @@ import ( "testing" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-p2p" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/go-wire" proto "github.com/tendermint/tendermint/benchmarks/proto" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" ) func BenchmarkEncodeStatusWire(b *testing.B) { b.StopTimer() - pubKey := crypto.GenPrivKeyEd25519().PubKey().(crypto.PubKeyEd25519) + pubKey := crypto.GenPrivKeyEd25519().PubKey() status := &ctypes.ResultStatus{ NodeInfo: &p2p.NodeInfo{ - PubKey: pubKey, + PubKey: pubKey.Unwrap().(crypto.PubKeyEd25519), Moniker: "SOMENAME", Network: "SOMENAME", RemoteAddr: "SOMEADDR", @@ -40,7 +40,7 @@ func BenchmarkEncodeStatusWire(b *testing.B) { func BenchmarkEncodeNodeInfoWire(b *testing.B) { b.StopTimer() - pubKey := crypto.GenPrivKeyEd25519().PubKey().(crypto.PubKeyEd25519) + pubKey := crypto.GenPrivKeyEd25519().PubKey().Unwrap().(crypto.PubKeyEd25519) nodeInfo := &p2p.NodeInfo{ PubKey: pubKey, Moniker: "SOMENAME", @@ -61,7 +61,7 @@ func BenchmarkEncodeNodeInfoWire(b *testing.B) { func BenchmarkEncodeNodeInfoBinary(b *testing.B) { b.StopTimer() - pubKey := crypto.GenPrivKeyEd25519().PubKey().(crypto.PubKeyEd25519) + pubKey := crypto.GenPrivKeyEd25519().PubKey().Unwrap().(crypto.PubKeyEd25519) nodeInfo := &p2p.NodeInfo{ PubKey: pubKey, Moniker: "SOMENAME", @@ -83,7 +83,7 @@ func BenchmarkEncodeNodeInfoBinary(b *testing.B) { func BenchmarkEncodeNodeInfoProto(b *testing.B) { b.StopTimer() - pubKey := crypto.GenPrivKeyEd25519().PubKey().(crypto.PubKeyEd25519) + pubKey := crypto.GenPrivKeyEd25519().PubKey().Unwrap().(crypto.PubKeyEd25519) pubKey2 := &proto.PubKey{Ed25519: &proto.PubKeyEd25519{Bytes: pubKey[:]}} nodeInfo := &proto.NodeInfo{ PubKey: pubKey2, diff --git a/benchmarks/map_test.go b/benchmarks/map_test.go index ee538e0f..80edaff7 100644 --- a/benchmarks/map_test.go +++ b/benchmarks/map_test.go @@ -1,7 +1,7 @@ package benchmarks import ( - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "testing" ) diff --git a/benchmarks/os_test.go b/benchmarks/os_test.go index 49a160cd..2c4611c8 100644 --- a/benchmarks/os_test.go +++ b/benchmarks/os_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) func BenchmarkFileWrite(b *testing.B) { diff --git a/benchmarks/simu/counter.go b/benchmarks/simu/counter.go index 36d1e35d..f384d357 100644 --- a/benchmarks/simu/counter.go +++ b/benchmarks/simu/counter.go @@ -7,11 +7,11 @@ import ( "fmt" "github.com/gorilla/websocket" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-rpc/client" - "github.com/tendermint/go-rpc/types" + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/types" "github.com/tendermint/go-wire" - _ "github.com/tendermint/tendermint/rpc/core/types" // Register RPCResponse > Result types + _ "github.com/tendermint/tendermint/rpc/tendermint/core/types" // Register RPCResponse > Result types ) func main() { diff --git a/blockchain/log.go b/blockchain/log.go index 29dc03f6..9ffee729 100644 --- a/blockchain/log.go +++ b/blockchain/log.go @@ -1,7 +1,7 @@ package blockchain import ( - "github.com/tendermint/go-logger" + "github.com/tendermint/tmlibs/logger" ) var log = logger.New("module", "blockchain") diff --git a/blockchain/pool.go b/blockchain/pool.go index 0deacd26..6422ef2d 100644 --- a/blockchain/pool.go +++ b/blockchain/pool.go @@ -5,8 +5,8 @@ import ( "sync" "time" - . "github.com/tendermint/go-common" - flow "github.com/tendermint/go-flowrate/flowrate" + . "github.com/tendermint/tmlibs/common" + flow "github.com/tendermint/tmlibs/flowrate" "github.com/tendermint/tendermint/types" ) diff --git a/blockchain/pool_test.go b/blockchain/pool_test.go index 220bc5ce..a2eb83a3 100644 --- a/blockchain/pool_test.go +++ b/blockchain/pool_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/types" ) diff --git a/blockchain/reactor.go b/blockchain/reactor.go index f88bccc3..1ea5650e 100644 --- a/blockchain/reactor.go +++ b/blockchain/reactor.go @@ -6,13 +6,14 @@ import ( "reflect" "time" - cmn "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" - "github.com/tendermint/go-p2p" + "github.com/spf13/viper" + "github.com/tendermint/go-wire" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) const ( @@ -43,7 +44,7 @@ type consensusReactor interface { type BlockchainReactor struct { p2p.BaseReactor - config cfg.Config + config *viper.Viper state *sm.State proxyAppConn proxy.AppConnConsensus // same as consensus.proxyAppConn store *BlockStore @@ -57,7 +58,7 @@ type BlockchainReactor struct { } // NewBlockchainReactor returns new reactor instance. -func NewBlockchainReactor(config cfg.Config, state *sm.State, proxyAppConn proxy.AppConnConsensus, store *BlockStore, fastSync bool) *BlockchainReactor { +func NewBlockchainReactor(config *viper.Viper, state *sm.State, proxyAppConn proxy.AppConnConsensus, store *BlockStore, fastSync bool) *BlockchainReactor { if state.LastBlockHeight == store.Height()-1 { store.height-- // XXX HACK, make this better } diff --git a/blockchain/store.go b/blockchain/store.go index ac7cfdaf..a96aa0fb 100644 --- a/blockchain/store.go +++ b/blockchain/store.go @@ -7,8 +7,8 @@ import ( "io" "sync" - . "github.com/tendermint/go-common" - dbm "github.com/tendermint/go-db" + . "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/go-wire" "github.com/tendermint/tendermint/types" ) diff --git a/cmd/tendermint/commands/init.go b/cmd/tendermint/commands/init.go index 366ca4e8..75bc2bf9 100644 --- a/cmd/tendermint/commands/init.go +++ b/cmd/tendermint/commands/init.go @@ -5,7 +5,7 @@ import ( "github.com/spf13/cobra" - cmn "github.com/tendermint/go-common" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/types" ) diff --git a/cmd/tendermint/commands/probe_upnp.go b/cmd/tendermint/commands/probe_upnp.go index bff433bc..330dcf17 100644 --- a/cmd/tendermint/commands/probe_upnp.go +++ b/cmd/tendermint/commands/probe_upnp.go @@ -6,20 +6,20 @@ import ( "github.com/spf13/cobra" - "github.com/tendermint/go-p2p/upnp" + "github.com/tendermint/tendermint/p2p/upnp" ) var probeUpnpCmd = &cobra.Command{ Use: "probe_upnp", Short: "Test UPnP functionality", - Run: probeUpnp, + RunE: probeUpnp, } func init() { RootCmd.AddCommand(probeUpnpCmd) } -func probeUpnp(cmd *cobra.Command, args []string) { +func probeUpnp(cmd *cobra.Command, args []string) error { capabilities, err := upnp.Probe() if err != nil { @@ -28,9 +28,9 @@ func probeUpnp(cmd *cobra.Command, args []string) { fmt.Println("Probe success!") jsonBytes, err := json.Marshal(capabilities) if err != nil { - panic(err) + return err } fmt.Println(string(jsonBytes)) } - + return nil } diff --git a/cmd/tendermint/commands/reset_priv_validator.go b/cmd/tendermint/commands/reset_priv_validator.go index 4b7d2df4..a611935b 100644 --- a/cmd/tendermint/commands/reset_priv_validator.go +++ b/cmd/tendermint/commands/reset_priv_validator.go @@ -4,8 +4,8 @@ import ( "os" "github.com/spf13/cobra" + "github.com/spf13/viper" - cfg "github.com/tendermint/go-config" "github.com/tendermint/log15" "github.com/tendermint/tendermint/types" ) @@ -36,16 +36,19 @@ func resetAll(cmd *cobra.Command, args []string) { // XXX: this is totally unsafe. // it's only suitable for testnets. func resetPrivValidator(cmd *cobra.Command, args []string) { - ResetPrivValidator(config, log) + resetPrivValidatorLocal(config, log) } // Exported so other CLI tools can use it -func ResetAll(c cfg.Config, l log15.Logger) { - ResetPrivValidator(c, l) - os.RemoveAll(c.GetString("db_dir")) +func ResetAll(c *viper.Viper, l log15.Logger) { + resetPrivValidatorLocal(c, l) + dataDir := c.GetString("db_dir") + os.RemoveAll(dataDir) + l.Notice("Removed all data", "dir", dataDir) } -func ResetPrivValidator(c cfg.Config, l log15.Logger) { +func resetPrivValidatorLocal(c *viper.Viper, l log15.Logger) { + // Get PrivValidator var privValidator *types.PrivValidator privValidatorFile := c.GetString("priv_validator_file") diff --git a/cmd/tendermint/commands/root.go b/cmd/tendermint/commands/root.go index 0cbaa289..de5b71bd 100644 --- a/cmd/tendermint/commands/root.go +++ b/cmd/tendermint/commands/root.go @@ -3,8 +3,8 @@ package commands import ( "github.com/spf13/cobra" - "github.com/tendermint/go-logger" tmcfg "github.com/tendermint/tendermint/config/tendermint" + "github.com/tendermint/tmlibs/logger" ) var ( diff --git a/cmd/tendermint/commands/run_node.go b/cmd/tendermint/commands/run_node.go index a04b52d0..b2b23af5 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -1,21 +1,22 @@ package commands import ( + "fmt" "io/ioutil" "time" "github.com/spf13/cobra" - . "github.com/tendermint/go-common" "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) var runNodeCmd = &cobra.Command{ Use: "node", Short: "Run the tendermint node", PreRun: setConfigFlags, - Run: runNode, + RunE: runNode, } //flags @@ -82,30 +83,30 @@ func setConfigFlags(cmd *cobra.Command, args []string) { // should import github.com/tendermint/tendermint/node and implement // their own run_node to call node.NewNode (instead of node.NewNodeDefault) // with their custom priv validator and/or custom proxy.ClientCreator -func runNode(cmd *cobra.Command, args []string) { +func runNode(cmd *cobra.Command, args []string) error { // Wait until the genesis doc becomes available // This is for Mintnet compatibility. // TODO: If Mintnet gets deprecated or genesis_file is // always available, remove. genDocFile := config.GetString("genesis_file") - if !FileExists(genDocFile) { - log.Notice(Fmt("Waiting for genesis file %v...", genDocFile)) + if !cmn.FileExists(genDocFile) { + log.Notice(cmn.Fmt("Waiting for genesis file %v...", genDocFile)) for { time.Sleep(time.Second) - if !FileExists(genDocFile) { + if !cmn.FileExists(genDocFile) { continue } jsonBlob, err := ioutil.ReadFile(genDocFile) if err != nil { - Exit(Fmt("Couldn't read GenesisDoc file: %v", err)) + return fmt.Errorf("Couldn't read GenesisDoc file: %v", err) } genDoc, err := types.GenesisDocFromJSON(jsonBlob) if err != nil { - Exit(Fmt("Error reading GenesisDoc: %v", err)) + return fmt.Errorf("Error reading GenesisDoc: %v", err) } if genDoc.ChainID == "" { - Exit(Fmt("Genesis doc %v must include non-empty chain_id", genDocFile)) + return fmt.Errorf("Genesis doc %v must include non-empty chain_id", genDocFile) } config.Set("chain_id", genDoc.ChainID) } @@ -114,11 +115,13 @@ func runNode(cmd *cobra.Command, args []string) { // Create & start node n := node.NewNodeDefault(config) if _, err := n.Start(); err != nil { - Exit(Fmt("Failed to start node: %v", err)) + return fmt.Errorf("Failed to start node: %v", err) } else { log.Notice("Started node", "nodeInfo", n.Switch().NodeInfo()) } // Trap signal, run forever. n.RunForever() + + return nil } diff --git a/cmd/tendermint/commands/show_validator.go b/cmd/tendermint/commands/show_validator.go index 4aa80ae1..951fd5a8 100644 --- a/cmd/tendermint/commands/show_validator.go +++ b/cmd/tendermint/commands/show_validator.go @@ -5,7 +5,7 @@ import ( "github.com/spf13/cobra" - "github.com/tendermint/go-wire" + data "github.com/tendermint/go-wire/data" "github.com/tendermint/tendermint/types" ) @@ -22,5 +22,6 @@ func init() { func showValidator(cmd *cobra.Command, args []string) { privValidatorFile := config.GetString("priv_validator_file") privValidator := types.LoadOrGenPrivValidator(privValidatorFile) - fmt.Println(string(wire.JSONBytesPretty(privValidator.PubKey))) + pubKeyJSONBytes, _ := data.ToJSON(privValidator.PubKey) + fmt.Println(string(pubKeyJSONBytes)) } diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index 0a2e00ad..58767eb0 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" - cmn "github.com/tendermint/go-common" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/types" ) diff --git a/config/tendermint/config.go b/config/tendermint/config.go index 5ddde460..9011e015 100644 --- a/config/tendermint/config.go +++ b/config/tendermint/config.go @@ -5,8 +5,8 @@ import ( "path" "strings" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" + "github.com/spf13/viper" + cmn "github.com/tendermint/tmlibs/common" ) func getTMRoot(rootDir string) string { @@ -25,85 +25,89 @@ func getTMRoot(rootDir string) string { func initTMRoot(rootDir string) { rootDir = getTMRoot(rootDir) - EnsureDir(rootDir, 0700) - EnsureDir(rootDir+"/data", 0700) + cmn.EnsureDir(rootDir, 0700) + cmn.EnsureDir(rootDir+"/data", 0700) configFilePath := path.Join(rootDir, "config.toml") // Write default config file if missing. - if !FileExists(configFilePath) { + if !cmn.FileExists(configFilePath) { // Ask user for moniker // moniker := cfg.Prompt("Type hostname: ", "anonymous") - MustWriteFile(configFilePath, []byte(defaultConfig("anonymous")), 0644) + cmn.MustWriteFile(configFilePath, []byte(defaultConfig("anonymous")), 0644) } } -func GetConfig(rootDir string) cfg.Config { +func GetConfig(rootDir string) *viper.Viper { rootDir = getTMRoot(rootDir) initTMRoot(rootDir) - configFilePath := path.Join(rootDir, "config.toml") - mapConfig, err := cfg.ReadMapConfigFromFile(configFilePath) + config := viper.New() + config.SetConfigName("config") + config.SetConfigType("toml") + config.AddConfigPath(rootDir) + err := config.ReadInConfig() if err != nil { - Exit(Fmt("Could not read config: %v", err)) + cmn.Exit(cmn.Fmt("Could not read config from directory %v: %v", rootDir, err)) } + //config.WatchConfig() // Set defaults or panic - if mapConfig.IsSet("chain_id") { - Exit("Cannot set 'chain_id' via config.toml") + if config.IsSet("chain_id") { + cmn.Exit("Cannot set 'chain_id' via config.toml") } - if mapConfig.IsSet("revision_file") { - Exit("Cannot set 'revision_file' via config.toml. It must match what's in the Makefile") + if config.IsSet("revision_file") { + cmn.Exit("Cannot set 'revision_file' via config.toml. It must match what's in the Makefile") } - mapConfig.SetRequired("chain_id") // blows up if you try to use it before setting. - mapConfig.SetDefault("genesis_file", rootDir+"/genesis.json") - mapConfig.SetDefault("proxy_app", "tcp://127.0.0.1:46658") - mapConfig.SetDefault("abci", "socket") - mapConfig.SetDefault("moniker", "anonymous") - mapConfig.SetDefault("node_laddr", "tcp://0.0.0.0:46656") - mapConfig.SetDefault("seeds", "") - // mapConfig.SetDefault("seeds", "goldenalchemist.chaintest.net:46656") - mapConfig.SetDefault("fast_sync", true) - mapConfig.SetDefault("skip_upnp", false) - mapConfig.SetDefault("addrbook_file", rootDir+"/addrbook.json") - mapConfig.SetDefault("addrbook_strict", true) // disable to allow connections locally - mapConfig.SetDefault("pex_reactor", false) // enable for peer exchange - mapConfig.SetDefault("priv_validator_file", rootDir+"/priv_validator.json") - mapConfig.SetDefault("db_backend", "leveldb") - mapConfig.SetDefault("db_dir", rootDir+"/data") - mapConfig.SetDefault("log_level", "info") - mapConfig.SetDefault("rpc_laddr", "tcp://0.0.0.0:46657") - mapConfig.SetDefault("grpc_laddr", "") - mapConfig.SetDefault("prof_laddr", "") - mapConfig.SetDefault("revision_file", rootDir+"/revision") - mapConfig.SetDefault("cs_wal_file", rootDir+"/data/cs.wal/wal") - mapConfig.SetDefault("cs_wal_light", false) - mapConfig.SetDefault("filter_peers", false) + //mapConfig.SetRequired("chain_id") // blows up if you try to use it before setting. + config.SetDefault("genesis_file", rootDir+"/genesis.json") + config.SetDefault("proxy_app", "tcp://127.0.0.1:46658") + config.SetDefault("abci", "socket") + config.SetDefault("moniker", "anonymous") + config.SetDefault("node_laddr", "tcp://0.0.0.0:46656") + config.SetDefault("seeds", "") + // config.SetDefault("seeds", "goldenalchemist.chaintest.net:46656") + config.SetDefault("fast_sync", true) + config.SetDefault("skip_upnp", false) + config.SetDefault("addrbook_file", rootDir+"/addrbook.json") + config.SetDefault("addrbook_strict", true) // disable to allow connections locally + config.SetDefault("pex_reactor", false) // enable for peer exchange + config.SetDefault("priv_validator_file", rootDir+"/priv_validator.json") + config.SetDefault("db_backend", "leveldb") + config.SetDefault("db_dir", rootDir+"/data") + config.SetDefault("log_level", "info") + config.SetDefault("rpc_laddr", "tcp://0.0.0.0:46657") + config.SetDefault("grpc_laddr", "") + config.SetDefault("prof_laddr", "") + config.SetDefault("revision_file", rootDir+"/revision") + config.SetDefault("cs_wal_file", rootDir+"/data/cs.wal/wal") + config.SetDefault("cs_wal_light", false) + config.SetDefault("filter_peers", false) - mapConfig.SetDefault("block_size", 10000) // max number of txs - mapConfig.SetDefault("block_part_size", 65536) // part size 64K - mapConfig.SetDefault("disable_data_hash", false) + config.SetDefault("block_size", 10000) // max number of txs + config.SetDefault("block_part_size", 65536) // part size 64K + config.SetDefault("disable_data_hash", false) // all timeouts are in ms - mapConfig.SetDefault("timeout_handshake", 10000) - mapConfig.SetDefault("timeout_propose", 3000) - mapConfig.SetDefault("timeout_propose_delta", 500) - mapConfig.SetDefault("timeout_prevote", 1000) - mapConfig.SetDefault("timeout_prevote_delta", 500) - mapConfig.SetDefault("timeout_precommit", 1000) - mapConfig.SetDefault("timeout_precommit_delta", 500) - mapConfig.SetDefault("timeout_commit", 1000) + config.SetDefault("timeout_handshake", 10000) + config.SetDefault("timeout_propose", 3000) + config.SetDefault("timeout_propose_delta", 500) + config.SetDefault("timeout_prevote", 1000) + config.SetDefault("timeout_prevote_delta", 500) + config.SetDefault("timeout_precommit", 1000) + config.SetDefault("timeout_precommit_delta", 500) + config.SetDefault("timeout_commit", 1000) // make progress asap (no `timeout_commit`) on full precommit votes - mapConfig.SetDefault("skip_timeout_commit", false) - mapConfig.SetDefault("mempool_recheck", true) - mapConfig.SetDefault("mempool_recheck_empty", true) - mapConfig.SetDefault("mempool_broadcast", true) - mapConfig.SetDefault("mempool_wal_dir", rootDir+"/data/mempool.wal") + config.SetDefault("skip_timeout_commit", false) + config.SetDefault("mempool_recheck", true) + config.SetDefault("mempool_recheck_empty", true) + config.SetDefault("mempool_broadcast", true) + config.SetDefault("mempool_wal_dir", rootDir+"/data/mempool.wal") - mapConfig.SetDefault("tx_index", "kv") + config.SetDefault("tx_index", "kv") - return mapConfig + return config } var defaultConfigTmpl = `# This is a TOML config file. diff --git a/config/tendermint_test/config.go b/config/tendermint_test/config.go index 9d405dc9..1a87d526 100644 --- a/config/tendermint_test/config.go +++ b/config/tendermint_test/config.go @@ -3,13 +3,15 @@ package tendermint_test import ( + "fmt" "os" "path" "strings" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" - "github.com/tendermint/go-logger" + "github.com/spf13/viper" + + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/logger" ) func init() { @@ -43,7 +45,6 @@ func initTMRoot(rootDir string) { // Write default config file if missing. if !FileExists(configFilePath) { // Ask user for moniker - // moniker := cfg.Prompt("Type hostname: ", "anonymous") MustWriteFile(configFilePath, []byte(defaultConfig("anonymous")), 0644) } if !FileExists(genesisFilePath) { @@ -53,65 +54,70 @@ func initTMRoot(rootDir string) { MustWriteFile(privFilePath, []byte(defaultPrivValidator), 0644) } -func ResetConfig(localPath string) cfg.Config { +func ResetConfig(localPath string) *viper.Viper { rootDir := os.Getenv("HOME") + "/.tendermint_test/" + localPath initTMRoot(rootDir) - configFilePath := path.Join(rootDir, "config.toml") - mapConfig, err := cfg.ReadMapConfigFromFile(configFilePath) + config := viper.New() + config.SetConfigName("config") + config.SetConfigType("toml") + config.AddConfigPath(rootDir) + err := config.ReadInConfig() if err != nil { Exit(Fmt("Could not read config: %v", err)) } + //config.WatchConfig() // Set defaults or panic - if mapConfig.IsSet("chain_id") { - Exit("Cannot set 'chain_id' via config.toml") + if config.IsSet("chain_id") { + Exit(fmt.Sprintf("Cannot set 'chain_id' via config.toml:\n %v\n %v\n ", config.Get("chain_id"), rootDir)) } - mapConfig.SetDefault("chain_id", "tendermint_test") - mapConfig.SetDefault("genesis_file", rootDir+"/genesis.json") - mapConfig.SetDefault("proxy_app", "dummy") - mapConfig.SetDefault("abci", "socket") - mapConfig.SetDefault("moniker", "anonymous") - mapConfig.SetDefault("node_laddr", "tcp://0.0.0.0:36656") - mapConfig.SetDefault("fast_sync", false) - mapConfig.SetDefault("skip_upnp", true) - mapConfig.SetDefault("addrbook_file", rootDir+"/addrbook.json") - mapConfig.SetDefault("addrbook_strict", true) // disable to allow connections locally - mapConfig.SetDefault("pex_reactor", false) // enable for peer exchange - mapConfig.SetDefault("priv_validator_file", rootDir+"/priv_validator.json") - mapConfig.SetDefault("db_backend", "memdb") - mapConfig.SetDefault("db_dir", rootDir+"/data") - mapConfig.SetDefault("log_level", "info") - mapConfig.SetDefault("rpc_laddr", "tcp://0.0.0.0:36657") - mapConfig.SetDefault("grpc_laddr", "tcp://0.0.0.0:36658") - mapConfig.SetDefault("prof_laddr", "") - mapConfig.SetDefault("revision_file", rootDir+"/revision") - mapConfig.SetDefault("cs_wal_file", rootDir+"/data/cs.wal/wal") - mapConfig.SetDefault("cs_wal_light", false) - mapConfig.SetDefault("filter_peers", false) - mapConfig.SetDefault("block_size", 10000) - mapConfig.SetDefault("block_part_size", 65536) // part size 64K - mapConfig.SetDefault("disable_data_hash", false) - mapConfig.SetDefault("timeout_handshake", 10000) - mapConfig.SetDefault("timeout_propose", 2000) - mapConfig.SetDefault("timeout_propose_delta", 1) - mapConfig.SetDefault("timeout_prevote", 10) - mapConfig.SetDefault("timeout_prevote_delta", 1) - mapConfig.SetDefault("timeout_precommit", 10) - mapConfig.SetDefault("timeout_precommit_delta", 1) - mapConfig.SetDefault("timeout_commit", 10) - mapConfig.SetDefault("skip_timeout_commit", true) - mapConfig.SetDefault("mempool_recheck", true) - mapConfig.SetDefault("mempool_recheck_empty", true) - mapConfig.SetDefault("mempool_broadcast", true) - mapConfig.SetDefault("mempool_wal_dir", "") + config.SetDefault("chain_id", "tendermint_test") + config.SetDefault("genesis_file", rootDir+"/genesis.json") + config.SetDefault("proxy_app", "dummy") + config.SetDefault("abci", "socket") + config.SetDefault("moniker", "anonymous") + config.SetDefault("node_laddr", "tcp://0.0.0.0:36656") + config.SetDefault("fast_sync", false) + config.SetDefault("skip_upnp", true) + config.SetDefault("addrbook_file", rootDir+"/addrbook.json") + config.SetDefault("addrbook_strict", true) // disable to allow connections locally + config.SetDefault("pex_reactor", false) // enable for peer exchange + config.SetDefault("priv_validator_file", rootDir+"/priv_validator.json") + config.SetDefault("db_backend", "memdb") + config.SetDefault("db_dir", rootDir+"/data") + config.SetDefault("log_level", "info") + config.SetDefault("rpc_laddr", "tcp://0.0.0.0:36657") + config.SetDefault("grpc_laddr", "tcp://0.0.0.0:36658") + config.SetDefault("prof_laddr", "") + config.SetDefault("revision_file", rootDir+"/revision") + config.SetDefault("cs_wal_file", rootDir+"/data/cs.wal/wal") + config.SetDefault("cs_wal_light", false) + config.SetDefault("filter_peers", false) - mapConfig.SetDefault("tx_index", "kv") + config.SetDefault("block_size", 10000) + config.SetDefault("block_part_size", 65536) // part size 64K + config.SetDefault("disable_data_hash", false) + config.SetDefault("timeout_handshake", 10000) + config.SetDefault("timeout_propose", 2000) + config.SetDefault("timeout_propose_delta", 1) + config.SetDefault("timeout_prevote", 10) + config.SetDefault("timeout_prevote_delta", 1) + config.SetDefault("timeout_precommit", 10) + config.SetDefault("timeout_precommit_delta", 1) + config.SetDefault("timeout_commit", 10) + config.SetDefault("skip_timeout_commit", true) + config.SetDefault("mempool_recheck", true) + config.SetDefault("mempool_recheck_empty", true) + config.SetDefault("mempool_broadcast", true) + config.SetDefault("mempool_wal_dir", "") - logger.SetLogLevel(mapConfig.GetString("log_level")) + config.SetDefault("tx_index", "kv") - return mapConfig + logger.SetLogLevel(config.GetString("log_level")) + + return config } var defaultConfigTmpl = `# This is a TOML config file. @@ -137,10 +143,10 @@ var defaultGenesis = `{ "chain_id": "tendermint_test", "validators": [ { - "pub_key": [ - 1, - "3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" - ], + "pub_key": { + "type": "ed25519", + "data":"3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" + }, "amount": 10, "name": "" } @@ -150,14 +156,14 @@ var defaultGenesis = `{ var defaultPrivValidator = `{ "address": "D028C9981F7A87F3093672BF0D5B0E2A1B3ED456", - "pub_key": [ - 1, - "3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" - ], - "priv_key": [ - 1, - "27F82582AEFAE7AB151CFB01C48BB6C1A0DA78F9BDDA979A9F70A84D074EB07D3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" - ], + "pub_key": { + "type": "ed25519", + "data": "3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" + }, + "priv_key": { + "type": "ed25519", + "data": "27F82582AEFAE7AB151CFB01C48BB6C1A0DA78F9BDDA979A9F70A84D074EB07D3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" + }, "last_height": 0, "last_round": 0, "last_step": 0 diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index cd62f3f0..b9edf463 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -5,13 +5,13 @@ import ( "testing" "time" - "github.com/tendermint/tendermint/config/tendermint_test" + "github.com/spf13/viper" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" - "github.com/tendermint/go-events" - "github.com/tendermint/go-p2p" + "github.com/tendermint/tendermint/config/tendermint_test" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/events" ) func init() { @@ -36,7 +36,7 @@ func TestByzantine(t *testing.T) { switches := make([]*p2p.Switch, N) for i := 0; i < N; i++ { - switches[i] = p2p.NewSwitch(cfg.NewMapConfig(nil)) + switches[i] = p2p.NewSwitch(viper.New()) } reactors := make([]p2p.Reactor, N) diff --git a/consensus/common_test.go b/consensus/common_test.go index 334c66dc..469fc9c6 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -11,23 +11,24 @@ import ( "testing" "time" + "github.com/spf13/viper" + abcicli "github.com/tendermint/abci/client" abci "github.com/tendermint/abci/types" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" - dbm "github.com/tendermint/go-db" - "github.com/tendermint/go-p2p" bc "github.com/tendermint/tendermint/blockchain" "github.com/tendermint/tendermint/config/tendermint_test" mempl "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + . "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/abci/example/counter" "github.com/tendermint/abci/example/dummy" ) -var config cfg.Config // NOTE: must be reset for each _test.go file +var config *viper.Viper // NOTE: must be reset for each _test.go file var ensureTimeout = time.Duration(2) func ensureDir(dir string, mode os.FileMode) { @@ -233,7 +234,7 @@ func newConsensusState(state *sm.State, pv *types.PrivValidator, app abci.Applic return newConsensusStateWithConfig(config, state, pv, app) } -func newConsensusStateWithConfig(thisConfig cfg.Config, state *sm.State, pv *types.PrivValidator, app abci.Application) *ConsensusState { +func newConsensusStateWithConfig(thisConfig *viper.Viper, state *sm.State, pv *types.PrivValidator, app abci.Application) *ConsensusState { // Get BlockStore blockDB := dbm.NewMemDB() blockStore := bc.NewBlockStore(blockDB) @@ -256,7 +257,7 @@ func newConsensusStateWithConfig(thisConfig cfg.Config, state *sm.State, pv *typ return cs } -func loadPrivValidator(conf cfg.Config) *types.PrivValidator { +func loadPrivValidator(conf *viper.Viper) *types.PrivValidator { privValidatorFile := conf.GetString("priv_validator_file") ensureDir(path.Dir(privValidatorFile), 0700) privValidator := types.LoadOrGenPrivValidator(privValidatorFile) diff --git a/consensus/height_vote_set.go b/consensus/height_vote_set.go index e7f4be3b..b2aebb09 100644 --- a/consensus/height_vote_set.go +++ b/consensus/height_vote_set.go @@ -4,7 +4,7 @@ import ( "strings" "sync" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/types" ) diff --git a/consensus/height_vote_set_test.go b/consensus/height_vote_set_test.go index 3bede25c..a86cba4f 100644 --- a/consensus/height_vote_set_test.go +++ b/consensus/height_vote_set_test.go @@ -3,7 +3,7 @@ package consensus import ( "testing" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/config/tendermint_test" "github.com/tendermint/tendermint/types" ) diff --git a/consensus/log.go b/consensus/log.go index edf7a0a8..3e46c5ee 100644 --- a/consensus/log.go +++ b/consensus/log.go @@ -1,7 +1,7 @@ package consensus import ( - "github.com/tendermint/go-logger" + "github.com/tendermint/tmlibs/logger" ) var log = logger.New("module", "consensus") diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index 6bfdfda9..8808185d 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -9,7 +9,7 @@ import ( "github.com/tendermint/tendermint/config/tendermint_test" "github.com/tendermint/tendermint/types" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) func init() { diff --git a/consensus/reactor.go b/consensus/reactor.go index c3b1c590..5fe45cc0 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -8,8 +8,8 @@ import ( "sync" "time" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-p2p" + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/go-wire" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index bc26ffc0..66827c15 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -8,8 +8,8 @@ import ( "github.com/tendermint/tendermint/config/tendermint_test" - "github.com/tendermint/go-events" - "github.com/tendermint/go-p2p" + "github.com/tendermint/tmlibs/events" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" "github.com/tendermint/abci/example/dummy" ) diff --git a/consensus/replay.go b/consensus/replay.go index bd0975f4..5bc0c261 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -10,11 +10,12 @@ import ( "strings" "time" + "github.com/spf13/viper" + abci "github.com/tendermint/abci/types" - auto "github.com/tendermint/go-autofile" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" "github.com/tendermint/go-wire" + auto "github.com/tendermint/tmlibs/autofile" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" @@ -108,7 +109,7 @@ func (cs *ConsensusState) catchupReplay(csHeight int) error { gr.Close() } if found { - return errors.New(Fmt("WAL should not contain #ENDHEIGHT %d.", csHeight)) + return errors.New(cmn.Fmt("WAL should not contain #ENDHEIGHT %d.", csHeight)) } // Search for last height marker @@ -143,7 +144,7 @@ func (cs *ConsensusState) catchupReplay(csHeight int) error { } // TODO (0.10.0): uncomment - // return errors.New(Fmt("Cannot replay height %d. WAL does not contain #ENDHEIGHT for %d.", csHeight, csHeight-1)) + // return errors.New(cmn.Fmt("Cannot replay height %d. WAL does not contain #ENDHEIGHT for %d.", csHeight, csHeight-1)) } log.Notice("Catchup by replaying consensus messages", "height", csHeight) @@ -199,14 +200,14 @@ func makeHeightSearchFunc(height int) auto.SearchFunc { // we were last and using the WAL to recover there type Handshaker struct { - config cfg.Config + config *viper.Viper state *sm.State store types.BlockStore nBlocks int // number of blocks applied to the state } -func NewHandshaker(config cfg.Config, state *sm.State, store types.BlockStore) *Handshaker { +func NewHandshaker(config *viper.Viper, state *sm.State, store types.BlockStore) *Handshaker { return &Handshaker{config, state, store, 0} } @@ -221,7 +222,7 @@ func (h *Handshaker) Handshake(proxyApp proxy.AppConns) error { // handshake is done via info request on the query conn res, err := proxyApp.Query().InfoSync() if err != nil { - return errors.New(Fmt("Error calling Info: %v", err)) + return errors.New(cmn.Fmt("Error calling Info: %v", err)) } blockHeight := int(res.LastBlockHeight) // XXX: beware overflow @@ -238,7 +239,7 @@ func (h *Handshaker) Handshake(proxyApp proxy.AppConns) error { return nil } else if err != nil { - return errors.New(Fmt("Error on replay: %v", err)) + return errors.New(cmn.Fmt("Error on replay: %v", err)) } log.Notice("Completed ABCI Handshake - Tendermint and App are synced", "appHeight", blockHeight, "appHash", appHash) @@ -266,11 +267,11 @@ func (h *Handshaker) ReplayBlocks(appHash []byte, appBlockHeight int, proxyApp p } else if storeBlockHeight < stateBlockHeight { // the state should never be ahead of the store (this is under tendermint's control) - PanicSanity(Fmt("StateBlockHeight (%d) > StoreBlockHeight (%d)", stateBlockHeight, storeBlockHeight)) + cmn.PanicSanity(cmn.Fmt("StateBlockHeight (%d) > StoreBlockHeight (%d)", stateBlockHeight, storeBlockHeight)) } else if storeBlockHeight > stateBlockHeight+1 { // store should be at most one ahead of the state (this is under tendermint's control) - PanicSanity(Fmt("StoreBlockHeight (%d) > StateBlockHeight + 1 (%d)", storeBlockHeight, stateBlockHeight+1)) + cmn.PanicSanity(cmn.Fmt("StoreBlockHeight (%d) > StateBlockHeight + 1 (%d)", storeBlockHeight, stateBlockHeight+1)) } // Now either store is equal to state, or one ahead. @@ -313,7 +314,7 @@ func (h *Handshaker) ReplayBlocks(appHash []byte, appBlockHeight int, proxyApp p } - PanicSanity("Should never happen") + cmn.PanicSanity("Should never happen") return nil, nil } @@ -368,7 +369,7 @@ func (h *Handshaker) replayBlock(height int, proxyApp proxy.AppConnConsensus) ([ func (h *Handshaker) checkAppHash(appHash []byte) error { if !bytes.Equal(h.state.AppHash, appHash) { - panic(errors.New(Fmt("Tendermint state.AppHash does not match AppHash after replay. Got %X, expected %X", appHash, h.state.AppHash)).Error()) + panic(errors.New(cmn.Fmt("Tendermint state.AppHash does not match AppHash after replay. Got %X, expected %X", appHash, h.state.AppHash)).Error()) return nil } return nil diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 5ad1b945..1d2a91f3 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -8,24 +8,25 @@ import ( "strconv" "strings" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" - dbm "github.com/tendermint/go-db" + "github.com/spf13/viper" + bc "github.com/tendermint/tendermint/blockchain" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" ) //-------------------------------------------------------- // replay messages interactively or all at once -func RunReplayFile(config cfg.Config, walFile string, console bool) { +func RunReplayFile(config *viper.Viper, walFile string, console bool) { consensusState := newConsensusStateForReplay(config) if err := consensusState.ReplayFile(walFile, console); err != nil { - Exit(Fmt("Error during consensus replay: %v", err)) + cmn.Exit(cmn.Fmt("Error during consensus replay: %v", err)) } } @@ -114,7 +115,7 @@ func (pb *playback) replayReset(count int, newStepCh chan interface{}) error { pb.fp = fp pb.scanner = bufio.NewScanner(fp) count = pb.count - count - log.Notice(Fmt("Reseting from %d to %d", pb.count, count)) + log.Notice(cmn.Fmt("Reseting from %d to %d", pb.count, count)) pb.count = 0 pb.cs = newCS for i := 0; pb.scanner.Scan() && i < count; i++ { @@ -149,9 +150,9 @@ func (pb *playback) replayConsoleLoop() int { bufReader := bufio.NewReader(os.Stdin) line, more, err := bufReader.ReadLine() if more { - Exit("input is too long") + cmn.Exit("input is too long") } else if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } tokens := strings.Split(string(line), " ") @@ -236,7 +237,7 @@ func (pb *playback) replayConsoleLoop() int { //-------------------------------------------------------------------------------- // convenience for replay mode -func newConsensusStateForReplay(config cfg.Config) *ConsensusState { +func newConsensusStateForReplay(config *viper.Viper) *ConsensusState { // Get BlockStore blockStoreDB := dbm.NewDB("blockstore", config.GetString("db_backend"), config.GetString("db_dir")) blockStore := bc.NewBlockStore(blockStoreDB) @@ -249,7 +250,7 @@ func newConsensusStateForReplay(config cfg.Config) *ConsensusState { proxyApp := proxy.NewAppConns(config, proxy.DefaultClientCreator(config), NewHandshaker(config, state, blockStore)) _, err := proxyApp.Start() if err != nil { - Exit(Fmt("Error starting proxy app conns: %v", err)) + cmn.Exit(cmn.Fmt("Error starting proxy app conns: %v", err)) } // add the chainid to the global config @@ -258,7 +259,7 @@ func newConsensusStateForReplay(config cfg.Config) *ConsensusState { // Make event switch eventSwitch := types.NewEventSwitch() if _, err := eventSwitch.Start(); err != nil { - Exit(Fmt("Failed to start event switch: %v", err)) + cmn.Exit(cmn.Fmt("Failed to start event switch: %v", err)) } mempool := mempl.NewMempool(config, proxyApp.Mempool()) diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 43204ab7..e486af83 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -12,17 +12,17 @@ import ( "testing" "time" - "github.com/tendermint/tendermint/config/tendermint_test" + "github.com/spf13/viper" "github.com/tendermint/abci/example/dummy" - cmn "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" "github.com/tendermint/go-crypto" - dbm "github.com/tendermint/go-db" "github.com/tendermint/go-wire" + "github.com/tendermint/tendermint/config/tendermint_test" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" ) func init() { @@ -131,7 +131,13 @@ func runReplayTest(t *testing.T, cs *ConsensusState, walFile string, newBlockCh thisCase *testCase, i int) { cs.config.Set("cs_wal_file", walFile) - cs.Start() + started, err := cs.Start() + if err != nil { + t.Fatalf("Cannot start consensus: %v", err) + } + if !started { + t.Error("Consensus did not start") + } // Wait to make a new block. // This is just a signal that we haven't halted; its not something contained in the WAL itself. // Assuming the consensus state is running, replay of any WAL, including the empty one, @@ -402,7 +408,7 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, } -func buildTMStateFromChain(config cfg.Config, state *sm.State, chain []*types.Block, mode uint) []byte { +func buildTMStateFromChain(config *viper.Viper, state *sm.State, chain []*types.Block, mode uint) []byte { // run the whole chain against this client to build up the tendermint state clientCreator := proxy.NewLocalClientCreator(dummy.NewPersistentDummyApplication(path.Join(config.GetString("db_dir"), "1"))) proxyApp := proxy.NewAppConns(config, clientCreator, nil) // sm.NewHandshaker(config, state, store, ReplayLastBlock)) @@ -596,7 +602,7 @@ func makeBlockchain(t *testing.T, chainID string, nBlocks int, privVal *types.Pr } // fresh state and mock store -func stateAndStore(config cfg.Config, pubKey crypto.PubKey) (*sm.State, *mockBlockStore) { +func stateAndStore(config *viper.Viper, pubKey crypto.PubKey) (*sm.State, *mockBlockStore) { stateDB := dbm.NewMemDB() return sm.MakeGenesisState(stateDB, &types.GenesisDoc{ ChainID: config.GetString("chain_id"), @@ -611,13 +617,13 @@ func stateAndStore(config cfg.Config, pubKey crypto.PubKey) (*sm.State, *mockBlo // mock block store type mockBlockStore struct { - config cfg.Config + config *viper.Viper chain []*types.Block commits []*types.Commit } // TODO: NewBlockStore(db.NewMemDB) ... -func NewMockBlockStore(config cfg.Config) *mockBlockStore { +func NewMockBlockStore(config *viper.Viper) *mockBlockStore { return &mockBlockStore{config, nil, nil} } diff --git a/consensus/state.go b/consensus/state.go index 6ff97ddc..28836a07 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -10,13 +10,13 @@ import ( "time" "github.com/ebuchman/fail-test" + "github.com/spf13/viper" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" "github.com/tendermint/go-wire" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) //----------------------------------------------------------------------------- @@ -56,7 +56,7 @@ func (tp *TimeoutParams) Commit(t time.Time) time.Time { } // InitTimeoutParamsFromConfig initializes parameters from config -func InitTimeoutParamsFromConfig(config cfg.Config) *TimeoutParams { +func InitTimeoutParamsFromConfig(config *viper.Viper) *TimeoutParams { return &TimeoutParams{ Propose0: config.GetInt("timeout_propose"), ProposeDelta: config.GetInt("timeout_propose_delta"), @@ -222,9 +222,9 @@ type PrivValidator interface { // Tracks consensus state across block heights and rounds. type ConsensusState struct { - BaseService + cmn.BaseService - config cfg.Config + config *viper.Viper proxyAppConn proxy.AppConnConsensus blockStore types.BlockStore mempool types.Mempool @@ -255,7 +255,7 @@ type ConsensusState struct { done chan struct{} } -func NewConsensusState(config cfg.Config, state *sm.State, proxyAppConn proxy.AppConnConsensus, blockStore types.BlockStore, mempool types.Mempool) *ConsensusState { +func NewConsensusState(config *viper.Viper, state *sm.State, proxyAppConn proxy.AppConnConsensus, blockStore types.BlockStore, mempool types.Mempool) *ConsensusState { cs := &ConsensusState{ config: config, proxyAppConn: proxyAppConn, @@ -276,7 +276,7 @@ func NewConsensusState(config cfg.Config, state *sm.State, proxyAppConn proxy.Ap // Don't call scheduleRound0 yet. // We do that upon Start(). cs.reconstructLastCommit(state) - cs.BaseService = *NewBaseService(log, "ConsensusState", cs) + cs.BaseService = *cmn.NewBaseService(log, "ConsensusState", cs) return cs } @@ -290,7 +290,7 @@ func (cs *ConsensusState) SetEventSwitch(evsw types.EventSwitch) { func (cs *ConsensusState) String() string { // better not to access shared variables - return Fmt("ConsensusState") //(H:%v R:%v S:%v", cs.Height, cs.Round, cs.Step) + return cmn.Fmt("ConsensusState") //(H:%v R:%v S:%v", cs.Height, cs.Round, cs.Step) } func (cs *ConsensusState) GetState() *sm.State { @@ -398,7 +398,7 @@ func (cs *ConsensusState) Wait() { // Open file to log all consensus messages and timeouts for deterministic accountability func (cs *ConsensusState) OpenWAL(walFile string) (err error) { - err = EnsureDir(path.Dir(walFile), 0700) + err = cmn.EnsureDir(path.Dir(walFile), 0700) if err != nil { log.Error("Error ensuring ConsensusState wal dir", "error", err.Error()) return err @@ -519,11 +519,11 @@ func (cs *ConsensusState) reconstructLastCommit(state *sm.State) { } added, err := lastPrecommits.AddVote(precommit) if !added || err != nil { - PanicCrisis(Fmt("Failed to reconstruct LastCommit: %v", err)) + cmn.PanicCrisis(cmn.Fmt("Failed to reconstruct LastCommit: %v", err)) } } if !lastPrecommits.HasTwoThirdsMajority() { - PanicSanity("Failed to reconstruct LastCommit: Does not have +2/3 maj") + cmn.PanicSanity("Failed to reconstruct LastCommit: Does not have +2/3 maj") } cs.LastCommit = lastPrecommits } @@ -532,13 +532,13 @@ func (cs *ConsensusState) reconstructLastCommit(state *sm.State) { // The round becomes 0 and cs.Step becomes RoundStepNewHeight. func (cs *ConsensusState) updateToState(state *sm.State) { if cs.CommitRound > -1 && 0 < cs.Height && cs.Height != state.LastBlockHeight { - PanicSanity(Fmt("updateToState() expected state height of %v but found %v", + cmn.PanicSanity(cmn.Fmt("updateToState() expected state height of %v but found %v", cs.Height, state.LastBlockHeight)) } if cs.state != nil && cs.state.LastBlockHeight+1 != cs.Height { // This might happen when someone else is mutating cs.state. // Someone forgot to pass in state.Copy() somewhere?! - PanicSanity(Fmt("Inconsistent cs.state.LastBlockHeight+1 %v vs cs.Height %v", + cmn.PanicSanity(cmn.Fmt("Inconsistent cs.state.LastBlockHeight+1 %v vs cs.Height %v", cs.state.LastBlockHeight+1, cs.Height)) } @@ -555,7 +555,7 @@ func (cs *ConsensusState) updateToState(state *sm.State) { lastPrecommits := (*types.VoteSet)(nil) if cs.CommitRound > -1 && cs.Votes != nil { if !cs.Votes.Precommits(cs.CommitRound).HasTwoThirdsMajority() { - PanicSanity("updateToState(state) called but last Precommit round didn't have +2/3") + cmn.PanicSanity("updateToState(state) called but last Precommit round didn't have +2/3") } lastPrecommits = cs.Votes.Precommits(cs.CommitRound) } @@ -723,7 +723,7 @@ func (cs *ConsensusState) handleTimeout(ti timeoutInfo, rs RoundState) { types.FireEventTimeoutWait(cs.evsw, cs.RoundStateEvent()) cs.enterNewRound(ti.Height, ti.Round+1) default: - panic(Fmt("Invalid timeout step: %v", ti.Step)) + panic(cmn.Fmt("Invalid timeout step: %v", ti.Step)) } } @@ -738,7 +738,7 @@ func (cs *ConsensusState) handleTimeout(ti timeoutInfo, rs RoundState) { // NOTE: cs.StartTime was already set for height. func (cs *ConsensusState) enterNewRound(height int, round int) { if cs.Height != height || round < cs.Round || (cs.Round == round && cs.Step != RoundStepNewHeight) { - log.Debug(Fmt("enterNewRound(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("enterNewRound(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) return } @@ -746,7 +746,7 @@ func (cs *ConsensusState) enterNewRound(height int, round int) { log.Warn("Need to set a buffer and log.Warn() here for sanity.", "startTime", cs.StartTime, "now", now) } - log.Notice(Fmt("enterNewRound(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Notice(cmn.Fmt("enterNewRound(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) // Increment validators if necessary validators := cs.Validators @@ -780,10 +780,10 @@ func (cs *ConsensusState) enterNewRound(height int, round int) { // Enter: from NewRound(height,round). func (cs *ConsensusState) enterPropose(height int, round int) { if cs.Height != height || round < cs.Round || (cs.Round == round && RoundStepPropose <= cs.Step) { - log.Debug(Fmt("enterPropose(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("enterPropose(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) return } - log.Info(Fmt("enterPropose(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Info(cmn.Fmt("enterPropose(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) defer func() { // Done enterPropose: @@ -850,7 +850,7 @@ func (cs *ConsensusState) defaultDecideProposal(height, round int) { cs.sendInternalMessage(msgInfo{&BlockPartMessage{cs.Height, cs.Round, part}, ""}) } log.Info("Signed proposal", "height", height, "round", round, "proposal", proposal) - log.Debug(Fmt("Signed proposal block: %v", block)) + log.Debug(cmn.Fmt("Signed proposal block: %v", block)) } else { if !cs.replayMode { log.Warn("enterPropose: Error signing proposal", "height", height, "round", round, "error", err) @@ -906,7 +906,7 @@ func (cs *ConsensusState) createProposalBlock() (block *types.Block, blockParts // Otherwise vote nil. func (cs *ConsensusState) enterPrevote(height int, round int) { if cs.Height != height || round < cs.Round || (cs.Round == round && RoundStepPrevote <= cs.Step) { - log.Debug(Fmt("enterPrevote(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("enterPrevote(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) return } @@ -924,7 +924,7 @@ func (cs *ConsensusState) enterPrevote(height int, round int) { // TODO: catchup event? } - log.Info(Fmt("enterPrevote(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Info(cmn.Fmt("enterPrevote(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) // Sign and broadcast vote as necessary cs.doPrevote(height, round) @@ -967,13 +967,13 @@ func (cs *ConsensusState) defaultDoPrevote(height int, round int) { // Enter: any +2/3 prevotes at next round. func (cs *ConsensusState) enterPrevoteWait(height int, round int) { if cs.Height != height || round < cs.Round || (cs.Round == round && RoundStepPrevoteWait <= cs.Step) { - log.Debug(Fmt("enterPrevoteWait(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("enterPrevoteWait(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) return } if !cs.Votes.Prevotes(round).HasTwoThirdsAny() { - PanicSanity(Fmt("enterPrevoteWait(%v/%v), but Prevotes does not have any +2/3 votes", height, round)) + cmn.PanicSanity(cmn.Fmt("enterPrevoteWait(%v/%v), but Prevotes does not have any +2/3 votes", height, round)) } - log.Info(Fmt("enterPrevoteWait(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Info(cmn.Fmt("enterPrevoteWait(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) defer func() { // Done enterPrevoteWait: @@ -993,11 +993,11 @@ func (cs *ConsensusState) enterPrevoteWait(height int, round int) { // else, precommit nil otherwise. func (cs *ConsensusState) enterPrecommit(height int, round int) { if cs.Height != height || round < cs.Round || (cs.Round == round && RoundStepPrecommit <= cs.Step) { - log.Debug(Fmt("enterPrecommit(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("enterPrecommit(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) return } - log.Info(Fmt("enterPrecommit(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Info(cmn.Fmt("enterPrecommit(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) defer func() { // Done enterPrecommit: @@ -1024,7 +1024,7 @@ func (cs *ConsensusState) enterPrecommit(height int, round int) { // the latest POLRound should be this round polRound, _ := cs.Votes.POLInfo() if polRound < round { - PanicSanity(Fmt("This POLRound should be %v but got %", round, polRound)) + cmn.PanicSanity(cmn.Fmt("This POLRound should be %v but got %", round, polRound)) } // +2/3 prevoted nil. Unlock and precommit nil. @@ -1058,7 +1058,7 @@ func (cs *ConsensusState) enterPrecommit(height int, round int) { log.Notice("enterPrecommit: +2/3 prevoted proposal block. Locking", "hash", blockID.Hash) // Validate the block. if err := cs.state.ValidateBlock(cs.ProposalBlock); err != nil { - PanicConsensus(Fmt("enterPrecommit: +2/3 prevoted for an invalid block: %v", err)) + cmn.PanicConsensus(cmn.Fmt("enterPrecommit: +2/3 prevoted for an invalid block: %v", err)) } cs.LockedRound = round cs.LockedBlock = cs.ProposalBlock @@ -1087,13 +1087,13 @@ func (cs *ConsensusState) enterPrecommit(height int, round int) { // Enter: any +2/3 precommits for next round. func (cs *ConsensusState) enterPrecommitWait(height int, round int) { if cs.Height != height || round < cs.Round || (cs.Round == round && RoundStepPrecommitWait <= cs.Step) { - log.Debug(Fmt("enterPrecommitWait(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("enterPrecommitWait(%v/%v): Invalid args. Current step: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) return } if !cs.Votes.Precommits(round).HasTwoThirdsAny() { - PanicSanity(Fmt("enterPrecommitWait(%v/%v), but Precommits does not have any +2/3 votes", height, round)) + cmn.PanicSanity(cmn.Fmt("enterPrecommitWait(%v/%v), but Precommits does not have any +2/3 votes", height, round)) } - log.Info(Fmt("enterPrecommitWait(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + log.Info(cmn.Fmt("enterPrecommitWait(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) defer func() { // Done enterPrecommitWait: @@ -1109,10 +1109,10 @@ func (cs *ConsensusState) enterPrecommitWait(height int, round int) { // Enter: +2/3 precommits for block func (cs *ConsensusState) enterCommit(height int, commitRound int) { if cs.Height != height || RoundStepCommit <= cs.Step { - log.Debug(Fmt("enterCommit(%v/%v): Invalid args. Current step: %v/%v/%v", height, commitRound, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("enterCommit(%v/%v): Invalid args. Current step: %v/%v/%v", height, commitRound, cs.Height, cs.Round, cs.Step)) return } - log.Info(Fmt("enterCommit(%v/%v). Current: %v/%v/%v", height, commitRound, cs.Height, cs.Round, cs.Step)) + log.Info(cmn.Fmt("enterCommit(%v/%v). Current: %v/%v/%v", height, commitRound, cs.Height, cs.Round, cs.Step)) defer func() { // Done enterCommit: @@ -1128,7 +1128,7 @@ func (cs *ConsensusState) enterCommit(height int, commitRound int) { blockID, ok := cs.Votes.Precommits(commitRound).TwoThirdsMajority() if !ok { - PanicSanity("RunActionCommit() expects +2/3 precommits") + cmn.PanicSanity("RunActionCommit() expects +2/3 precommits") } // The Locked* fields no longer matter. @@ -1155,7 +1155,7 @@ func (cs *ConsensusState) enterCommit(height int, commitRound int) { // If we have the block AND +2/3 commits for it, finalize. func (cs *ConsensusState) tryFinalizeCommit(height int) { if cs.Height != height { - PanicSanity(Fmt("tryFinalizeCommit() cs.Height: %v vs height: %v", cs.Height, height)) + cmn.PanicSanity(cmn.Fmt("tryFinalizeCommit() cs.Height: %v vs height: %v", cs.Height, height)) } blockID, ok := cs.Votes.Precommits(cs.CommitRound).TwoThirdsMajority() @@ -1176,7 +1176,7 @@ func (cs *ConsensusState) tryFinalizeCommit(height int) { // Increment height and goto RoundStepNewHeight func (cs *ConsensusState) finalizeCommit(height int) { if cs.Height != height || cs.Step != RoundStepCommit { - log.Debug(Fmt("finalizeCommit(%v): Invalid args. Current step: %v/%v/%v", height, cs.Height, cs.Round, cs.Step)) + log.Debug(cmn.Fmt("finalizeCommit(%v): Invalid args. Current step: %v/%v/%v", height, cs.Height, cs.Round, cs.Step)) return } @@ -1184,21 +1184,21 @@ func (cs *ConsensusState) finalizeCommit(height int) { block, blockParts := cs.ProposalBlock, cs.ProposalBlockParts if !ok { - PanicSanity(Fmt("Cannot finalizeCommit, commit does not have two thirds majority")) + cmn.PanicSanity(cmn.Fmt("Cannot finalizeCommit, commit does not have two thirds majority")) } if !blockParts.HasHeader(blockID.PartsHeader) { - PanicSanity(Fmt("Expected ProposalBlockParts header to be commit header")) + cmn.PanicSanity(cmn.Fmt("Expected ProposalBlockParts header to be commit header")) } if !block.HashesTo(blockID.Hash) { - PanicSanity(Fmt("Cannot finalizeCommit, ProposalBlock does not hash to commit hash")) + cmn.PanicSanity(cmn.Fmt("Cannot finalizeCommit, ProposalBlock does not hash to commit hash")) } if err := cs.state.ValidateBlock(block); err != nil { - PanicConsensus(Fmt("+2/3 committed an invalid block: %v", err)) + cmn.PanicConsensus(cmn.Fmt("+2/3 committed an invalid block: %v", err)) } - log.Notice(Fmt("Finalizing commit of block with %d txs", block.NumTxs), + log.Notice(cmn.Fmt("Finalizing commit of block with %d txs", block.NumTxs), "height", block.Height, "hash", block.Hash(), "root", block.AppHash) - log.Info(Fmt("%v", block)) + log.Info(cmn.Fmt("%v", block)) fail.Fail() // XXX @@ -1393,7 +1393,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerKey string) (added bool, } added, err = cs.LastCommit.AddVote(vote) if added { - log.Info(Fmt("Added to lastPrecommits: %v", cs.LastCommit.StringShort())) + log.Info(cmn.Fmt("Added to lastPrecommits: %v", cs.LastCommit.StringShort())) types.FireEventVote(cs.evsw, types.EventDataVote{vote}) // if we can skip timeoutCommit and have all the votes now, @@ -1474,7 +1474,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerKey string) (added bool, cs.enterPrecommitWait(height, vote.Round) } default: - PanicSanity(Fmt("Unexpected vote type %X", vote.Type)) // Should not happen. + cmn.PanicSanity(cmn.Fmt("Unexpected vote type %X", vote.Type)) // Should not happen. } } // Either duplicate, or error upon cs.Votes.AddByIndex() diff --git a/consensus/state_test.go b/consensus/state_test.go index b7d9a42d..d2d34e3a 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/config/tendermint_test" "github.com/tendermint/tendermint/types" ) diff --git a/consensus/test_data/build.sh b/consensus/test_data/build.sh old mode 100644 new mode 100755 index 2759c0e3..93a74b8b --- a/consensus/test_data/build.sh +++ b/consensus/test_data/build.sh @@ -1,18 +1,23 @@ -#! /bin/bash +#!/usr/bin/env bash # XXX: removes tendermint dir -cd $GOPATH/src/github.com/tendermint/tendermint +cd "$GOPATH/src/github.com/tendermint/tendermint" || exit 1 + +# Make sure we have a tendermint command. +if ! hash tendermint 2>/dev/null; then + make install +fi # specify a dir to copy # TODO: eventually we should replace with `tendermint init --test` -DIR=$HOME/.tendermint_test/consensus_state_test +DIR_TO_COPY=$HOME/.tendermint_test/consensus_state_test -rm -rf $HOME/.tendermint -cp -r $DIR $HOME/.tendermint +rm -rf "$HOME/.tendermint" +cp -r "$DIR_TO_COPY" "$HOME/.tendermint" function reset(){ - rm -rf $HOME/.tendermint/data + rm -rf "$HOME/.tendermint/data" tendermint unsafe_reset_priv_validator } @@ -24,8 +29,8 @@ tendermint node --proxy_app=dummy &> /dev/null & sleep 5 killall tendermint -# /q would print up to and including the match, then quit. -# /Q doesn't include the match. +# /q would print up to and including the match, then quit. +# /Q doesn't include the match. # http://unix.stackexchange.com/questions/11305/grep-show-all-the-file-up-to-the-match sed '/ENDHEIGHT: 1/Q' ~/.tendermint/data/cs.wal/wal > consensus/test_data/empty_block.cswal @@ -80,7 +85,7 @@ reset -case "$1" in +case "$1" in "small_block1") small_block1 ;; diff --git a/consensus/ticker.go b/consensus/ticker.go index b318597d..f8031e57 100644 --- a/consensus/ticker.go +++ b/consensus/ticker.go @@ -3,7 +3,7 @@ package consensus import ( "time" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) var ( diff --git a/consensus/version.go b/consensus/version.go index 34886db3..84f1ec81 100644 --- a/consensus/version.go +++ b/consensus/version.go @@ -1,7 +1,7 @@ package consensus import ( - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) // kind of arbitrary diff --git a/consensus/wal.go b/consensus/wal.go index a89eff5e..e855694a 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -3,8 +3,8 @@ package consensus import ( "time" - auto "github.com/tendermint/go-autofile" - . "github.com/tendermint/go-common" + auto "github.com/tendermint/tmlibs/autofile" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/go-wire" "github.com/tendermint/tendermint/types" ) diff --git a/docs/architecture/merkle.md b/docs/architecture/merkle.md index 72998db8..4e769aed 100644 --- a/docs/architecture/merkle.md +++ b/docs/architecture/merkle.md @@ -2,7 +2,7 @@ To allow the efficient creation of an ABCi app, tendermint wishes to provide a reference implemention of a key-value store that provides merkle proofs of the data. These proofs then quickly allow the ABCi app to provide an apphash to the consensus engine, as well as a full proof to any client. -This engine is currently implemented in `go-merkle` with `merkleeyes` providing a language-agnostic binding via ABCi. It uses `go-db` bindings internally to persist data to leveldb. +This engine is currently implemented in `go-merkle` with `merkleeyes` providing a language-agnostic binding via ABCi. It uses `tmlibs/db` bindings internally to persist data to leveldb. What are some of the requirements of this store: diff --git a/glide.lock b/glide.lock index 42712c23..7720c946 100644 --- a/glide.lock +++ b/glide.lock @@ -1,18 +1,18 @@ -hash: d9724aa287c40d1b3856b6565f09235d809c8b2f7c6537c04f597137c0d6cd26 -updated: 2017-04-21T13:09:25.708801802-04:00 +hash: 6f8962f6ca0e25b8e43bc6e496bd46c9ff3a79dcf789578efeeaee2fffc39c6e +updated: 2017-04-25T14:48:36.999444976-04:00 imports: - name: github.com/btcsuite/btcd - version: 4b348c1d33373d672edd83fc576892d0e46686d2 + version: 583684b21bfbde9b5fc4403916fd7c807feb0289 subpackages: - btcec -- name: github.com/BurntSushi/toml - version: b26d9c308763d68093482582cea63d69be07a0f0 - name: github.com/davecgh/go-spew version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 subpackages: - spew - name: github.com/ebuchman/fail-test version: 95f809107225be108efcf10a3509e4ea6ceef3c4 +- name: github.com/fsnotify/fsnotify + version: 4da3e2cfbabc9f751898f250b49f2439785783a1 - name: github.com/go-stack/stack version: 100eb0c0a9c5b306ca2fb4f165df21d80ada4b82 - name: github.com/gogo/protobuf @@ -20,39 +20,67 @@ imports: subpackages: - proto - name: github.com/golang/protobuf - version: 2bba0603135d7d7f5cb73b2125beeda19c09f4ef + version: 8ee79997227bf9b34611aee7946ae64735e6fd93 subpackages: - proto - - ptypes/any - name: github.com/golang/snappy - version: 553a641470496b2327abcac10b36396bd98e45c9 + version: d9eb7a3d35ec988b8585d4a0068e462c27d28380 - name: github.com/gorilla/websocket version: 3ab3a8b8831546bd18fd182c20687ca853b2bb13 +- name: github.com/hashicorp/hcl + version: 630949a3c5fa3c613328e1b8256052cbc2327c9b + subpackages: + - hcl/ast + - hcl/parser + - hcl/scanner + - hcl/strconv + - hcl/token + - json/parser + - json/scanner + - json/token - name: github.com/inconshreveable/mousetrap version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 - name: github.com/jmhodges/levigo version: c42d9e0ca023e2198120196f842701bb4c55d7b9 +- name: github.com/magiconair/properties + version: 51463bfca2576e06c62a8504b5c0f06d61312647 - name: github.com/mattn/go-colorable - version: ded68f7a9561c023e790de24279db7ebf473ea80 + version: d228849504861217f796da67fae4f6e347643f15 - name: github.com/mattn/go-isatty - version: fc9e8d8ef48496124e79ae0df75490096eccf6fe + version: 30a891c33c7cde7b02a981314b4228ec99380cca +- name: github.com/mitchellh/mapstructure + version: 53818660ed4955e899c0bcafa97299a388bd7c8e +- name: github.com/pelletier/go-buffruneio + version: c37440a7cf42ac63b919c752ca73a85067e05992 +- name: github.com/pelletier/go-toml + version: 13d49d4606eb801b8f01ae542b4afc4c6ee3d84a - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: - difflib +- name: github.com/spf13/afero + version: 9be650865eab0c12963d8753212f4f9c66cdcf12 + subpackages: + - mem +- name: github.com/spf13/cast + version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 - name: github.com/spf13/cobra - version: 10f6b9d7e1631a54ad07c5c0fb71c28a1abfd3c2 + version: fcd0c5a1df88f5d6784cb4feead962c3f3d0b66c +- name: github.com/spf13/jwalterweatherman + version: fa7ca7e836cf3a8bb4ebf799f472c12d7e903d66 - name: github.com/spf13/pflag - version: 2300d0f8576fe575f71aaa5b9bbe4e1b0dc2eb51 + version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 +- name: github.com/spf13/viper + version: 5d46e70da8c0b6f812e0b170b7a985753b5c63cb - name: github.com/stretchr/testify version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0 subpackages: - assert - require - name: github.com/syndtr/goleveldb - version: 8c81ea47d4c41a385645e133e15510fc6a2a74b4 + version: 23851d93a2292dcc56e71a18ec9e0624d84a0f65 subpackages: - leveldb - leveldb/cache @@ -67,7 +95,7 @@ imports: - leveldb/table - leveldb/util - name: github.com/tendermint/abci - version: 56e13d87f4e3ec1ea756957d6b23caa6ebcf0998 + version: c709d3cc857929a8dd36a90da3640122d7e75770 subpackages: - client - example/counter @@ -79,56 +107,37 @@ imports: subpackages: - edwards25519 - extra25519 -- name: github.com/tendermint/go-autofile - version: 48b17de82914e1ec2f134ce823ba426337d2c518 -- name: github.com/tendermint/go-clist - version: 3baa390bbaf7634251c42ad69a8682e7e3990552 -- name: github.com/tendermint/go-common - version: f9e3db037330c8a8d61d3966de8473eaf01154fa - subpackages: - - test -- name: github.com/tendermint/go-config - version: 620dcbbd7d587cf3599dedbf329b64311b0c307a - name: github.com/tendermint/go-crypto - version: 0ca2c6fdb0706001ca4c4b9b80c9f428e8cf39da -- name: github.com/tendermint/go-data - version: e7fcc6d081ec8518912fcdc103188275f83a3ee5 -- name: github.com/tendermint/go-db - version: 9643f60bc2578693844aacf380a7c32e4c029fee -- name: github.com/tendermint/go-events - version: f8ffbfb2be3483e9e7927495590a727f51c0c11f -- name: github.com/tendermint/go-flowrate - version: a20c98e61957faa93b4014fbd902f20ab9317a6a - subpackages: - - flowrate -- name: github.com/tendermint/go-logger - version: cefb3a45c0bf3c493a04e9bcd9b1540528be59f2 -- name: github.com/tendermint/go-merkle - version: 714d4d04557fd068a7c2a1748241ce8428015a96 -- name: github.com/tendermint/go-p2p - version: e8f33a47846708269d373f9c8080613d6c4f66b2 - subpackages: - - upnp -- name: github.com/tendermint/go-rpc - version: 2c8df0ee6b60d8ac33662df13a4e358c679e02bf - subpackages: - - client - - server - - types + version: 9b95da8fa4187f6799558d89b271dc8ab6485615 - name: github.com/tendermint/go-wire - version: c1c9a57ab8038448ddea1714c0698f8051e5748c + version: 334005c236d19c632fb5f073f9de3b0fab6a522b + subpackages: + - data - name: github.com/tendermint/log15 version: ae0f3d6450da9eac7074b439c8e1c3cabf0d5ce6 subpackages: - term - name: github.com/tendermint/merkleeyes - version: 9fb76efa5aebe773a598f97e68e75fe53d520e70 + version: 6fd69aa0871a4e685a5570aa7ab3d12e4068a722 subpackages: - app - client + - iavl - testutil +- name: github.com/tendermint/tmlibs + version: df250b69416a35a943a6e2a92118667e9ef031d4 + subpackages: + - autofile + - clist + - common + - db + - events + - flowrate + - logger + - merkle + - test - name: golang.org/x/crypto - version: 96846453c37f0876340a66a47f3f75b1f3a6cd2d + version: 7c6cc321c680f03b9ef0764448e780704f486b51 subpackages: - curve25519 - nacl/box @@ -139,7 +148,7 @@ imports: - ripemd160 - salsa20/salsa - name: golang.org/x/net - version: c8c74377599bd978aee1cf3b9b63a8634051cec2 + version: 61557ac0112b576429a0df080e1c2cef5dfbb642 subpackages: - context - http2 @@ -149,33 +158,27 @@ imports: - lex/httplex - trace - name: golang.org/x/sys - version: ea9bcade75cb975a0b9738936568ab388b845617 + version: d75a52659825e75fff6158388dddc6a5b04f9ba5 subpackages: - unix - name: golang.org/x/text - version: 19e3104b43db45fca0303f489a9536087b184802 + version: f4b4367115ec2de254587813edaa901bc1c723a8 subpackages: - - secure/bidirule - transform - - unicode/bidi - unicode/norm -- name: google.golang.org/genproto - version: 411e09b969b1170a9f0c467558eb4c4c110d9c77 - subpackages: - - googleapis/rpc/status - name: google.golang.org/grpc - version: 6914ab1e338c92da4218a23d27fcd03d0ad78d46 + version: cbcceb2942a489498cf22b2f918536e819d33f0a subpackages: - codes - credentials - grpclog - internal - - keepalive - metadata - naming - peer - stats - - status - tap - transport +- name: gopkg.in/yaml.v2 + version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b testImports: [] diff --git a/glide.yaml b/glide.yaml index cdb083e6..f06a16b3 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,56 +1,56 @@ package: github.com/tendermint/tendermint import: -- package: github.com/tendermint/go-autofile - version: develop -- package: github.com/tendermint/go-clist - version: develop -- package: github.com/tendermint/go-common - version: develop -- package: github.com/tendermint/go-config +- package: github.com/ebuchman/fail-test +- package: github.com/gogo/protobuf + subpackages: + - proto +- package: github.com/golang/protobuf + subpackages: + - proto +- package: github.com/gorilla/websocket +- package: github.com/pkg/errors +- package: github.com/spf13/cobra +- package: github.com/spf13/viper +- package: github.com/stretchr/testify + subpackages: + - require +- package: github.com/tendermint/abci version: develop + subpackages: + - client + - example/dummy + - types - package: github.com/tendermint/go-crypto version: develop -- package: github.com/tendermint/go-data - version: develop -- package: github.com/tendermint/go-db - version: develop -- package: github.com/tendermint/go-events - version: develop -- package: github.com/tendermint/go-logger - version: develop -- package: github.com/tendermint/go-merkle - version: develop -- package: github.com/tendermint/go-p2p - version: develop -- package: github.com/tendermint/go-rpc - version: develop - package: github.com/tendermint/go-wire version: develop -- package: github.com/tendermint/abci - version: develop -- package: github.com/tendermint/go-flowrate + subpackages: + - data - package: github.com/tendermint/log15 -- package: github.com/tendermint/ed25519 +- package: github.com/tendermint/tmlibs + version: develop + subpackages: + - autofile + - clist + - common + - db + - events + - flowrate + - logger + - merkle +- package: golang.org/x/crypto + subpackages: + - nacl/box + - nacl/secretbox + - ripemd160 +- package: golang.org/x/net + subpackages: + - context +- package: google.golang.org/grpc +testImport: - package: github.com/tendermint/merkleeyes version: develop subpackages: - app -- package: github.com/gogo/protobuf - version: ^0.3 - subpackages: - - proto -- package: github.com/gorilla/websocket - version: ^1.1.0 -- package: github.com/spf13/cobra -- package: github.com/spf13/pflag -- package: github.com/pkg/errors - version: ^0.8.0 -- package: golang.org/x/crypto - subpackages: - - ripemd160 -testImport: -- package: github.com/stretchr/testify - version: ^1.1.4 - subpackages: - - assert - - require + - iavl + - testutil diff --git a/mempool/log.go b/mempool/log.go index 90eb8703..ac9596d5 100644 --- a/mempool/log.go +++ b/mempool/log.go @@ -1,7 +1,7 @@ package mempool import ( - "github.com/tendermint/go-logger" + "github.com/tendermint/tmlibs/logger" ) var log = logger.New("module", "mempool") diff --git a/mempool/mempool.go b/mempool/mempool.go index e960f520..621f0931 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -7,13 +7,14 @@ import ( "sync/atomic" "time" + "github.com/spf13/viper" + abci "github.com/tendermint/abci/types" - auto "github.com/tendermint/go-autofile" - "github.com/tendermint/go-clist" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" + auto "github.com/tendermint/tmlibs/autofile" + "github.com/tendermint/tmlibs/clist" + . "github.com/tendermint/tmlibs/common" ) /* @@ -47,7 +48,7 @@ TODO: Better handle abci client errors. (make it automatically handle connection const cacheSize = 100000 type Mempool struct { - config cfg.Config + config *viper.Viper proxyMtx sync.Mutex proxyAppConn proxy.AppConnMempool @@ -66,7 +67,7 @@ type Mempool struct { wal *auto.AutoFile } -func NewMempool(config cfg.Config, proxyAppConn proxy.AppConnMempool) *Mempool { +func NewMempool(config *viper.Viper, proxyAppConn proxy.AppConnMempool) *Mempool { mempool := &Mempool{ config: config, proxyAppConn: proxyAppConn, diff --git a/mempool/reactor.go b/mempool/reactor.go index 4531edee..6bea53b0 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -6,12 +6,13 @@ import ( "reflect" "time" + "github.com/spf13/viper" + abci "github.com/tendermint/abci/types" - "github.com/tendermint/go-clist" - cfg "github.com/tendermint/go-config" - "github.com/tendermint/go-p2p" "github.com/tendermint/go-wire" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" + "github.com/tendermint/tmlibs/clist" ) const ( @@ -24,12 +25,12 @@ const ( // MempoolReactor handles mempool tx broadcasting amongst peers. type MempoolReactor struct { p2p.BaseReactor - config cfg.Config + config *viper.Viper Mempool *Mempool evsw types.EventSwitch } -func NewMempoolReactor(config cfg.Config, mempool *Mempool) *MempoolReactor { +func NewMempoolReactor(config *viper.Viper, mempool *Mempool) *MempoolReactor { memR := &MempoolReactor{ config: config, Mempool: mempool, diff --git a/node/log.go b/node/log.go index 36b45149..946d963c 100644 --- a/node/log.go +++ b/node/log.go @@ -1,7 +1,7 @@ package node import ( - "github.com/tendermint/go-logger" + "github.com/tendermint/tmlibs/logger" ) var log = logger.New("module", "node") diff --git a/node/node.go b/node/node.go index 99077948..1c66c00e 100644 --- a/node/node.go +++ b/node/node.go @@ -7,27 +7,28 @@ import ( "net/http" "strings" + "github.com/spf13/viper" + abci "github.com/tendermint/abci/types" - cmn "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" crypto "github.com/tendermint/go-crypto" - dbm "github.com/tendermint/go-db" - p2p "github.com/tendermint/go-p2p" - rpc "github.com/tendermint/go-rpc" - rpcserver "github.com/tendermint/go-rpc/server" wire "github.com/tendermint/go-wire" bc "github.com/tendermint/tendermint/blockchain" "github.com/tendermint/tendermint/consensus" mempl "github.com/tendermint/tendermint/mempool" + p2p "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" - rpccore "github.com/tendermint/tendermint/rpc/core" - grpccore "github.com/tendermint/tendermint/rpc/grpc" + rpc "github.com/tendermint/tendermint/rpc" + rpcserver "github.com/tendermint/tendermint/rpc/server" + rpccore "github.com/tendermint/tendermint/rpc/tendermint/core" + grpccore "github.com/tendermint/tendermint/rpc/tendermint/grpc" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/state/txindex/kv" "github.com/tendermint/tendermint/state/txindex/null" "github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/version" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" _ "net/http/pprof" ) @@ -36,7 +37,7 @@ type Node struct { cmn.BaseService // config - config cfg.Config // user config + config *viper.Viper // user config genesisDoc *types.GenesisDoc // initial validator set privValidator *types.PrivValidator // local node's validator key @@ -57,14 +58,14 @@ type Node struct { txIndexer txindex.TxIndexer } -func NewNodeDefault(config cfg.Config) *Node { +func NewNodeDefault(config *viper.Viper) *Node { // Get PrivValidator privValidatorFile := config.GetString("priv_validator_file") privValidator := types.LoadOrGenPrivValidator(privValidatorFile) return NewNode(config, privValidator, proxy.DefaultClientCreator(config)) } -func NewNode(config cfg.Config, privValidator *types.PrivValidator, clientCreator proxy.ClientCreator) *Node { +func NewNode(config *viper.Viper, privValidator *types.PrivValidator, clientCreator proxy.ClientCreator) *Node { // Get BlockStore blockStoreDB := dbm.NewDB("blockstore", config.GetString("db_backend"), config.GetString("db_dir")) @@ -134,7 +135,11 @@ func NewNode(config cfg.Config, privValidator *types.PrivValidator, clientCreato consensusReactor := consensus.NewConsensusReactor(consensusState, fastSync) // Make p2p network switch - sw := p2p.NewSwitch(config.GetConfig("p2p")) + p2pConfig := viper.New() + if config.IsSet("p2p") { //TODO verify this necessary, where is this ever set? + p2pConfig = config.Get("p2p").(*viper.Viper) + } + sw := p2p.NewSwitch(p2pConfig) sw.AddReactor("MEMPOOL", mempoolReactor) sw.AddReactor("BLOCKCHAIN", bcReactor) sw.AddReactor("CONSENSUS", consensusReactor) @@ -372,7 +377,7 @@ func (n *Node) makeNodeInfo() *p2p.NodeInfo { } nodeInfo := &p2p.NodeInfo{ - PubKey: n.privKey.PubKey().(crypto.PubKeyEd25519), + PubKey: n.privKey.PubKey().Unwrap().(crypto.PubKeyEd25519), Moniker: n.config.GetString("moniker"), Network: n.config.GetString("chain_id"), Version: version.Version, diff --git a/p2p/CHANGELOG.md b/p2p/CHANGELOG.md new file mode 100644 index 00000000..cae2f4c9 --- /dev/null +++ b/p2p/CHANGELOG.md @@ -0,0 +1,78 @@ +# Changelog + +## 0.5.0 (April 21, 2017) + +BREAKING CHANGES: + +- Remove or unexport methods from FuzzedConnection: Active, Mode, ProbDropRW, ProbDropConn, ProbSleep, MaxDelayMilliseconds, Fuzz +- switch.AddPeerWithConnection is unexported and replaced by switch.AddPeer +- switch.DialPeerWithAddress takes a bool, setting the peer as persistent or not + +FEATURES: + +- Persistent peers: any peer considered a "seed" will be reconnected to when the connection is dropped + + +IMPROVEMENTS: + +- Many more tests and comments +- Refactor configurations for less dependence on go-config. Introduces new structs PeerConfig, MConnConfig, FuzzConnConfig +- New methods on peer: CloseConn, HandshakeTimeout, IsPersistent, Addr, PubKey +- NewNetAddress supports a testing mode where the address defaults to 0.0.0.0:0 + + +## 0.4.0 (March 6, 2017) + +BREAKING CHANGES: + +- DialSeeds now takes an AddrBook and returns an error: `DialSeeds(*AddrBook, []string) error` +- NewNetAddressString now returns an error: `NewNetAddressString(string) (*NetAddress, error)` + +FEATURES: + +- `NewNetAddressStrings([]string) ([]*NetAddress, error)` +- `AddrBook.Save()` + +IMPROVEMENTS: + +- PexReactor responsible for starting and stopping the AddrBook + +BUG FIXES: + +- DialSeeds returns an error instead of panicking on bad addresses + +## 0.3.5 (January 12, 2017) + +FEATURES + +- Toggle strict routability in the AddrBook + +BUG FIXES + +- Close filtered out connections +- Fixes for MakeConnectedSwitches and Connect2Switches + +## 0.3.4 (August 10, 2016) + +FEATURES: + +- Optionally filter connections by address or public key + +## 0.3.3 (May 12, 2016) + +FEATURES: + +- FuzzConn + +## 0.3.2 (March 12, 2016) + +IMPROVEMENTS: + +- Memory optimizations + +## 0.3.1 () + +FEATURES: + +- Configurable parameters + diff --git a/p2p/Dockerfile b/p2p/Dockerfile new file mode 100644 index 00000000..6c71b2f8 --- /dev/null +++ b/p2p/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:latest + +RUN curl https://glide.sh/get | sh + +RUN mkdir -p /go/src/github.com/tendermint/tendermint/p2p +WORKDIR /go/src/github.com/tendermint/tendermint/p2p + +COPY glide.yaml /go/src/github.com/tendermint/tendermint/p2p/ +COPY glide.lock /go/src/github.com/tendermint/tendermint/p2p/ + +RUN glide install + +COPY . /go/src/github.com/tendermint/tendermint/p2p diff --git a/p2p/README.md b/p2p/README.md new file mode 100644 index 00000000..bf0a5c4d --- /dev/null +++ b/p2p/README.md @@ -0,0 +1,79 @@ +# `tendermint/tendermint/p2p` + +[![CircleCI](https://circleci.com/gh/tendermint/tendermint/p2p.svg?style=svg)](https://circleci.com/gh/tendermint/tendermint/p2p) + +`tendermint/tendermint/p2p` provides an abstraction around peer-to-peer communication.
+ +## Peer/MConnection/Channel + +Each peer has one `MConnection` (multiplex connection) instance. + +__multiplex__ *noun* a system or signal involving simultaneous transmission of +several messages along a single channel of communication. + +Each `MConnection` handles message transmission on multiple abstract communication +`Channel`s. Each channel has a globally unique byte id. +The byte id and the relative priorities of each `Channel` are configured upon +initialization of the connection. + +There are two methods for sending messages: +```go +func (m MConnection) Send(chID byte, msg interface{}) bool {} +func (m MConnection) TrySend(chID byte, msg interface{}) bool {} +``` + +`Send(chID, msg)` is a blocking call that waits until `msg` is successfully queued +for the channel with the given id byte `chID`. The message `msg` is serialized +using the `tendermint/wire` submodule's `WriteBinary()` reflection routine. + +`TrySend(chID, msg)` is a nonblocking call that returns false if the channel's +queue is full. + +`Send()` and `TrySend()` are also exposed for each `Peer`. + +## Switch/Reactor + +The `Switch` handles peer connections and exposes an API to receive incoming messages +on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one +or more `Channels`. So while sending outgoing messages is typically performed on the peer, +incoming messages are received on the reactor. + +```go +// Declare a MyReactor reactor that handles messages on MyChannelID. +type MyReactor struct{} + +func (reactor MyReactor) GetChannels() []*ChannelDescriptor { + return []*ChannelDescriptor{ChannelDescriptor{ID:MyChannelID, Priority: 1}} +} + +func (reactor MyReactor) Receive(chID byte, peer *Peer, msgBytes []byte) { + r, n, err := bytes.NewBuffer(msgBytes), new(int64), new(error) + msgString := ReadString(r, n, err) + fmt.Println(msgString) +} + +// Other Reactor methods omitted for brevity +... + +switch := NewSwitch([]Reactor{MyReactor{}}) + +... + +// Send a random message to all outbound connections +for _, peer := range switch.Peers().List() { + if peer.IsOutbound() { + peer.Send(MyChannelID, "Here's a random message") + } +} +``` + +### PexReactor/AddrBook + +A `PEXReactor` reactor implementation is provided to automate peer discovery. + +```go +book := p2p.NewAddrBook(addrBookFilePath) +pexReactor := p2p.NewPEXReactor(book) +... +switch := NewSwitch([]Reactor{pexReactor, myReactor, ...}) +``` diff --git a/p2p/addrbook.go b/p2p/addrbook.go new file mode 100644 index 00000000..e68cc7b3 --- /dev/null +++ b/p2p/addrbook.go @@ -0,0 +1,839 @@ +// Modified for Tendermint +// Originally Copyright (c) 2013-2014 Conformal Systems LLC. +// https://github.com/conformal/btcd/blob/master/LICENSE + +package p2p + +import ( + "encoding/binary" + "encoding/json" + "math" + "math/rand" + "net" + "os" + "sync" + "time" + + . "github.com/tendermint/tmlibs/common" + crypto "github.com/tendermint/go-crypto" +) + +const ( + // addresses under which the address manager will claim to need more addresses. + needAddressThreshold = 1000 + + // interval used to dump the address cache to disk for future use. + dumpAddressInterval = time.Minute * 2 + + // max addresses in each old address bucket. + oldBucketSize = 64 + + // buckets we split old addresses over. + oldBucketCount = 64 + + // max addresses in each new address bucket. + newBucketSize = 64 + + // buckets that we spread new addresses over. + newBucketCount = 256 + + // old buckets over which an address group will be spread. + oldBucketsPerGroup = 4 + + // new buckets over which an source address group will be spread. + newBucketsPerGroup = 32 + + // buckets a frequently seen new address may end up in. + maxNewBucketsPerAddress = 4 + + // days before which we assume an address has vanished + // if we have not seen it announced in that long. + numMissingDays = 30 + + // tries without a single success before we assume an address is bad. + numRetries = 3 + + // max failures we will accept without a success before considering an address bad. + maxFailures = 10 + + // days since the last success before we will consider evicting an address. + minBadDays = 7 + + // % of total addresses known returned by GetSelection. + getSelectionPercent = 23 + + // min addresses that must be returned by GetSelection. Useful for bootstrapping. + minGetSelection = 32 + + // max addresses returned by GetSelection + // NOTE: this must match "maxPexMessageSize" + maxGetSelection = 250 + + // current version of the on-disk format. + serializationVersion = 1 +) + +const ( + bucketTypeNew = 0x01 + bucketTypeOld = 0x02 +) + +// AddrBook - concurrency safe peer address manager. +type AddrBook struct { + BaseService + + mtx sync.Mutex + filePath string + routabilityStrict bool + rand *rand.Rand + key string + ourAddrs map[string]*NetAddress + addrLookup map[string]*knownAddress // new & old + addrNew []map[string]*knownAddress + addrOld []map[string]*knownAddress + wg sync.WaitGroup + nOld int + nNew int +} + +// NewAddrBook creates a new address book. +// Use Start to begin processing asynchronous address updates. +func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook { + am := &AddrBook{ + rand: rand.New(rand.NewSource(time.Now().UnixNano())), + ourAddrs: make(map[string]*NetAddress), + addrLookup: make(map[string]*knownAddress), + filePath: filePath, + routabilityStrict: routabilityStrict, + } + am.init() + am.BaseService = *NewBaseService(log, "AddrBook", am) + return am +} + +// When modifying this, don't forget to update loadFromFile() +func (a *AddrBook) init() { + a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits + // New addr buckets + a.addrNew = make([]map[string]*knownAddress, newBucketCount) + for i := range a.addrNew { + a.addrNew[i] = make(map[string]*knownAddress) + } + // Old addr buckets + a.addrOld = make([]map[string]*knownAddress, oldBucketCount) + for i := range a.addrOld { + a.addrOld[i] = make(map[string]*knownAddress) + } +} + +// OnStart implements Service. +func (a *AddrBook) OnStart() error { + a.BaseService.OnStart() + a.loadFromFile(a.filePath) + a.wg.Add(1) + go a.saveRoutine() + return nil +} + +// OnStop implements Service. +func (a *AddrBook) OnStop() { + a.BaseService.OnStop() +} + +func (a *AddrBook) Wait() { + a.wg.Wait() +} + +func (a *AddrBook) AddOurAddress(addr *NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + log.Info("Add our address to book", "addr", addr) + a.ourAddrs[addr.String()] = addr +} + +func (a *AddrBook) OurAddresses() []*NetAddress { + addrs := []*NetAddress{} + for _, addr := range a.ourAddrs { + addrs = append(addrs, addr) + } + return addrs +} + +// NOTE: addr must not be nil +func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + log.Info("Add address to book", "addr", addr, "src", src) + a.addAddress(addr, src) +} + +func (a *AddrBook) NeedMoreAddrs() bool { + return a.Size() < needAddressThreshold +} + +func (a *AddrBook) Size() int { + a.mtx.Lock() + defer a.mtx.Unlock() + return a.size() +} + +func (a *AddrBook) size() int { + return a.nNew + a.nOld +} + +// Pick an address to connect to with new/old bias. +func (a *AddrBook) PickAddress(newBias int) *NetAddress { + a.mtx.Lock() + defer a.mtx.Unlock() + + if a.size() == 0 { + return nil + } + if newBias > 100 { + newBias = 100 + } + if newBias < 0 { + newBias = 0 + } + + // Bias between new and old addresses. + oldCorrelation := math.Sqrt(float64(a.nOld)) * (100.0 - float64(newBias)) + newCorrelation := math.Sqrt(float64(a.nNew)) * float64(newBias) + + if (newCorrelation+oldCorrelation)*a.rand.Float64() < oldCorrelation { + // pick random Old bucket. + var bucket map[string]*knownAddress = nil + for len(bucket) == 0 { + bucket = a.addrOld[a.rand.Intn(len(a.addrOld))] + } + // pick a random ka from bucket. + randIndex := a.rand.Intn(len(bucket)) + for _, ka := range bucket { + if randIndex == 0 { + return ka.Addr + } + randIndex-- + } + PanicSanity("Should not happen") + } else { + // pick random New bucket. + var bucket map[string]*knownAddress = nil + for len(bucket) == 0 { + bucket = a.addrNew[a.rand.Intn(len(a.addrNew))] + } + // pick a random ka from bucket. + randIndex := a.rand.Intn(len(bucket)) + for _, ka := range bucket { + if randIndex == 0 { + return ka.Addr + } + randIndex-- + } + PanicSanity("Should not happen") + } + return nil +} + +func (a *AddrBook) MarkGood(addr *NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + ka := a.addrLookup[addr.String()] + if ka == nil { + return + } + ka.markGood() + if ka.isNew() { + a.moveToOld(ka) + } +} + +func (a *AddrBook) MarkAttempt(addr *NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + ka := a.addrLookup[addr.String()] + if ka == nil { + return + } + ka.markAttempt() +} + +// MarkBad currently just ejects the address. In the future, consider +// blacklisting. +func (a *AddrBook) MarkBad(addr *NetAddress) { + a.RemoveAddress(addr) +} + +// RemoveAddress removes the address from the book. +func (a *AddrBook) RemoveAddress(addr *NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + ka := a.addrLookup[addr.String()] + if ka == nil { + return + } + log.Info("Remove address from book", "addr", addr) + a.removeFromAllBuckets(ka) +} + +/* Peer exchange */ + +// GetSelection randomly selects some addresses (old & new). Suitable for peer-exchange protocols. +func (a *AddrBook) GetSelection() []*NetAddress { + a.mtx.Lock() + defer a.mtx.Unlock() + + if a.size() == 0 { + return nil + } + + allAddr := make([]*NetAddress, a.size()) + i := 0 + for _, v := range a.addrLookup { + allAddr[i] = v.Addr + i++ + } + + numAddresses := MaxInt( + MinInt(minGetSelection, len(allAddr)), + len(allAddr)*getSelectionPercent/100) + numAddresses = MinInt(maxGetSelection, numAddresses) + + // Fisher-Yates shuffle the array. We only need to do the first + // `numAddresses' since we are throwing the rest. + for i := 0; i < numAddresses; i++ { + // pick a number between current index and the end + j := rand.Intn(len(allAddr)-i) + i + allAddr[i], allAddr[j] = allAddr[j], allAddr[i] + } + + // slice off the limit we are willing to share. + return allAddr[:numAddresses] +} + +/* Loading & Saving */ + +type addrBookJSON struct { + Key string + Addrs []*knownAddress +} + +func (a *AddrBook) saveToFile(filePath string) { + log.Info("Saving AddrBook to file", "size", a.Size()) + + a.mtx.Lock() + defer a.mtx.Unlock() + // Compile Addrs + addrs := []*knownAddress{} + for _, ka := range a.addrLookup { + addrs = append(addrs, ka) + } + + aJSON := &addrBookJSON{ + Key: a.key, + Addrs: addrs, + } + + jsonBytes, err := json.MarshalIndent(aJSON, "", "\t") + if err != nil { + log.Error("Failed to save AddrBook to file", "err", err) + return + } + err = WriteFileAtomic(filePath, jsonBytes, 0644) + if err != nil { + log.Error("Failed to save AddrBook to file", "file", filePath, "error", err) + } +} + +// Returns false if file does not exist. +// Panics if file is corrupt. +func (a *AddrBook) loadFromFile(filePath string) bool { + // If doesn't exist, do nothing. + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + + // Load addrBookJSON{} + r, err := os.Open(filePath) + if err != nil { + PanicCrisis(Fmt("Error opening file %s: %v", filePath, err)) + } + defer r.Close() + aJSON := &addrBookJSON{} + dec := json.NewDecoder(r) + err = dec.Decode(aJSON) + if err != nil { + PanicCrisis(Fmt("Error reading file %s: %v", filePath, err)) + } + + // Restore all the fields... + // Restore the key + a.key = aJSON.Key + // Restore .addrNew & .addrOld + for _, ka := range aJSON.Addrs { + for _, bucketIndex := range ka.Buckets { + bucket := a.getBucket(ka.BucketType, bucketIndex) + bucket[ka.Addr.String()] = ka + } + a.addrLookup[ka.Addr.String()] = ka + if ka.BucketType == bucketTypeNew { + a.nNew++ + } else { + a.nOld++ + } + } + return true +} + +// Save saves the book. +func (a *AddrBook) Save() { + log.Info("Saving AddrBook to file", "size", a.Size()) + a.saveToFile(a.filePath) +} + +/* Private methods */ + +func (a *AddrBook) saveRoutine() { + dumpAddressTicker := time.NewTicker(dumpAddressInterval) +out: + for { + select { + case <-dumpAddressTicker.C: + a.saveToFile(a.filePath) + case <-a.Quit: + break out + } + } + dumpAddressTicker.Stop() + a.saveToFile(a.filePath) + a.wg.Done() + log.Notice("Address handler done") +} + +func (a *AddrBook) getBucket(bucketType byte, bucketIdx int) map[string]*knownAddress { + switch bucketType { + case bucketTypeNew: + return a.addrNew[bucketIdx] + case bucketTypeOld: + return a.addrOld[bucketIdx] + default: + PanicSanity("Should not happen") + return nil + } +} + +// Adds ka to new bucket. Returns false if it couldn't do it cuz buckets full. +// NOTE: currently it always returns true. +func (a *AddrBook) addToNewBucket(ka *knownAddress, bucketIdx int) bool { + // Sanity check + if ka.isOld() { + log.Warn(Fmt("Cannot add address already in old bucket to a new bucket: %v", ka)) + return false + } + + addrStr := ka.Addr.String() + bucket := a.getBucket(bucketTypeNew, bucketIdx) + + // Already exists? + if _, ok := bucket[addrStr]; ok { + return true + } + + // Enforce max addresses. + if len(bucket) > newBucketSize { + log.Notice("new bucket is full, expiring old ") + a.expireNew(bucketIdx) + } + + // Add to bucket. + bucket[addrStr] = ka + if ka.addBucketRef(bucketIdx) == 1 { + a.nNew++ + } + + // Ensure in addrLookup + a.addrLookup[addrStr] = ka + + return true +} + +// Adds ka to old bucket. Returns false if it couldn't do it cuz buckets full. +func (a *AddrBook) addToOldBucket(ka *knownAddress, bucketIdx int) bool { + // Sanity check + if ka.isNew() { + log.Warn(Fmt("Cannot add new address to old bucket: %v", ka)) + return false + } + if len(ka.Buckets) != 0 { + log.Warn(Fmt("Cannot add already old address to another old bucket: %v", ka)) + return false + } + + addrStr := ka.Addr.String() + bucket := a.getBucket(bucketTypeNew, bucketIdx) + + // Already exists? + if _, ok := bucket[addrStr]; ok { + return true + } + + // Enforce max addresses. + if len(bucket) > oldBucketSize { + return false + } + + // Add to bucket. + bucket[addrStr] = ka + if ka.addBucketRef(bucketIdx) == 1 { + a.nOld++ + } + + // Ensure in addrLookup + a.addrLookup[addrStr] = ka + + return true +} + +func (a *AddrBook) removeFromBucket(ka *knownAddress, bucketType byte, bucketIdx int) { + if ka.BucketType != bucketType { + log.Warn(Fmt("Bucket type mismatch: %v", ka)) + return + } + bucket := a.getBucket(bucketType, bucketIdx) + delete(bucket, ka.Addr.String()) + if ka.removeBucketRef(bucketIdx) == 0 { + if bucketType == bucketTypeNew { + a.nNew-- + } else { + a.nOld-- + } + delete(a.addrLookup, ka.Addr.String()) + } +} + +func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) { + for _, bucketIdx := range ka.Buckets { + bucket := a.getBucket(ka.BucketType, bucketIdx) + delete(bucket, ka.Addr.String()) + } + ka.Buckets = nil + if ka.BucketType == bucketTypeNew { + a.nNew-- + } else { + a.nOld-- + } + delete(a.addrLookup, ka.Addr.String()) +} + +func (a *AddrBook) pickOldest(bucketType byte, bucketIdx int) *knownAddress { + bucket := a.getBucket(bucketType, bucketIdx) + var oldest *knownAddress + for _, ka := range bucket { + if oldest == nil || ka.LastAttempt.Before(oldest.LastAttempt) { + oldest = ka + } + } + return oldest +} + +func (a *AddrBook) addAddress(addr, src *NetAddress) { + if a.routabilityStrict && !addr.Routable() { + log.Warn(Fmt("Cannot add non-routable address %v", addr)) + return + } + if _, ok := a.ourAddrs[addr.String()]; ok { + // Ignore our own listener address. + return + } + + ka := a.addrLookup[addr.String()] + + if ka != nil { + // Already old. + if ka.isOld() { + return + } + // Already in max new buckets. + if len(ka.Buckets) == maxNewBucketsPerAddress { + return + } + // The more entries we have, the less likely we are to add more. + factor := int32(2 * len(ka.Buckets)) + if a.rand.Int31n(factor) != 0 { + return + } + } else { + ka = newKnownAddress(addr, src) + } + + bucket := a.calcNewBucket(addr, src) + a.addToNewBucket(ka, bucket) + + log.Notice("Added new address", "address", addr, "total", a.size()) +} + +// Make space in the new buckets by expiring the really bad entries. +// If no bad entries are available we remove the oldest. +func (a *AddrBook) expireNew(bucketIdx int) { + for addrStr, ka := range a.addrNew[bucketIdx] { + // If an entry is bad, throw it away + if ka.isBad() { + log.Notice(Fmt("expiring bad address %v", addrStr)) + a.removeFromBucket(ka, bucketTypeNew, bucketIdx) + return + } + } + + // If we haven't thrown out a bad entry, throw out the oldest entry + oldest := a.pickOldest(bucketTypeNew, bucketIdx) + a.removeFromBucket(oldest, bucketTypeNew, bucketIdx) +} + +// Promotes an address from new to old. +// TODO: Move to old probabilistically. +// The better a node is, the less likely it should be evicted from an old bucket. +func (a *AddrBook) moveToOld(ka *knownAddress) { + // Sanity check + if ka.isOld() { + log.Warn(Fmt("Cannot promote address that is already old %v", ka)) + return + } + if len(ka.Buckets) == 0 { + log.Warn(Fmt("Cannot promote address that isn't in any new buckets %v", ka)) + return + } + + // Remember one of the buckets in which ka is in. + freedBucket := ka.Buckets[0] + // Remove from all (new) buckets. + a.removeFromAllBuckets(ka) + // It's officially old now. + ka.BucketType = bucketTypeOld + + // Try to add it to its oldBucket destination. + oldBucketIdx := a.calcOldBucket(ka.Addr) + added := a.addToOldBucket(ka, oldBucketIdx) + if !added { + // No room, must evict something + oldest := a.pickOldest(bucketTypeOld, oldBucketIdx) + a.removeFromBucket(oldest, bucketTypeOld, oldBucketIdx) + // Find new bucket to put oldest in + newBucketIdx := a.calcNewBucket(oldest.Addr, oldest.Src) + added := a.addToNewBucket(oldest, newBucketIdx) + // No space in newBucket either, just put it in freedBucket from above. + if !added { + added := a.addToNewBucket(oldest, freedBucket) + if !added { + log.Warn(Fmt("Could not migrate oldest %v to freedBucket %v", oldest, freedBucket)) + } + } + // Finally, add to bucket again. + added = a.addToOldBucket(ka, oldBucketIdx) + if !added { + log.Warn(Fmt("Could not re-add ka %v to oldBucketIdx %v", ka, oldBucketIdx)) + } + } +} + +// doublesha256( key + sourcegroup + +// int64(doublesha256(key + group + sourcegroup))%bucket_per_group ) % num_new_buckets +func (a *AddrBook) calcNewBucket(addr, src *NetAddress) int { + data1 := []byte{} + data1 = append(data1, []byte(a.key)...) + data1 = append(data1, []byte(a.groupKey(addr))...) + data1 = append(data1, []byte(a.groupKey(src))...) + hash1 := doubleSha256(data1) + hash64 := binary.BigEndian.Uint64(hash1) + hash64 %= newBucketsPerGroup + var hashbuf [8]byte + binary.BigEndian.PutUint64(hashbuf[:], hash64) + data2 := []byte{} + data2 = append(data2, []byte(a.key)...) + data2 = append(data2, a.groupKey(src)...) + data2 = append(data2, hashbuf[:]...) + + hash2 := doubleSha256(data2) + return int(binary.BigEndian.Uint64(hash2) % newBucketCount) +} + +// doublesha256( key + group + +// int64(doublesha256(key + addr))%buckets_per_group ) % num_old_buckets +func (a *AddrBook) calcOldBucket(addr *NetAddress) int { + data1 := []byte{} + data1 = append(data1, []byte(a.key)...) + data1 = append(data1, []byte(addr.String())...) + hash1 := doubleSha256(data1) + hash64 := binary.BigEndian.Uint64(hash1) + hash64 %= oldBucketsPerGroup + var hashbuf [8]byte + binary.BigEndian.PutUint64(hashbuf[:], hash64) + data2 := []byte{} + data2 = append(data2, []byte(a.key)...) + data2 = append(data2, a.groupKey(addr)...) + data2 = append(data2, hashbuf[:]...) + + hash2 := doubleSha256(data2) + return int(binary.BigEndian.Uint64(hash2) % oldBucketCount) +} + +// Return a string representing the network group of this address. +// This is the /16 for IPv6, the /32 (/36 for he.net) for IPv6, the string +// "local" for a local address and the string "unroutable for an unroutable +// address. +func (a *AddrBook) groupKey(na *NetAddress) string { + if a.routabilityStrict && na.Local() { + return "local" + } + if a.routabilityStrict && !na.Routable() { + return "unroutable" + } + + if ipv4 := na.IP.To4(); ipv4 != nil { + return (&net.IPNet{IP: na.IP, Mask: net.CIDRMask(16, 32)}).String() + } + if na.RFC6145() || na.RFC6052() { + // last four bytes are the ip address + ip := net.IP(na.IP[12:16]) + return (&net.IPNet{IP: ip, Mask: net.CIDRMask(16, 32)}).String() + } + + if na.RFC3964() { + ip := net.IP(na.IP[2:7]) + return (&net.IPNet{IP: ip, Mask: net.CIDRMask(16, 32)}).String() + + } + if na.RFC4380() { + // teredo tunnels have the last 4 bytes as the v4 address XOR + // 0xff. + ip := net.IP(make([]byte, 4)) + for i, byte := range na.IP[12:16] { + ip[i] = byte ^ 0xff + } + return (&net.IPNet{IP: ip, Mask: net.CIDRMask(16, 32)}).String() + } + + // OK, so now we know ourselves to be a IPv6 address. + // bitcoind uses /32 for everything, except for Hurricane Electric's + // (he.net) IP range, which it uses /36 for. + bits := 32 + heNet := &net.IPNet{IP: net.ParseIP("2001:470::"), + Mask: net.CIDRMask(32, 128)} + if heNet.Contains(na.IP) { + bits = 36 + } + + return (&net.IPNet{IP: na.IP, Mask: net.CIDRMask(bits, 128)}).String() +} + +//----------------------------------------------------------------------------- + +/* + knownAddress + + tracks information about a known network address that is used + to determine how viable an address is. +*/ +type knownAddress struct { + Addr *NetAddress + Src *NetAddress + Attempts int32 + LastAttempt time.Time + LastSuccess time.Time + BucketType byte + Buckets []int +} + +func newKnownAddress(addr *NetAddress, src *NetAddress) *knownAddress { + return &knownAddress{ + Addr: addr, + Src: src, + Attempts: 0, + LastAttempt: time.Now(), + BucketType: bucketTypeNew, + Buckets: nil, + } +} + +func (ka *knownAddress) isOld() bool { + return ka.BucketType == bucketTypeOld +} + +func (ka *knownAddress) isNew() bool { + return ka.BucketType == bucketTypeNew +} + +func (ka *knownAddress) markAttempt() { + now := time.Now() + ka.LastAttempt = now + ka.Attempts += 1 +} + +func (ka *knownAddress) markGood() { + now := time.Now() + ka.LastAttempt = now + ka.Attempts = 0 + ka.LastSuccess = now +} + +func (ka *knownAddress) addBucketRef(bucketIdx int) int { + for _, bucket := range ka.Buckets { + if bucket == bucketIdx { + log.Warn(Fmt("Bucket already exists in ka.Buckets: %v", ka)) + return -1 + } + } + ka.Buckets = append(ka.Buckets, bucketIdx) + return len(ka.Buckets) +} + +func (ka *knownAddress) removeBucketRef(bucketIdx int) int { + buckets := []int{} + for _, bucket := range ka.Buckets { + if bucket != bucketIdx { + buckets = append(buckets, bucket) + } + } + if len(buckets) != len(ka.Buckets)-1 { + log.Warn(Fmt("bucketIdx not found in ka.Buckets: %v", ka)) + return -1 + } + ka.Buckets = buckets + return len(ka.Buckets) +} + +/* + An address is bad if the address in question has not been tried in the last + minute and meets one of the following criteria: + + 1) It claims to be from the future + 2) It hasn't been seen in over a month + 3) It has failed at least three times and never succeeded + 4) It has failed ten times in the last week + + All addresses that meet these criteria are assumed to be worthless and not + worth keeping hold of. +*/ +func (ka *knownAddress) isBad() bool { + // Has been attempted in the last minute --> good + if ka.LastAttempt.Before(time.Now().Add(-1 * time.Minute)) { + return false + } + + // Over a month old? + if ka.LastAttempt.After(time.Now().Add(-1 * numMissingDays * time.Hour * 24)) { + return true + } + + // Never succeeded? + if ka.LastSuccess.IsZero() && ka.Attempts >= numRetries { + return true + } + + // Hasn't succeeded in too long? + if ka.LastSuccess.Before(time.Now().Add(-1*minBadDays*time.Hour*24)) && + ka.Attempts >= maxFailures { + return true + } + + return false +} diff --git a/p2p/addrbook_test.go b/p2p/addrbook_test.go new file mode 100644 index 00000000..16aea8ef --- /dev/null +++ b/p2p/addrbook_test.go @@ -0,0 +1,166 @@ +package p2p + +import ( + "fmt" + "io/ioutil" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +func createTempFileName(prefix string) string { + f, err := ioutil.TempFile("", prefix) + if err != nil { + panic(err) + } + fname := f.Name() + err = f.Close() + if err != nil { + panic(err) + } + return fname +} + +func TestAddrBookSaveLoad(t *testing.T) { + fname := createTempFileName("addrbook_test") + + // 0 addresses + book := NewAddrBook(fname, true) + book.saveToFile(fname) + + book = NewAddrBook(fname, true) + book.loadFromFile(fname) + + assert.Zero(t, book.Size()) + + // 100 addresses + randAddrs := randNetAddressPairs(t, 100) + + for _, addrSrc := range randAddrs { + book.AddAddress(addrSrc.addr, addrSrc.src) + } + + assert.Equal(t, 100, book.Size()) + book.saveToFile(fname) + + book = NewAddrBook(fname, true) + book.loadFromFile(fname) + + assert.Equal(t, 100, book.Size()) +} + +func TestAddrBookLookup(t *testing.T) { + fname := createTempFileName("addrbook_test") + + randAddrs := randNetAddressPairs(t, 100) + + book := NewAddrBook(fname, true) + for _, addrSrc := range randAddrs { + addr := addrSrc.addr + src := addrSrc.src + book.AddAddress(addr, src) + + ka := book.addrLookup[addr.String()] + assert.NotNil(t, ka, "Expected to find KnownAddress %v but wasn't there.", addr) + + if !(ka.Addr.Equals(addr) && ka.Src.Equals(src)) { + t.Fatalf("KnownAddress doesn't match addr & src") + } + } +} + +func TestAddrBookPromoteToOld(t *testing.T) { + fname := createTempFileName("addrbook_test") + + randAddrs := randNetAddressPairs(t, 100) + + book := NewAddrBook(fname, true) + for _, addrSrc := range randAddrs { + book.AddAddress(addrSrc.addr, addrSrc.src) + } + + // Attempt all addresses. + for _, addrSrc := range randAddrs { + book.MarkAttempt(addrSrc.addr) + } + + // Promote half of them + for i, addrSrc := range randAddrs { + if i%2 == 0 { + book.MarkGood(addrSrc.addr) + } + } + + // TODO: do more testing :) + + selection := book.GetSelection() + t.Logf("selection: %v", selection) + + if len(selection) > book.Size() { + t.Errorf("selection could not be bigger than the book") + } +} + +func TestAddrBookHandlesDuplicates(t *testing.T) { + fname := createTempFileName("addrbook_test") + + book := NewAddrBook(fname, true) + + randAddrs := randNetAddressPairs(t, 100) + + differentSrc := randIPv4Address(t) + for _, addrSrc := range randAddrs { + book.AddAddress(addrSrc.addr, addrSrc.src) + book.AddAddress(addrSrc.addr, addrSrc.src) // duplicate + book.AddAddress(addrSrc.addr, differentSrc) // different src + } + + assert.Equal(t, 100, book.Size()) +} + +type netAddressPair struct { + addr *NetAddress + src *NetAddress +} + +func randNetAddressPairs(t *testing.T, n int) []netAddressPair { + randAddrs := make([]netAddressPair, n) + for i := 0; i < n; i++ { + randAddrs[i] = netAddressPair{addr: randIPv4Address(t), src: randIPv4Address(t)} + } + return randAddrs +} + +func randIPv4Address(t *testing.T) *NetAddress { + for { + ip := fmt.Sprintf("%v.%v.%v.%v", + rand.Intn(254)+1, + rand.Intn(255), + rand.Intn(255), + rand.Intn(255), + ) + port := rand.Intn(65535-1) + 1 + addr, err := NewNetAddressString(fmt.Sprintf("%v:%v", ip, port)) + assert.Nil(t, err, "error generating rand network address") + if addr.Routable() { + return addr + } + } +} + +func TestAddrBookRemoveAddress(t *testing.T) { + fname := createTempFileName("addrbook_test") + book := NewAddrBook(fname, true) + + addr := randIPv4Address(t) + book.AddAddress(addr, addr) + assert.Equal(t, 1, book.Size()) + + book.RemoveAddress(addr) + assert.Equal(t, 0, book.Size()) + + nonExistingAddr := randIPv4Address(t) + book.RemoveAddress(nonExistingAddr) + assert.Equal(t, 0, book.Size()) +} diff --git a/p2p/config.go b/p2p/config.go new file mode 100644 index 00000000..2e70d16d --- /dev/null +++ b/p2p/config.go @@ -0,0 +1,45 @@ +package p2p + +import ( + "github.com/spf13/viper" +) + +const ( + // Switch config keys + configKeyDialTimeoutSeconds = "dial_timeout_seconds" + configKeyHandshakeTimeoutSeconds = "handshake_timeout_seconds" + configKeyMaxNumPeers = "max_num_peers" + configKeyAuthEnc = "authenticated_encryption" + + // MConnection config keys + configKeySendRate = "send_rate" + configKeyRecvRate = "recv_rate" + + // Fuzz params + configFuzzEnable = "fuzz_enable" // use the fuzz wrapped conn + configFuzzMode = "fuzz_mode" // eg. drop, delay + configFuzzMaxDelayMilliseconds = "fuzz_max_delay_milliseconds" + configFuzzProbDropRW = "fuzz_prob_drop_rw" + configFuzzProbDropConn = "fuzz_prob_drop_conn" + configFuzzProbSleep = "fuzz_prob_sleep" +) + +func setConfigDefaults(config *viper.Viper) { + // Switch default config + config.SetDefault(configKeyDialTimeoutSeconds, 3) + config.SetDefault(configKeyHandshakeTimeoutSeconds, 20) + config.SetDefault(configKeyMaxNumPeers, 50) + config.SetDefault(configKeyAuthEnc, true) + + // MConnection default config + config.SetDefault(configKeySendRate, 512000) // 500KB/s + config.SetDefault(configKeyRecvRate, 512000) // 500KB/s + + // Fuzz defaults + config.SetDefault(configFuzzEnable, false) + config.SetDefault(configFuzzMode, FuzzModeDrop) + config.SetDefault(configFuzzMaxDelayMilliseconds, 3000) + config.SetDefault(configFuzzProbDropRW, 0.2) + config.SetDefault(configFuzzProbDropConn, 0.00) + config.SetDefault(configFuzzProbSleep, 0.00) +} diff --git a/p2p/connection.go b/p2p/connection.go new file mode 100644 index 00000000..629ab7b0 --- /dev/null +++ b/p2p/connection.go @@ -0,0 +1,686 @@ +package p2p + +import ( + "bufio" + "fmt" + "io" + "math" + "net" + "runtime/debug" + "sync/atomic" + "time" + + wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + flow "github.com/tendermint/tmlibs/flowrate" +) + +const ( + numBatchMsgPackets = 10 + minReadBufferSize = 1024 + minWriteBufferSize = 65536 + updateState = 2 * time.Second + pingTimeout = 40 * time.Second + flushThrottle = 100 * time.Millisecond + + defaultSendQueueCapacity = 1 + defaultSendRate = int64(512000) // 500KB/s + defaultRecvBufferCapacity = 4096 + defaultRecvMessageCapacity = 22020096 // 21MB + defaultRecvRate = int64(512000) // 500KB/s + defaultSendTimeout = 10 * time.Second +) + +type receiveCbFunc func(chID byte, msgBytes []byte) +type errorCbFunc func(interface{}) + +/* +Each peer has one `MConnection` (multiplex connection) instance. + +__multiplex__ *noun* a system or signal involving simultaneous transmission of +several messages along a single channel of communication. + +Each `MConnection` handles message transmission on multiple abstract communication +`Channel`s. Each channel has a globally unique byte id. +The byte id and the relative priorities of each `Channel` are configured upon +initialization of the connection. + +There are two methods for sending messages: + func (m MConnection) Send(chID byte, msg interface{}) bool {} + func (m MConnection) TrySend(chID byte, msg interface{}) bool {} + +`Send(chID, msg)` is a blocking call that waits until `msg` is successfully queued +for the channel with the given id byte `chID`, or until the request times out. +The message `msg` is serialized using the `tendermint/wire` submodule's +`WriteBinary()` reflection routine. + +`TrySend(chID, msg)` is a nonblocking call that returns false if the channel's +queue is full. + +Inbound message bytes are handled with an onReceive callback function. +*/ +type MConnection struct { + cmn.BaseService + + conn net.Conn + bufReader *bufio.Reader + bufWriter *bufio.Writer + sendMonitor *flow.Monitor + recvMonitor *flow.Monitor + send chan struct{} + pong chan struct{} + channels []*Channel + channelsIdx map[byte]*Channel + onReceive receiveCbFunc + onError errorCbFunc + errored uint32 + config *MConnConfig + + quit chan struct{} + flushTimer *cmn.ThrottleTimer // flush writes as necessary but throttled. + pingTimer *cmn.RepeatTimer // send pings periodically + chStatsTimer *cmn.RepeatTimer // update channel stats periodically + + LocalAddress *NetAddress + RemoteAddress *NetAddress +} + +// MConnConfig is a MConnection configuration. +type MConnConfig struct { + SendRate int64 + RecvRate int64 +} + +// DefaultMConnConfig returns the default config. +func DefaultMConnConfig() *MConnConfig { + return &MConnConfig{ + SendRate: defaultSendRate, + RecvRate: defaultRecvRate, + } +} + +// NewMConnection wraps net.Conn and creates multiplex connection +func NewMConnection(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc) *MConnection { + return NewMConnectionWithConfig( + conn, + chDescs, + onReceive, + onError, + DefaultMConnConfig()) +} + +// NewMConnectionWithConfig wraps net.Conn and creates multiplex connection with a config +func NewMConnectionWithConfig(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc, config *MConnConfig) *MConnection { + mconn := &MConnection{ + conn: conn, + bufReader: bufio.NewReaderSize(conn, minReadBufferSize), + bufWriter: bufio.NewWriterSize(conn, minWriteBufferSize), + sendMonitor: flow.New(0, 0), + recvMonitor: flow.New(0, 0), + send: make(chan struct{}, 1), + pong: make(chan struct{}), + onReceive: onReceive, + onError: onError, + config: config, + + LocalAddress: NewNetAddress(conn.LocalAddr()), + RemoteAddress: NewNetAddress(conn.RemoteAddr()), + } + + // Create channels + var channelsIdx = map[byte]*Channel{} + var channels = []*Channel{} + + for _, desc := range chDescs { + descCopy := *desc // copy the desc else unsafe access across connections + channel := newChannel(mconn, &descCopy) + channelsIdx[channel.id] = channel + channels = append(channels, channel) + } + mconn.channels = channels + mconn.channelsIdx = channelsIdx + + mconn.BaseService = *cmn.NewBaseService(log, "MConnection", mconn) + + return mconn +} + +func (c *MConnection) OnStart() error { + c.BaseService.OnStart() + c.quit = make(chan struct{}) + c.flushTimer = cmn.NewThrottleTimer("flush", flushThrottle) + c.pingTimer = cmn.NewRepeatTimer("ping", pingTimeout) + c.chStatsTimer = cmn.NewRepeatTimer("chStats", updateState) + go c.sendRoutine() + go c.recvRoutine() + return nil +} + +func (c *MConnection) OnStop() { + c.BaseService.OnStop() + c.flushTimer.Stop() + c.pingTimer.Stop() + c.chStatsTimer.Stop() + if c.quit != nil { + close(c.quit) + } + c.conn.Close() + // We can't close pong safely here because + // recvRoutine may write to it after we've stopped. + // Though it doesn't need to get closed at all, + // we close it @ recvRoutine. + // close(c.pong) +} + +func (c *MConnection) String() string { + return fmt.Sprintf("MConn{%v}", c.conn.RemoteAddr()) +} + +func (c *MConnection) flush() { + log.Debug("Flush", "conn", c) + err := c.bufWriter.Flush() + if err != nil { + log.Warn("MConnection flush failed", "error", err) + } +} + +// Catch panics, usually caused by remote disconnects. +func (c *MConnection) _recover() { + if r := recover(); r != nil { + stack := debug.Stack() + err := cmn.StackError{r, stack} + c.stopForError(err) + } +} + +func (c *MConnection) stopForError(r interface{}) { + c.Stop() + if atomic.CompareAndSwapUint32(&c.errored, 0, 1) { + if c.onError != nil { + c.onError(r) + } + } +} + +// Queues a message to be sent to channel. +func (c *MConnection) Send(chID byte, msg interface{}) bool { + if !c.IsRunning() { + return false + } + + log.Debug("Send", "channel", chID, "conn", c, "msg", msg) //, "bytes", wire.BinaryBytes(msg)) + + // Send message to channel. + channel, ok := c.channelsIdx[chID] + if !ok { + log.Error(cmn.Fmt("Cannot send bytes, unknown channel %X", chID)) + return false + } + + success := channel.sendBytes(wire.BinaryBytes(msg)) + if success { + // Wake up sendRoutine if necessary + select { + case c.send <- struct{}{}: + default: + } + } else { + log.Warn("Send failed", "channel", chID, "conn", c, "msg", msg) + } + return success +} + +// Queues a message to be sent to channel. +// Nonblocking, returns true if successful. +func (c *MConnection) TrySend(chID byte, msg interface{}) bool { + if !c.IsRunning() { + return false + } + + log.Debug("TrySend", "channel", chID, "conn", c, "msg", msg) + + // Send message to channel. + channel, ok := c.channelsIdx[chID] + if !ok { + log.Error(cmn.Fmt("Cannot send bytes, unknown channel %X", chID)) + return false + } + + ok = channel.trySendBytes(wire.BinaryBytes(msg)) + if ok { + // Wake up sendRoutine if necessary + select { + case c.send <- struct{}{}: + default: + } + } + + return ok +} + +// CanSend returns true if you can send more data onto the chID, false +// otherwise. Use only as a heuristic. +func (c *MConnection) CanSend(chID byte) bool { + if !c.IsRunning() { + return false + } + + channel, ok := c.channelsIdx[chID] + if !ok { + log.Error(cmn.Fmt("Unknown channel %X", chID)) + return false + } + return channel.canSend() +} + +// sendRoutine polls for packets to send from channels. +func (c *MConnection) sendRoutine() { + defer c._recover() + +FOR_LOOP: + for { + var n int + var err error + select { + case <-c.flushTimer.Ch: + // NOTE: flushTimer.Set() must be called every time + // something is written to .bufWriter. + c.flush() + case <-c.chStatsTimer.Ch: + for _, channel := range c.channels { + channel.updateStats() + } + case <-c.pingTimer.Ch: + log.Debug("Send Ping") + wire.WriteByte(packetTypePing, c.bufWriter, &n, &err) + c.sendMonitor.Update(int(n)) + c.flush() + case <-c.pong: + log.Debug("Send Pong") + wire.WriteByte(packetTypePong, c.bufWriter, &n, &err) + c.sendMonitor.Update(int(n)) + c.flush() + case <-c.quit: + break FOR_LOOP + case <-c.send: + // Send some msgPackets + eof := c.sendSomeMsgPackets() + if !eof { + // Keep sendRoutine awake. + select { + case c.send <- struct{}{}: + default: + } + } + } + + if !c.IsRunning() { + break FOR_LOOP + } + if err != nil { + log.Warn("Connection failed @ sendRoutine", "conn", c, "error", err) + c.stopForError(err) + break FOR_LOOP + } + } + + // Cleanup +} + +// Returns true if messages from channels were exhausted. +// Blocks in accordance to .sendMonitor throttling. +func (c *MConnection) sendSomeMsgPackets() bool { + // Block until .sendMonitor says we can write. + // Once we're ready we send more than we asked for, + // but amortized it should even out. + c.sendMonitor.Limit(maxMsgPacketTotalSize, atomic.LoadInt64(&c.config.SendRate), true) + + // Now send some msgPackets. + for i := 0; i < numBatchMsgPackets; i++ { + if c.sendMsgPacket() { + return true + } + } + return false +} + +// Returns true if messages from channels were exhausted. +func (c *MConnection) sendMsgPacket() bool { + // Choose a channel to create a msgPacket from. + // The chosen channel will be the one whose recentlySent/priority is the least. + var leastRatio float32 = math.MaxFloat32 + var leastChannel *Channel + for _, channel := range c.channels { + // If nothing to send, skip this channel + if !channel.isSendPending() { + continue + } + // Get ratio, and keep track of lowest ratio. + ratio := float32(channel.recentlySent) / float32(channel.priority) + if ratio < leastRatio { + leastRatio = ratio + leastChannel = channel + } + } + + // Nothing to send? + if leastChannel == nil { + return true + } else { + // log.Info("Found a msgPacket to send") + } + + // Make & send a msgPacket from this channel + n, err := leastChannel.writeMsgPacketTo(c.bufWriter) + if err != nil { + log.Warn("Failed to write msgPacket", "error", err) + c.stopForError(err) + return true + } + c.sendMonitor.Update(int(n)) + c.flushTimer.Set() + return false +} + +// recvRoutine reads msgPackets and reconstructs the message using the channels' "recving" buffer. +// After a whole message has been assembled, it's pushed to onReceive(). +// Blocks depending on how the connection is throttled. +func (c *MConnection) recvRoutine() { + defer c._recover() + +FOR_LOOP: + for { + // Block until .recvMonitor says we can read. + c.recvMonitor.Limit(maxMsgPacketTotalSize, atomic.LoadInt64(&c.config.RecvRate), true) + + /* + // Peek into bufReader for debugging + if numBytes := c.bufReader.Buffered(); numBytes > 0 { + log.Info("Peek connection buffer", "numBytes", numBytes, "bytes", log15.Lazy{func() []byte { + bytes, err := c.bufReader.Peek(MinInt(numBytes, 100)) + if err == nil { + return bytes + } else { + log.Warn("Error peeking connection buffer", "error", err) + return nil + } + }}) + } + */ + + // Read packet type + var n int + var err error + pktType := wire.ReadByte(c.bufReader, &n, &err) + c.recvMonitor.Update(int(n)) + if err != nil { + if c.IsRunning() { + log.Warn("Connection failed @ recvRoutine (reading byte)", "conn", c, "error", err) + c.stopForError(err) + } + break FOR_LOOP + } + + // Read more depending on packet type. + switch pktType { + case packetTypePing: + // TODO: prevent abuse, as they cause flush()'s. + log.Debug("Receive Ping") + c.pong <- struct{}{} + case packetTypePong: + // do nothing + log.Debug("Receive Pong") + case packetTypeMsg: + pkt, n, err := msgPacket{}, int(0), error(nil) + wire.ReadBinaryPtr(&pkt, c.bufReader, maxMsgPacketTotalSize, &n, &err) + c.recvMonitor.Update(int(n)) + if err != nil { + if c.IsRunning() { + log.Warn("Connection failed @ recvRoutine", "conn", c, "error", err) + c.stopForError(err) + } + break FOR_LOOP + } + channel, ok := c.channelsIdx[pkt.ChannelID] + if !ok || channel == nil { + cmn.PanicQ(cmn.Fmt("Unknown channel %X", pkt.ChannelID)) + } + msgBytes, err := channel.recvMsgPacket(pkt) + if err != nil { + if c.IsRunning() { + log.Warn("Connection failed @ recvRoutine", "conn", c, "error", err) + c.stopForError(err) + } + break FOR_LOOP + } + if msgBytes != nil { + log.Debug("Received bytes", "chID", pkt.ChannelID, "msgBytes", msgBytes) + c.onReceive(pkt.ChannelID, msgBytes) + } + default: + cmn.PanicSanity(cmn.Fmt("Unknown message type %X", pktType)) + } + + // TODO: shouldn't this go in the sendRoutine? + // Better to send a ping packet when *we* haven't sent anything for a while. + c.pingTimer.Reset() + } + + // Cleanup + close(c.pong) + for _ = range c.pong { + // Drain + } +} + +type ConnectionStatus struct { + SendMonitor flow.Status + RecvMonitor flow.Status + Channels []ChannelStatus +} + +type ChannelStatus struct { + ID byte + SendQueueCapacity int + SendQueueSize int + Priority int + RecentlySent int64 +} + +func (c *MConnection) Status() ConnectionStatus { + var status ConnectionStatus + status.SendMonitor = c.sendMonitor.Status() + status.RecvMonitor = c.recvMonitor.Status() + status.Channels = make([]ChannelStatus, len(c.channels)) + for i, channel := range c.channels { + status.Channels[i] = ChannelStatus{ + ID: channel.id, + SendQueueCapacity: cap(channel.sendQueue), + SendQueueSize: int(channel.sendQueueSize), // TODO use atomic + Priority: channel.priority, + RecentlySent: channel.recentlySent, + } + } + return status +} + +//----------------------------------------------------------------------------- + +type ChannelDescriptor struct { + ID byte + Priority int + SendQueueCapacity int + RecvBufferCapacity int + RecvMessageCapacity int +} + +func (chDesc *ChannelDescriptor) FillDefaults() { + if chDesc.SendQueueCapacity == 0 { + chDesc.SendQueueCapacity = defaultSendQueueCapacity + } + if chDesc.RecvBufferCapacity == 0 { + chDesc.RecvBufferCapacity = defaultRecvBufferCapacity + } + if chDesc.RecvMessageCapacity == 0 { + chDesc.RecvMessageCapacity = defaultRecvMessageCapacity + } +} + +// TODO: lowercase. +// NOTE: not goroutine-safe. +type Channel struct { + conn *MConnection + desc *ChannelDescriptor + id byte + sendQueue chan []byte + sendQueueSize int32 // atomic. + recving []byte + sending []byte + priority int + recentlySent int64 // exponential moving average +} + +func newChannel(conn *MConnection, desc *ChannelDescriptor) *Channel { + desc.FillDefaults() + if desc.Priority <= 0 { + cmn.PanicSanity("Channel default priority must be a postive integer") + } + return &Channel{ + conn: conn, + desc: desc, + id: desc.ID, + sendQueue: make(chan []byte, desc.SendQueueCapacity), + recving: make([]byte, 0, desc.RecvBufferCapacity), + priority: desc.Priority, + } +} + +// Queues message to send to this channel. +// Goroutine-safe +// Times out (and returns false) after defaultSendTimeout +func (ch *Channel) sendBytes(bytes []byte) bool { + select { + case ch.sendQueue <- bytes: + atomic.AddInt32(&ch.sendQueueSize, 1) + return true + case <-time.After(defaultSendTimeout): + return false + } +} + +// Queues message to send to this channel. +// Nonblocking, returns true if successful. +// Goroutine-safe +func (ch *Channel) trySendBytes(bytes []byte) bool { + select { + case ch.sendQueue <- bytes: + atomic.AddInt32(&ch.sendQueueSize, 1) + return true + default: + return false + } +} + +// Goroutine-safe +func (ch *Channel) loadSendQueueSize() (size int) { + return int(atomic.LoadInt32(&ch.sendQueueSize)) +} + +// Goroutine-safe +// Use only as a heuristic. +func (ch *Channel) canSend() bool { + return ch.loadSendQueueSize() < defaultSendQueueCapacity +} + +// Returns true if any msgPackets are pending to be sent. +// Call before calling nextMsgPacket() +// Goroutine-safe +func (ch *Channel) isSendPending() bool { + if len(ch.sending) == 0 { + if len(ch.sendQueue) == 0 { + return false + } + ch.sending = <-ch.sendQueue + } + return true +} + +// Creates a new msgPacket to send. +// Not goroutine-safe +func (ch *Channel) nextMsgPacket() msgPacket { + packet := msgPacket{} + packet.ChannelID = byte(ch.id) + packet.Bytes = ch.sending[:cmn.MinInt(maxMsgPacketPayloadSize, len(ch.sending))] + if len(ch.sending) <= maxMsgPacketPayloadSize { + packet.EOF = byte(0x01) + ch.sending = nil + atomic.AddInt32(&ch.sendQueueSize, -1) // decrement sendQueueSize + } else { + packet.EOF = byte(0x00) + ch.sending = ch.sending[cmn.MinInt(maxMsgPacketPayloadSize, len(ch.sending)):] + } + return packet +} + +// Writes next msgPacket to w. +// Not goroutine-safe +func (ch *Channel) writeMsgPacketTo(w io.Writer) (n int, err error) { + packet := ch.nextMsgPacket() + log.Debug("Write Msg Packet", "conn", ch.conn, "packet", packet) + wire.WriteByte(packetTypeMsg, w, &n, &err) + wire.WriteBinary(packet, w, &n, &err) + if err == nil { + ch.recentlySent += int64(n) + } + return +} + +// Handles incoming msgPackets. Returns a msg bytes if msg is complete. +// Not goroutine-safe +func (ch *Channel) recvMsgPacket(packet msgPacket) ([]byte, error) { + // log.Debug("Read Msg Packet", "conn", ch.conn, "packet", packet) + if ch.desc.RecvMessageCapacity < len(ch.recving)+len(packet.Bytes) { + return nil, wire.ErrBinaryReadOverflow + } + ch.recving = append(ch.recving, packet.Bytes...) + if packet.EOF == byte(0x01) { + msgBytes := ch.recving + // clear the slice without re-allocating. + // http://stackoverflow.com/questions/16971741/how-do-you-clear-a-slice-in-go + // suggests this could be a memory leak, but we might as well keep the memory for the channel until it closes, + // at which point the recving slice stops being used and should be garbage collected + ch.recving = ch.recving[:0] // make([]byte, 0, ch.desc.RecvBufferCapacity) + return msgBytes, nil + } + return nil, nil +} + +// Call this periodically to update stats for throttling purposes. +// Not goroutine-safe +func (ch *Channel) updateStats() { + // Exponential decay of stats. + // TODO: optimize. + ch.recentlySent = int64(float64(ch.recentlySent) * 0.8) +} + +//----------------------------------------------------------------------------- + +const ( + maxMsgPacketPayloadSize = 1024 + maxMsgPacketOverheadSize = 10 // It's actually lower but good enough + maxMsgPacketTotalSize = maxMsgPacketPayloadSize + maxMsgPacketOverheadSize + packetTypePing = byte(0x01) + packetTypePong = byte(0x02) + packetTypeMsg = byte(0x03) +) + +// Messages in channels are chopped into smaller msgPackets for multiplexing. +type msgPacket struct { + ChannelID byte + EOF byte // 1 means message ends here. + Bytes []byte +} + +func (p msgPacket) String() string { + return fmt.Sprintf("MsgPacket{%X:%X T:%X}", p.ChannelID, p.Bytes, p.EOF) +} diff --git a/p2p/connection_test.go b/p2p/connection_test.go new file mode 100644 index 00000000..993d2d73 --- /dev/null +++ b/p2p/connection_test.go @@ -0,0 +1,139 @@ +package p2p_test + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + p2p "github.com/tendermint/tendermint/p2p" +) + +func createMConnection(conn net.Conn) *p2p.MConnection { + onReceive := func(chID byte, msgBytes []byte) { + } + onError := func(r interface{}) { + } + return createMConnectionWithCallbacks(conn, onReceive, onError) +} + +func createMConnectionWithCallbacks(conn net.Conn, onReceive func(chID byte, msgBytes []byte), onError func(r interface{})) *p2p.MConnection { + chDescs := []*p2p.ChannelDescriptor{&p2p.ChannelDescriptor{ID: 0x01, Priority: 1, SendQueueCapacity: 1}} + return p2p.NewMConnection(conn, chDescs, onReceive, onError) +} + +func TestMConnectionSend(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + mconn := createMConnection(client) + _, err := mconn.Start() + require.Nil(err) + defer mconn.Stop() + + msg := "Ant-Man" + assert.True(mconn.Send(0x01, msg)) + // Note: subsequent Send/TrySend calls could pass because we are reading from + // the send queue in a separate goroutine. + server.Read(make([]byte, len(msg))) + assert.True(mconn.CanSend(0x01)) + + msg = "Spider-Man" + assert.True(mconn.TrySend(0x01, msg)) + server.Read(make([]byte, len(msg))) + + assert.False(mconn.CanSend(0x05), "CanSend should return false because channel is unknown") + assert.False(mconn.Send(0x05, "Absorbing Man"), "Send should return false because channel is unknown") +} + +func TestMConnectionReceive(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + receivedCh := make(chan []byte) + errorsCh := make(chan interface{}) + onReceive := func(chID byte, msgBytes []byte) { + receivedCh <- msgBytes + } + onError := func(r interface{}) { + errorsCh <- r + } + mconn1 := createMConnectionWithCallbacks(client, onReceive, onError) + _, err := mconn1.Start() + require.Nil(err) + defer mconn1.Stop() + + mconn2 := createMConnection(server) + _, err = mconn2.Start() + require.Nil(err) + defer mconn2.Stop() + + msg := "Cyclops" + assert.True(mconn2.Send(0x01, msg)) + + select { + case receivedBytes := <-receivedCh: + assert.Equal([]byte(msg), receivedBytes[2:]) // first 3 bytes are internal + case err := <-errorsCh: + t.Fatalf("Expected %s, got %+v", msg, err) + case <-time.After(500 * time.Millisecond): + t.Fatalf("Did not receive %s message in 500ms", msg) + } +} + +func TestMConnectionStatus(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + mconn := createMConnection(client) + _, err := mconn.Start() + require.Nil(err) + defer mconn.Stop() + + status := mconn.Status() + assert.NotNil(status) + assert.Zero(status.Channels[0].SendQueueSize) +} + +func TestMConnectionStopsAndReturnsError(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + receivedCh := make(chan []byte) + errorsCh := make(chan interface{}) + onReceive := func(chID byte, msgBytes []byte) { + receivedCh <- msgBytes + } + onError := func(r interface{}) { + errorsCh <- r + } + mconn := createMConnectionWithCallbacks(client, onReceive, onError) + _, err := mconn.Start() + require.Nil(err) + defer mconn.Stop() + + client.Close() + + select { + case receivedBytes := <-receivedCh: + t.Fatalf("Expected error, got %v", receivedBytes) + case err := <-errorsCh: + assert.NotNil(err) + assert.False(mconn.IsRunning()) + case <-time.After(500 * time.Millisecond): + t.Fatal("Did not receive error in 500ms") + } +} diff --git a/p2p/fuzz.go b/p2p/fuzz.go new file mode 100644 index 00000000..aefac986 --- /dev/null +++ b/p2p/fuzz.go @@ -0,0 +1,173 @@ +package p2p + +import ( + "math/rand" + "net" + "sync" + "time" +) + +const ( + // FuzzModeDrop is a mode in which we randomly drop reads/writes, connections or sleep + FuzzModeDrop = iota + // FuzzModeDelay is a mode in which we randomly sleep + FuzzModeDelay +) + +// FuzzedConnection wraps any net.Conn and depending on the mode either delays +// reads/writes or randomly drops reads/writes/connections. +type FuzzedConnection struct { + conn net.Conn + + mtx sync.Mutex + start <-chan time.Time + active bool + + config *FuzzConnConfig +} + +// FuzzConnConfig is a FuzzedConnection configuration. +type FuzzConnConfig struct { + Mode int + MaxDelay time.Duration + ProbDropRW float64 + ProbDropConn float64 + ProbSleep float64 +} + +// DefaultFuzzConnConfig returns the default config. +func DefaultFuzzConnConfig() *FuzzConnConfig { + return &FuzzConnConfig{ + Mode: FuzzModeDrop, + MaxDelay: 3 * time.Second, + ProbDropRW: 0.2, + ProbDropConn: 0.00, + ProbSleep: 0.00, + } +} + +// FuzzConn creates a new FuzzedConnection. Fuzzing starts immediately. +func FuzzConn(conn net.Conn) net.Conn { + return FuzzConnFromConfig(conn, DefaultFuzzConnConfig()) +} + +// FuzzConnFromConfig creates a new FuzzedConnection from a config. Fuzzing +// starts immediately. +func FuzzConnFromConfig(conn net.Conn, config *FuzzConnConfig) net.Conn { + return &FuzzedConnection{ + conn: conn, + start: make(<-chan time.Time), + active: true, + config: config, + } +} + +// FuzzConnAfter creates a new FuzzedConnection. Fuzzing starts when the +// duration elapses. +func FuzzConnAfter(conn net.Conn, d time.Duration) net.Conn { + return FuzzConnAfterFromConfig(conn, d, DefaultFuzzConnConfig()) +} + +// FuzzConnAfterFromConfig creates a new FuzzedConnection from a config. +// Fuzzing starts when the duration elapses. +func FuzzConnAfterFromConfig(conn net.Conn, d time.Duration, config *FuzzConnConfig) net.Conn { + return &FuzzedConnection{ + conn: conn, + start: time.After(d), + active: false, + config: config, + } +} + +// Config returns the connection's config. +func (fc *FuzzedConnection) Config() *FuzzConnConfig { + return fc.config +} + +// Read implements net.Conn. +func (fc *FuzzedConnection) Read(data []byte) (n int, err error) { + if fc.fuzz() { + return 0, nil + } + return fc.conn.Read(data) +} + +// Write implements net.Conn. +func (fc *FuzzedConnection) Write(data []byte) (n int, err error) { + if fc.fuzz() { + return 0, nil + } + return fc.conn.Write(data) +} + +// Close implements net.Conn. +func (fc *FuzzedConnection) Close() error { return fc.conn.Close() } + +// LocalAddr implements net.Conn. +func (fc *FuzzedConnection) LocalAddr() net.Addr { return fc.conn.LocalAddr() } + +// RemoteAddr implements net.Conn. +func (fc *FuzzedConnection) RemoteAddr() net.Addr { return fc.conn.RemoteAddr() } + +// SetDeadline implements net.Conn. +func (fc *FuzzedConnection) SetDeadline(t time.Time) error { return fc.conn.SetDeadline(t) } + +// SetReadDeadline implements net.Conn. +func (fc *FuzzedConnection) SetReadDeadline(t time.Time) error { + return fc.conn.SetReadDeadline(t) +} + +// SetWriteDeadline implements net.Conn. +func (fc *FuzzedConnection) SetWriteDeadline(t time.Time) error { + return fc.conn.SetWriteDeadline(t) +} + +func (fc *FuzzedConnection) randomDuration() time.Duration { + maxDelayMillis := int(fc.config.MaxDelay.Nanoseconds() / 1000) + return time.Millisecond * time.Duration(rand.Int()%maxDelayMillis) +} + +// implements the fuzz (delay, kill conn) +// and returns whether or not the read/write should be ignored +func (fc *FuzzedConnection) fuzz() bool { + if !fc.shouldFuzz() { + return false + } + + switch fc.config.Mode { + case FuzzModeDrop: + // randomly drop the r/w, drop the conn, or sleep + r := rand.Float64() + if r <= fc.config.ProbDropRW { + return true + } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn { + // XXX: can't this fail because machine precision? + // XXX: do we need an error? + fc.Close() + return true + } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep { + time.Sleep(fc.randomDuration()) + } + case FuzzModeDelay: + // sleep a bit + time.Sleep(fc.randomDuration()) + } + return false +} + +func (fc *FuzzedConnection) shouldFuzz() bool { + if fc.active { + return true + } + + fc.mtx.Lock() + defer fc.mtx.Unlock() + + select { + case <-fc.start: + fc.active = true + return true + default: + return false + } +} diff --git a/p2p/ip_range_counter.go b/p2p/ip_range_counter.go new file mode 100644 index 00000000..85d9d407 --- /dev/null +++ b/p2p/ip_range_counter.go @@ -0,0 +1,29 @@ +package p2p + +import ( + "strings" +) + +// TODO Test +func AddToIPRangeCounts(counts map[string]int, ip string) map[string]int { + changes := make(map[string]int) + ipParts := strings.Split(ip, ":") + for i := 1; i < len(ipParts); i++ { + prefix := strings.Join(ipParts[:i], ":") + counts[prefix] += 1 + changes[prefix] = counts[prefix] + } + return changes +} + +// TODO Test +func CheckIPRangeCounts(counts map[string]int, limits []int) bool { + for prefix, count := range counts { + ipParts := strings.Split(prefix, ":") + numParts := len(ipParts) + if limits[numParts] < count { + return false + } + } + return true +} diff --git a/p2p/listener.go b/p2p/listener.go new file mode 100644 index 00000000..2acf4d06 --- /dev/null +++ b/p2p/listener.go @@ -0,0 +1,217 @@ +package p2p + +import ( + "fmt" + "net" + "strconv" + "time" + + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tendermint/p2p/upnp" +) + +type Listener interface { + Connections() <-chan net.Conn + InternalAddress() *NetAddress + ExternalAddress() *NetAddress + String() string + Stop() bool +} + +// Implements Listener +type DefaultListener struct { + BaseService + + listener net.Listener + intAddr *NetAddress + extAddr *NetAddress + connections chan net.Conn +} + +const ( + numBufferedConnections = 10 + defaultExternalPort = 8770 + tryListenSeconds = 5 +) + +func splitHostPort(addr string) (host string, port int) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + PanicSanity(err) + } + port, err = strconv.Atoi(portStr) + if err != nil { + PanicSanity(err) + } + return host, port +} + +// skipUPNP: If true, does not try getUPNPExternalAddress() +func NewDefaultListener(protocol string, lAddr string, skipUPNP bool) Listener { + // Local listen IP & port + lAddrIP, lAddrPort := splitHostPort(lAddr) + + // Create listener + var listener net.Listener + var err error + for i := 0; i < tryListenSeconds; i++ { + listener, err = net.Listen(protocol, lAddr) + if err == nil { + break + } else if i < tryListenSeconds-1 { + time.Sleep(time.Second * 1) + } + } + if err != nil { + PanicCrisis(err) + } + // Actual listener local IP & port + listenerIP, listenerPort := splitHostPort(listener.Addr().String()) + log.Info("Local listener", "ip", listenerIP, "port", listenerPort) + + // Determine internal address... + var intAddr *NetAddress + intAddr, err = NewNetAddressString(lAddr) + if err != nil { + PanicCrisis(err) + } + + // Determine external address... + var extAddr *NetAddress + if !skipUPNP { + // If the lAddrIP is INADDR_ANY, try UPnP + if lAddrIP == "" || lAddrIP == "0.0.0.0" { + extAddr = getUPNPExternalAddress(lAddrPort, listenerPort) + } + } + // Otherwise just use the local address... + if extAddr == nil { + extAddr = getNaiveExternalAddress(listenerPort) + } + if extAddr == nil { + PanicCrisis("Could not determine external address!") + } + + dl := &DefaultListener{ + listener: listener, + intAddr: intAddr, + extAddr: extAddr, + connections: make(chan net.Conn, numBufferedConnections), + } + dl.BaseService = *NewBaseService(log, "DefaultListener", dl) + dl.Start() // Started upon construction + return dl +} + +func (l *DefaultListener) OnStart() error { + l.BaseService.OnStart() + go l.listenRoutine() + return nil +} + +func (l *DefaultListener) OnStop() { + l.BaseService.OnStop() + l.listener.Close() +} + +// Accept connections and pass on the channel +func (l *DefaultListener) listenRoutine() { + for { + conn, err := l.listener.Accept() + + if !l.IsRunning() { + break // Go to cleanup + } + + // listener wasn't stopped, + // yet we encountered an error. + if err != nil { + PanicCrisis(err) + } + + l.connections <- conn + } + + // Cleanup + close(l.connections) + for _ = range l.connections { + // Drain + } +} + +// A channel of inbound connections. +// It gets closed when the listener closes. +func (l *DefaultListener) Connections() <-chan net.Conn { + return l.connections +} + +func (l *DefaultListener) InternalAddress() *NetAddress { + return l.intAddr +} + +func (l *DefaultListener) ExternalAddress() *NetAddress { + return l.extAddr +} + +// NOTE: The returned listener is already Accept()'ing. +// So it's not suitable to pass into http.Serve(). +func (l *DefaultListener) NetListener() net.Listener { + return l.listener +} + +func (l *DefaultListener) String() string { + return fmt.Sprintf("Listener(@%v)", l.extAddr) +} + +/* external address helpers */ + +// UPNP external address discovery & port mapping +func getUPNPExternalAddress(externalPort, internalPort int) *NetAddress { + log.Info("Getting UPNP external address") + nat, err := upnp.Discover() + if err != nil { + log.Info("Could not perform UPNP discover", "error", err) + return nil + } + + ext, err := nat.GetExternalAddress() + if err != nil { + log.Info("Could not get UPNP external address", "error", err) + return nil + } + + // UPnP can't seem to get the external port, so let's just be explicit. + if externalPort == 0 { + externalPort = defaultExternalPort + } + + externalPort, err = nat.AddPortMapping("tcp", externalPort, internalPort, "tendermint", 0) + if err != nil { + log.Info("Could not add UPNP port mapping", "error", err) + return nil + } + + log.Info("Got UPNP external address", "address", ext) + return NewNetAddressIPPort(ext, uint16(externalPort)) +} + +// TODO: use syscalls: http://pastebin.com/9exZG4rh +func getNaiveExternalAddress(port int) *NetAddress { + addrs, err := net.InterfaceAddrs() + if err != nil { + PanicCrisis(Fmt("Could not fetch interface addresses: %v", err)) + } + + for _, a := range addrs { + ipnet, ok := a.(*net.IPNet) + if !ok { + continue + } + v4 := ipnet.IP.To4() + if v4 == nil || v4[0] == 127 { + continue + } // loopback + return NewNetAddressIPPort(ipnet.IP, uint16(port)) + } + return nil +} diff --git a/p2p/listener_test.go b/p2p/listener_test.go new file mode 100644 index 00000000..0f8a5494 --- /dev/null +++ b/p2p/listener_test.go @@ -0,0 +1,40 @@ +package p2p + +import ( + "bytes" + "testing" +) + +func TestListener(t *testing.T) { + // Create a listener + l := NewDefaultListener("tcp", ":8001", true) + + // Dial the listener + lAddr := l.ExternalAddress() + connOut, err := lAddr.Dial() + if err != nil { + t.Fatalf("Could not connect to listener address %v", lAddr) + } else { + t.Logf("Created a connection to listener address %v", lAddr) + } + connIn, ok := <-l.Connections() + if !ok { + t.Fatalf("Could not get inbound connection from listener") + } + + msg := []byte("hi!") + go connIn.Write(msg) + b := make([]byte, 32) + n, err := connOut.Read(b) + if err != nil { + t.Fatalf("Error reading off connection: %v", err) + } + + b = b[:n] + if !bytes.Equal(msg, b) { + t.Fatalf("Got %s, expected %s", b, msg) + } + + // Close the server, no longer needed. + l.Stop() +} diff --git a/p2p/log.go b/p2p/log.go new file mode 100644 index 00000000..af320340 --- /dev/null +++ b/p2p/log.go @@ -0,0 +1,7 @@ +package p2p + +import ( + "github.com/tendermint/tmlibs/logger" +) + +var log = logger.New("module", "p2p") diff --git a/p2p/netaddress.go b/p2p/netaddress.go new file mode 100644 index 00000000..09787481 --- /dev/null +++ b/p2p/netaddress.go @@ -0,0 +1,253 @@ +// Modified for Tendermint +// Originally Copyright (c) 2013-2014 Conformal Systems LLC. +// https://github.com/conformal/btcd/blob/master/LICENSE + +package p2p + +import ( + "errors" + "flag" + "net" + "strconv" + "time" + + cmn "github.com/tendermint/tmlibs/common" +) + +// NetAddress defines information about a peer on the network +// including its IP address, and port. +type NetAddress struct { + IP net.IP + Port uint16 + str string +} + +// NewNetAddress returns a new NetAddress using the provided TCP +// address. When testing, other net.Addr (except TCP) will result in +// using 0.0.0.0:0. When normal run, other net.Addr (except TCP) will +// panic. +// TODO: socks proxies? +func NewNetAddress(addr net.Addr) *NetAddress { + tcpAddr, ok := addr.(*net.TCPAddr) + if !ok { + if flag.Lookup("test.v") == nil { // normal run + cmn.PanicSanity(cmn.Fmt("Only TCPAddrs are supported. Got: %v", addr)) + } else { // in testing + return NewNetAddressIPPort(net.IP("0.0.0.0"), 0) + } + } + ip := tcpAddr.IP + port := uint16(tcpAddr.Port) + return NewNetAddressIPPort(ip, port) +} + +// NewNetAddressString returns a new NetAddress using the provided +// address in the form of "IP:Port". Also resolves the host if host +// is not an IP. +func NewNetAddressString(addr string) (*NetAddress, error) { + + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + ip := net.ParseIP(host) + if ip == nil { + if len(host) > 0 { + ips, err := net.LookupIP(host) + if err != nil { + return nil, err + } + ip = ips[0] + } + } + + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil, err + } + + na := NewNetAddressIPPort(ip, uint16(port)) + return na, nil +} + +// NewNetAddressStrings returns an array of NetAddress'es build using +// the provided strings. +func NewNetAddressStrings(addrs []string) ([]*NetAddress, error) { + netAddrs := make([]*NetAddress, len(addrs)) + for i, addr := range addrs { + netAddr, err := NewNetAddressString(addr) + if err != nil { + return nil, errors.New(cmn.Fmt("Error in address %s: %v", addr, err)) + } + netAddrs[i] = netAddr + } + return netAddrs, nil +} + +// NewNetAddressIPPort returns a new NetAddress using the provided IP +// and port number. +func NewNetAddressIPPort(ip net.IP, port uint16) *NetAddress { + na := &NetAddress{ + IP: ip, + Port: port, + str: net.JoinHostPort( + ip.String(), + strconv.FormatUint(uint64(port), 10), + ), + } + return na +} + +// Equals reports whether na and other are the same addresses. +func (na *NetAddress) Equals(other interface{}) bool { + if o, ok := other.(*NetAddress); ok { + return na.String() == o.String() + } + + return false +} + +func (na *NetAddress) Less(other interface{}) bool { + if o, ok := other.(*NetAddress); ok { + return na.String() < o.String() + } + + cmn.PanicSanity("Cannot compare unequal types") + return false +} + +// String representation. +func (na *NetAddress) String() string { + if na.str == "" { + na.str = net.JoinHostPort( + na.IP.String(), + strconv.FormatUint(uint64(na.Port), 10), + ) + } + return na.str +} + +// Dial calls net.Dial on the address. +func (na *NetAddress) Dial() (net.Conn, error) { + conn, err := net.Dial("tcp", na.String()) + if err != nil { + return nil, err + } + return conn, nil +} + +// DialTimeout calls net.DialTimeout on the address. +func (na *NetAddress) DialTimeout(timeout time.Duration) (net.Conn, error) { + conn, err := net.DialTimeout("tcp", na.String(), timeout) + if err != nil { + return nil, err + } + return conn, nil +} + +// Routable returns true if the address is routable. +func (na *NetAddress) Routable() bool { + // TODO(oga) bitcoind doesn't include RFC3849 here, but should we? + return na.Valid() && !(na.RFC1918() || na.RFC3927() || na.RFC4862() || + na.RFC4193() || na.RFC4843() || na.Local()) +} + +// For IPv4 these are either a 0 or all bits set address. For IPv6 a zero +// address or one that matches the RFC3849 documentation address format. +func (na *NetAddress) Valid() bool { + return na.IP != nil && !(na.IP.IsUnspecified() || na.RFC3849() || + na.IP.Equal(net.IPv4bcast)) +} + +// Local returns true if it is a local address. +func (na *NetAddress) Local() bool { + return na.IP.IsLoopback() || zero4.Contains(na.IP) +} + +// ReachabilityTo checks whenever o can be reached from na. +func (na *NetAddress) ReachabilityTo(o *NetAddress) int { + const ( + Unreachable = 0 + Default = iota + Teredo + Ipv6_weak + Ipv4 + Ipv6_strong + Private + ) + if !na.Routable() { + return Unreachable + } else if na.RFC4380() { + if !o.Routable() { + return Default + } else if o.RFC4380() { + return Teredo + } else if o.IP.To4() != nil { + return Ipv4 + } else { // ipv6 + return Ipv6_weak + } + } else if na.IP.To4() != nil { + if o.Routable() && o.IP.To4() != nil { + return Ipv4 + } + return Default + } else /* ipv6 */ { + var tunnelled bool + // Is our v6 is tunnelled? + if o.RFC3964() || o.RFC6052() || o.RFC6145() { + tunnelled = true + } + if !o.Routable() { + return Default + } else if o.RFC4380() { + return Teredo + } else if o.IP.To4() != nil { + return Ipv4 + } else if tunnelled { + // only prioritise ipv6 if we aren't tunnelling it. + return Ipv6_weak + } + return Ipv6_strong + } +} + +// RFC1918: IPv4 Private networks (10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12) +// RFC3849: IPv6 Documentation address (2001:0DB8::/32) +// RFC3927: IPv4 Autoconfig (169.254.0.0/16) +// RFC3964: IPv6 6to4 (2002::/16) +// RFC4193: IPv6 unique local (FC00::/7) +// RFC4380: IPv6 Teredo tunneling (2001::/32) +// RFC4843: IPv6 ORCHID: (2001:10::/28) +// RFC4862: IPv6 Autoconfig (FE80::/64) +// RFC6052: IPv6 well known prefix (64:FF9B::/96) +// RFC6145: IPv6 IPv4 translated address ::FFFF:0:0:0/96 +var rfc1918_10 = net.IPNet{IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(8, 32)} +var rfc1918_192 = net.IPNet{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)} +var rfc1918_172 = net.IPNet{IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(12, 32)} +var rfc3849 = net.IPNet{IP: net.ParseIP("2001:0DB8::"), Mask: net.CIDRMask(32, 128)} +var rfc3927 = net.IPNet{IP: net.ParseIP("169.254.0.0"), Mask: net.CIDRMask(16, 32)} +var rfc3964 = net.IPNet{IP: net.ParseIP("2002::"), Mask: net.CIDRMask(16, 128)} +var rfc4193 = net.IPNet{IP: net.ParseIP("FC00::"), Mask: net.CIDRMask(7, 128)} +var rfc4380 = net.IPNet{IP: net.ParseIP("2001::"), Mask: net.CIDRMask(32, 128)} +var rfc4843 = net.IPNet{IP: net.ParseIP("2001:10::"), Mask: net.CIDRMask(28, 128)} +var rfc4862 = net.IPNet{IP: net.ParseIP("FE80::"), Mask: net.CIDRMask(64, 128)} +var rfc6052 = net.IPNet{IP: net.ParseIP("64:FF9B::"), Mask: net.CIDRMask(96, 128)} +var rfc6145 = net.IPNet{IP: net.ParseIP("::FFFF:0:0:0"), Mask: net.CIDRMask(96, 128)} +var zero4 = net.IPNet{IP: net.ParseIP("0.0.0.0"), Mask: net.CIDRMask(8, 32)} + +func (na *NetAddress) RFC1918() bool { + return rfc1918_10.Contains(na.IP) || + rfc1918_192.Contains(na.IP) || + rfc1918_172.Contains(na.IP) +} +func (na *NetAddress) RFC3849() bool { return rfc3849.Contains(na.IP) } +func (na *NetAddress) RFC3927() bool { return rfc3927.Contains(na.IP) } +func (na *NetAddress) RFC3964() bool { return rfc3964.Contains(na.IP) } +func (na *NetAddress) RFC4193() bool { return rfc4193.Contains(na.IP) } +func (na *NetAddress) RFC4380() bool { return rfc4380.Contains(na.IP) } +func (na *NetAddress) RFC4843() bool { return rfc4843.Contains(na.IP) } +func (na *NetAddress) RFC4862() bool { return rfc4862.Contains(na.IP) } +func (na *NetAddress) RFC6052() bool { return rfc6052.Contains(na.IP) } +func (na *NetAddress) RFC6145() bool { return rfc6145.Contains(na.IP) } diff --git a/p2p/netaddress_test.go b/p2p/netaddress_test.go new file mode 100644 index 00000000..db871fde --- /dev/null +++ b/p2p/netaddress_test.go @@ -0,0 +1,113 @@ +package p2p + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewNetAddress(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") + require.Nil(err) + addr := NewNetAddress(tcpAddr) + + assert.Equal("127.0.0.1:8080", addr.String()) + + assert.NotPanics(func() { + NewNetAddress(&net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8000}) + }, "Calling NewNetAddress with UDPAddr should not panic in testing") +} + +func TestNewNetAddressString(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + tests := []struct { + addr string + correct bool + }{ + {"127.0.0.1:8080", true}, + {"127.0.0:8080", false}, + {"a", false}, + {"127.0.0.1:a", false}, + {"a:8080", false}, + {"8082", false}, + {"127.0.0:8080000", false}, + } + + for _, t := range tests { + addr, err := NewNetAddressString(t.addr) + if t.correct { + require.Nil(err) + assert.Equal(t.addr, addr.String()) + } else { + require.NotNil(err) + } + } +} + +func TestNewNetAddressStrings(t *testing.T) { + assert, require := assert.New(t), require.New(t) + addrs, err := NewNetAddressStrings([]string{"127.0.0.1:8080", "127.0.0.2:8080"}) + require.Nil(err) + + assert.Equal(2, len(addrs)) +} + +func TestNewNetAddressIPPort(t *testing.T) { + assert := assert.New(t) + addr := NewNetAddressIPPort(net.ParseIP("127.0.0.1"), 8080) + + assert.Equal("127.0.0.1:8080", addr.String()) +} + +func TestNetAddressProperties(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // TODO add more test cases + tests := []struct { + addr string + valid bool + local bool + routable bool + }{ + {"127.0.0.1:8080", true, true, false}, + {"ya.ru:80", true, false, true}, + } + + for _, t := range tests { + addr, err := NewNetAddressString(t.addr) + require.Nil(err) + + assert.Equal(t.valid, addr.Valid()) + assert.Equal(t.local, addr.Local()) + assert.Equal(t.routable, addr.Routable()) + } +} + +func TestNetAddressReachabilityTo(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // TODO add more test cases + tests := []struct { + addr string + other string + reachability int + }{ + {"127.0.0.1:8080", "127.0.0.1:8081", 0}, + {"ya.ru:80", "127.0.0.1:8080", 1}, + } + + for _, t := range tests { + addr, err := NewNetAddressString(t.addr) + require.Nil(err) + + other, err := NewNetAddressString(t.other) + require.Nil(err) + + assert.Equal(t.reachability, addr.ReachabilityTo(other)) + } +} diff --git a/p2p/peer.go b/p2p/peer.go new file mode 100644 index 00000000..bf819087 --- /dev/null +++ b/p2p/peer.go @@ -0,0 +1,304 @@ +package p2p + +import ( + "fmt" + "io" + "net" + "time" + + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" +) + +// Peer could be marked as persistent, in which case you can use +// Redial function to reconnect. Note that inbound peers can't be +// made persistent. They should be made persistent on the other end. +// +// Before using a peer, you will need to perform a handshake on connection. +type Peer struct { + cmn.BaseService + + outbound bool + + conn net.Conn // source connection + mconn *MConnection // multiplex connection + + persistent bool + config *PeerConfig + + *NodeInfo + Key string + Data *cmn.CMap // User data. +} + +// PeerConfig is a Peer configuration. +type PeerConfig struct { + AuthEnc bool // authenticated encryption + + HandshakeTimeout time.Duration + DialTimeout time.Duration + + MConfig *MConnConfig + + Fuzz bool // fuzz connection (for testing) + FuzzConfig *FuzzConnConfig +} + +// DefaultPeerConfig returns the default config. +func DefaultPeerConfig() *PeerConfig { + return &PeerConfig{ + AuthEnc: true, + HandshakeTimeout: 2 * time.Second, + DialTimeout: 3 * time.Second, + MConfig: DefaultMConnConfig(), + Fuzz: false, + FuzzConfig: DefaultFuzzConnConfig(), + } +} + +func newOutboundPeer(addr *NetAddress, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519) (*Peer, error) { + return newOutboundPeerWithConfig(addr, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, DefaultPeerConfig()) +} + +func newOutboundPeerWithConfig(addr *NetAddress, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) { + conn, err := dial(addr, config) + if err != nil { + return nil, errors.Wrap(err, "Error creating peer") + } + + peer, err := newPeerFromConnAndConfig(conn, true, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config) + if err != nil { + conn.Close() + return nil, err + } + return peer, nil +} + +func newInboundPeer(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519) (*Peer, error) { + return newInboundPeerWithConfig(conn, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, DefaultPeerConfig()) +} + +func newInboundPeerWithConfig(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) { + return newPeerFromConnAndConfig(conn, false, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config) +} + +func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) { + conn := rawConn + + // Fuzz connection + if config.Fuzz { + // so we have time to do peer handshakes and get set up + conn = FuzzConnAfterFromConfig(conn, 10*time.Second, config.FuzzConfig) + } + + // Encrypt connection + if config.AuthEnc { + conn.SetDeadline(time.Now().Add(config.HandshakeTimeout)) + + var err error + conn, err = MakeSecretConnection(conn, ourNodePrivKey) + if err != nil { + return nil, errors.Wrap(err, "Error creating peer") + } + } + + // Key and NodeInfo are set after Handshake + p := &Peer{ + outbound: outbound, + conn: conn, + config: config, + Data: cmn.NewCMap(), + } + + p.mconn = createMConnection(conn, p, reactorsByCh, chDescs, onPeerError, config.MConfig) + + p.BaseService = *cmn.NewBaseService(log, "Peer", p) + + return p, nil +} + +// CloseConn should be used when the peer was created, but never started. +func (p *Peer) CloseConn() { + p.conn.Close() +} + +// makePersistent marks the peer as persistent. +func (p *Peer) makePersistent() { + if !p.outbound { + panic("inbound peers can't be made persistent") + } + + p.persistent = true +} + +// IsPersistent returns true if the peer is persitent, false otherwise. +func (p *Peer) IsPersistent() bool { + return p.persistent +} + +// HandshakeTimeout performs a handshake between a given node and the peer. +// NOTE: blocking +func (p *Peer) HandshakeTimeout(ourNodeInfo *NodeInfo, timeout time.Duration) error { + // Set deadline for handshake so we don't block forever on conn.ReadFull + p.conn.SetDeadline(time.Now().Add(timeout)) + + var peerNodeInfo = new(NodeInfo) + var err1 error + var err2 error + cmn.Parallel( + func() { + var n int + wire.WriteBinary(ourNodeInfo, p.conn, &n, &err1) + }, + func() { + var n int + wire.ReadBinary(peerNodeInfo, p.conn, maxNodeInfoSize, &n, &err2) + log.Notice("Peer handshake", "peerNodeInfo", peerNodeInfo) + }) + if err1 != nil { + return errors.Wrap(err1, "Error during handshake/write") + } + if err2 != nil { + return errors.Wrap(err2, "Error during handshake/read") + } + + if p.config.AuthEnc { + // Check that the professed PubKey matches the sconn's. + if !peerNodeInfo.PubKey.Equals(p.PubKey().Wrap()) { + return fmt.Errorf("Ignoring connection with unmatching pubkey: %v vs %v", + peerNodeInfo.PubKey, p.PubKey()) + } + } + + // Remove deadline + p.conn.SetDeadline(time.Time{}) + + peerNodeInfo.RemoteAddr = p.Addr().String() + + p.NodeInfo = peerNodeInfo + p.Key = peerNodeInfo.PubKey.KeyString() + + return nil +} + +// Addr returns peer's network address. +func (p *Peer) Addr() net.Addr { + return p.conn.RemoteAddr() +} + +// PubKey returns peer's public key. +func (p *Peer) PubKey() crypto.PubKeyEd25519 { + if p.config.AuthEnc { + return p.conn.(*SecretConnection).RemotePubKey() + } + if p.NodeInfo == nil { + panic("Attempt to get peer's PubKey before calling Handshake") + } + return p.PubKey() +} + +// OnStart implements BaseService. +func (p *Peer) OnStart() error { + p.BaseService.OnStart() + _, err := p.mconn.Start() + return err +} + +// OnStop implements BaseService. +func (p *Peer) OnStop() { + p.BaseService.OnStop() + p.mconn.Stop() +} + +// Connection returns underlying MConnection. +func (p *Peer) Connection() *MConnection { + return p.mconn +} + +// IsOutbound returns true if the connection is outbound, false otherwise. +func (p *Peer) IsOutbound() bool { + return p.outbound +} + +// Send msg to the channel identified by chID byte. Returns false if the send +// queue is full after timeout, specified by MConnection. +func (p *Peer) Send(chID byte, msg interface{}) bool { + if !p.IsRunning() { + // see Switch#Broadcast, where we fetch the list of peers and loop over + // them - while we're looping, one peer may be removed and stopped. + return false + } + return p.mconn.Send(chID, msg) +} + +// TrySend msg to the channel identified by chID byte. Immediately returns +// false if the send queue is full. +func (p *Peer) TrySend(chID byte, msg interface{}) bool { + if !p.IsRunning() { + return false + } + return p.mconn.TrySend(chID, msg) +} + +// CanSend returns true if the send queue is not full, false otherwise. +func (p *Peer) CanSend(chID byte) bool { + if !p.IsRunning() { + return false + } + return p.mconn.CanSend(chID) +} + +// WriteTo writes the peer's public key to w. +func (p *Peer) WriteTo(w io.Writer) (n int64, err error) { + var n_ int + wire.WriteString(p.Key, w, &n_, &err) + n += int64(n_) + return +} + +// String representation. +func (p *Peer) String() string { + if p.outbound { + return fmt.Sprintf("Peer{%v %v out}", p.mconn, p.Key[:12]) + } + + return fmt.Sprintf("Peer{%v %v in}", p.mconn, p.Key[:12]) +} + +// Equals reports whenever 2 peers are actually represent the same node. +func (p *Peer) Equals(other *Peer) bool { + return p.Key == other.Key +} + +// Get the data for a given key. +func (p *Peer) Get(key string) interface{} { + return p.Data.Get(key) +} + +func dial(addr *NetAddress, config *PeerConfig) (net.Conn, error) { + log.Info("Dialing address", "address", addr) + conn, err := addr.DialTimeout(config.DialTimeout) + if err != nil { + log.Info("Failed dialing address", "address", addr, "error", err) + return nil, err + } + return conn, nil +} + +func createMConnection(conn net.Conn, p *Peer, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), config *MConnConfig) *MConnection { + onReceive := func(chID byte, msgBytes []byte) { + reactor := reactorsByCh[chID] + if reactor == nil { + cmn.PanicSanity(cmn.Fmt("Unknown channel %X", chID)) + } + reactor.Receive(chID, p, msgBytes) + } + + onError := func(r interface{}) { + onPeerError(p, r) + } + + return NewMConnectionWithConfig(conn, chDescs, onReceive, onError, config) +} diff --git a/p2p/peer_set.go b/p2p/peer_set.go new file mode 100644 index 00000000..f3bc1eda --- /dev/null +++ b/p2p/peer_set.go @@ -0,0 +1,115 @@ +package p2p + +import ( + "sync" +) + +// IPeerSet has a (immutable) subset of the methods of PeerSet. +type IPeerSet interface { + Has(key string) bool + Get(key string) *Peer + List() []*Peer + Size() int +} + +//----------------------------------------------------------------------------- + +// PeerSet is a special structure for keeping a table of peers. +// Iteration over the peers is super fast and thread-safe. +// We also track how many peers per IP range and avoid too many +type PeerSet struct { + mtx sync.Mutex + lookup map[string]*peerSetItem + list []*Peer +} + +type peerSetItem struct { + peer *Peer + index int +} + +func NewPeerSet() *PeerSet { + return &PeerSet{ + lookup: make(map[string]*peerSetItem), + list: make([]*Peer, 0, 256), + } +} + +// Returns false if peer with key (PubKeyEd25519) is already in set +// or if we have too many peers from the peer's IP range +func (ps *PeerSet) Add(peer *Peer) error { + ps.mtx.Lock() + defer ps.mtx.Unlock() + if ps.lookup[peer.Key] != nil { + return ErrSwitchDuplicatePeer + } + + index := len(ps.list) + // Appending is safe even with other goroutines + // iterating over the ps.list slice. + ps.list = append(ps.list, peer) + ps.lookup[peer.Key] = &peerSetItem{peer, index} + return nil +} + +func (ps *PeerSet) Has(peerKey string) bool { + ps.mtx.Lock() + defer ps.mtx.Unlock() + _, ok := ps.lookup[peerKey] + return ok +} + +func (ps *PeerSet) Get(peerKey string) *Peer { + ps.mtx.Lock() + defer ps.mtx.Unlock() + item, ok := ps.lookup[peerKey] + if ok { + return item.peer + } else { + return nil + } +} + +func (ps *PeerSet) Remove(peer *Peer) { + ps.mtx.Lock() + defer ps.mtx.Unlock() + item := ps.lookup[peer.Key] + if item == nil { + return + } + + index := item.index + // Copy the list but without the last element. + // (we must copy because we're mutating the list) + newList := make([]*Peer, len(ps.list)-1) + copy(newList, ps.list) + // If it's the last peer, that's an easy special case. + if index == len(ps.list)-1 { + ps.list = newList + delete(ps.lookup, peer.Key) + return + } + + // Move the last item from ps.list to "index" in list. + lastPeer := ps.list[len(ps.list)-1] + lastPeerKey := lastPeer.Key + lastPeerItem := ps.lookup[lastPeerKey] + newList[index] = lastPeer + lastPeerItem.index = index + ps.list = newList + delete(ps.lookup, peer.Key) + +} + +func (ps *PeerSet) Size() int { + ps.mtx.Lock() + defer ps.mtx.Unlock() + return len(ps.list) +} + +// threadsafe list of peers. +func (ps *PeerSet) List() []*Peer { + ps.mtx.Lock() + defer ps.mtx.Unlock() + return ps.list +} diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go new file mode 100644 index 00000000..a17f9d65 --- /dev/null +++ b/p2p/peer_set_test.go @@ -0,0 +1,67 @@ +package p2p + +import ( + "math/rand" + "testing" + + . "github.com/tendermint/tmlibs/common" +) + +// Returns an empty dummy peer +func randPeer() *Peer { + return &Peer{ + Key: RandStr(12), + NodeInfo: &NodeInfo{ + RemoteAddr: Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256), + ListenAddr: Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256), + }, + } +} + +func TestAddRemoveOne(t *testing.T) { + peerSet := NewPeerSet() + + peer := randPeer() + err := peerSet.Add(peer) + if err != nil { + t.Errorf("Failed to add new peer") + } + if peerSet.Size() != 1 { + t.Errorf("Failed to add new peer and increment size") + } + + peerSet.Remove(peer) + if peerSet.Has(peer.Key) { + t.Errorf("Failed to remove peer") + } + if peerSet.Size() != 0 { + t.Errorf("Failed to remove peer and decrement size") + } +} + +func TestAddRemoveMany(t *testing.T) { + peerSet := NewPeerSet() + + peers := []*Peer{} + N := 100 + for i := 0; i < N; i++ { + peer := randPeer() + if err := peerSet.Add(peer); err != nil { + t.Errorf("Failed to add new peer") + } + if peerSet.Size() != i+1 { + t.Errorf("Failed to add new peer and increment size") + } + peers = append(peers, peer) + } + + for i, peer := range peers { + peerSet.Remove(peer) + if peerSet.Has(peer.Key) { + t.Errorf("Failed to remove peer") + } + if peerSet.Size() != len(peers)-i-1 { + t.Errorf("Failed to remove peer and decrement size") + } + } +} diff --git a/p2p/peer_test.go b/p2p/peer_test.go new file mode 100644 index 00000000..0ac77634 --- /dev/null +++ b/p2p/peer_test.go @@ -0,0 +1,156 @@ +package p2p + +import ( + golog "log" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + crypto "github.com/tendermint/go-crypto" +) + +func TestPeerBasic(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // simulate remote peer + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: DefaultPeerConfig()} + rp.Start() + defer rp.Stop() + + p, err := createOutboundPeerAndPerformHandshake(rp.Addr(), DefaultPeerConfig()) + require.Nil(err) + + p.Start() + defer p.Stop() + + assert.True(p.IsRunning()) + assert.True(p.IsOutbound()) + assert.False(p.IsPersistent()) + p.makePersistent() + assert.True(p.IsPersistent()) + assert.Equal(rp.Addr().String(), p.Addr().String()) + assert.Equal(rp.PubKey(), p.PubKey()) +} + +func TestPeerWithoutAuthEnc(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + config := DefaultPeerConfig() + config.AuthEnc = false + + // simulate remote peer + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: config} + rp.Start() + defer rp.Stop() + + p, err := createOutboundPeerAndPerformHandshake(rp.Addr(), config) + require.Nil(err) + + p.Start() + defer p.Stop() + + assert.True(p.IsRunning()) +} + +func TestPeerSend(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + config := DefaultPeerConfig() + config.AuthEnc = false + + // simulate remote peer + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: config} + rp.Start() + defer rp.Stop() + + p, err := createOutboundPeerAndPerformHandshake(rp.Addr(), config) + require.Nil(err) + + p.Start() + defer p.Stop() + + assert.True(p.CanSend(0x01)) + assert.True(p.Send(0x01, "Asylum")) +} + +func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) (*Peer, error) { + chDescs := []*ChannelDescriptor{ + &ChannelDescriptor{ID: 0x01, Priority: 1}, + } + reactorsByCh := map[byte]Reactor{0x01: NewTestReactor(chDescs, true)} + pk := crypto.GenPrivKeyEd25519() + p, err := newOutboundPeerWithConfig(addr, reactorsByCh, chDescs, func(p *Peer, r interface{}) {}, pk, config) + if err != nil { + return nil, err + } + err = p.HandshakeTimeout(&NodeInfo{ + PubKey: pk.PubKey().Unwrap().(crypto.PubKeyEd25519), + Moniker: "host_peer", + Network: "testing", + Version: "123.123.123", + }, 1*time.Second) + if err != nil { + return nil, err + } + return p, nil +} + +type remotePeer struct { + PrivKey crypto.PrivKeyEd25519 + Config *PeerConfig + addr *NetAddress + quit chan struct{} +} + +func (p *remotePeer) Addr() *NetAddress { + return p.addr +} + +func (p *remotePeer) PubKey() crypto.PubKeyEd25519 { + return p.PrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519) +} + +func (p *remotePeer) Start() { + l, e := net.Listen("tcp", "127.0.0.1:0") // any available address + if e != nil { + golog.Fatalf("net.Listen tcp :0: %+v", e) + } + p.addr = NewNetAddress(l.Addr()) + p.quit = make(chan struct{}) + go p.accept(l) +} + +func (p *remotePeer) Stop() { + close(p.quit) +} + +func (p *remotePeer) accept(l net.Listener) { + for { + conn, err := l.Accept() + if err != nil { + golog.Fatalf("Failed to accept conn: %+v", err) + } + peer, err := newInboundPeerWithConfig(conn, make(map[byte]Reactor), make([]*ChannelDescriptor, 0), func(p *Peer, r interface{}) {}, p.PrivKey, p.Config) + if err != nil { + golog.Fatalf("Failed to create a peer: %+v", err) + } + err = peer.HandshakeTimeout(&NodeInfo{ + PubKey: p.PrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519), + Moniker: "remote_peer", + Network: "testing", + Version: "123.123.123", + }, 1*time.Second) + if err != nil { + golog.Fatalf("Failed to perform handshake: %+v", err) + } + select { + case <-p.quit: + conn.Close() + return + default: + } + } +} diff --git a/p2p/pex_reactor.go b/p2p/pex_reactor.go new file mode 100644 index 00000000..03a383c8 --- /dev/null +++ b/p2p/pex_reactor.go @@ -0,0 +1,358 @@ +package p2p + +import ( + "bytes" + "fmt" + "math/rand" + "reflect" + "time" + + cmn "github.com/tendermint/tmlibs/common" + wire "github.com/tendermint/go-wire" +) + +const ( + // PexChannel is a channel for PEX messages + PexChannel = byte(0x00) + + // period to ensure peers connected + defaultEnsurePeersPeriod = 30 * time.Second + minNumOutboundPeers = 10 + maxPexMessageSize = 1048576 // 1MB + + // maximum messages one peer can send to us during `msgCountByPeerFlushInterval` + defaultMaxMsgCountByPeer = 1000 + msgCountByPeerFlushInterval = 1 * time.Hour +) + +// PEXReactor handles PEX (peer exchange) and ensures that an +// adequate number of peers are connected to the switch. +// +// It uses `AddrBook` (address book) to store `NetAddress`es of the peers. +// +// ## Preventing abuse +// +// For now, it just limits the number of messages from one peer to +// `defaultMaxMsgCountByPeer` messages per `msgCountByPeerFlushInterval` (1000 +// msg/hour). +// +// NOTE [2017-01-17]: +// Limiting is fine for now. Maybe down the road we want to keep track of the +// quality of peer messages so if peerA keeps telling us about peers we can't +// connect to then maybe we should care less about peerA. But I don't think +// that kind of complexity is priority right now. +type PEXReactor struct { + BaseReactor + + sw *Switch + book *AddrBook + ensurePeersPeriod time.Duration + + // tracks message count by peer, so we can prevent abuse + msgCountByPeer *cmn.CMap + maxMsgCountByPeer uint16 +} + +// NewPEXReactor creates new PEX reactor. +func NewPEXReactor(b *AddrBook) *PEXReactor { + r := &PEXReactor{ + book: b, + ensurePeersPeriod: defaultEnsurePeersPeriod, + msgCountByPeer: cmn.NewCMap(), + maxMsgCountByPeer: defaultMaxMsgCountByPeer, + } + r.BaseReactor = *NewBaseReactor(log, "PEXReactor", r) + return r +} + +// OnStart implements BaseService +func (r *PEXReactor) OnStart() error { + r.BaseReactor.OnStart() + r.book.Start() + go r.ensurePeersRoutine() + go r.flushMsgCountByPeer() + return nil +} + +// OnStop implements BaseService +func (r *PEXReactor) OnStop() { + r.BaseReactor.OnStop() + r.book.Stop() +} + +// GetChannels implements Reactor +func (r *PEXReactor) GetChannels() []*ChannelDescriptor { + return []*ChannelDescriptor{ + &ChannelDescriptor{ + ID: PexChannel, + Priority: 1, + SendQueueCapacity: 10, + }, + } +} + +// AddPeer implements Reactor by adding peer to the address book (if inbound) +// or by requesting more addresses (if outbound). +func (r *PEXReactor) AddPeer(p *Peer) { + if p.IsOutbound() { + // For outbound peers, the address is already in the books. + // Either it was added in DialSeeds or when we + // received the peer's address in r.Receive + if r.book.NeedMoreAddrs() { + r.RequestPEX(p) + } + } else { // For inbound connections, the peer is its own source + addr, err := NewNetAddressString(p.ListenAddr) + if err != nil { + // this should never happen + log.Error("Error in AddPeer: invalid peer address", "addr", p.ListenAddr, "error", err) + return + } + r.book.AddAddress(addr, addr) + } +} + +// RemovePeer implements Reactor. +func (r *PEXReactor) RemovePeer(p *Peer, reason interface{}) { + // If we aren't keeping track of local temp data for each peer here, then we + // don't have to do anything. +} + +// Receive implements Reactor by handling incoming PEX messages. +func (r *PEXReactor) Receive(chID byte, src *Peer, msgBytes []byte) { + srcAddr := src.Connection().RemoteAddress + srcAddrStr := srcAddr.String() + + r.IncrementMsgCountForPeer(srcAddrStr) + if r.ReachedMaxMsgCountForPeer(srcAddrStr) { + log.Warn("Maximum number of messages reached for peer", "peer", srcAddrStr) + // TODO remove src from peers? + return + } + + _, msg, err := DecodeMessage(msgBytes) + if err != nil { + log.Warn("Error decoding message", "error", err) + return + } + log.Notice("Received message", "msg", msg) + + switch msg := msg.(type) { + case *pexRequestMessage: + // src requested some peers. + r.SendAddrs(src, r.book.GetSelection()) + case *pexAddrsMessage: + // We received some peer addresses from src. + // (We don't want to get spammed with bad peers) + for _, addr := range msg.Addrs { + if addr != nil { + r.book.AddAddress(addr, srcAddr) + } + } + default: + log.Warn(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) + } +} + +// RequestPEX asks peer for more addresses. +func (r *PEXReactor) RequestPEX(p *Peer) { + p.Send(PexChannel, struct{ PexMessage }{&pexRequestMessage{}}) +} + +// SendAddrs sends addrs to the peer. +func (r *PEXReactor) SendAddrs(p *Peer, addrs []*NetAddress) { + p.Send(PexChannel, struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) +} + +// SetEnsurePeersPeriod sets period to ensure peers connected. +func (r *PEXReactor) SetEnsurePeersPeriod(d time.Duration) { + r.ensurePeersPeriod = d +} + +// SetMaxMsgCountByPeer sets maximum messages one peer can send to us during 'msgCountByPeerFlushInterval'. +func (r *PEXReactor) SetMaxMsgCountByPeer(v uint16) { + r.maxMsgCountByPeer = v +} + +// ReachedMaxMsgCountForPeer returns true if we received too many +// messages from peer with address `addr`. +// NOTE: assumes the value in the CMap is non-nil +func (r *PEXReactor) ReachedMaxMsgCountForPeer(addr string) bool { + return r.msgCountByPeer.Get(addr).(uint16) >= r.maxMsgCountByPeer +} + +// Increment or initialize the msg count for the peer in the CMap +func (r *PEXReactor) IncrementMsgCountForPeer(addr string) { + var count uint16 + countI := r.msgCountByPeer.Get(addr) + if countI != nil { + count = countI.(uint16) + } + count++ + r.msgCountByPeer.Set(addr, count) +} + +// Ensures that sufficient peers are connected. (continuous) +func (r *PEXReactor) ensurePeersRoutine() { + // Randomize when routine starts + ensurePeersPeriodMs := r.ensurePeersPeriod.Nanoseconds() / 1e6 + time.Sleep(time.Duration(rand.Int63n(ensurePeersPeriodMs)) * time.Millisecond) + + // fire once immediately. + r.ensurePeers() + + // fire periodically + ticker := time.NewTicker(r.ensurePeersPeriod) + + for { + select { + case <-ticker.C: + r.ensurePeers() + case <-r.Quit: + ticker.Stop() + return + } + } +} + +// ensurePeers ensures that sufficient peers are connected. (once) +// +// Old bucket / New bucket are arbitrary categories to denote whether an +// address is vetted or not, and this needs to be determined over time via a +// heuristic that we haven't perfected yet, or, perhaps is manually edited by +// the node operator. It should not be used to compute what addresses are +// already connected or not. +// +// TODO Basically, we need to work harder on our good-peer/bad-peer marking. +// What we're currently doing in terms of marking good/bad peers is just a +// placeholder. It should not be the case that an address becomes old/vetted +// upon a single successful connection. +func (r *PEXReactor) ensurePeers() { + numOutPeers, _, numDialing := r.Switch.NumPeers() + numToDial := minNumOutboundPeers - (numOutPeers + numDialing) + log.Info("Ensure peers", "numOutPeers", numOutPeers, "numDialing", numDialing, "numToDial", numToDial) + if numToDial <= 0 { + return + } + + toDial := make(map[string]*NetAddress) + + // Try to pick numToDial addresses to dial. + for i := 0; i < numToDial; i++ { + // The purpose of newBias is to first prioritize old (more vetted) peers + // when we have few connections, but to allow for new (less vetted) peers + // if we already have many connections. This algorithm isn't perfect, but + // it somewhat ensures that we prioritize connecting to more-vetted + // peers. + newBias := cmn.MinInt(numOutPeers, 8)*10 + 10 + var picked *NetAddress + // Try to fetch a new peer 3 times. + // This caps the maximum number of tries to 3 * numToDial. + for j := 0; j < 3; j++ { + try := r.book.PickAddress(newBias) + if try == nil { + break + } + _, alreadySelected := toDial[try.IP.String()] + alreadyDialing := r.Switch.IsDialing(try) + alreadyConnected := r.Switch.Peers().Has(try.IP.String()) + if alreadySelected || alreadyDialing || alreadyConnected { + // log.Info("Cannot dial address", "addr", try, + // "alreadySelected", alreadySelected, + // "alreadyDialing", alreadyDialing, + // "alreadyConnected", alreadyConnected) + continue + } else { + log.Info("Will dial address", "addr", try) + picked = try + break + } + } + if picked == nil { + continue + } + toDial[picked.IP.String()] = picked + } + + // Dial picked addresses + for _, item := range toDial { + go func(picked *NetAddress) { + _, err := r.Switch.DialPeerWithAddress(picked, false) + if err != nil { + r.book.MarkAttempt(picked) + } + }(item) + } + + // If we need more addresses, pick a random peer and ask for more. + if r.book.NeedMoreAddrs() { + if peers := r.Switch.Peers().List(); len(peers) > 0 { + i := rand.Int() % len(peers) + peer := peers[i] + log.Info("No addresses to dial. Sending pexRequest to random peer", "peer", peer) + r.RequestPEX(peer) + } + } +} + +func (r *PEXReactor) flushMsgCountByPeer() { + ticker := time.NewTicker(msgCountByPeerFlushInterval) + + for { + select { + case <-ticker.C: + r.msgCountByPeer.Clear() + case <-r.Quit: + ticker.Stop() + return + } + } +} + +//----------------------------------------------------------------------------- +// Messages + +const ( + msgTypeRequest = byte(0x01) + msgTypeAddrs = byte(0x02) +) + +// PexMessage is a primary type for PEX messages. Underneath, it could contain +// either pexRequestMessage, or pexAddrsMessage messages. +type PexMessage interface{} + +var _ = wire.RegisterInterface( + struct{ PexMessage }{}, + wire.ConcreteType{&pexRequestMessage{}, msgTypeRequest}, + wire.ConcreteType{&pexAddrsMessage{}, msgTypeAddrs}, +) + +// DecodeMessage implements interface registered above. +func DecodeMessage(bz []byte) (msgType byte, msg PexMessage, err error) { + msgType = bz[0] + n := new(int) + r := bytes.NewReader(bz) + msg = wire.ReadBinary(struct{ PexMessage }{}, r, maxPexMessageSize, n, &err).(struct{ PexMessage }).PexMessage + return +} + +/* +A pexRequestMessage requests additional peer addresses. +*/ +type pexRequestMessage struct { +} + +func (m *pexRequestMessage) String() string { + return "[pexRequest]" +} + +/* +A message with announced peer addresses. +*/ +type pexAddrsMessage struct { + Addrs []*NetAddress +} + +func (m *pexAddrsMessage) String() string { + return fmt.Sprintf("[pexAddrs %v]", m.Addrs) +} diff --git a/p2p/pex_reactor_test.go b/p2p/pex_reactor_test.go new file mode 100644 index 00000000..aed6c758 --- /dev/null +++ b/p2p/pex_reactor_test.go @@ -0,0 +1,163 @@ +package p2p + +import ( + "io/ioutil" + "math/rand" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + cmn "github.com/tendermint/tmlibs/common" + wire "github.com/tendermint/go-wire" +) + +func TestPEXReactorBasic(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) + book := NewAddrBook(dir+"addrbook.json", true) + + r := NewPEXReactor(book) + + assert.NotNil(r) + assert.NotEmpty(r.GetChannels()) +} + +func TestPEXReactorAddRemovePeer(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) + book := NewAddrBook(dir+"addrbook.json", true) + + r := NewPEXReactor(book) + + size := book.Size() + peer := createRandomPeer(false) + + r.AddPeer(peer) + assert.Equal(size+1, book.Size()) + + r.RemovePeer(peer, "peer not available") + assert.Equal(size+1, book.Size()) + + outboundPeer := createRandomPeer(true) + + r.AddPeer(outboundPeer) + assert.Equal(size+1, book.Size(), "outbound peers should not be added to the address book") + + r.RemovePeer(outboundPeer, "peer not available") + assert.Equal(size+1, book.Size()) +} + +func TestPEXReactorRunning(t *testing.T) { + require := require.New(t) + + N := 3 + switches := make([]*Switch, N) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) + book := NewAddrBook(dir+"addrbook.json", false) + + // create switches + for i := 0; i < N; i++ { + switches[i] = makeSwitch(i, "127.0.0.1", "123.123.123", func(i int, sw *Switch) *Switch { + r := NewPEXReactor(book) + r.SetEnsurePeersPeriod(250 * time.Millisecond) + sw.AddReactor("pex", r) + return sw + }) + } + + // fill the address book and add listeners + for _, s := range switches { + addr, _ := NewNetAddressString(s.NodeInfo().ListenAddr) + book.AddAddress(addr, addr) + s.AddListener(NewDefaultListener("tcp", s.NodeInfo().ListenAddr, true)) + } + + // start switches + for _, s := range switches { + _, err := s.Start() // start switch and reactors + require.Nil(err) + } + + time.Sleep(1 * time.Second) + + // check peers are connected after some time + for _, s := range switches { + outbound, inbound, _ := s.NumPeers() + if outbound+inbound == 0 { + t.Errorf("%v expected to be connected to at least one peer", s.NodeInfo().ListenAddr) + } + } + + // stop them + for _, s := range switches { + s.Stop() + } +} + +func TestPEXReactorReceive(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) + book := NewAddrBook(dir+"addrbook.json", true) + + r := NewPEXReactor(book) + + peer := createRandomPeer(false) + + size := book.Size() + netAddr, _ := NewNetAddressString(peer.ListenAddr) + addrs := []*NetAddress{netAddr} + msg := wire.BinaryBytes(struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) + r.Receive(PexChannel, peer, msg) + assert.Equal(size+1, book.Size()) + + msg = wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) + r.Receive(PexChannel, peer, msg) +} + +func TestPEXReactorAbuseFromPeer(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) + book := NewAddrBook(dir+"addrbook.json", true) + + r := NewPEXReactor(book) + r.SetMaxMsgCountByPeer(5) + + peer := createRandomPeer(false) + + msg := wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) + for i := 0; i < 10; i++ { + r.Receive(PexChannel, peer, msg) + } + + assert.True(r.ReachedMaxMsgCountForPeer(peer.ListenAddr)) +} + +func createRandomPeer(outbound bool) *Peer { + addr := cmn.Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256) + netAddr, _ := NewNetAddressString(addr) + return &Peer{ + Key: cmn.RandStr(12), + NodeInfo: &NodeInfo{ + ListenAddr: addr, + }, + outbound: outbound, + mconn: &MConnection{RemoteAddress: netAddr}, + } +} diff --git a/p2p/secret_connection.go b/p2p/secret_connection.go new file mode 100644 index 00000000..446c4f18 --- /dev/null +++ b/p2p/secret_connection.go @@ -0,0 +1,346 @@ +// Uses nacl's secret_box to encrypt a net.Conn. +// It is (meant to be) an implementation of the STS protocol. +// Note we do not (yet) assume that a remote peer's pubkey +// is known ahead of time, and thus we are technically +// still vulnerable to MITM. (TODO!) +// See docs/sts-final.pdf for more info +package p2p + +import ( + "bytes" + crand "crypto/rand" + "crypto/sha256" + "encoding/binary" + "errors" + "io" + "net" + "time" + + "golang.org/x/crypto/nacl/box" + "golang.org/x/crypto/nacl/secretbox" + "golang.org/x/crypto/ripemd160" + + "github.com/tendermint/go-crypto" + "github.com/tendermint/go-wire" + . "github.com/tendermint/tmlibs/common" +) + +// 2 + 1024 == 1026 total frame size +const dataLenSize = 2 // uint16 to describe the length, is <= dataMaxSize +const dataMaxSize = 1024 +const totalFrameSize = dataMaxSize + dataLenSize +const sealedFrameSize = totalFrameSize + secretbox.Overhead +const authSigMsgSize = (32 + 1) + (64 + 1) // fixed size (length prefixed) byte arrays + +// Implements net.Conn +type SecretConnection struct { + conn io.ReadWriteCloser + recvBuffer []byte + recvNonce *[24]byte + sendNonce *[24]byte + remPubKey crypto.PubKeyEd25519 + shrSecret *[32]byte // shared secret +} + +// Performs handshake and returns a new authenticated SecretConnection. +// Returns nil if error in handshake. +// Caller should call conn.Close() +// See docs/sts-final.pdf for more information. +func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKeyEd25519) (*SecretConnection, error) { + + locPubKey := locPrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519) + + // Generate ephemeral keys for perfect forward secrecy. + locEphPub, locEphPriv := genEphKeys() + + // Write local ephemeral pubkey and receive one too. + // NOTE: every 32-byte string is accepted as a Curve25519 public key + // (see DJB's Curve25519 paper: http://cr.yp.to/ecdh/curve25519-20060209.pdf) + remEphPub, err := shareEphPubKey(conn, locEphPub) + if err != nil { + return nil, err + } + + // Compute common shared secret. + shrSecret := computeSharedSecret(remEphPub, locEphPriv) + + // Sort by lexical order. + loEphPub, hiEphPub := sort32(locEphPub, remEphPub) + + // Generate nonces to use for secretbox. + recvNonce, sendNonce := genNonces(loEphPub, hiEphPub, locEphPub == loEphPub) + + // Generate common challenge to sign. + challenge := genChallenge(loEphPub, hiEphPub) + + // Construct SecretConnection. + sc := &SecretConnection{ + conn: conn, + recvBuffer: nil, + recvNonce: recvNonce, + sendNonce: sendNonce, + shrSecret: shrSecret, + } + + // Sign the challenge bytes for authentication. + locSignature := signChallenge(challenge, locPrivKey) + + // Share (in secret) each other's pubkey & challenge signature + authSigMsg, err := shareAuthSignature(sc, locPubKey, locSignature) + if err != nil { + return nil, err + } + remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig + if !remPubKey.VerifyBytes(challenge[:], remSignature) { + return nil, errors.New("Challenge verification failed") + } + + // We've authorized. + sc.remPubKey = remPubKey.Unwrap().(crypto.PubKeyEd25519) + return sc, nil +} + +// Returns authenticated remote pubkey +func (sc *SecretConnection) RemotePubKey() crypto.PubKeyEd25519 { + return sc.remPubKey +} + +// Writes encrypted frames of `sealedFrameSize` +// CONTRACT: data smaller than dataMaxSize is read atomically. +func (sc *SecretConnection) Write(data []byte) (n int, err error) { + for 0 < len(data) { + var frame []byte = make([]byte, totalFrameSize) + var chunk []byte + if dataMaxSize < len(data) { + chunk = data[:dataMaxSize] + data = data[dataMaxSize:] + } else { + chunk = data + data = nil + } + chunkLength := len(chunk) + binary.BigEndian.PutUint16(frame, uint16(chunkLength)) + copy(frame[dataLenSize:], chunk) + + // encrypt the frame + var sealedFrame = make([]byte, sealedFrameSize) + secretbox.Seal(sealedFrame[:0], frame, sc.sendNonce, sc.shrSecret) + // fmt.Printf("secretbox.Seal(sealed:%X,sendNonce:%X,shrSecret:%X\n", sealedFrame, sc.sendNonce, sc.shrSecret) + incr2Nonce(sc.sendNonce) + // end encryption + + _, err := sc.conn.Write(sealedFrame) + if err != nil { + return n, err + } else { + n += len(chunk) + } + } + return +} + +// CONTRACT: data smaller than dataMaxSize is read atomically. +func (sc *SecretConnection) Read(data []byte) (n int, err error) { + if 0 < len(sc.recvBuffer) { + n_ := copy(data, sc.recvBuffer) + sc.recvBuffer = sc.recvBuffer[n_:] + return + } + + sealedFrame := make([]byte, sealedFrameSize) + _, err = io.ReadFull(sc.conn, sealedFrame) + if err != nil { + return + } + + // decrypt the frame + var frame = make([]byte, totalFrameSize) + // fmt.Printf("secretbox.Open(sealed:%X,recvNonce:%X,shrSecret:%X\n", sealedFrame, sc.recvNonce, sc.shrSecret) + _, ok := secretbox.Open(frame[:0], sealedFrame, sc.recvNonce, sc.shrSecret) + if !ok { + return n, errors.New("Failed to decrypt SecretConnection") + } + incr2Nonce(sc.recvNonce) + // end decryption + + var chunkLength = binary.BigEndian.Uint16(frame) // read the first two bytes + if chunkLength > dataMaxSize { + return 0, errors.New("chunkLength is greater than dataMaxSize") + } + var chunk = frame[dataLenSize : dataLenSize+chunkLength] + + n = copy(data, chunk) + sc.recvBuffer = chunk[n:] + return +} + +// Implements net.Conn +func (sc *SecretConnection) Close() error { return sc.conn.Close() } +func (sc *SecretConnection) LocalAddr() net.Addr { return sc.conn.(net.Conn).LocalAddr() } +func (sc *SecretConnection) RemoteAddr() net.Addr { return sc.conn.(net.Conn).RemoteAddr() } +func (sc *SecretConnection) SetDeadline(t time.Time) error { return sc.conn.(net.Conn).SetDeadline(t) } +func (sc *SecretConnection) SetReadDeadline(t time.Time) error { + return sc.conn.(net.Conn).SetReadDeadline(t) +} +func (sc *SecretConnection) SetWriteDeadline(t time.Time) error { + return sc.conn.(net.Conn).SetWriteDeadline(t) +} + +func genEphKeys() (ephPub, ephPriv *[32]byte) { + var err error + ephPub, ephPriv, err = box.GenerateKey(crand.Reader) + if err != nil { + PanicCrisis("Could not generate ephemeral keypairs") + } + return +} + +func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[32]byte, err error) { + var err1, err2 error + + Parallel( + func() { + _, err1 = conn.Write(locEphPub[:]) + }, + func() { + remEphPub = new([32]byte) + _, err2 = io.ReadFull(conn, remEphPub[:]) + }, + ) + + if err1 != nil { + return nil, err1 + } + if err2 != nil { + return nil, err2 + } + + return remEphPub, nil +} + +func computeSharedSecret(remPubKey, locPrivKey *[32]byte) (shrSecret *[32]byte) { + shrSecret = new([32]byte) + box.Precompute(shrSecret, remPubKey, locPrivKey) + return +} + +func sort32(foo, bar *[32]byte) (lo, hi *[32]byte) { + if bytes.Compare(foo[:], bar[:]) < 0 { + lo = foo + hi = bar + } else { + lo = bar + hi = foo + } + return +} + +func genNonces(loPubKey, hiPubKey *[32]byte, locIsLo bool) (recvNonce, sendNonce *[24]byte) { + nonce1 := hash24(append(loPubKey[:], hiPubKey[:]...)) + nonce2 := new([24]byte) + copy(nonce2[:], nonce1[:]) + nonce2[len(nonce2)-1] ^= 0x01 + if locIsLo { + recvNonce = nonce1 + sendNonce = nonce2 + } else { + recvNonce = nonce2 + sendNonce = nonce1 + } + return +} + +func genChallenge(loPubKey, hiPubKey *[32]byte) (challenge *[32]byte) { + return hash32(append(loPubKey[:], hiPubKey[:]...)) +} + +func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKeyEd25519) (signature crypto.SignatureEd25519) { + signature = locPrivKey.Sign(challenge[:]).Unwrap().(crypto.SignatureEd25519) + return +} + +type authSigMessage struct { + Key crypto.PubKey + Sig crypto.Signature +} + +func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKeyEd25519, signature crypto.SignatureEd25519) (*authSigMessage, error) { + var recvMsg authSigMessage + var err1, err2 error + + Parallel( + func() { + msgBytes := wire.BinaryBytes(authSigMessage{pubKey.Wrap(), signature.Wrap()}) + _, err1 = sc.Write(msgBytes) + }, + func() { + readBuffer := make([]byte, authSigMsgSize) + _, err2 = io.ReadFull(sc, readBuffer) + if err2 != nil { + return + } + n := int(0) // not used. + recvMsg = wire.ReadBinary(authSigMessage{}, bytes.NewBuffer(readBuffer), authSigMsgSize, &n, &err2).(authSigMessage) + }) + + if err1 != nil { + return nil, err1 + } + if err2 != nil { + return nil, err2 + } + + return &recvMsg, nil +} + +func verifyChallengeSignature(challenge *[32]byte, remPubKey crypto.PubKeyEd25519, remSignature crypto.SignatureEd25519) bool { + return remPubKey.VerifyBytes(challenge[:], remSignature.Wrap()) +} + +//-------------------------------------------------------------------------------- + +// sha256 +func hash32(input []byte) (res *[32]byte) { + hasher := sha256.New() + hasher.Write(input) // does not error + resSlice := hasher.Sum(nil) + res = new([32]byte) + copy(res[:], resSlice) + return +} + +// We only fill in the first 20 bytes with ripemd160 +func hash24(input []byte) (res *[24]byte) { + hasher := ripemd160.New() + hasher.Write(input) // does not error + resSlice := hasher.Sum(nil) + res = new([24]byte) + copy(res[:], resSlice) + return +} + +// ripemd160 +func hash20(input []byte) (res *[20]byte) { + hasher := ripemd160.New() + hasher.Write(input) // does not error + resSlice := hasher.Sum(nil) + res = new([20]byte) + copy(res[:], resSlice) + return +} + +// increment nonce big-endian by 2 with wraparound. +func incr2Nonce(nonce *[24]byte) { + incrNonce(nonce) + incrNonce(nonce) +} + +// increment nonce big-endian by 1 with wraparound. +func incrNonce(nonce *[24]byte) { + for i := 23; 0 <= i; i-- { + nonce[i] += 1 + if nonce[i] != 0 { + return + } + } +} diff --git a/p2p/secret_connection_test.go b/p2p/secret_connection_test.go new file mode 100644 index 00000000..3dd962f8 --- /dev/null +++ b/p2p/secret_connection_test.go @@ -0,0 +1,202 @@ +package p2p + +import ( + "bytes" + "io" + "testing" + + "github.com/tendermint/go-crypto" + . "github.com/tendermint/tmlibs/common" +) + +type dummyConn struct { + *io.PipeReader + *io.PipeWriter +} + +func (drw dummyConn) Close() (err error) { + err2 := drw.PipeWriter.CloseWithError(io.EOF) + err1 := drw.PipeReader.Close() + if err2 != nil { + return err + } + return err1 +} + +// Each returned ReadWriteCloser is akin to a net.Connection +func makeDummyConnPair() (fooConn, barConn dummyConn) { + barReader, fooWriter := io.Pipe() + fooReader, barWriter := io.Pipe() + return dummyConn{fooReader, fooWriter}, dummyConn{barReader, barWriter} +} + +func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection) { + fooConn, barConn := makeDummyConnPair() + fooPrvKey := crypto.GenPrivKeyEd25519() + fooPubKey := fooPrvKey.PubKey().Unwrap().(crypto.PubKeyEd25519) + barPrvKey := crypto.GenPrivKeyEd25519() + barPubKey := barPrvKey.PubKey().Unwrap().(crypto.PubKeyEd25519) + + Parallel( + func() { + var err error + fooSecConn, err = MakeSecretConnection(fooConn, fooPrvKey) + if err != nil { + tb.Errorf("Failed to establish SecretConnection for foo: %v", err) + return + } + remotePubBytes := fooSecConn.RemotePubKey() + if !bytes.Equal(remotePubBytes[:], barPubKey[:]) { + tb.Errorf("Unexpected fooSecConn.RemotePubKey. Expected %v, got %v", + barPubKey, fooSecConn.RemotePubKey()) + } + }, + func() { + var err error + barSecConn, err = MakeSecretConnection(barConn, barPrvKey) + if barSecConn == nil { + tb.Errorf("Failed to establish SecretConnection for bar: %v", err) + return + } + remotePubBytes := barSecConn.RemotePubKey() + if !bytes.Equal(remotePubBytes[:], fooPubKey[:]) { + tb.Errorf("Unexpected barSecConn.RemotePubKey. Expected %v, got %v", + fooPubKey, barSecConn.RemotePubKey()) + } + }) + + return +} + +func TestSecretConnectionHandshake(t *testing.T) { + fooSecConn, barSecConn := makeSecretConnPair(t) + fooSecConn.Close() + barSecConn.Close() +} + +func TestSecretConnectionReadWrite(t *testing.T) { + fooConn, barConn := makeDummyConnPair() + fooWrites, barWrites := []string{}, []string{} + fooReads, barReads := []string{}, []string{} + + // Pre-generate the things to write (for foo & bar) + for i := 0; i < 100; i++ { + fooWrites = append(fooWrites, RandStr((RandInt()%(dataMaxSize*5))+1)) + barWrites = append(barWrites, RandStr((RandInt()%(dataMaxSize*5))+1)) + } + + // A helper that will run with (fooConn, fooWrites, fooReads) and vice versa + genNodeRunner := func(nodeConn dummyConn, nodeWrites []string, nodeReads *[]string) func() { + return func() { + // Node handskae + nodePrvKey := crypto.GenPrivKeyEd25519() + nodeSecretConn, err := MakeSecretConnection(nodeConn, nodePrvKey) + if err != nil { + t.Errorf("Failed to establish SecretConnection for node: %v", err) + return + } + // In parallel, handle reads and writes + Parallel( + func() { + // Node writes + for _, nodeWrite := range nodeWrites { + n, err := nodeSecretConn.Write([]byte(nodeWrite)) + if err != nil { + t.Errorf("Failed to write to nodeSecretConn: %v", err) + return + } + if n != len(nodeWrite) { + t.Errorf("Failed to write all bytes. Expected %v, wrote %v", len(nodeWrite), n) + return + } + } + nodeConn.PipeWriter.Close() + }, + func() { + // Node reads + readBuffer := make([]byte, dataMaxSize) + for { + n, err := nodeSecretConn.Read(readBuffer) + if err == io.EOF { + return + } else if err != nil { + t.Errorf("Failed to read from nodeSecretConn: %v", err) + return + } + *nodeReads = append(*nodeReads, string(readBuffer[:n])) + } + nodeConn.PipeReader.Close() + }) + } + } + + // Run foo & bar in parallel + Parallel( + genNodeRunner(fooConn, fooWrites, &fooReads), + genNodeRunner(barConn, barWrites, &barReads), + ) + + // A helper to ensure that the writes and reads match. + // Additionally, small writes (<= dataMaxSize) must be atomically read. + compareWritesReads := func(writes []string, reads []string) { + for { + // Pop next write & corresponding reads + var read, write string = "", writes[0] + var readCount = 0 + for _, readChunk := range reads { + read += readChunk + readCount += 1 + if len(write) <= len(read) { + break + } + if len(write) <= dataMaxSize { + break // atomicity of small writes + } + } + // Compare + if write != read { + t.Errorf("Expected to read %X, got %X", write, read) + } + // Iterate + writes = writes[1:] + reads = reads[readCount:] + if len(writes) == 0 { + break + } + } + } + + compareWritesReads(fooWrites, barReads) + compareWritesReads(barWrites, fooReads) + +} + +func BenchmarkSecretConnection(b *testing.B) { + b.StopTimer() + fooSecConn, barSecConn := makeSecretConnPair(b) + fooWriteText := RandStr(dataMaxSize) + // Consume reads from bar's reader + go func() { + readBuffer := make([]byte, dataMaxSize) + for { + _, err := barSecConn.Read(readBuffer) + if err == io.EOF { + return + } else if err != nil { + b.Fatalf("Failed to read from barSecConn: %v", err) + } + } + }() + + b.StartTimer() + for i := 0; i < b.N; i++ { + _, err := fooSecConn.Write([]byte(fooWriteText)) + if err != nil { + b.Fatalf("Failed to write to fooSecConn: %v", err) + } + } + b.StopTimer() + + fooSecConn.Close() + //barSecConn.Close() race condition +} diff --git a/p2p/switch.go b/p2p/switch.go new file mode 100644 index 00000000..7e9c0a9c --- /dev/null +++ b/p2p/switch.go @@ -0,0 +1,594 @@ +package p2p + +import ( + "errors" + "fmt" + "math/rand" + "net" + "time" + + "github.com/spf13/viper" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/log15" + . "github.com/tendermint/tmlibs/common" +) + +const ( + reconnectAttempts = 30 + reconnectInterval = 3 * time.Second +) + +type Reactor interface { + Service // Start, Stop + + SetSwitch(*Switch) + GetChannels() []*ChannelDescriptor + AddPeer(peer *Peer) + RemovePeer(peer *Peer, reason interface{}) + Receive(chID byte, peer *Peer, msgBytes []byte) +} + +//-------------------------------------- + +type BaseReactor struct { + BaseService // Provides Start, Stop, .Quit + Switch *Switch +} + +func NewBaseReactor(log log15.Logger, name string, impl Reactor) *BaseReactor { + return &BaseReactor{ + BaseService: *NewBaseService(log, name, impl), + Switch: nil, + } +} + +func (br *BaseReactor) SetSwitch(sw *Switch) { + br.Switch = sw +} +func (_ *BaseReactor) GetChannels() []*ChannelDescriptor { return nil } +func (_ *BaseReactor) AddPeer(peer *Peer) {} +func (_ *BaseReactor) RemovePeer(peer *Peer, reason interface{}) {} +func (_ *BaseReactor) Receive(chID byte, peer *Peer, msgBytes []byte) {} + +//----------------------------------------------------------------------------- + +/* +The `Switch` handles peer connections and exposes an API to receive incoming messages +on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one +or more `Channels`. So while sending outgoing messages is typically performed on the peer, +incoming messages are received on the reactor. +*/ +type Switch struct { + BaseService + + config *viper.Viper + listeners []Listener + reactors map[string]Reactor + chDescs []*ChannelDescriptor + reactorsByCh map[byte]Reactor + peers *PeerSet + dialing *CMap + nodeInfo *NodeInfo // our node info + nodePrivKey crypto.PrivKeyEd25519 // our node privkey + + filterConnByAddr func(net.Addr) error + filterConnByPubKey func(crypto.PubKeyEd25519) error +} + +var ( + ErrSwitchDuplicatePeer = errors.New("Duplicate peer") + ErrSwitchMaxPeersPerIPRange = errors.New("IP range has too many peers") +) + +func NewSwitch(config *viper.Viper) *Switch { + setConfigDefaults(config) + + sw := &Switch{ + config: config, + reactors: make(map[string]Reactor), + chDescs: make([]*ChannelDescriptor, 0), + reactorsByCh: make(map[byte]Reactor), + peers: NewPeerSet(), + dialing: NewCMap(), + nodeInfo: nil, + } + sw.BaseService = *NewBaseService(log, "P2P Switch", sw) + return sw +} + +// Not goroutine safe. +func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { + // Validate the reactor. + // No two reactors can share the same channel. + reactorChannels := reactor.GetChannels() + for _, chDesc := range reactorChannels { + chID := chDesc.ID + if sw.reactorsByCh[chID] != nil { + PanicSanity(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor)) + } + sw.chDescs = append(sw.chDescs, chDesc) + sw.reactorsByCh[chID] = reactor + } + sw.reactors[name] = reactor + reactor.SetSwitch(sw) + return reactor +} + +// Not goroutine safe. +func (sw *Switch) Reactors() map[string]Reactor { + return sw.reactors +} + +// Not goroutine safe. +func (sw *Switch) Reactor(name string) Reactor { + return sw.reactors[name] +} + +// Not goroutine safe. +func (sw *Switch) AddListener(l Listener) { + sw.listeners = append(sw.listeners, l) +} + +// Not goroutine safe. +func (sw *Switch) Listeners() []Listener { + return sw.listeners +} + +// Not goroutine safe. +func (sw *Switch) IsListening() bool { + return len(sw.listeners) > 0 +} + +// Not goroutine safe. +func (sw *Switch) SetNodeInfo(nodeInfo *NodeInfo) { + sw.nodeInfo = nodeInfo +} + +// Not goroutine safe. +func (sw *Switch) NodeInfo() *NodeInfo { + return sw.nodeInfo +} + +// Not goroutine safe. +// NOTE: Overwrites sw.nodeInfo.PubKey +func (sw *Switch) SetNodePrivKey(nodePrivKey crypto.PrivKeyEd25519) { + sw.nodePrivKey = nodePrivKey + if sw.nodeInfo != nil { + sw.nodeInfo.PubKey = nodePrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519) + } +} + +// Switch.Start() starts all the reactors, peers, and listeners. +func (sw *Switch) OnStart() error { + sw.BaseService.OnStart() + // Start reactors + for _, reactor := range sw.reactors { + _, err := reactor.Start() + if err != nil { + return err + } + } + // Start peers + for _, peer := range sw.peers.List() { + sw.startInitPeer(peer) + } + // Start listeners + for _, listener := range sw.listeners { + go sw.listenerRoutine(listener) + } + return nil +} + +func (sw *Switch) OnStop() { + sw.BaseService.OnStop() + // Stop listeners + for _, listener := range sw.listeners { + listener.Stop() + } + sw.listeners = nil + // Stop peers + for _, peer := range sw.peers.List() { + peer.Stop() + sw.peers.Remove(peer) + } + // Stop reactors + for _, reactor := range sw.reactors { + reactor.Stop() + } +} + +// NOTE: This performs a blocking handshake before the peer is added. +// CONTRACT: If error is returned, peer is nil, and conn is immediately closed. +func (sw *Switch) AddPeer(peer *Peer) error { + if err := sw.FilterConnByAddr(peer.Addr()); err != nil { + return err + } + + if err := sw.FilterConnByPubKey(peer.PubKey()); err != nil { + return err + } + + if err := peer.HandshakeTimeout(sw.nodeInfo, time.Duration(sw.config.GetInt(configKeyHandshakeTimeoutSeconds))*time.Second); err != nil { + return err + } + + // Avoid self + if sw.nodeInfo.PubKey.Equals(peer.PubKey().Wrap()) { + return errors.New("Ignoring connection from self") + } + + // Check version, chain id + if err := sw.nodeInfo.CompatibleWith(peer.NodeInfo); err != nil { + return err + } + + // Add the peer to .peers + // ignore if duplicate or if we already have too many for that IP range + if err := sw.peers.Add(peer); err != nil { + log.Notice("Ignoring peer", "error", err, "peer", peer) + peer.Stop() + return err + } + + // Start peer + if sw.IsRunning() { + sw.startInitPeer(peer) + } + + log.Notice("Added peer", "peer", peer) + return nil +} + +func (sw *Switch) FilterConnByAddr(addr net.Addr) error { + if sw.filterConnByAddr != nil { + return sw.filterConnByAddr(addr) + } + return nil +} + +func (sw *Switch) FilterConnByPubKey(pubkey crypto.PubKeyEd25519) error { + if sw.filterConnByPubKey != nil { + return sw.filterConnByPubKey(pubkey) + } + return nil + +} + +func (sw *Switch) SetAddrFilter(f func(net.Addr) error) { + sw.filterConnByAddr = f +} + +func (sw *Switch) SetPubKeyFilter(f func(crypto.PubKeyEd25519) error) { + sw.filterConnByPubKey = f +} + +func (sw *Switch) startInitPeer(peer *Peer) { + peer.Start() // spawn send/recv routines + for _, reactor := range sw.reactors { + reactor.AddPeer(peer) + } +} + +// Dial a list of seeds asynchronously in random order +func (sw *Switch) DialSeeds(addrBook *AddrBook, seeds []string) error { + + netAddrs, err := NewNetAddressStrings(seeds) + if err != nil { + return err + } + + if addrBook != nil { + // add seeds to `addrBook` + ourAddrS := sw.nodeInfo.ListenAddr + ourAddr, _ := NewNetAddressString(ourAddrS) + for _, netAddr := range netAddrs { + // do not add ourselves + if netAddr.Equals(ourAddr) { + continue + } + addrBook.AddAddress(netAddr, ourAddr) + } + addrBook.Save() + } + + // permute the list, dial them in random order. + perm := rand.Perm(len(netAddrs)) + for i := 0; i < len(perm); i++ { + go func(i int) { + time.Sleep(time.Duration(rand.Int63n(3000)) * time.Millisecond) + j := perm[i] + sw.dialSeed(netAddrs[j]) + }(i) + } + return nil +} + +func (sw *Switch) dialSeed(addr *NetAddress) { + peer, err := sw.DialPeerWithAddress(addr, true) + if err != nil { + log.Error("Error dialing seed", "error", err) + return + } else { + log.Notice("Connected to seed", "peer", peer) + } +} + +func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) (*Peer, error) { + sw.dialing.Set(addr.IP.String(), addr) + defer sw.dialing.Delete(addr.IP.String()) + + peer, err := newOutboundPeerWithConfig(addr, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, peerConfigFromGoConfig(sw.config)) + if err != nil { + log.Info("Failed dialing peer", "address", addr, "error", err) + return nil, err + } + if persistent { + peer.makePersistent() + } + err = sw.AddPeer(peer) + if err != nil { + log.Info("Failed adding peer", "address", addr, "error", err) + peer.CloseConn() + return nil, err + } + log.Notice("Dialed and added peer", "address", addr, "peer", peer) + return peer, nil +} + +func (sw *Switch) IsDialing(addr *NetAddress) bool { + return sw.dialing.Has(addr.IP.String()) +} + +// Broadcast runs a go routine for each attempted send, which will block +// trying to send for defaultSendTimeoutSeconds. Returns a channel +// which receives success values for each attempted send (false if times out) +// NOTE: Broadcast uses goroutines, so order of broadcast may not be preserved. +func (sw *Switch) Broadcast(chID byte, msg interface{}) chan bool { + successChan := make(chan bool, len(sw.peers.List())) + log.Debug("Broadcast", "channel", chID, "msg", msg) + for _, peer := range sw.peers.List() { + go func(peer *Peer) { + success := peer.Send(chID, msg) + successChan <- success + }(peer) + } + return successChan +} + +// Returns the count of outbound/inbound and outbound-dialing peers. +func (sw *Switch) NumPeers() (outbound, inbound, dialing int) { + peers := sw.peers.List() + for _, peer := range peers { + if peer.outbound { + outbound++ + } else { + inbound++ + } + } + dialing = sw.dialing.Size() + return +} + +func (sw *Switch) Peers() IPeerSet { + return sw.peers +} + +// Disconnect from a peer due to external error, retry if it is a persistent peer. +// TODO: make record depending on reason. +func (sw *Switch) StopPeerForError(peer *Peer, reason interface{}) { + addr := NewNetAddress(peer.Addr()) + log.Notice("Stopping peer for error", "peer", peer, "error", reason) + sw.stopAndRemovePeer(peer, reason) + + if peer.IsPersistent() { + go func() { + log.Notice("Reconnecting to peer", "peer", peer) + for i := 1; i < reconnectAttempts; i++ { + if !sw.IsRunning() { + return + } + + peer, err := sw.DialPeerWithAddress(addr, true) + if err != nil { + if i == reconnectAttempts { + log.Notice("Error reconnecting to peer. Giving up", "tries", i, "error", err) + return + } + log.Notice("Error reconnecting to peer. Trying again", "tries", i, "error", err) + time.Sleep(reconnectInterval) + continue + } + + log.Notice("Reconnected to peer", "peer", peer) + return + } + }() + } +} + +// Disconnect from a peer gracefully. +// TODO: handle graceful disconnects. +func (sw *Switch) StopPeerGracefully(peer *Peer) { + log.Notice("Stopping peer gracefully") + sw.stopAndRemovePeer(peer, nil) +} + +func (sw *Switch) stopAndRemovePeer(peer *Peer, reason interface{}) { + sw.peers.Remove(peer) + peer.Stop() + for _, reactor := range sw.reactors { + reactor.RemovePeer(peer, reason) + } +} + +func (sw *Switch) listenerRoutine(l Listener) { + for { + inConn, ok := <-l.Connections() + if !ok { + break + } + + // ignore connection if we already have enough + maxPeers := sw.config.GetInt(configKeyMaxNumPeers) + if maxPeers <= sw.peers.Size() { + log.Info("Ignoring inbound connection: already have enough peers", "address", inConn.RemoteAddr().String(), "numPeers", sw.peers.Size(), "max", maxPeers) + continue + } + + // New inbound connection! + err := sw.addPeerWithConnectionAndConfig(inConn, peerConfigFromGoConfig(sw.config)) + if err != nil { + log.Notice("Ignoring inbound connection: error while adding peer", "address", inConn.RemoteAddr().String(), "error", err) + continue + } + + // NOTE: We don't yet have the listening port of the + // remote (if they have a listener at all). + // The peerHandshake will handle that + } + + // cleanup +} + +//----------------------------------------------------------------------------- + +type SwitchEventNewPeer struct { + Peer *Peer +} + +type SwitchEventDonePeer struct { + Peer *Peer + Error interface{} +} + +//------------------------------------------------------------------ +// Switches connected via arbitrary net.Conn; useful for testing + +// Returns n switches, connected according to the connect func. +// If connect==Connect2Switches, the switches will be fully connected. +// initSwitch defines how the ith switch should be initialized (ie. with what reactors). +// NOTE: panics if any switch fails to start. +func MakeConnectedSwitches(n int, initSwitch func(int, *Switch) *Switch, connect func([]*Switch, int, int)) []*Switch { + switches := make([]*Switch, n) + for i := 0; i < n; i++ { + switches[i] = makeSwitch(i, "testing", "123.123.123", initSwitch) + } + + if err := StartSwitches(switches); err != nil { + panic(err) + } + + for i := 0; i < n; i++ { + for j := i; j < n; j++ { + connect(switches, i, j) + } + } + + return switches +} + +var PanicOnAddPeerErr = false + +// Will connect switches i and j via net.Pipe() +// Blocks until a conection is established. +// NOTE: caller ensures i and j are within bounds +func Connect2Switches(switches []*Switch, i, j int) { + switchI := switches[i] + switchJ := switches[j] + c1, c2 := net.Pipe() + doneCh := make(chan struct{}) + go func() { + err := switchI.addPeerWithConnection(c1) + if PanicOnAddPeerErr && err != nil { + panic(err) + } + doneCh <- struct{}{} + }() + go func() { + err := switchJ.addPeerWithConnection(c2) + if PanicOnAddPeerErr && err != nil { + panic(err) + } + doneCh <- struct{}{} + }() + <-doneCh + <-doneCh +} + +func StartSwitches(switches []*Switch) error { + for _, s := range switches { + _, err := s.Start() // start switch and reactors + if err != nil { + return err + } + } + return nil +} + +func makeSwitch(i int, network, version string, initSwitch func(int, *Switch) *Switch) *Switch { + privKey := crypto.GenPrivKeyEd25519() + // new switch, add reactors + // TODO: let the config be passed in? + s := initSwitch(i, NewSwitch(viper.New())) + s.SetNodeInfo(&NodeInfo{ + PubKey: privKey.PubKey().Unwrap().(crypto.PubKeyEd25519), + Moniker: Fmt("switch%d", i), + Network: network, + Version: version, + RemoteAddr: Fmt("%v:%v", network, rand.Intn(64512)+1023), + ListenAddr: Fmt("%v:%v", network, rand.Intn(64512)+1023), + }) + s.SetNodePrivKey(privKey) + return s +} + +func (sw *Switch) addPeerWithConnection(conn net.Conn) error { + peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey) + if err != nil { + conn.Close() + return err + } + + if err = sw.AddPeer(peer); err != nil { + conn.Close() + return err + } + + return nil +} + +func (sw *Switch) addPeerWithConnectionAndConfig(conn net.Conn, config *PeerConfig) error { + peer, err := newInboundPeerWithConfig(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, config) + if err != nil { + conn.Close() + return err + } + + if err = sw.AddPeer(peer); err != nil { + conn.Close() + return err + } + + return nil +} + +func peerConfigFromGoConfig(config *viper.Viper) *PeerConfig { + return &PeerConfig{ + AuthEnc: config.GetBool(configKeyAuthEnc), + Fuzz: config.GetBool(configFuzzEnable), + HandshakeTimeout: time.Duration(config.GetInt(configKeyHandshakeTimeoutSeconds)) * time.Second, + DialTimeout: time.Duration(config.GetInt(configKeyDialTimeoutSeconds)) * time.Second, + MConfig: &MConnConfig{ + SendRate: int64(config.GetInt(configKeySendRate)), + RecvRate: int64(config.GetInt(configKeyRecvRate)), + }, + FuzzConfig: &FuzzConnConfig{ + Mode: config.GetInt(configFuzzMode), + MaxDelay: time.Duration(config.GetInt(configFuzzMaxDelayMilliseconds)) * time.Millisecond, + ProbDropRW: config.GetFloat64(configFuzzProbDropRW), + ProbDropConn: config.GetFloat64(configFuzzProbDropConn), + ProbSleep: config.GetFloat64(configFuzzProbSleep), + }, + } +} diff --git a/p2p/switch_test.go b/p2p/switch_test.go new file mode 100644 index 00000000..38529d75 --- /dev/null +++ b/p2p/switch_test.go @@ -0,0 +1,331 @@ +package p2p + +import ( + "bytes" + "fmt" + "net" + "sync" + "testing" + "time" + + "github.com/spf13/viper" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" + . "github.com/tendermint/tmlibs/common" +) + +var ( + config *viper.Viper +) + +func init() { + config = viper.New() + setConfigDefaults(config) +} + +type PeerMessage struct { + PeerKey string + Bytes []byte + Counter int +} + +type TestReactor struct { + BaseReactor + + mtx sync.Mutex + channels []*ChannelDescriptor + peersAdded []*Peer + peersRemoved []*Peer + logMessages bool + msgsCounter int + msgsReceived map[byte][]PeerMessage +} + +func NewTestReactor(channels []*ChannelDescriptor, logMessages bool) *TestReactor { + tr := &TestReactor{ + channels: channels, + logMessages: logMessages, + msgsReceived: make(map[byte][]PeerMessage), + } + tr.BaseReactor = *NewBaseReactor(log, "TestReactor", tr) + return tr +} + +func (tr *TestReactor) GetChannels() []*ChannelDescriptor { + return tr.channels +} + +func (tr *TestReactor) AddPeer(peer *Peer) { + tr.mtx.Lock() + defer tr.mtx.Unlock() + tr.peersAdded = append(tr.peersAdded, peer) +} + +func (tr *TestReactor) RemovePeer(peer *Peer, reason interface{}) { + tr.mtx.Lock() + defer tr.mtx.Unlock() + tr.peersRemoved = append(tr.peersRemoved, peer) +} + +func (tr *TestReactor) Receive(chID byte, peer *Peer, msgBytes []byte) { + if tr.logMessages { + tr.mtx.Lock() + defer tr.mtx.Unlock() + //fmt.Printf("Received: %X, %X\n", chID, msgBytes) + tr.msgsReceived[chID] = append(tr.msgsReceived[chID], PeerMessage{peer.Key, msgBytes, tr.msgsCounter}) + tr.msgsCounter++ + } +} + +func (tr *TestReactor) getMsgs(chID byte) []PeerMessage { + tr.mtx.Lock() + defer tr.mtx.Unlock() + return tr.msgsReceived[chID] +} + +//----------------------------------------------------------------------------- + +// convenience method for creating two switches connected to each other. +// XXX: note this uses net.Pipe and not a proper TCP conn +func makeSwitchPair(t testing.TB, initSwitch func(int, *Switch) *Switch) (*Switch, *Switch) { + // Create two switches that will be interconnected. + switches := MakeConnectedSwitches(2, initSwitch, Connect2Switches) + return switches[0], switches[1] +} + +func initSwitchFunc(i int, sw *Switch) *Switch { + // Make two reactors of two channels each + sw.AddReactor("foo", NewTestReactor([]*ChannelDescriptor{ + &ChannelDescriptor{ID: byte(0x00), Priority: 10}, + &ChannelDescriptor{ID: byte(0x01), Priority: 10}, + }, true)) + sw.AddReactor("bar", NewTestReactor([]*ChannelDescriptor{ + &ChannelDescriptor{ID: byte(0x02), Priority: 10}, + &ChannelDescriptor{ID: byte(0x03), Priority: 10}, + }, true)) + return sw +} + +func TestSwitches(t *testing.T) { + s1, s2 := makeSwitchPair(t, initSwitchFunc) + defer s1.Stop() + defer s2.Stop() + + if s1.Peers().Size() != 1 { + t.Errorf("Expected exactly 1 peer in s1, got %v", s1.Peers().Size()) + } + if s2.Peers().Size() != 1 { + t.Errorf("Expected exactly 1 peer in s2, got %v", s2.Peers().Size()) + } + + // Lets send some messages + ch0Msg := "channel zero" + ch1Msg := "channel foo" + ch2Msg := "channel bar" + + s1.Broadcast(byte(0x00), ch0Msg) + s1.Broadcast(byte(0x01), ch1Msg) + s1.Broadcast(byte(0x02), ch2Msg) + + // Wait for things to settle... + time.Sleep(5000 * time.Millisecond) + + // Check message on ch0 + ch0Msgs := s2.Reactor("foo").(*TestReactor).getMsgs(byte(0x00)) + if len(ch0Msgs) != 1 { + t.Errorf("Expected to have received 1 message in ch0") + } + if !bytes.Equal(ch0Msgs[0].Bytes, wire.BinaryBytes(ch0Msg)) { + t.Errorf("Unexpected message bytes. Wanted: %X, Got: %X", wire.BinaryBytes(ch0Msg), ch0Msgs[0].Bytes) + } + + // Check message on ch1 + ch1Msgs := s2.Reactor("foo").(*TestReactor).getMsgs(byte(0x01)) + if len(ch1Msgs) != 1 { + t.Errorf("Expected to have received 1 message in ch1") + } + if !bytes.Equal(ch1Msgs[0].Bytes, wire.BinaryBytes(ch1Msg)) { + t.Errorf("Unexpected message bytes. Wanted: %X, Got: %X", wire.BinaryBytes(ch1Msg), ch1Msgs[0].Bytes) + } + + // Check message on ch2 + ch2Msgs := s2.Reactor("bar").(*TestReactor).getMsgs(byte(0x02)) + if len(ch2Msgs) != 1 { + t.Errorf("Expected to have received 1 message in ch2") + } + if !bytes.Equal(ch2Msgs[0].Bytes, wire.BinaryBytes(ch2Msg)) { + t.Errorf("Unexpected message bytes. Wanted: %X, Got: %X", wire.BinaryBytes(ch2Msg), ch2Msgs[0].Bytes) + } + +} + +func TestConnAddrFilter(t *testing.T) { + s1 := makeSwitch(1, "testing", "123.123.123", initSwitchFunc) + s2 := makeSwitch(1, "testing", "123.123.123", initSwitchFunc) + + c1, c2 := net.Pipe() + + s1.SetAddrFilter(func(addr net.Addr) error { + if addr.String() == c1.RemoteAddr().String() { + return fmt.Errorf("Error: pipe is blacklisted") + } + return nil + }) + + // connect to good peer + go func() { + s1.addPeerWithConnection(c1) + }() + go func() { + s2.addPeerWithConnection(c2) + }() + + // Wait for things to happen, peers to get added... + time.Sleep(100 * time.Millisecond * time.Duration(4)) + + defer s1.Stop() + defer s2.Stop() + if s1.Peers().Size() != 0 { + t.Errorf("Expected s1 not to connect to peers, got %d", s1.Peers().Size()) + } + if s2.Peers().Size() != 0 { + t.Errorf("Expected s2 not to connect to peers, got %d", s2.Peers().Size()) + } +} + +func TestConnPubKeyFilter(t *testing.T) { + s1 := makeSwitch(1, "testing", "123.123.123", initSwitchFunc) + s2 := makeSwitch(1, "testing", "123.123.123", initSwitchFunc) + + c1, c2 := net.Pipe() + + // set pubkey filter + s1.SetPubKeyFilter(func(pubkey crypto.PubKeyEd25519) error { + if bytes.Equal(pubkey.Bytes(), s2.nodeInfo.PubKey.Bytes()) { + return fmt.Errorf("Error: pipe is blacklisted") + } + return nil + }) + + // connect to good peer + go func() { + s1.addPeerWithConnection(c1) + }() + go func() { + s2.addPeerWithConnection(c2) + }() + + // Wait for things to happen, peers to get added... + time.Sleep(100 * time.Millisecond * time.Duration(4)) + + defer s1.Stop() + defer s2.Stop() + if s1.Peers().Size() != 0 { + t.Errorf("Expected s1 not to connect to peers, got %d", s1.Peers().Size()) + } + if s2.Peers().Size() != 0 { + t.Errorf("Expected s2 not to connect to peers, got %d", s2.Peers().Size()) + } +} + +func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + sw := makeSwitch(1, "testing", "123.123.123", initSwitchFunc) + sw.Start() + defer sw.Stop() + + // simulate remote peer + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: DefaultPeerConfig()} + rp.Start() + defer rp.Stop() + + peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey) + require.Nil(err) + err = sw.AddPeer(peer) + require.Nil(err) + + // simulate failure by closing connection + peer.CloseConn() + + time.Sleep(100 * time.Millisecond) + + assert.Zero(sw.Peers().Size()) + assert.False(peer.IsRunning()) +} + +func TestSwitchReconnectsToPersistentPeer(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + sw := makeSwitch(1, "testing", "123.123.123", initSwitchFunc) + sw.Start() + defer sw.Stop() + + // simulate remote peer + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: DefaultPeerConfig()} + rp.Start() + defer rp.Stop() + + peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey) + peer.makePersistent() + require.Nil(err) + err = sw.AddPeer(peer) + require.Nil(err) + + // simulate failure by closing connection + peer.CloseConn() + + time.Sleep(100 * time.Millisecond) + + assert.NotZero(sw.Peers().Size()) + assert.False(peer.IsRunning()) +} + +func BenchmarkSwitches(b *testing.B) { + + b.StopTimer() + + s1, s2 := makeSwitchPair(b, func(i int, sw *Switch) *Switch { + // Make bar reactors of bar channels each + sw.AddReactor("foo", NewTestReactor([]*ChannelDescriptor{ + &ChannelDescriptor{ID: byte(0x00), Priority: 10}, + &ChannelDescriptor{ID: byte(0x01), Priority: 10}, + }, false)) + sw.AddReactor("bar", NewTestReactor([]*ChannelDescriptor{ + &ChannelDescriptor{ID: byte(0x02), Priority: 10}, + &ChannelDescriptor{ID: byte(0x03), Priority: 10}, + }, false)) + return sw + }) + defer s1.Stop() + defer s2.Stop() + + // Allow time for goroutines to boot up + time.Sleep(1000 * time.Millisecond) + b.StartTimer() + + numSuccess, numFailure := 0, 0 + + // Send random message from foo channel to another + for i := 0; i < b.N; i++ { + chID := byte(i % 4) + successChan := s1.Broadcast(chID, "test data") + for s := range successChan { + if s { + numSuccess++ + } else { + numFailure++ + } + } + } + + log.Warn(Fmt("success: %v, failure: %v", numSuccess, numFailure)) + + // Allow everything to flush before stopping switches & closing connections. + b.StopTimer() + time.Sleep(1000 * time.Millisecond) + +} diff --git a/p2p/types.go b/p2p/types.go new file mode 100644 index 00000000..4f3e4c1d --- /dev/null +++ b/p2p/types.go @@ -0,0 +1,77 @@ +package p2p + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/tendermint/go-crypto" +) + +const maxNodeInfoSize = 10240 // 10Kb + +type NodeInfo struct { + PubKey crypto.PubKeyEd25519 `json:"pub_key"` + Moniker string `json:"moniker"` + Network string `json:"network"` + RemoteAddr string `json:"remote_addr"` + ListenAddr string `json:"listen_addr"` + Version string `json:"version"` // major.minor.revision + Other []string `json:"other"` // other application specific data +} + +// CONTRACT: two nodes are compatible if the major/minor versions match and network match +func (info *NodeInfo) CompatibleWith(other *NodeInfo) error { + iMajor, iMinor, _, iErr := splitVersion(info.Version) + oMajor, oMinor, _, oErr := splitVersion(other.Version) + + // if our own version number is not formatted right, we messed up + if iErr != nil { + return iErr + } + + // version number must be formatted correctly ("x.x.x") + if oErr != nil { + return oErr + } + + // major version must match + if iMajor != oMajor { + return fmt.Errorf("Peer is on a different major version. Got %v, expected %v", oMajor, iMajor) + } + + // minor version must match + if iMinor != oMinor { + return fmt.Errorf("Peer is on a different minor version. Got %v, expected %v", oMinor, iMinor) + } + + // nodes must be on the same network + if info.Network != other.Network { + return fmt.Errorf("Peer is on a different network. Got %v, expected %v", other.Network, info.Network) + } + + return nil +} + +func (info *NodeInfo) ListenHost() string { + host, _, _ := net.SplitHostPort(info.ListenAddr) + return host +} + +func (info *NodeInfo) ListenPort() int { + _, port, _ := net.SplitHostPort(info.ListenAddr) + port_i, err := strconv.Atoi(port) + if err != nil { + return -1 + } + return port_i +} + +func splitVersion(version string) (string, string, string, error) { + spl := strings.Split(version, ".") + if len(spl) != 3 { + return "", "", "", fmt.Errorf("Invalid version format %v", version) + } + return spl[0], spl[1], spl[2], nil +} diff --git a/p2p/upnp/README.md b/p2p/upnp/README.md new file mode 100644 index 00000000..557d05bd --- /dev/null +++ b/p2p/upnp/README.md @@ -0,0 +1,5 @@ +# `tendermint/p2p/upnp` + +## Resources + +* http://www.upnp-hacks.org/upnp.html diff --git a/p2p/upnp/log.go b/p2p/upnp/log.go new file mode 100644 index 00000000..45e44439 --- /dev/null +++ b/p2p/upnp/log.go @@ -0,0 +1,7 @@ +package upnp + +import ( + "github.com/tendermint/tmlibs/logger" +) + +var log = logger.New("module", "upnp") diff --git a/p2p/upnp/probe.go b/p2p/upnp/probe.go new file mode 100644 index 00000000..5488de58 --- /dev/null +++ b/p2p/upnp/probe.go @@ -0,0 +1,111 @@ +package upnp + +import ( + "errors" + "fmt" + "net" + "time" + + . "github.com/tendermint/tmlibs/common" +) + +type UPNPCapabilities struct { + PortMapping bool + Hairpin bool +} + +func makeUPNPListener(intPort int, extPort int) (NAT, net.Listener, net.IP, error) { + nat, err := Discover() + if err != nil { + return nil, nil, nil, errors.New(fmt.Sprintf("NAT upnp could not be discovered: %v", err)) + } + log.Info(Fmt("ourIP: %v", nat.(*upnpNAT).ourIP)) + + ext, err := nat.GetExternalAddress() + if err != nil { + return nat, nil, nil, errors.New(fmt.Sprintf("External address error: %v", err)) + } + log.Info(Fmt("External address: %v", ext)) + + port, err := nat.AddPortMapping("tcp", extPort, intPort, "Tendermint UPnP Probe", 0) + if err != nil { + return nat, nil, ext, errors.New(fmt.Sprintf("Port mapping error: %v", err)) + } + log.Info(Fmt("Port mapping mapped: %v", port)) + + // also run the listener, open for all remote addresses. + listener, err := net.Listen("tcp", fmt.Sprintf(":%v", intPort)) + if err != nil { + return nat, nil, ext, errors.New(fmt.Sprintf("Error establishing listener: %v", err)) + } + return nat, listener, ext, nil +} + +func testHairpin(listener net.Listener, extAddr string) (supportsHairpin bool) { + // Listener + go func() { + inConn, err := listener.Accept() + if err != nil { + log.Notice(Fmt("Listener.Accept() error: %v", err)) + return + } + log.Info(Fmt("Accepted incoming connection: %v -> %v", inConn.LocalAddr(), inConn.RemoteAddr())) + buf := make([]byte, 1024) + n, err := inConn.Read(buf) + if err != nil { + log.Notice(Fmt("Incoming connection read error: %v", err)) + return + } + log.Info(Fmt("Incoming connection read %v bytes: %X", n, buf)) + if string(buf) == "test data" { + supportsHairpin = true + return + } + }() + + // Establish outgoing + outConn, err := net.Dial("tcp", extAddr) + if err != nil { + log.Notice(Fmt("Outgoing connection dial error: %v", err)) + return + } + + n, err := outConn.Write([]byte("test data")) + if err != nil { + log.Notice(Fmt("Outgoing connection write error: %v", err)) + return + } + log.Info(Fmt("Outgoing connection wrote %v bytes", n)) + + // Wait for data receipt + time.Sleep(1 * time.Second) + return +} + +func Probe() (caps UPNPCapabilities, err error) { + log.Info("Probing for UPnP!") + + intPort, extPort := 8001, 8001 + + nat, listener, ext, err := makeUPNPListener(intPort, extPort) + if err != nil { + return + } + caps.PortMapping = true + + // Deferred cleanup + defer func() { + err = nat.DeletePortMapping("tcp", intPort, extPort) + if err != nil { + log.Warn(Fmt("Port mapping delete error: %v", err)) + } + listener.Close() + }() + + supportsHairpin := testHairpin(listener, fmt.Sprintf("%v:%v", ext, extPort)) + if supportsHairpin { + caps.Hairpin = true + } + + return +} diff --git a/p2p/upnp/upnp.go b/p2p/upnp/upnp.go new file mode 100644 index 00000000..3d6c5503 --- /dev/null +++ b/p2p/upnp/upnp.go @@ -0,0 +1,380 @@ +/* +Taken from taipei-torrent + +Just enough UPnP to be able to forward ports +*/ +package upnp + +// BUG(jae): TODO: use syscalls to get actual ourIP. http://pastebin.com/9exZG4rh + +import ( + "bytes" + "encoding/xml" + "errors" + "io/ioutil" + "net" + "net/http" + "strconv" + "strings" + "time" +) + +type upnpNAT struct { + serviceURL string + ourIP string + urnDomain string +} + +// protocol is either "udp" or "tcp" +type NAT interface { + GetExternalAddress() (addr net.IP, err error) + AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) + DeletePortMapping(protocol string, externalPort, internalPort int) (err error) +} + +func Discover() (nat NAT, err error) { + ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") + if err != nil { + return + } + conn, err := net.ListenPacket("udp4", ":0") + if err != nil { + return + } + socket := conn.(*net.UDPConn) + defer socket.Close() + + err = socket.SetDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + return + } + + st := "InternetGatewayDevice:1" + + buf := bytes.NewBufferString( + "M-SEARCH * HTTP/1.1\r\n" + + "HOST: 239.255.255.250:1900\r\n" + + "ST: ssdp:all\r\n" + + "MAN: \"ssdp:discover\"\r\n" + + "MX: 2\r\n\r\n") + message := buf.Bytes() + answerBytes := make([]byte, 1024) + for i := 0; i < 3; i++ { + _, err = socket.WriteToUDP(message, ssdp) + if err != nil { + return + } + var n int + n, _, err = socket.ReadFromUDP(answerBytes) + for { + n, _, err = socket.ReadFromUDP(answerBytes) + if err != nil { + break + } + answer := string(answerBytes[0:n]) + if strings.Index(answer, st) < 0 { + continue + } + // HTTP header field names are case-insensitive. + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + locString := "\r\nlocation:" + answer = strings.ToLower(answer) + locIndex := strings.Index(answer, locString) + if locIndex < 0 { + continue + } + loc := answer[locIndex+len(locString):] + endIndex := strings.Index(loc, "\r\n") + if endIndex < 0 { + continue + } + locURL := strings.TrimSpace(loc[0:endIndex]) + var serviceURL, urnDomain string + serviceURL, urnDomain, err = getServiceURL(locURL) + if err != nil { + return + } + var ourIP net.IP + ourIP, err = localIPv4() + if err != nil { + return + } + nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain} + return + } + } + err = errors.New("UPnP port discovery failed.") + return +} + +type Envelope struct { + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` + Soap *SoapBody +} +type SoapBody struct { + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` + ExternalIP *ExternalIPAddressResponse +} + +type ExternalIPAddressResponse struct { + XMLName xml.Name `xml:"GetExternalIPAddressResponse"` + IPAddress string `xml:"NewExternalIPAddress"` +} + +type ExternalIPAddress struct { + XMLName xml.Name `xml:"NewExternalIPAddress"` + IP string +} + +type UPNPService struct { + ServiceType string `xml:"serviceType"` + ControlURL string `xml:"controlURL"` +} + +type DeviceList struct { + Device []Device `xml:"device"` +} + +type ServiceList struct { + Service []UPNPService `xml:"service"` +} + +type Device struct { + XMLName xml.Name `xml:"device"` + DeviceType string `xml:"deviceType"` + DeviceList DeviceList `xml:"deviceList"` + ServiceList ServiceList `xml:"serviceList"` +} + +type Root struct { + Device Device +} + +func getChildDevice(d *Device, deviceType string) *Device { + dl := d.DeviceList.Device + for i := 0; i < len(dl); i++ { + if strings.Index(dl[i].DeviceType, deviceType) >= 0 { + return &dl[i] + } + } + return nil +} + +func getChildService(d *Device, serviceType string) *UPNPService { + sl := d.ServiceList.Service + for i := 0; i < len(sl); i++ { + if strings.Index(sl[i].ServiceType, serviceType) >= 0 { + return &sl[i] + } + } + return nil +} + +func localIPv4() (net.IP, error) { + tt, err := net.Interfaces() + if err != nil { + return nil, err + } + for _, t := range tt { + aa, err := t.Addrs() + if err != nil { + return nil, err + } + for _, a := range aa { + ipnet, ok := a.(*net.IPNet) + if !ok { + continue + } + v4 := ipnet.IP.To4() + if v4 == nil || v4[0] == 127 { // loopback address + continue + } + return v4, nil + } + } + return nil, errors.New("cannot find local IP address") +} + +func getServiceURL(rootURL string) (url, urnDomain string, err error) { + r, err := http.Get(rootURL) + if err != nil { + return + } + defer r.Body.Close() + if r.StatusCode >= 400 { + err = errors.New(string(r.StatusCode)) + return + } + var root Root + err = xml.NewDecoder(r.Body).Decode(&root) + if err != nil { + return + } + a := &root.Device + if strings.Index(a.DeviceType, "InternetGatewayDevice:1") < 0 { + err = errors.New("No InternetGatewayDevice") + return + } + b := getChildDevice(a, "WANDevice:1") + if b == nil { + err = errors.New("No WANDevice") + return + } + c := getChildDevice(b, "WANConnectionDevice:1") + if c == nil { + err = errors.New("No WANConnectionDevice") + return + } + d := getChildService(c, "WANIPConnection:1") + if d == nil { + // Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice, + // instead of under WanConnectionDevice + d = getChildService(b, "WANIPConnection:1") + + if d == nil { + err = errors.New("No WANIPConnection") + return + } + } + // Extract the domain name, which isn't always 'schemas-upnp-org' + urnDomain = strings.Split(d.ServiceType, ":")[1] + url = combineURL(rootURL, d.ControlURL) + return +} + +func combineURL(rootURL, subURL string) string { + protocolEnd := "://" + protoEndIndex := strings.Index(rootURL, protocolEnd) + a := rootURL[protoEndIndex+len(protocolEnd):] + rootIndex := strings.Index(a, "/") + return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL +} + +func soapRequest(url, function, message, domain string) (r *http.Response, err error) { + fullMessage := "" + + "\r\n" + + "" + message + "" + + req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") + req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") + //req.Header.Set("Transfer-Encoding", "chunked") + req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"") + req.Header.Set("Connection", "Close") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") + + // log.Stderr("soapRequest ", req) + + r, err = http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + /*if r.Body != nil { + defer r.Body.Close() + }*/ + + if r.StatusCode >= 400 { + // log.Stderr(function, r.StatusCode) + err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) + r = nil + return + } + return +} + +type statusInfo struct { + externalIpAddress string +} + +func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) { + + message := "\r\n" + + "" + + var response *http.Response + response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) + if response != nil { + defer response.Body.Close() + } + if err != nil { + return + } + var envelope Envelope + data, err := ioutil.ReadAll(response.Body) + reader := bytes.NewReader(data) + xml.NewDecoder(reader).Decode(&envelope) + + info = statusInfo{envelope.Soap.ExternalIP.IPAddress} + + if err != nil { + return + } + + return +} + +func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { + info, err := n.getExternalIPAddress() + if err != nil { + return + } + addr = net.ParseIP(info.externalIpAddress) + return +} + +func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { + // A single concatenation would break ARM compilation. + message := "\r\n" + + "" + strconv.Itoa(externalPort) + message += "" + protocol + "" + message += "" + strconv.Itoa(internalPort) + "" + + "" + n.ourIP + "" + + "1" + message += description + + "" + strconv.Itoa(timeout) + + "" + + var response *http.Response + response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) + if response != nil { + defer response.Body.Close() + } + if err != nil { + return + } + + // TODO: check response to see if the port was forwarded + // log.Println(message, response) + // JAE: + // body, err := ioutil.ReadAll(response.Body) + // fmt.Println(string(body), err) + mappedExternalPort = externalPort + _ = response + return +} + +func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { + + message := "\r\n" + + "" + strconv.Itoa(externalPort) + + "" + protocol + "" + + "" + + var response *http.Response + response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) + if response != nil { + defer response.Body.Close() + } + if err != nil { + return + } + + // TODO: check response to see if the port was deleted + // log.Println(message, response) + _ = response + return +} diff --git a/p2p/util.go b/p2p/util.go new file mode 100644 index 00000000..2be32026 --- /dev/null +++ b/p2p/util.go @@ -0,0 +1,15 @@ +package p2p + +import ( + "crypto/sha256" +) + +// doubleSha256 calculates sha256(sha256(b)) and returns the resulting bytes. +func doubleSha256(b []byte) []byte { + hasher := sha256.New() + hasher.Write(b) + sum := hasher.Sum(nil) + hasher.Reset() + hasher.Write(sum) + return hasher.Sum(nil) +} diff --git a/p2p/version.go b/p2p/version.go new file mode 100644 index 00000000..9a4c7bba --- /dev/null +++ b/p2p/version.go @@ -0,0 +1,3 @@ +package p2p + +const Version = "0.5.0" diff --git a/proxy/app_conn_test.go b/proxy/app_conn_test.go index 2054175e..f9c6afc9 100644 --- a/proxy/app_conn_test.go +++ b/proxy/app_conn_test.go @@ -4,11 +4,11 @@ import ( "strings" "testing" - . "github.com/tendermint/go-common" abcicli "github.com/tendermint/abci/client" "github.com/tendermint/abci/example/dummy" "github.com/tendermint/abci/server" "github.com/tendermint/abci/types" + cmn "github.com/tendermint/tmlibs/common" ) //---------------------------------------- @@ -44,43 +44,43 @@ func (app *appConnTest) InfoSync() (types.ResponseInfo, error) { var SOCKET = "socket" func TestEcho(t *testing.T) { - sockPath := Fmt("unix:///tmp/echo_%v.sock", RandStr(6)) + sockPath := cmn.Fmt("unix:///tmp/echo_%v.sock", cmn.RandStr(6)) clientCreator := NewRemoteClientCreator(sockPath, SOCKET, true) // Start server s, err := server.NewSocketServer(sockPath, dummy.NewDummyApplication()) if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } defer s.Stop() // Start client cli, err := clientCreator.NewABCIClient() if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } proxy := NewAppConnTest(cli) t.Log("Connected") for i := 0; i < 1000; i++ { - proxy.EchoAsync(Fmt("echo-%v", i)) + proxy.EchoAsync(cmn.Fmt("echo-%v", i)) } proxy.FlushSync() } func BenchmarkEcho(b *testing.B) { b.StopTimer() // Initialize - sockPath := Fmt("unix:///tmp/echo_%v.sock", RandStr(6)) + sockPath := cmn.Fmt("unix:///tmp/echo_%v.sock", cmn.RandStr(6)) clientCreator := NewRemoteClientCreator(sockPath, SOCKET, true) // Start server s, err := server.NewSocketServer(sockPath, dummy.NewDummyApplication()) if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } defer s.Stop() // Start client cli, err := clientCreator.NewABCIClient() if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } proxy := NewAppConnTest(cli) b.Log("Connected") @@ -98,18 +98,18 @@ func BenchmarkEcho(b *testing.B) { } func TestInfo(t *testing.T) { - sockPath := Fmt("unix:///tmp/echo_%v.sock", RandStr(6)) + sockPath := cmn.Fmt("unix:///tmp/echo_%v.sock", cmn.RandStr(6)) clientCreator := NewRemoteClientCreator(sockPath, SOCKET, true) // Start server s, err := server.NewSocketServer(sockPath, dummy.NewDummyApplication()) if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } defer s.Stop() // Start client cli, err := clientCreator.NewABCIClient() if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } proxy := NewAppConnTest(cli) t.Log("Connected") diff --git a/proxy/client.go b/proxy/client.go index ea905198..4c50f38b 100644 --- a/proxy/client.go +++ b/proxy/client.go @@ -4,10 +4,11 @@ import ( "fmt" "sync" + "github.com/spf13/viper" + abcicli "github.com/tendermint/abci/client" "github.com/tendermint/abci/example/dummy" "github.com/tendermint/abci/types" - cfg "github.com/tendermint/go-config" ) // NewABCIClient returns newly connected client @@ -63,7 +64,7 @@ func (r *remoteClientCreator) NewABCIClient() (abcicli.Client, error) { //----------------------------------------------------------------- // default -func DefaultClientCreator(config cfg.Config) ClientCreator { +func DefaultClientCreator(config *viper.Viper) ClientCreator { addr := config.GetString("proxy_app") transport := config.GetString("abci") diff --git a/proxy/log.go b/proxy/log.go index 45d31b87..d1d241bf 100644 --- a/proxy/log.go +++ b/proxy/log.go @@ -1,7 +1,7 @@ package proxy import ( - "github.com/tendermint/go-logger" + "github.com/tendermint/tmlibs/logger" ) var log = logger.New("module", "proxy") diff --git a/proxy/multi_app_conn.go b/proxy/multi_app_conn.go index 81e01aa2..9f70713e 100644 --- a/proxy/multi_app_conn.go +++ b/proxy/multi_app_conn.go @@ -1,8 +1,8 @@ package proxy import ( - cmn "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" + "github.com/spf13/viper" + cmn "github.com/tendermint/tmlibs/common" ) //----------------------------- @@ -16,7 +16,7 @@ type AppConns interface { Query() AppConnQuery } -func NewAppConns(config cfg.Config, clientCreator ClientCreator, handshaker Handshaker) AppConns { +func NewAppConns(config *viper.Viper, clientCreator ClientCreator, handshaker Handshaker) AppConns { return NewMultiAppConn(config, clientCreator, handshaker) } @@ -34,7 +34,7 @@ type Handshaker interface { type multiAppConn struct { cmn.BaseService - config cfg.Config + config *viper.Viper handshaker Handshaker @@ -46,7 +46,7 @@ type multiAppConn struct { } // Make all necessary abci connections to the application -func NewMultiAppConn(config cfg.Config, clientCreator ClientCreator, handshaker Handshaker) *multiAppConn { +func NewMultiAppConn(config *viper.Viper, clientCreator ClientCreator, handshaker Handshaker) *multiAppConn { multiAppConn := &multiAppConn{ config: config, handshaker: handshaker, diff --git a/rpc/Dockerfile b/rpc/Dockerfile new file mode 100644 index 00000000..c2241267 --- /dev/null +++ b/rpc/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:latest + +RUN mkdir -p /go/src/github.com/tendermint/tendermint/rpc +WORKDIR /go/src/github.com/tendermint/tendermint/rpc + +COPY Makefile /go/src/github.com/tendermint/tendermint/rpc/ +# COPY glide.yaml /go/src/github.com/tendermint/tendermint/rpc/ +# COPY glide.lock /go/src/github.com/tendermint/tendermint/rpc/ + +COPY . /go/src/github.com/tendermint/tendermint/rpc + +RUN make get_deps diff --git a/rpc/Makefile b/rpc/Makefile new file mode 100644 index 00000000..0937558a --- /dev/null +++ b/rpc/Makefile @@ -0,0 +1,18 @@ +PACKAGES=$(shell go list ./... | grep -v "test") + +all: get_deps test + +test: + @echo "--> Running go test --race" + @go test --race $(PACKAGES) + @echo "--> Running integration tests" + @bash ./test/integration_test.sh + +get_deps: + @echo "--> Running go get" + @go get -v -d $(PACKAGES) + @go list -f '{{join .TestImports "\n"}}' ./... | \ + grep -v /vendor/ | sort | uniq | \ + xargs go get -v -d + +.PHONY: all test get_deps diff --git a/rpc/README.md b/rpc/README.md new file mode 100644 index 00000000..f1ef7485 --- /dev/null +++ b/rpc/README.md @@ -0,0 +1,128 @@ +# tendermint/rpc + +[![CircleCI](https://circleci.com/gh/tendermint/tendermint/rpc.svg?style=svg)](https://circleci.com/gh/tendermint/tendermint/rpc) + +HTTP RPC server supporting calls via uri params, jsonrpc, and jsonrpc over websockets + +# Client Requests + +Suppose we want to expose the rpc function `HelloWorld(name string, num int)`. + +## GET (URI) + +As a GET request, it would have URI encoded parameters, and look like: + +``` +curl 'http://localhost:8008/hello_world?name="my_world"&num=5' +``` + +Note the `'` around the url, which is just so bash doesn't ignore the quotes in `"my_world"`. +This should also work: + +``` +curl http://localhost:8008/hello_world?name=\"my_world\"&num=5 +``` + +A GET request to `/` returns a list of available endpoints. +For those which take arguments, the arguments will be listed in order, with `_` where the actual value should be. + +## POST (JSONRPC) + +As a POST request, we use JSONRPC. For instance, the same request would have this as the body: + +``` +{ + "jsonrpc": "2.0", + "id": "anything", + "method": "hello_world", + "params": { + "name": "my_world", + "num": 5 + } +} +``` + +With the above saved in file `data.json`, we can make the request with + +``` +curl --data @data.json http://localhost:8008 +``` + +## WebSocket (JSONRPC) + +All requests are exposed over websocket in the same form as the POST JSONRPC. +Websocket connections are available at their own endpoint, typically `/websocket`, +though this is configurable when starting the server. + +# Server Definition + +Define some types and routes: + +``` +// Define a type for results and register concrete versions with go-wire +type Result interface{} + +type ResultStatus struct { + Value string +} + +var _ = wire.RegisterInterface( + struct{ Result }{}, + wire.ConcreteType{&ResultStatus{}, 0x1}, +) + +// Define some routes +var Routes = map[string]*rpcserver.RPCFunc{ + "status": rpcserver.NewRPCFunc(StatusResult, "arg"), +} + +// an rpc function +func StatusResult(v string) (Result, error) { + return &ResultStatus{v}, nil +} + +``` + +Now start the server: + +``` +mux := http.NewServeMux() +rpcserver.RegisterRPCFuncs(mux, Routes) +wm := rpcserver.NewWebsocketManager(Routes, nil) +mux.HandleFunc("/websocket", wm.WebsocketHandler) +go func() { + _, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux) + if err != nil { + panic(err) + } +}() + +``` + +Note that unix sockets are supported as well (eg. `/path/to/socket` instead of `0.0.0.0:8008`) + +Now see all available endpoints by sending a GET request to `0.0.0.0:8008`. +Each route is available as a GET request, as a JSONRPCv2 POST request, and via JSONRPCv2 over websockets. + + +# Examples + +* [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) +* [tm-monitor](https://github.com/tendermint/tools/blob/master/tm-monitor/rpc.go) + +## CHANGELOG + +### 0.7.0 + +BREAKING CHANGES: + +- removed `Client` empty interface +- `ClientJSONRPC#Call` `params` argument became a map +- rename `ClientURI` -> `URIClient`, `ClientJSONRPC` -> `JSONRPCClient` + +IMPROVEMENTS: + +- added `HTTPClient` interface, which can be used for both `ClientURI` +and `ClientJSONRPC` +- all params are now optional (Golang's default will be used if some param is missing) +- added `Call` method to `WSClient` (see method's doc for details) diff --git a/rpc/circle.yml b/rpc/circle.yml new file mode 100644 index 00000000..0308a4e7 --- /dev/null +++ b/rpc/circle.yml @@ -0,0 +1,21 @@ +machine: + environment: + GOPATH: /home/ubuntu/.go_workspace + REPO: $GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME + hosts: + circlehost: 127.0.0.1 + localhost: 127.0.0.1 + +checkout: + post: + - rm -rf $REPO + - mkdir -p $HOME/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME + - mv $HOME/$CIRCLE_PROJECT_REPONAME $REPO + +dependencies: + override: + - "cd $REPO && make get_deps" + +test: + override: + - "cd $REPO && make test" diff --git a/rpc/client/args_test.go b/rpc/client/args_test.go new file mode 100644 index 00000000..ccabd0d2 --- /dev/null +++ b/rpc/client/args_test.go @@ -0,0 +1,39 @@ +package rpcclient + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type Tx []byte + +type Foo struct { + Bar int + Baz string +} + +func TestArgToJSON(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := []struct { + input interface{} + expected string + }{ + {[]byte("1234"), "0x31323334"}, + {Tx("654"), "0x363534"}, + {Foo{7, "hello"}, `{"Bar":7,"Baz":"hello"}`}, + } + + for i, tc := range cases { + args := map[string]interface{}{"data": tc.input} + err := argsToJson(args) + require.Nil(err, "%d: %+v", i, err) + require.Equal(1, len(args), "%d", i) + data, ok := args["data"].(string) + require.True(ok, "%d: %#v", i, args["data"]) + assert.Equal(tc.expected, data, "%d", i) + } +} diff --git a/rpc/client/http_client.go b/rpc/client/http_client.go new file mode 100644 index 00000000..38a7f20f --- /dev/null +++ b/rpc/client/http_client.go @@ -0,0 +1,199 @@ +package rpcclient + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "reflect" + "strings" + + "github.com/pkg/errors" + wire "github.com/tendermint/go-wire" + types "github.com/tendermint/tendermint/rpc/types" +) + +// HTTPClient is a common interface for JSONRPCClient and URIClient. +type HTTPClient interface { + Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) +} + +// TODO: Deprecate support for IP:PORT or /path/to/socket +func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, error)) { + + parts := strings.SplitN(remoteAddr, "://", 2) + var protocol, address string + if len(parts) != 2 { + log.Warn("WARNING (tendermint/rpc): Please use fully formed listening addresses, including the tcp:// or unix:// prefix") + protocol = types.SocketType(remoteAddr) + address = remoteAddr + } else { + protocol, address = parts[0], parts[1] + } + + trimmedAddress := strings.Replace(address, "/", ".", -1) // replace / with . for http requests (dummy domain) + return trimmedAddress, func(proto, addr string) (net.Conn, error) { + return net.Dial(protocol, address) + } +} + +// We overwrite the http.Client.Dial so we can do http over tcp or unix. +// remoteAddr should be fully featured (eg. with tcp:// or unix://) +func makeHTTPClient(remoteAddr string) (string, *http.Client) { + address, dialer := makeHTTPDialer(remoteAddr) + return "http://" + address, &http.Client{ + Transport: &http.Transport{ + Dial: dialer, + }, + } +} + +//------------------------------------------------------------------------------------ + +// JSON rpc takes params as a slice +type JSONRPCClient struct { + address string + client *http.Client +} + +func NewJSONRPCClient(remote string) *JSONRPCClient { + address, client := makeHTTPClient(remote) + return &JSONRPCClient{ + address: address, + client: client, + } +} + +func (c *JSONRPCClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { + // we need this step because we attempt to decode values using `go-wire` + // (handlers.go:176) on the server side + encodedParams := make(map[string]interface{}) + for k, v := range params { + bytes := json.RawMessage(wire.JSONBytes(v)) + encodedParams[k] = &bytes + } + request := types.RPCRequest{ + JSONRPC: "2.0", + Method: method, + Params: encodedParams, + ID: "", + } + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + // log.Info(string(requestBytes)) + requestBuf := bytes.NewBuffer(requestBytes) + // log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) + httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) + if err != nil { + return nil, err + } + defer httpResponse.Body.Close() + responseBytes, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + return nil, err + } + // log.Info(Fmt("RPC response: %v", string(responseBytes))) + return unmarshalResponseBytes(responseBytes, result) +} + +//------------------------------------------------------------- + +// URI takes params as a map +type URIClient struct { + address string + client *http.Client +} + +func NewURIClient(remote string) *URIClient { + address, client := makeHTTPClient(remote) + return &URIClient{ + address: address, + client: client, + } +} + +func (c *URIClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { + values, err := argsToURLValues(params) + if err != nil { + return nil, err + } + // log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) + resp, err := c.client.PostForm(c.address+"/"+method, values) + if err != nil { + return nil, err + } + defer resp.Body.Close() + responseBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return unmarshalResponseBytes(responseBytes, result) +} + +//------------------------------------------------ + +func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface{}, error) { + // read response + // if rpc/core/types is imported, the result will unmarshal + // into the correct type + // log.Notice("response", "response", string(responseBytes)) + var err error + response := &types.RPCResponse{} + err = json.Unmarshal(responseBytes, response) + if err != nil { + return nil, errors.Errorf("Error unmarshalling rpc response: %v", err) + } + errorStr := response.Error + if errorStr != "" { + return nil, errors.Errorf("Response error: %v", errorStr) + } + // unmarshal the RawMessage into the result + result = wire.ReadJSONPtr(result, *response.Result, &err) + if err != nil { + return nil, errors.Errorf("Error unmarshalling rpc response result: %v", err) + } + return result, nil +} + +func argsToURLValues(args map[string]interface{}) (url.Values, error) { + values := make(url.Values) + if len(args) == 0 { + return values, nil + } + err := argsToJson(args) + if err != nil { + return nil, err + } + for key, val := range args { + values.Set(key, val.(string)) + } + return values, nil +} + +func argsToJson(args map[string]interface{}) error { + var n int + var err error + for k, v := range args { + rt := reflect.TypeOf(v) + isByteSlice := rt.Kind() == reflect.Slice && rt.Elem().Kind() == reflect.Uint8 + if isByteSlice { + bytes := reflect.ValueOf(v).Bytes() + args[k] = fmt.Sprintf("0x%X", bytes) + continue + } + + // Pass everything else to go-wire + buf := new(bytes.Buffer) + wire.WriteJSON(v, buf, &n, &err) + if err != nil { + return err + } + args[k] = buf.String() + } + return nil +} diff --git a/rpc/client/log.go b/rpc/client/log.go new file mode 100644 index 00000000..8b33e2f1 --- /dev/null +++ b/rpc/client/log.go @@ -0,0 +1,7 @@ +package rpcclient + +import ( + "github.com/tendermint/log15" +) + +var log = log15.New("module", "rpcclient") diff --git a/rpc/client/ws_client.go b/rpc/client/ws_client.go new file mode 100644 index 00000000..70522265 --- /dev/null +++ b/rpc/client/ws_client.go @@ -0,0 +1,172 @@ +package rpcclient + +import ( + "encoding/json" + "net" + "net/http" + "time" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" + cmn "github.com/tendermint/tmlibs/common" + types "github.com/tendermint/tendermint/rpc/types" + wire "github.com/tendermint/go-wire" +) + +const ( + wsResultsChannelCapacity = 10 + wsErrorsChannelCapacity = 1 + wsWriteTimeoutSeconds = 10 +) + +type WSClient struct { + cmn.BaseService + Address string // IP:PORT or /path/to/socket + Endpoint string // /websocket/url/endpoint + Dialer func(string, string) (net.Conn, error) + *websocket.Conn + ResultsCh chan json.RawMessage // closes upon WSClient.Stop() + ErrorsCh chan error // closes upon WSClient.Stop() +} + +// create a new connection +func NewWSClient(remoteAddr, endpoint string) *WSClient { + addr, dialer := makeHTTPDialer(remoteAddr) + wsClient := &WSClient{ + Address: addr, + Dialer: dialer, + Endpoint: endpoint, + Conn: nil, + } + wsClient.BaseService = *cmn.NewBaseService(log, "WSClient", wsClient) + return wsClient +} + +func (wsc *WSClient) String() string { + return wsc.Address + ", " + wsc.Endpoint +} + +// OnStart implements cmn.BaseService interface +func (wsc *WSClient) OnStart() error { + wsc.BaseService.OnStart() + err := wsc.dial() + if err != nil { + return err + } + wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) + wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) + go wsc.receiveEventsRoutine() + return nil +} + +// OnReset implements cmn.BaseService interface +func (wsc *WSClient) OnReset() error { + return nil +} + +func (wsc *WSClient) dial() error { + + // Dial + dialer := &websocket.Dialer{ + NetDial: wsc.Dialer, + Proxy: http.ProxyFromEnvironment, + } + rHeader := http.Header{} + con, _, err := dialer.Dial("ws://"+wsc.Address+wsc.Endpoint, rHeader) + if err != nil { + return err + } + // Set the ping/pong handlers + con.SetPingHandler(func(m string) error { + // NOTE: https://github.com/gorilla/websocket/issues/97 + go con.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) + return nil + }) + con.SetPongHandler(func(m string) error { + // NOTE: https://github.com/gorilla/websocket/issues/97 + return nil + }) + wsc.Conn = con + return nil +} + +// OnStop implements cmn.BaseService interface +func (wsc *WSClient) OnStop() { + wsc.BaseService.OnStop() + wsc.Conn.Close() + // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. +} + +func (wsc *WSClient) receiveEventsRoutine() { + for { + _, data, err := wsc.ReadMessage() + if err != nil { + log.Info("WSClient failed to read message", "error", err, "data", string(data)) + wsc.Stop() + break + } else { + var response types.RPCResponse + err := json.Unmarshal(data, &response) + if err != nil { + log.Info("WSClient failed to parse message", "error", err, "data", string(data)) + wsc.ErrorsCh <- err + continue + } + if response.Error != "" { + wsc.ErrorsCh <- errors.Errorf(response.Error) + continue + } + wsc.ResultsCh <- *response.Result + } + } + // this must be modified in the same go-routine that reads from the + // connection to avoid race conditions + wsc.Conn = nil + + // Cleanup + close(wsc.ResultsCh) + close(wsc.ErrorsCh) +} + +// Subscribe to an event. Note the server must have a "subscribe" route +// defined. +func (wsc *WSClient) Subscribe(eventid string) error { + err := wsc.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "subscribe", + Params: map[string]interface{}{"event": eventid}, + }) + return err +} + +// Unsubscribe from an event. Note the server must have a "unsubscribe" route +// defined. +func (wsc *WSClient) Unsubscribe(eventid string) error { + err := wsc.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "unsubscribe", + Params: map[string]interface{}{"event": eventid}, + }) + return err +} + +// Call asynchronously calls a given method by sending an RPCRequest to the +// server. Results will be available on ResultsCh, errors, if any, on ErrorsCh. +func (wsc *WSClient) Call(method string, params map[string]interface{}) error { + // we need this step because we attempt to decode values using `go-wire` + // (handlers.go:470) on the server side + encodedParams := make(map[string]interface{}) + for k, v := range params { + bytes := json.RawMessage(wire.JSONBytes(v)) + encodedParams[k] = &bytes + } + err := wsc.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + Method: method, + Params: encodedParams, + ID: "", + }) + return err +} diff --git a/rpc/rpc_test.go b/rpc/rpc_test.go new file mode 100644 index 00000000..ef8832a8 --- /dev/null +++ b/rpc/rpc_test.go @@ -0,0 +1,298 @@ +package rpc + +import ( + "bytes" + crand "crypto/rand" + "fmt" + "math/rand" + "net/http" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + wire "github.com/tendermint/go-wire" + client "github.com/tendermint/tendermint/rpc/client" + server "github.com/tendermint/tendermint/rpc/server" + types "github.com/tendermint/tendermint/rpc/types" +) + +// Client and Server should work over tcp or unix sockets +const ( + tcpAddr = "tcp://0.0.0.0:46657" + + unixSocket = "/tmp/rpc.sock" + unixAddr = "unix:///tmp/rpc.sock" + + websocketEndpoint = "/websocket/endpoint" +) + +// Define a type for results and register concrete versions +type Result interface{} + +type ResultEcho struct { + Value string +} + +type ResultEchoBytes struct { + Value []byte +} + +var _ = wire.RegisterInterface( + struct{ Result }{}, + wire.ConcreteType{&ResultEcho{}, 0x1}, + wire.ConcreteType{&ResultEchoBytes{}, 0x2}, +) + +// Define some routes +var Routes = map[string]*server.RPCFunc{ + "echo": server.NewRPCFunc(EchoResult, "arg"), + "echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"), + "echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"), +} + +func EchoResult(v string) (Result, error) { + return &ResultEcho{v}, nil +} + +func EchoWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { + return &ResultEcho{v}, nil +} + +func EchoBytesResult(v []byte) (Result, error) { + return &ResultEchoBytes{v}, nil +} + +// launch unix and tcp servers +func init() { + cmd := exec.Command("rm", "-f", unixSocket) + err := cmd.Start() + if err != nil { + panic(err) + } + if err = cmd.Wait(); err != nil { + panic(err) + } + + mux := http.NewServeMux() + server.RegisterRPCFuncs(mux, Routes) + wm := server.NewWebsocketManager(Routes, nil) + mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) + go func() { + _, err := server.StartHTTPServer(tcpAddr, mux) + if err != nil { + panic(err) + } + }() + + mux2 := http.NewServeMux() + server.RegisterRPCFuncs(mux2, Routes) + wm = server.NewWebsocketManager(Routes, nil) + mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) + go func() { + _, err := server.StartHTTPServer(unixAddr, mux2) + if err != nil { + panic(err) + } + }() + + // wait for servers to start + time.Sleep(time.Second * 2) +} + +func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { + params := map[string]interface{}{ + "arg": val, + } + var result Result + if _, err := cl.Call("echo", params, &result); err != nil { + return "", err + } + return result.(*ResultEcho).Value, nil +} + +func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + var result Result + if _, err := cl.Call("echo_bytes", params, &result); err != nil { + return []byte{}, err + } + return result.(*ResultEchoBytes).Value, nil +} + +func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { + val := "acbd" + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaHTTP(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) +} + +func echoViaWS(cl *client.WSClient, val string) (string, error) { + params := map[string]interface{}{ + "arg": val, + } + err := cl.Call("echo", params) + if err != nil { + return "", err + } + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + return "", nil + } + return (*result).(*ResultEcho).Value, nil + case err := <-cl.ErrorsCh: + return "", err + } +} + +func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + err := cl.Call("echo_bytes", params) + if err != nil { + return []byte{}, err + } + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + return []byte{}, nil + } + return (*result).(*ResultEchoBytes).Value, nil + case err := <-cl.ErrorsCh: + return []byte{}, err + } +} + +func testWithWSClient(t *testing.T, cl *client.WSClient) { + val := "acbd" + got, err := echoViaWS(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaWS(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) +} + +//------------- + +func TestServersAndClientsBasic(t *testing.T) { + serverAddrs := [...]string{tcpAddr, unixAddr} + for _, addr := range serverAddrs { + cl1 := client.NewURIClient(addr) + fmt.Printf("=== testing server on %s using %v client", addr, cl1) + testWithHTTPClient(t, cl1) + + cl2 := client.NewJSONRPCClient(tcpAddr) + fmt.Printf("=== testing server on %s using %v client", addr, cl2) + testWithHTTPClient(t, cl2) + + cl3 := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl3.Start() + require.Nil(t, err) + fmt.Printf("=== testing server on %s using %v client", addr, cl3) + testWithWSClient(t, cl3) + cl3.Stop() + } +} + +func TestHexStringArg(t *testing.T) { + cl := client.NewURIClient(tcpAddr) + // should NOT be handled as hex + val := "0xabc" + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) +} + +func TestQuotedStringArg(t *testing.T) { + cl := client.NewURIClient(tcpAddr) + // should NOT be unquoted + val := "\"abc\"" + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) +} + +func TestWSNewWSRPCFunc(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + val := "acbd" + params := map[string]interface{}{ + "arg": val, + } + err = cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "echo_ws", + Params: params, + }) + require.Nil(t, err) + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + require.Nil(t, err) + got := (*result).(*ResultEcho).Value + assert.Equal(t, got, val) + case err := <-cl.ErrorsCh: + t.Fatal(err) + } +} + +func TestWSHandlesArrayParams(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + val := "acbd" + params := []interface{}{val} + err = cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "echo_ws", + Params: params, + }) + require.Nil(t, err) + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + require.Nil(t, err) + got := (*result).(*ResultEcho).Value + assert.Equal(t, got, val) + case err := <-cl.ErrorsCh: + t.Fatalf("%+v", err) + } +} + +func randBytes(t *testing.T) []byte { + n := rand.Intn(10) + 2 + buf := make([]byte, n) + _, err := crand.Read(buf) + require.Nil(t, err) + return bytes.Replace(buf, []byte("="), []byte{100}, -1) +} diff --git a/rpc/server/handlers.go b/rpc/server/handlers.go new file mode 100644 index 00000000..f5b3659d --- /dev/null +++ b/rpc/server/handlers.go @@ -0,0 +1,649 @@ +package rpcserver + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "sort" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" + types "github.com/tendermint/tendermint/rpc/types" + wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + events "github.com/tendermint/tmlibs/events" +) + +// Adds a route for each function in the funcMap, as well as general jsonrpc and websocket handlers for all functions. +// "result" is the interface on which the result objects are registered, and is popualted with every RPCResponse +func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc) { + // HTTP endpoints + for funcName, rpcFunc := range funcMap { + mux.HandleFunc("/"+funcName, makeHTTPHandler(rpcFunc)) + } + + // JSONRPC endpoints + mux.HandleFunc("/", makeJSONRPCHandler(funcMap)) +} + +//------------------------------------- +// function introspection + +// holds all type information for each function +type RPCFunc struct { + f reflect.Value // underlying rpc function + args []reflect.Type // type of each function arg + returns []reflect.Type // type of each return arg + argNames []string // name of each argument + ws bool // websocket only +} + +// wraps a function for quicker introspection +// f is the function, args are comma separated argument names +func NewRPCFunc(f interface{}, args string) *RPCFunc { + return newRPCFunc(f, args, false) +} + +func NewWSRPCFunc(f interface{}, args string) *RPCFunc { + return newRPCFunc(f, args, true) +} + +func newRPCFunc(f interface{}, args string, ws bool) *RPCFunc { + var argNames []string + if args != "" { + argNames = strings.Split(args, ",") + } + return &RPCFunc{ + f: reflect.ValueOf(f), + args: funcArgTypes(f), + returns: funcReturnTypes(f), + argNames: argNames, + ws: ws, + } +} + +// return a function's argument types +func funcArgTypes(f interface{}) []reflect.Type { + t := reflect.TypeOf(f) + n := t.NumIn() + typez := make([]reflect.Type, n) + for i := 0; i < n; i++ { + typez[i] = t.In(i) + } + return typez +} + +// return a function's return types +func funcReturnTypes(f interface{}) []reflect.Type { + t := reflect.TypeOf(f) + n := t.NumOut() + typez := make([]reflect.Type, n) + for i := 0; i < n; i++ { + typez[i] = t.Out(i) + } + return typez +} + +// function introspection +//----------------------------------------------------------------------------- +// rpc.json + +// jsonrpc calls grab the given method's function info and runs reflect.Call +func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + b, _ := ioutil.ReadAll(r.Body) + // if its an empty request (like from a browser), + // just display a list of functions + if len(b) == 0 { + writeListOfEndpoints(w, r, funcMap) + return + } + + var request types.RPCRequest + err := json.Unmarshal(b, &request) + if err != nil { + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unmarshalling request: %v", err.Error()))) + return + } + if len(r.URL.Path) > 1 { + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Invalid JSONRPC endpoint %s", r.URL.Path))) + return + } + rpcFunc := funcMap[request.Method] + if rpcFunc == nil { + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) + return + } + if rpcFunc.ws { + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) + return + } + args, err := jsonParamsToArgsRPC(rpcFunc, request.Params) + if err != nil { + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) + return + } + returns := rpcFunc.f.Call(args) + log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) + result, err := unreflectResult(returns) + if err != nil { + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, err.Error())) + return + } + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, "")) + } +} + +// Convert a []interface{} OR a map[string]interface{} to properly typed values +// +// argsOffset should be 0 for RPC calls, and 1 for WS requests, where len(rpcFunc.args) != len(rpcFunc.argNames). +// Example: +// rpcFunc.args = [rpctypes.WSRPCContext string] +// rpcFunc.argNames = ["arg"] +func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([]reflect.Value, error) { + values := make([]reflect.Value, len(rpcFunc.argNames)) + + switch params := paramsI.(type) { + + case map[string]interface{}: + for i, argName := range rpcFunc.argNames { + argType := rpcFunc.args[i+argsOffset] + + // decode param if provided + if param, ok := params[argName]; ok && "" != param { + v, err := _jsonObjectToArg(argType, param) + if err != nil { + return nil, err + } + values[i] = v + } else { // use default for that type + values[i] = reflect.Zero(argType) + } + } + case []interface{}: + if len(rpcFunc.argNames) != len(params) { + return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames), rpcFunc.argNames, len(params), params)) + } + values := make([]reflect.Value, len(params)) + for i, p := range params { + ty := rpcFunc.args[i+argsOffset] + v, err := _jsonObjectToArg(ty, p) + if err != nil { + return nil, err + } + values[i] = v + } + return values, nil + default: + return nil, fmt.Errorf("Unknown type for JSON params %v. Expected map[string]interface{} or []interface{}", reflect.TypeOf(paramsI)) + } + return values, nil +} + +// Convert a []interface{} OR a map[string]interface{} to properly typed values +func jsonParamsToArgsRPC(rpcFunc *RPCFunc, paramsI interface{}) ([]reflect.Value, error) { + return jsonParamsToArgs(rpcFunc, paramsI, 0) +} + +// Same as above, but with the first param the websocket connection +func jsonParamsToArgsWS(rpcFunc *RPCFunc, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { + values, err := jsonParamsToArgs(rpcFunc, paramsI, 1) + if err != nil { + return nil, err + } + return append([]reflect.Value{reflect.ValueOf(wsCtx)}, values...), nil +} + +func _jsonObjectToArg(ty reflect.Type, object interface{}) (reflect.Value, error) { + var err error + v := reflect.New(ty) + wire.ReadJSONObjectPtr(v.Interface(), object, &err) + if err != nil { + return v, err + } + v = v.Elem() + return v, nil +} + +// rpc.json +//----------------------------------------------------------------------------- +// rpc.http + +// convert from a function name to the http handler +func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) { + // Exception for websocket endpoints + if rpcFunc.ws { + return func(w http.ResponseWriter, r *http.Request) { + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, "This RPC method is only for websockets")) + } + } + // All other endpoints + return func(w http.ResponseWriter, r *http.Request) { + log.Debug("HTTP HANDLER", "req", r) + args, err := httpParamsToArgs(rpcFunc, r) + if err != nil { + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error converting http params to args: %v", err.Error()))) + return + } + returns := rpcFunc.f.Call(args) + log.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) + result, err := unreflectResult(returns) + if err != nil { + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, err.Error())) + return + } + WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, "")) + } +} + +// Covert an http query to a list of properly typed values. +// To be properly decoded the arg must be a concrete type from tendermint (if its an interface). +func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) { + values := make([]reflect.Value, len(rpcFunc.args)) + + for i, name := range rpcFunc.argNames { + argType := rpcFunc.args[i] + + values[i] = reflect.Zero(argType) // set default for that type + + arg := GetParam(r, name) + // log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) + + if "" == arg { + continue + } + + v, err, ok := nonJsonToArg(argType, arg) + if err != nil { + return nil, err + } + if ok { + values[i] = v + continue + } + + // Pass values to go-wire + values[i], err = _jsonStringToArg(argType, arg) + if err != nil { + return nil, err + } + } + + return values, nil +} + +func _jsonStringToArg(ty reflect.Type, arg string) (reflect.Value, error) { + var err error + v := reflect.New(ty) + wire.ReadJSONPtr(v.Interface(), []byte(arg), &err) + if err != nil { + return v, err + } + v = v.Elem() + return v, nil +} + +func nonJsonToArg(ty reflect.Type, arg string) (reflect.Value, error, bool) { + isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`) + isHexString := strings.HasPrefix(strings.ToLower(arg), "0x") + expectingString := ty.Kind() == reflect.String + expectingByteSlice := ty.Kind() == reflect.Slice && ty.Elem().Kind() == reflect.Uint8 + + if isHexString { + if !expectingString && !expectingByteSlice { + err := errors.Errorf("Got a hex string arg, but expected '%s'", + ty.Kind().String()) + return reflect.ValueOf(nil), err, false + } + + var value []byte + value, err := hex.DecodeString(arg[2:]) + if err != nil { + return reflect.ValueOf(nil), err, false + } + if ty.Kind() == reflect.String { + return reflect.ValueOf(string(value)), nil, true + } + return reflect.ValueOf([]byte(value)), nil, true + } + + if isQuotedString && expectingByteSlice { + var err error + v := reflect.New(reflect.TypeOf("")) + wire.ReadJSONPtr(v.Interface(), []byte(arg), &err) + if err != nil { + return reflect.ValueOf(nil), err, false + } + v = v.Elem() + return reflect.ValueOf([]byte(v.String())), nil, true + } + + return reflect.ValueOf(nil), nil, false +} + +// rpc.http +//----------------------------------------------------------------------------- +// rpc.websocket + +const ( + writeChanCapacity = 1000 + wsWriteTimeoutSeconds = 30 // each write times out after this + wsReadTimeoutSeconds = 30 // connection times out if we haven't received *anything* in this long, not even pings. + wsPingTickerSeconds = 10 // send a ping every PingTickerSeconds. +) + +// a single websocket connection +// contains listener id, underlying ws connection, +// and the event switch for subscribing to events +type wsConnection struct { + cmn.BaseService + + remoteAddr string + baseConn *websocket.Conn + writeChan chan types.RPCResponse + readTimeout *time.Timer + pingTicker *time.Ticker + + funcMap map[string]*RPCFunc + evsw events.EventSwitch +} + +// new websocket connection wrapper +func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw events.EventSwitch) *wsConnection { + wsc := &wsConnection{ + remoteAddr: baseConn.RemoteAddr().String(), + baseConn: baseConn, + writeChan: make(chan types.RPCResponse, writeChanCapacity), // error when full. + funcMap: funcMap, + evsw: evsw, + } + wsc.BaseService = *cmn.NewBaseService(log, "wsConnection", wsc) + return wsc +} + +// wsc.Start() blocks until the connection closes. +func (wsc *wsConnection) OnStart() error { + wsc.BaseService.OnStart() + + // these must be set before the readRoutine is created, as it may + // call wsc.Stop(), which accesses these timers + wsc.readTimeout = time.NewTimer(time.Second * wsReadTimeoutSeconds) + wsc.pingTicker = time.NewTicker(time.Second * wsPingTickerSeconds) + + // Read subscriptions/unsubscriptions to events + go wsc.readRoutine() + + // Custom Ping handler to touch readTimeout + wsc.baseConn.SetPingHandler(func(m string) error { + // NOTE: https://github.com/gorilla/websocket/issues/97 + go wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) + wsc.readTimeout.Reset(time.Second * wsReadTimeoutSeconds) + return nil + }) + wsc.baseConn.SetPongHandler(func(m string) error { + // NOTE: https://github.com/gorilla/websocket/issues/97 + wsc.readTimeout.Reset(time.Second * wsReadTimeoutSeconds) + return nil + }) + go wsc.readTimeoutRoutine() + + // Write responses, BLOCKING. + wsc.writeRoutine() + return nil +} + +func (wsc *wsConnection) OnStop() { + wsc.BaseService.OnStop() + if wsc.evsw != nil { + wsc.evsw.RemoveListener(wsc.remoteAddr) + } + wsc.readTimeout.Stop() + wsc.pingTicker.Stop() + // The write loop closes the websocket connection + // when it exits its loop, and the read loop + // closes the writeChan +} + +func (wsc *wsConnection) readTimeoutRoutine() { + select { + case <-wsc.readTimeout.C: + log.Notice("Stopping connection due to read timeout") + wsc.Stop() + case <-wsc.Quit: + return + } +} + +// Implements WSRPCConnection +func (wsc *wsConnection) GetRemoteAddr() string { + return wsc.remoteAddr +} + +// Implements WSRPCConnection +func (wsc *wsConnection) GetEventSwitch() events.EventSwitch { + return wsc.evsw +} + +// Implements WSRPCConnection +// Blocking write to writeChan until service stops. +// Goroutine-safe +func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { + select { + case <-wsc.Quit: + return + case wsc.writeChan <- resp: + } +} + +// Implements WSRPCConnection +// Nonblocking write. +// Goroutine-safe +func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { + select { + case <-wsc.Quit: + return false + case wsc.writeChan <- resp: + return true + default: + return false + } +} + +// Read from the socket and subscribe to or unsubscribe from events +func (wsc *wsConnection) readRoutine() { + // Do not close writeChan, to allow WriteRPCResponse() to fail. + // defer close(wsc.writeChan) + + for { + select { + case <-wsc.Quit: + return + default: + var in []byte + // Do not set a deadline here like below: + // wsc.baseConn.SetReadDeadline(time.Now().Add(time.Second * wsReadTimeoutSeconds)) + // The client may not send anything for a while. + // We use `readTimeout` to handle read timeouts. + _, in, err := wsc.baseConn.ReadMessage() + if err != nil { + log.Notice("Failed to read from connection", "remote", wsc.remoteAddr, "err", err.Error()) + // an error reading the connection, + // kill the connection + wsc.Stop() + return + } + var request types.RPCRequest + err = json.Unmarshal(in, &request) + if err != nil { + errStr := fmt.Sprintf("Error unmarshaling data: %s", err.Error()) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, errStr)) + continue + } + + // Now, fetch the RPCFunc and execute it. + + rpcFunc := wsc.funcMap[request.Method] + if rpcFunc == nil { + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) + continue + } + var args []reflect.Value + if rpcFunc.ws { + wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} + args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) + } else { + args, err = jsonParamsToArgsRPC(rpcFunc, request.Params) + } + if err != nil { + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) + continue + } + returns := rpcFunc.f.Call(args) + log.Info("WSJSONRPC", "method", request.Method, "args", args, "returns", returns) + result, err := unreflectResult(returns) + if err != nil { + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) + continue + } else { + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, result, "")) + continue + } + + } + } +} + +// receives on a write channel and writes out on the socket +func (wsc *wsConnection) writeRoutine() { + defer wsc.baseConn.Close() + for { + select { + case <-wsc.Quit: + return + case <-wsc.pingTicker.C: + err := wsc.baseConn.WriteMessage(websocket.PingMessage, []byte{}) + if err != nil { + log.Error("Failed to write ping message on websocket", "error", err) + wsc.Stop() + return + } + case msg := <-wsc.writeChan: + jsonBytes, err := json.Marshal(msg) + if err != nil { + log.Error("Failed to marshal RPCResponse to JSON", "error", err) + } else { + wsc.baseConn.SetWriteDeadline(time.Now().Add(time.Second * wsWriteTimeoutSeconds)) + if err = wsc.baseConn.WriteMessage(websocket.TextMessage, jsonBytes); err != nil { + log.Warn("Failed to write response on websocket", "error", err) + wsc.Stop() + return + } + } + } + } +} + +//---------------------------------------- + +// Main manager for all websocket connections +// Holds the event switch +// NOTE: The websocket path is defined externally, e.g. in node/node.go +type WebsocketManager struct { + websocket.Upgrader + funcMap map[string]*RPCFunc + evsw events.EventSwitch +} + +func NewWebsocketManager(funcMap map[string]*RPCFunc, evsw events.EventSwitch) *WebsocketManager { + return &WebsocketManager{ + funcMap: funcMap, + evsw: evsw, + Upgrader: websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + // TODO + return true + }, + }, + } +} + +// Upgrade the request/response (via http.Hijack) and starts the wsConnection. +func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Request) { + wsConn, err := wm.Upgrade(w, r, nil) + if err != nil { + // TODO - return http error + log.Error("Failed to upgrade to websocket connection", "error", err) + return + } + + // register connection + con := NewWSConnection(wsConn, wm.funcMap, wm.evsw) + log.Notice("New websocket connection", "remote", con.remoteAddr) + con.Start() // Blocking +} + +// rpc.websocket +//----------------------------------------------------------------------------- + +// NOTE: assume returns is result struct and error. If error is not nil, return it +func unreflectResult(returns []reflect.Value) (interface{}, error) { + errV := returns[1] + if errV.Interface() != nil { + return nil, errors.Errorf("%v", errV.Interface()) + } + rv := returns[0] + // the result is a registered interface, + // we need a pointer to it so we can marshal with type byte + rvp := reflect.New(rv.Type()) + rvp.Elem().Set(rv) + return rvp.Interface(), nil +} + +// writes a list of available rpc endpoints as an html page +func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[string]*RPCFunc) { + noArgNames := []string{} + argNames := []string{} + for name, funcData := range funcMap { + if len(funcData.args) == 0 { + noArgNames = append(noArgNames, name) + } else { + argNames = append(argNames, name) + } + } + sort.Strings(noArgNames) + sort.Strings(argNames) + buf := new(bytes.Buffer) + buf.WriteString("") + buf.WriteString("
Available endpoints:
") + + for _, name := range noArgNames { + link := fmt.Sprintf("http://%s/%s", r.Host, name) + buf.WriteString(fmt.Sprintf("%s
", link, link)) + } + + buf.WriteString("
Endpoints that require arguments:
") + for _, name := range argNames { + link := fmt.Sprintf("http://%s/%s?", r.Host, name) + funcData := funcMap[name] + for i, argName := range funcData.argNames { + link += argName + "=_" + if i < len(funcData.argNames)-1 { + link += "&" + } + } + buf.WriteString(fmt.Sprintf("%s
", link, link)) + } + buf.WriteString("") + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(200) + w.Write(buf.Bytes()) +} diff --git a/rpc/server/http_params.go b/rpc/server/http_params.go new file mode 100644 index 00000000..56506067 --- /dev/null +++ b/rpc/server/http_params.go @@ -0,0 +1,90 @@ +package rpcserver + +import ( + "encoding/hex" + "net/http" + "regexp" + "strconv" + + "github.com/pkg/errors" +) + +var ( + // Parts of regular expressions + atom = "[A-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" + dotAtom = atom + `(?:\.` + atom + `)*` + domain = `[A-Z0-9.-]+\.[A-Z]{2,4}` + + RE_HEX = regexp.MustCompile(`^(?i)[a-f0-9]+$`) + RE_EMAIL = regexp.MustCompile(`^(?i)(` + dotAtom + `)@(` + dotAtom + `)$`) + RE_ADDRESS = regexp.MustCompile(`^(?i)[a-z0-9]{25,34}$`) + RE_HOST = regexp.MustCompile(`^(?i)(` + domain + `)$`) + + //RE_ID12 = regexp.MustCompile(`^[a-zA-Z0-9]{12}$`) +) + +func GetParam(r *http.Request, param string) string { + s := r.URL.Query().Get(param) + if s == "" { + s = r.FormValue(param) + } + return s +} + +func GetParamByteSlice(r *http.Request, param string) ([]byte, error) { + s := GetParam(r, param) + return hex.DecodeString(s) +} + +func GetParamInt64(r *http.Request, param string) (int64, error) { + s := GetParam(r, param) + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return i, nil +} + +func GetParamInt32(r *http.Request, param string) (int32, error) { + s := GetParam(r, param) + i, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return int32(i), nil +} + +func GetParamUint64(r *http.Request, param string) (uint64, error) { + s := GetParam(r, param) + i, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return i, nil +} + +func GetParamUint(r *http.Request, param string) (uint, error) { + s := GetParam(r, param) + i, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return uint(i), nil +} + +func GetParamRegexp(r *http.Request, param string, re *regexp.Regexp) (string, error) { + s := GetParam(r, param) + if !re.MatchString(s) { + return "", errors.Errorf(param, "Did not match regular expression %v", re.String()) + } + return s, nil +} + +func GetParamFloat64(r *http.Request, param string) (float64, error) { + s := GetParam(r, param) + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return f, nil +} diff --git a/rpc/server/http_server.go b/rpc/server/http_server.go new file mode 100644 index 00000000..a613be25 --- /dev/null +++ b/rpc/server/http_server.go @@ -0,0 +1,125 @@ +// Commons for HTTP handling +package rpcserver + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "net/http" + "runtime/debug" + "strings" + "time" + + "github.com/pkg/errors" + types "github.com/tendermint/tendermint/rpc/types" +) + +func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.Listener, err error) { + // listenAddr should be fully formed including tcp:// or unix:// prefix + var proto, addr string + parts := strings.SplitN(listenAddr, "://", 2) + if len(parts) != 2 { + log.Warn("WARNING (tendermint/rpc): Please use fully formed listening addresses, including the tcp:// or unix:// prefix") + // we used to allow addrs without tcp/unix prefix by checking for a colon + // TODO: Deprecate + proto = types.SocketType(listenAddr) + addr = listenAddr + // return nil, errors.Errorf("Invalid listener address %s", lisenAddr) + } else { + proto, addr = parts[0], parts[1] + } + + log.Notice(fmt.Sprintf("Starting RPC HTTP server on %s socket %v", proto, addr)) + listener, err = net.Listen(proto, addr) + if err != nil { + return nil, errors.Errorf("Failed to listen to %v: %v", listenAddr, err) + } + + go func() { + res := http.Serve( + listener, + RecoverAndLogHandler(handler), + ) + log.Crit("RPC HTTP server stopped", "result", res) + }() + return listener, nil +} + +func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { + // jsonBytes := wire.JSONBytesPretty(res) + jsonBytes, err := json.Marshal(res) + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(jsonBytes) +} + +//----------------------------------------------------------------------------- + +// Wraps an HTTP handler, adding error logging. +// If the inner function panics, the outer function recovers, logs, sends an +// HTTP 500 error response. +func RecoverAndLogHandler(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Wrap the ResponseWriter to remember the status + rww := &ResponseWriterWrapper{-1, w} + begin := time.Now() + + // Common headers + origin := r.Header.Get("Origin") + rww.Header().Set("Access-Control-Allow-Origin", origin) + rww.Header().Set("Access-Control-Allow-Credentials", "true") + rww.Header().Set("Access-Control-Expose-Headers", "X-Server-Time") + rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix())) + + defer func() { + // Send a 500 error if a panic happens during a handler. + // Without this, Chrome & Firefox were retrying aborted ajax requests, + // at least to my localhost. + if e := recover(); e != nil { + + // If RPCResponse + if res, ok := e.(types.RPCResponse); ok { + WriteRPCResponseHTTP(rww, res) + } else { + // For the rest, + log.Error("Panic in RPC HTTP handler", "error", e, "stack", string(debug.Stack())) + rww.WriteHeader(http.StatusInternalServerError) + WriteRPCResponseHTTP(rww, types.NewRPCResponse("", nil, fmt.Sprintf("Internal Server Error: %v", e))) + } + } + + // Finally, log. + durationMS := time.Since(begin).Nanoseconds() / 1000000 + if rww.Status == -1 { + rww.Status = 200 + } + log.Info("Served RPC HTTP response", + "method", r.Method, "url", r.URL, + "status", rww.Status, "duration", durationMS, + "remoteAddr", r.RemoteAddr, + ) + }() + + handler.ServeHTTP(rww, r) + }) +} + +// Remember the status for logging +type ResponseWriterWrapper struct { + Status int + http.ResponseWriter +} + +func (w *ResponseWriterWrapper) WriteHeader(status int) { + w.Status = status + w.ResponseWriter.WriteHeader(status) +} + +// implements http.Hijacker +func (w *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return w.ResponseWriter.(http.Hijacker).Hijack() +} diff --git a/rpc/server/log.go b/rpc/server/log.go new file mode 100644 index 00000000..704e22e3 --- /dev/null +++ b/rpc/server/log.go @@ -0,0 +1,7 @@ +package rpcserver + +import ( + "github.com/tendermint/log15" +) + +var log = log15.New("module", "rpcserver") diff --git a/rpc/client/event_test.go b/rpc/tendermint/client/event_test.go similarity index 96% rename from rpc/client/event_test.go rename to rpc/tendermint/client/event_test.go index cc421ad9..a82a284f 100644 --- a/rpc/client/event_test.go +++ b/rpc/tendermint/client/event_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" merktest "github.com/tendermint/merkleeyes/testutil" - "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/tendermint/client" "github.com/tendermint/tendermint/types" ) diff --git a/rpc/client/helpers.go b/rpc/tendermint/client/helpers.go similarity index 96% rename from rpc/client/helpers.go rename to rpc/tendermint/client/helpers.go index bd00c143..330bcd19 100644 --- a/rpc/client/helpers.go +++ b/rpc/tendermint/client/helpers.go @@ -4,8 +4,8 @@ import ( "time" "github.com/pkg/errors" - cmn "github.com/tendermint/go-common" - events "github.com/tendermint/go-events" + cmn "github.com/tendermint/tmlibs/common" + events "github.com/tendermint/tmlibs/events" "github.com/tendermint/tendermint/types" ) diff --git a/rpc/client/helpers_test.go b/rpc/tendermint/client/helpers_test.go similarity index 90% rename from rpc/client/helpers_test.go rename to rpc/tendermint/client/helpers_test.go index fe186122..1057a21b 100644 --- a/rpc/client/helpers_test.go +++ b/rpc/tendermint/client/helpers_test.go @@ -7,9 +7,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/rpc/client" - "github.com/tendermint/tendermint/rpc/client/mock" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/rpc/tendermint/client" + "github.com/tendermint/tendermint/rpc/tendermint/client/mock" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" ) func TestWaitForHeight(t *testing.T) { diff --git a/rpc/client/httpclient.go b/rpc/tendermint/client/httpclient.go similarity index 96% rename from rpc/client/httpclient.go rename to rpc/tendermint/client/httpclient.go index 04595e76..e8f404d2 100644 --- a/rpc/client/httpclient.go +++ b/rpc/tendermint/client/httpclient.go @@ -4,10 +4,11 @@ import ( "fmt" "github.com/pkg/errors" - events "github.com/tendermint/go-events" - "github.com/tendermint/go-rpc/client" + data "github.com/tendermint/go-wire/data" + events "github.com/tendermint/tmlibs/events" + "github.com/tendermint/tendermint/rpc/client" wire "github.com/tendermint/go-wire" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) @@ -67,7 +68,7 @@ func (c *HTTP) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return (*tmResult).(*ctypes.ResultABCIInfo), nil } -func (c *HTTP) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { +func (c *HTTP) ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) { tmResult := new(ctypes.TMResult) _, err := c.rpc.Call("abci_query", map[string]interface{}{"path": path, "data": data, "prove": prove}, @@ -197,7 +198,7 @@ type WSEvents struct { // used to maintain counts of actively listened events // so we can properly subscribe/unsubscribe // FIXME: thread-safety??? - // FIXME: reuse code from go-events??? + // FIXME: reuse code from tmlibs/events??? evtCount map[string]int // count how many time each event is subscribed listeners map[string][]string // keep track of which events each listener is listening to } diff --git a/rpc/client/interface.go b/rpc/tendermint/client/interface.go similarity index 93% rename from rpc/client/interface.go rename to rpc/tendermint/client/interface.go index 2ba89079..4726e00d 100644 --- a/rpc/client/interface.go +++ b/rpc/tendermint/client/interface.go @@ -20,7 +20,8 @@ implementation. package client import ( - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) @@ -30,7 +31,7 @@ import ( type ABCIClient interface { // reading from abci app ABCIInfo() (*ctypes.ResultABCIInfo, error) - ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) + ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) // writing to abci app BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) diff --git a/rpc/client/localclient.go b/rpc/tendermint/client/localclient.go similarity index 90% rename from rpc/client/localclient.go rename to rpc/tendermint/client/localclient.go index d0f0d11b..146d9659 100644 --- a/rpc/client/localclient.go +++ b/rpc/tendermint/client/localclient.go @@ -1,9 +1,10 @@ package client import ( + data "github.com/tendermint/go-wire/data" nm "github.com/tendermint/tendermint/node" - "github.com/tendermint/tendermint/rpc/core" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/rpc/tendermint/core" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) @@ -56,7 +57,7 @@ func (c Local) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return core.ABCIInfo() } -func (c Local) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { +func (c Local) ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) { return core.ABCIQuery(path, data, prove) } diff --git a/rpc/client/main_test.go b/rpc/tendermint/client/main_test.go similarity index 87% rename from rpc/client/main_test.go rename to rpc/tendermint/client/main_test.go index 5ec911e5..494b565a 100644 --- a/rpc/client/main_test.go +++ b/rpc/tendermint/client/main_test.go @@ -6,7 +6,7 @@ import ( meapp "github.com/tendermint/merkleeyes/app" nm "github.com/tendermint/tendermint/node" - rpctest "github.com/tendermint/tendermint/rpc/test" + rpctest "github.com/tendermint/tendermint/rpc/tendermint/test" ) var node *nm.Node diff --git a/rpc/client/mock/abci.go b/rpc/tendermint/client/mock/abci.go similarity index 90% rename from rpc/client/mock/abci.go rename to rpc/tendermint/client/mock/abci.go index 6f6fa1d4..d03728db 100644 --- a/rpc/client/mock/abci.go +++ b/rpc/tendermint/client/mock/abci.go @@ -2,8 +2,9 @@ package mock import ( abci "github.com/tendermint/abci/types" - "github.com/tendermint/tendermint/rpc/client" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + "github.com/tendermint/tendermint/rpc/tendermint/client" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) @@ -22,7 +23,7 @@ func (a ABCIApp) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return &ctypes.ResultABCIInfo{a.App.Info()}, nil } -func (a ABCIApp) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { +func (a ABCIApp) ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) { q := a.App.Query(abci.RequestQuery{data, path, 0, prove}) return &ctypes.ResultABCIQuery{q}, nil } @@ -79,7 +80,7 @@ func (m ABCIMock) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return &ctypes.ResultABCIInfo{res.(abci.ResponseInfo)}, nil } -func (m ABCIMock) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { +func (m ABCIMock) ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) { res, err := m.Query.GetResponse(QueryArgs{path, data, prove}) if err != nil { return nil, err @@ -131,7 +132,7 @@ func (r *ABCIRecorder) _assertABCIClient() client.ABCIClient { type QueryArgs struct { Path string - Data []byte + Data data.Bytes Prove bool } @@ -149,7 +150,7 @@ func (r *ABCIRecorder) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return res, err } -func (r *ABCIRecorder) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { +func (r *ABCIRecorder) ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) { res, err := r.Client.ABCIQuery(path, data, prove) r.addCall(Call{ Name: "abci_query", diff --git a/rpc/client/mock/abci_test.go b/rpc/tendermint/client/mock/abci_test.go similarity index 91% rename from rpc/client/mock/abci_test.go rename to rpc/tendermint/client/mock/abci_test.go index 823752ca..3fcca969 100644 --- a/rpc/client/mock/abci_test.go +++ b/rpc/tendermint/client/mock/abci_test.go @@ -10,10 +10,11 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/abci/example/dummy" abci "github.com/tendermint/abci/types" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" - "github.com/tendermint/tendermint/rpc/client/mock" + "github.com/tendermint/tendermint/rpc/tendermint/client/mock" ) func TestABCIMock(t *testing.T) { @@ -35,8 +36,8 @@ func TestABCIMock(t *testing.T) { BroadcastCommit: mock.Call{ Args: goodTx, Response: &ctypes.ResultBroadcastTxCommit{ - CheckTx: &abci.ResponseCheckTx{Data: []byte("stand")}, - DeliverTx: &abci.ResponseDeliverTx{Data: []byte("deliver")}, + CheckTx: &abci.ResponseCheckTx{Data: data.Bytes("stand")}, + DeliverTx: &abci.ResponseDeliverTx{Data: data.Bytes("deliver")}, }, Error: errors.New("bad tx"), }, @@ -91,7 +92,7 @@ func TestABCIRecorder(t *testing.T) { require.Equal(0, len(r.Calls)) r.ABCIInfo() - r.ABCIQuery("path", []byte("data"), true) + r.ABCIQuery("path", data.Bytes("data"), true) require.Equal(2, len(r.Calls)) info := r.Calls[0] @@ -163,7 +164,7 @@ func TestABCIApp(t *testing.T) { assert.True(res.DeliverTx.Code.IsOK()) // check the key - qres, err := m.ABCIQuery("/key", []byte(key), false) + qres, err := m.ABCIQuery("/key", data.Bytes(key), false) require.Nil(err) assert.EqualValues(value, qres.Response.Value) } diff --git a/rpc/client/mock/client.go b/rpc/tendermint/client/mock/client.go similarity index 91% rename from rpc/client/mock/client.go rename to rpc/tendermint/client/mock/client.go index a3cecfca..b2a91b74 100644 --- a/rpc/client/mock/client.go +++ b/rpc/tendermint/client/mock/client.go @@ -16,9 +16,10 @@ package mock import ( "reflect" - "github.com/tendermint/tendermint/rpc/client" - "github.com/tendermint/tendermint/rpc/core" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + "github.com/tendermint/tendermint/rpc/tendermint/client" + "github.com/tendermint/tendermint/rpc/tendermint/core" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) @@ -83,7 +84,7 @@ func (c Client) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return core.ABCIInfo() } -func (c Client) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { +func (c Client) ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) { return core.ABCIQuery(path, data, prove) } diff --git a/rpc/client/mock/status.go b/rpc/tendermint/client/mock/status.go similarity index 89% rename from rpc/client/mock/status.go rename to rpc/tendermint/client/mock/status.go index af0f5335..82ec4098 100644 --- a/rpc/client/mock/status.go +++ b/rpc/tendermint/client/mock/status.go @@ -1,8 +1,8 @@ package mock import ( - "github.com/tendermint/tendermint/rpc/client" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/rpc/tendermint/client" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" ) // StatusMock returns the result specified by the Call diff --git a/rpc/client/mock/status_test.go b/rpc/tendermint/client/mock/status_test.go similarity index 78% rename from rpc/client/mock/status_test.go rename to rpc/tendermint/client/mock/status_test.go index 3e695cd5..f5a2bc7f 100644 --- a/rpc/client/mock/status_test.go +++ b/rpc/tendermint/client/mock/status_test.go @@ -5,9 +5,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" - "github.com/tendermint/tendermint/rpc/client/mock" + "github.com/tendermint/tendermint/rpc/tendermint/client/mock" ) func TestStatus(t *testing.T) { @@ -16,8 +17,8 @@ func TestStatus(t *testing.T) { m := &mock.StatusMock{ Call: mock.Call{ Response: &ctypes.ResultStatus{ - LatestBlockHash: []byte("block"), - LatestAppHash: []byte("app"), + LatestBlockHash: data.Bytes("block"), + LatestAppHash: data.Bytes("app"), LatestBlockHeight: 10, }}, } diff --git a/rpc/client/rpc_test.go b/rpc/tendermint/client/rpc_test.go similarity index 96% rename from rpc/client/rpc_test.go rename to rpc/tendermint/client/rpc_test.go index d9f5d379..95c583c9 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/tendermint/client/rpc_test.go @@ -6,10 +6,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - merkle "github.com/tendermint/go-merkle" + "github.com/tendermint/merkleeyes/iavl" merktest "github.com/tendermint/merkleeyes/testutil" - "github.com/tendermint/tendermint/rpc/client" - rpctest "github.com/tendermint/tendermint/rpc/test" + "github.com/tendermint/tendermint/rpc/tendermint/client" + rpctest "github.com/tendermint/tendermint/rpc/tendermint/test" "github.com/tendermint/tendermint/types" ) @@ -175,7 +175,7 @@ func TestAppCalls(t *testing.T) { // and we got a proof that works! pres, err := c.ABCIQuery("/key", k, true) if assert.Nil(err) && assert.True(pres.Response.Code.IsOK()) { - proof, err := merkle.ReadProof(pres.Response.GetProof()) + proof, err := iavl.ReadProof(pres.Response.GetProof()) if assert.Nil(err) { key := pres.Response.GetKey() value := pres.Response.GetValue() diff --git a/rpc/core/abci.go b/rpc/tendermint/core/abci.go similarity index 74% rename from rpc/core/abci.go rename to rpc/tendermint/core/abci.go index 95772726..e56b92d7 100644 --- a/rpc/core/abci.go +++ b/rpc/tendermint/core/abci.go @@ -2,12 +2,13 @@ package core import ( abci "github.com/tendermint/abci/types" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" ) //----------------------------------------------------------------------------- -func ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { +func ABCIQuery(path string, data data.Bytes, prove bool) (*ctypes.ResultABCIQuery, error) { resQuery, err := proxyAppQuery.QuerySync(abci.RequestQuery{ Path: path, Data: data, diff --git a/rpc/core/blocks.go b/rpc/tendermint/core/blocks.go similarity index 95% rename from rpc/core/blocks.go rename to rpc/tendermint/core/blocks.go index 65e47d12..aa102255 100644 --- a/rpc/core/blocks.go +++ b/rpc/tendermint/core/blocks.go @@ -2,8 +2,8 @@ package core import ( "fmt" - . "github.com/tendermint/go-common" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + . "github.com/tendermint/tmlibs/common" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) diff --git a/rpc/core/consensus.go b/rpc/tendermint/core/consensus.go similarity index 92% rename from rpc/core/consensus.go rename to rpc/tendermint/core/consensus.go index e2ccce44..e62e61a0 100644 --- a/rpc/core/consensus.go +++ b/rpc/tendermint/core/consensus.go @@ -3,7 +3,7 @@ package core import ( "github.com/tendermint/go-wire" cm "github.com/tendermint/tendermint/consensus" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) diff --git a/rpc/core/dev.go b/rpc/tendermint/core/dev.go similarity index 95% rename from rpc/core/dev.go rename to rpc/tendermint/core/dev.go index 43a98953..2053d736 100644 --- a/rpc/core/dev.go +++ b/rpc/tendermint/core/dev.go @@ -6,7 +6,7 @@ import ( "runtime/pprof" "strconv" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" ) func UnsafeFlushMempool() (*ctypes.ResultUnsafeFlushMempool, error) { diff --git a/rpc/core/events.go b/rpc/tendermint/core/events.go similarity index 89% rename from rpc/core/events.go rename to rpc/tendermint/core/events.go index 7dc3c7c3..e12122e5 100644 --- a/rpc/core/events.go +++ b/rpc/tendermint/core/events.go @@ -1,8 +1,8 @@ package core import ( - "github.com/tendermint/go-rpc/types" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/rpc/types" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) diff --git a/rpc/core/log.go b/rpc/tendermint/core/log.go similarity index 100% rename from rpc/core/log.go rename to rpc/tendermint/core/log.go diff --git a/rpc/core/mempool.go b/rpc/tendermint/core/mempool.go similarity index 94% rename from rpc/core/mempool.go rename to rpc/tendermint/core/mempool.go index 4da83a1f..e3d82bf8 100644 --- a/rpc/core/mempool.go +++ b/rpc/tendermint/core/mempool.go @@ -5,7 +5,8 @@ import ( "time" abci "github.com/tendermint/abci/types" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) @@ -84,7 +85,7 @@ func BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { Data: deliverTxRes.Data, Log: deliverTxRes.Log, } - log.Notice("DeliverTx passed ", "tx", []byte(tx), "response", deliverTxR) + log.Notice("DeliverTx passed ", "tx", data.Bytes(tx), "response", deliverTxR) return &ctypes.ResultBroadcastTxCommit{ CheckTx: checkTxR, DeliverTx: deliverTxR, diff --git a/rpc/core/net.go b/rpc/tendermint/core/net.go similarity index 95% rename from rpc/core/net.go rename to rpc/tendermint/core/net.go index 31d9c34e..49945823 100644 --- a/rpc/core/net.go +++ b/rpc/tendermint/core/net.go @@ -3,7 +3,7 @@ package core import ( "fmt" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" ) //----------------------------------------------------------------------------- diff --git a/rpc/core/pipe.go b/rpc/tendermint/core/pipe.go similarity index 93% rename from rpc/core/pipe.go rename to rpc/tendermint/core/pipe.go index 4993ed99..b10ff473 100644 --- a/rpc/core/pipe.go +++ b/rpc/tendermint/core/pipe.go @@ -1,11 +1,11 @@ package core import ( - cfg "github.com/tendermint/go-config" + "github.com/spf13/viper" crypto "github.com/tendermint/go-crypto" - p2p "github.com/tendermint/go-p2p" "github.com/tendermint/tendermint/consensus" + p2p "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" @@ -34,7 +34,7 @@ var ( // external, thread safe interfaces eventSwitch types.EventSwitch proxyAppQuery proxy.AppConnQuery - config cfg.Config + config *viper.Viper // interfaces defined in types and above blockStore types.BlockStore @@ -49,7 +49,7 @@ var ( txIndexer txindex.TxIndexer ) -func SetConfig(c cfg.Config) { +func SetConfig(c *viper.Viper) { config = c } diff --git a/rpc/core/routes.go b/rpc/tendermint/core/routes.go similarity index 85% rename from rpc/core/routes.go rename to rpc/tendermint/core/routes.go index 38e60960..7ed18ef7 100644 --- a/rpc/core/routes.go +++ b/rpc/tendermint/core/routes.go @@ -1,9 +1,11 @@ package core import ( - rpc "github.com/tendermint/go-rpc/server" - "github.com/tendermint/go-rpc/types" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + rpc "github.com/tendermint/tendermint/rpc/server" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" + "github.com/tendermint/tendermint/rpc/types" + "github.com/tendermint/tendermint/types" ) // TODO: better system than "unsafe" prefix @@ -37,7 +39,9 @@ var Routes = map[string]*rpc.RPCFunc{ // control API "dial_seeds": rpc.NewRPCFunc(UnsafeDialSeedsResult, "seeds"), "unsafe_flush_mempool": rpc.NewRPCFunc(UnsafeFlushMempool, ""), - "unsafe_set_config": rpc.NewRPCFunc(UnsafeSetConfigResult, "type,key,value"), + + // config is not in general thread safe. expose specifics if you need em + // "unsafe_set_config": rpc.NewRPCFunc(UnsafeSetConfigResult, "type,key,value"), // profiler API "unsafe_start_cpu_profiler": rpc.NewRPCFunc(UnsafeStartCPUProfilerResult, "filename"), @@ -104,19 +108,19 @@ func TxResult(hash []byte, prove bool) (ctypes.TMResult, error) { return Tx(hash, prove) } -func BroadcastTxCommitResult(tx []byte) (ctypes.TMResult, error) { +func BroadcastTxCommitResult(tx types.Tx) (ctypes.TMResult, error) { return BroadcastTxCommit(tx) } -func BroadcastTxSyncResult(tx []byte) (ctypes.TMResult, error) { +func BroadcastTxSyncResult(tx types.Tx) (ctypes.TMResult, error) { return BroadcastTxSync(tx) } -func BroadcastTxAsyncResult(tx []byte) (ctypes.TMResult, error) { +func BroadcastTxAsyncResult(tx types.Tx) (ctypes.TMResult, error) { return BroadcastTxAsync(tx) } -func ABCIQueryResult(path string, data []byte, prove bool) (ctypes.TMResult, error) { +func ABCIQueryResult(path string, data data.Bytes, prove bool) (ctypes.TMResult, error) { return ABCIQuery(path, data, prove) } diff --git a/rpc/core/status.go b/rpc/tendermint/core/status.go similarity index 80% rename from rpc/core/status.go rename to rpc/tendermint/core/status.go index 96ed46ea..9dbfff86 100644 --- a/rpc/core/status.go +++ b/rpc/tendermint/core/status.go @@ -1,7 +1,8 @@ package core import ( - ctypes "github.com/tendermint/tendermint/rpc/core/types" + data "github.com/tendermint/go-wire/data" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/types" ) @@ -9,8 +10,8 @@ func Status() (*ctypes.ResultStatus, error) { latestHeight := blockStore.Height() var ( latestBlockMeta *types.BlockMeta - latestBlockHash []byte - latestAppHash []byte + latestBlockHash data.Bytes + latestAppHash data.Bytes latestBlockTime int64 ) if latestHeight != 0 { diff --git a/rpc/core/tx.go b/rpc/tendermint/core/tx.go similarity index 91% rename from rpc/core/tx.go rename to rpc/tendermint/core/tx.go index 7f3cdd03..7d6a2deb 100644 --- a/rpc/core/tx.go +++ b/rpc/tendermint/core/tx.go @@ -3,7 +3,7 @@ package core import ( "fmt" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/state/txindex/null" "github.com/tendermint/tendermint/types" ) diff --git a/rpc/core/types/responses.go b/rpc/tendermint/core/types/responses.go similarity index 95% rename from rpc/core/types/responses.go rename to rpc/tendermint/core/types/responses.go index 7cab8535..f88b297e 100644 --- a/rpc/core/types/responses.go +++ b/rpc/tendermint/core/types/responses.go @@ -5,8 +5,9 @@ import ( abci "github.com/tendermint/abci/types" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-p2p" - "github.com/tendermint/go-rpc/types" + data "github.com/tendermint/go-wire/data" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/rpc/types" "github.com/tendermint/go-wire" "github.com/tendermint/tendermint/types" ) @@ -34,8 +35,8 @@ type ResultCommit struct { type ResultStatus struct { NodeInfo *p2p.NodeInfo `json:"node_info"` PubKey crypto.PubKey `json:"pub_key"` - LatestBlockHash []byte `json:"latest_block_hash"` - LatestAppHash []byte `json:"latest_app_hash"` + LatestBlockHash data.Bytes `json:"latest_block_hash"` + LatestAppHash data.Bytes `json:"latest_app_hash"` LatestBlockHeight int `json:"latest_block_height"` LatestBlockTime int64 `json:"latest_block_time"` // nano } @@ -81,7 +82,7 @@ type ResultDumpConsensusState struct { type ResultBroadcastTx struct { Code abci.CodeType `json:"code"` - Data []byte `json:"data"` + Data data.Bytes `json:"data"` Log string `json:"log"` Hash []byte `json:"hash"` diff --git a/rpc/core/types/responses_test.go b/rpc/tendermint/core/types/responses_test.go similarity index 95% rename from rpc/core/types/responses_test.go rename to rpc/tendermint/core/types/responses_test.go index 69ee4fae..8eef1979 100644 --- a/rpc/core/types/responses_test.go +++ b/rpc/tendermint/core/types/responses_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/tendermint/go-p2p" + "github.com/tendermint/tendermint/p2p" ) func TestStatusIndexer(t *testing.T) { diff --git a/rpc/core/version.go b/rpc/tendermint/core/version.go similarity index 100% rename from rpc/core/version.go rename to rpc/tendermint/core/version.go diff --git a/rpc/grpc/api.go b/rpc/tendermint/grpc/api.go similarity index 85% rename from rpc/grpc/api.go rename to rpc/tendermint/grpc/api.go index fab811c2..f2816939 100644 --- a/rpc/grpc/api.go +++ b/rpc/tendermint/grpc/api.go @@ -1,7 +1,7 @@ package core_grpc import ( - core "github.com/tendermint/tendermint/rpc/core" + core "github.com/tendermint/tendermint/rpc/tendermint/core" context "golang.org/x/net/context" ) diff --git a/rpc/grpc/client_server.go b/rpc/tendermint/grpc/client_server.go similarity index 95% rename from rpc/grpc/client_server.go rename to rpc/tendermint/grpc/client_server.go index d760bf25..e6055ede 100644 --- a/rpc/grpc/client_server.go +++ b/rpc/tendermint/grpc/client_server.go @@ -8,7 +8,7 @@ import ( "google.golang.org/grpc" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) // Start the grpcServer in a go routine diff --git a/rpc/grpc/compile.sh b/rpc/tendermint/grpc/compile.sh similarity index 100% rename from rpc/grpc/compile.sh rename to rpc/tendermint/grpc/compile.sh diff --git a/rpc/grpc/types.pb.go b/rpc/tendermint/grpc/types.pb.go similarity index 100% rename from rpc/grpc/types.pb.go rename to rpc/tendermint/grpc/types.pb.go diff --git a/rpc/grpc/types.proto b/rpc/tendermint/grpc/types.proto similarity index 100% rename from rpc/grpc/types.proto rename to rpc/tendermint/grpc/types.proto diff --git a/rpc/test/client_test.go b/rpc/tendermint/test/client_test.go similarity index 81% rename from rpc/test/client_test.go rename to rpc/tendermint/test/client_test.go index 50e32605..11ee61a5 100644 --- a/rpc/test/client_test.go +++ b/rpc/tendermint/test/client_test.go @@ -10,13 +10,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + abci "github.com/tendermint/abci/types" - . "github.com/tendermint/go-common" - rpc "github.com/tendermint/go-rpc/client" - "github.com/tendermint/tendermint/rpc/core" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + rpc "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/tendermint/core" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" "github.com/tendermint/tendermint/state/txindex/null" "github.com/tendermint/tendermint/types" + . "github.com/tendermint/tmlibs/common" ) //-------------------------------------------------------------------------------- @@ -66,8 +67,8 @@ func TestJSONBroadcastTxSync(t *testing.T) { } func testBroadcastTxSync(t *testing.T, client rpc.HTTPClient) { - config.Set("block_size", 0) - defer config.Set("block_size", -1) + mem := node.MempoolReactor().Mempool + initMemSize := mem.Size() tmResult := new(ctypes.TMResult) tx := randBytes(t) _, err := client.Call("broadcast_tx_sync", map[string]interface{}{"tx": tx}, tmResult) @@ -75,8 +76,7 @@ func testBroadcastTxSync(t *testing.T, client rpc.HTTPClient) { res := (*tmResult).(*ctypes.ResultBroadcastTx) require.Equal(t, abci.CodeType_OK, res.Code) - mem := node.MempoolReactor().Mempool - require.Equal(t, 1, mem.Size()) + require.Equal(t, initMemSize+1, mem.Size()) txs := mem.Reap(1) require.EqualValues(t, tx, txs[0]) mem.Flush() @@ -85,10 +85,10 @@ func testBroadcastTxSync(t *testing.T, client rpc.HTTPClient) { //-------------------------------------------------------------------------------- // query -func testTxKV(t *testing.T) ([]byte, []byte, []byte) { +func testTxKV(t *testing.T) ([]byte, []byte, types.Tx) { k := randBytes(t) v := randBytes(t) - return k, v, []byte(Fmt("%s=%s", k, v)) + return k, v, types.Tx(Fmt("%s=%s", k, v)) } func sendTx(t *testing.T, client rpc.HTTPClient) ([]byte, []byte) { @@ -96,6 +96,10 @@ func sendTx(t *testing.T, client rpc.HTTPClient) ([]byte, []byte) { k, v, tx := testTxKV(t) _, err := client.Call("broadcast_tx_commit", map[string]interface{}{"tx": tx}, tmResult) require.Nil(t, err) + bres := (*tmResult).(*ctypes.ResultBroadcastTxCommit) + require.NotNil(t, 0, bres.DeliverTx, "%#v", bres) + require.EqualValues(t, 0, bres.CheckTx.GetCode(), "%#v", bres) + require.EqualValues(t, 0, bres.DeliverTx.GetCode(), "%#v", bres) return k, v } @@ -104,12 +108,12 @@ func TestURIABCIQuery(t *testing.T) { } func TestJSONABCIQuery(t *testing.T) { - testABCIQuery(t, GetURIClient()) + testABCIQuery(t, GetJSONClient()) } func testABCIQuery(t *testing.T, client rpc.HTTPClient) { k, _ := sendTx(t, client) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 500) tmResult := new(ctypes.TMResult) _, err := client.Call("abci_query", map[string]interface{}{"path": "", "data": k, "prove": false}, tmResult) @@ -353,51 +357,52 @@ func TestWSDoubleFire(t *testing.T) { }*/ //-------------------------------------------------------------------------------- +//TODO needs to be refactored so we don't use a mutable config but rather update specific values we're interested in // unsafe_set_config -var stringVal = "my string" -var intVal = 987654321 -var boolVal = true - -// don't change these -var testCasesUnsafeSetConfig = [][]string{ - []string{"string", "key1", stringVal}, - []string{"int", "key2", fmt.Sprintf("%v", intVal)}, - []string{"bool", "key3", fmt.Sprintf("%v", boolVal)}, -} - -func TestURIUnsafeSetConfig(t *testing.T) { - for _, testCase := range testCasesUnsafeSetConfig { - tmResult := new(ctypes.TMResult) - _, err := GetURIClient().Call("unsafe_set_config", map[string]interface{}{ - "type": testCase[0], - "key": testCase[1], - "value": testCase[2], - }, tmResult) - require.Nil(t, err) - } - testUnsafeSetConfig(t) -} - -func TestJSONUnsafeSetConfig(t *testing.T) { - for _, testCase := range testCasesUnsafeSetConfig { - tmResult := new(ctypes.TMResult) - _, err := GetJSONClient().Call("unsafe_set_config", - map[string]interface{}{"type": testCase[0], "key": testCase[1], "value": testCase[2]}, - tmResult) - require.Nil(t, err) - } - testUnsafeSetConfig(t) -} - -func testUnsafeSetConfig(t *testing.T) { - require := require.New(t) - s := config.GetString("key1") - require.Equal(stringVal, s) - - i := config.GetInt("key2") - require.Equal(intVal, i) - - b := config.GetBool("key3") - require.Equal(boolVal, b) -} +//var stringVal = "my string" +//var intVal = 987654321 +//var boolVal = true +// +//// don't change these +//var testCasesUnsafeSetConfig = [][]string{ +// []string{"string", "key1", stringVal}, +// []string{"int", "key2", fmt.Sprintf("%v", intVal)}, +// []string{"bool", "key3", fmt.Sprintf("%v", boolVal)}, +//} +// +//func TestURIUnsafeSetConfig(t *testing.T) { +// for _, testCase := range testCasesUnsafeSetConfig { +// tmResult := new(ctypes.TMResult) +// _, err := GetURIClient().Call("unsafe_set_config", map[string]interface{}{ +// "type": testCase[0], +// "key": testCase[1], +// "value": testCase[2], +// }, tmResult) +// require.Nil(t, err) +// } +// testUnsafeSetConfig(t) +//} +// +//func TestJSONUnsafeSetConfig(t *testing.T) { +// for _, testCase := range testCasesUnsafeSetConfig { +// tmResult := new(ctypes.TMResult) +// _, err := GetJSONClient().Call("unsafe_set_config", +// map[string]interface{}{"type": testCase[0], "key": testCase[1], "value": testCase[2]}, +// tmResult) +// require.Nil(t, err) +// } +// testUnsafeSetConfig(t) +//} +// +//func testUnsafeSetConfig(t *testing.T) { +// require := require.New(t) +// s := config.GetString("key1") +// require.Equal(stringVal, s) +// +// i := config.GetInt("key2") +// require.Equal(intVal, i) +// +// b := config.GetBool("key3") +// require.Equal(boolVal, b) +//} diff --git a/rpc/test/grpc_test.go b/rpc/tendermint/test/grpc_test.go similarity index 88% rename from rpc/test/grpc_test.go rename to rpc/tendermint/test/grpc_test.go index 4935a09d..4fcc3e85 100644 --- a/rpc/test/grpc_test.go +++ b/rpc/tendermint/test/grpc_test.go @@ -6,7 +6,7 @@ import ( "golang.org/x/net/context" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/rpc/grpc" + "github.com/tendermint/tendermint/rpc/tendermint/grpc" ) //------------------------------------------- diff --git a/rpc/test/helpers.go b/rpc/tendermint/test/helpers.go similarity index 93% rename from rpc/test/helpers.go rename to rpc/tendermint/test/helpers.go index 349980e9..4e281f8c 100644 --- a/rpc/test/helpers.go +++ b/rpc/tendermint/test/helpers.go @@ -9,24 +9,22 @@ import ( "testing" "time" + "github.com/spf13/viper" "github.com/stretchr/testify/require" - logger "github.com/tendermint/go-logger" wire "github.com/tendermint/go-wire" + logger "github.com/tendermint/tmlibs/logger" abci "github.com/tendermint/abci/types" - cfg "github.com/tendermint/go-config" - client "github.com/tendermint/go-rpc/client" "github.com/tendermint/tendermint/config/tendermint_test" nm "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/proxy" - ctypes "github.com/tendermint/tendermint/rpc/core/types" - core_grpc "github.com/tendermint/tendermint/rpc/grpc" + client "github.com/tendermint/tendermint/rpc/client" + ctypes "github.com/tendermint/tendermint/rpc/tendermint/core/types" + core_grpc "github.com/tendermint/tendermint/rpc/tendermint/grpc" "github.com/tendermint/tendermint/types" ) -var ( - config cfg.Config -) +var config *viper.Viper const tmLogLevel = "error" @@ -56,7 +54,7 @@ func makeAddrs() (string, string, string) { } // GetConfig returns a config for the test cases as a singleton -func GetConfig() cfg.Config { +func GetConfig() *viper.Viper { if config == nil { pathname := makePathname() config = tendermint_test.ResetConfig(pathname) diff --git a/rpc/test/main_test.go b/rpc/tendermint/test/main_test.go similarity index 100% rename from rpc/test/main_test.go rename to rpc/tendermint/test/main_test.go diff --git a/rpc/test/data.json b/rpc/test/data.json new file mode 100644 index 00000000..83283ec3 --- /dev/null +++ b/rpc/test/data.json @@ -0,0 +1,9 @@ +{ + "jsonrpc": "2.0", + "id": "", + "method": "hello_world", + "params": { + "name": "my_world", + "num": 5 + } +} diff --git a/rpc/test/integration_test.sh b/rpc/test/integration_test.sh new file mode 100755 index 00000000..7c23be7d --- /dev/null +++ b/rpc/test/integration_test.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -e + +# Get the directory of where this script is. +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Change into that dir because we expect that. +pushd "$DIR" + +echo "==> Building the server" +go build -o rpcserver main.go + +echo "==> (Re)starting the server" +PID=$(pgrep rpcserver || echo "") +if [[ $PID != "" ]]; then + kill -9 "$PID" +fi +./rpcserver & +PID=$! +sleep 2 + +echo "==> simple request" +R1=$(curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5') +R2=$(curl -s --data @data.json http://localhost:8008) +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> request with 0x-prefixed hex string arg" +R1=$(curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123') +R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi ABCD 123"},"error":""}' +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> request with missing params" +R1=$(curl -s 'http://localhost:8008/hello_world') +R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi 0"},"error":""}' +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> request with unquoted string arg" +R1=$(curl -s 'http://localhost:8008/hello_world?name=abcd&num=123') +R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: invalid character 'a' looking for beginning of value\"}" +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> request with string type when expecting number arg" +R1=$(curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd') +R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: Got a hex string arg, but expected 'int'\"}" +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> Stopping the server" +kill -9 $PID + +rm -f rpcserver + +popd +exit 0 diff --git a/rpc/test/main.go b/rpc/test/main.go new file mode 100644 index 00000000..ea91fcfd --- /dev/null +++ b/rpc/test/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "net/http" + + cmn "github.com/tendermint/tmlibs/common" + rpcserver "github.com/tendermint/tendermint/rpc/server" +) + +var routes = map[string]*rpcserver.RPCFunc{ + "hello_world": rpcserver.NewRPCFunc(HelloWorld, "name,num"), +} + +func HelloWorld(name string, num int) (Result, error) { + return Result{fmt.Sprintf("hi %s %d", name, num)}, nil +} + +type Result struct { + Result string +} + +func main() { + mux := http.NewServeMux() + rpcserver.RegisterRPCFuncs(mux, routes) + _, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux) + if err != nil { + cmn.Exit(err.Error()) + } + + // Wait forever + cmn.TrapSignal(func() { + }) + +} diff --git a/rpc/types/types.go b/rpc/types/types.go new file mode 100644 index 00000000..9c5f2625 --- /dev/null +++ b/rpc/types/types.go @@ -0,0 +1,93 @@ +package rpctypes + +import ( + "encoding/json" + "strings" + + wire "github.com/tendermint/go-wire" + events "github.com/tendermint/tmlibs/events" +) + +type RPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` // must be map[string]interface{} or []interface{} +} + +func NewRPCRequest(id string, method string, params map[string]interface{}) RPCRequest { + return RPCRequest{ + JSONRPC: "2.0", + ID: id, + Method: method, + Params: params, + } +} + +//---------------------------------------- + +/* +Result is a generic interface. +Applications should register type-bytes like so: + +var _ = wire.RegisterInterface( + struct{ Result }{}, + wire.ConcreteType{&ResultGenesis{}, ResultTypeGenesis}, + wire.ConcreteType{&ResultBlockchainInfo{}, ResultTypeBlockchainInfo}, + ... +) +*/ +type Result interface { +} + +//---------------------------------------- + +type RPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Result *json.RawMessage `json:"result"` + Error string `json:"error"` +} + +func NewRPCResponse(id string, res interface{}, err string) RPCResponse { + var raw *json.RawMessage + if res != nil { + rawMsg := json.RawMessage(wire.JSONBytes(res)) + raw = &rawMsg + } + return RPCResponse{ + JSONRPC: "2.0", + ID: id, + Result: raw, + Error: err, + } +} + +//---------------------------------------- + +// *wsConnection implements this interface. +type WSRPCConnection interface { + GetRemoteAddr() string + GetEventSwitch() events.EventSwitch + WriteRPCResponse(resp RPCResponse) + TryWriteRPCResponse(resp RPCResponse) bool +} + +// websocket-only RPCFuncs take this as the first parameter. +type WSRPCContext struct { + Request RPCRequest + WSRPCConnection +} + +//---------------------------------------- +// sockets +// +// Determine if its a unix or tcp socket. +// If tcp, must specify the port; `0.0.0.0` will return incorrectly as "unix" since there's no port +func SocketType(listenAddr string) string { + socketType := "unix" + if len(strings.Split(listenAddr, ":")) >= 2 { + socketType = "tcp" + } + return socketType +} diff --git a/rpc/version.go b/rpc/version.go new file mode 100644 index 00000000..8828f260 --- /dev/null +++ b/rpc/version.go @@ -0,0 +1,7 @@ +package rpc + +const Maj = "0" +const Min = "7" +const Fix = "0" + +const Version = Maj + "." + Min + "." + Fix diff --git a/state/errors.go b/state/errors.go index 32a9351c..50c5a2c0 100644 --- a/state/errors.go +++ b/state/errors.go @@ -1,7 +1,7 @@ package state import ( - . "github.com/tendermint/go-common" + cmn "github.com/tendermint/tmlibs/common" ) type ( @@ -36,20 +36,20 @@ type ( ) func (e ErrUnknownBlock) Error() string { - return Fmt("Could not find block #%d", e.Height) + return cmn.Fmt("Could not find block #%d", e.Height) } func (e ErrBlockHashMismatch) Error() string { - return Fmt("App block hash (%X) does not match core block hash (%X) for height %d", e.AppHash, e.CoreHash, e.Height) + return cmn.Fmt("App block hash (%X) does not match core block hash (%X) for height %d", e.AppHash, e.CoreHash, e.Height) } func (e ErrAppBlockHeightTooHigh) Error() string { - return Fmt("App block height (%d) is higher than core (%d)", e.AppHeight, e.CoreHeight) + return cmn.Fmt("App block height (%d) is higher than core (%d)", e.AppHeight, e.CoreHeight) } func (e ErrLastStateMismatch) Error() string { - return Fmt("Latest tendermint block (%d) LastAppHash (%X) does not match app's AppHash (%X)", e.Height, e.Core, e.App) + return cmn.Fmt("Latest tendermint block (%d) LastAppHash (%X) does not match app's AppHash (%X)", e.Height, e.Core, e.App) } func (e ErrStateMismatch) Error() string { - return Fmt("State after replay does not match saved state. Got ----\n%v\nExpected ----\n%v\n", e.Got, e.Expected) + return cmn.Fmt("State after replay does not match saved state. Got ----\n%v\nExpected ----\n%v\n", e.Got, e.Expected) } diff --git a/state/execution.go b/state/execution.go index 0b1aff69..b06974cd 100644 --- a/state/execution.go +++ b/state/execution.go @@ -6,11 +6,11 @@ import ( fail "github.com/ebuchman/fail-test" abci "github.com/tendermint/abci/types" - . "github.com/tendermint/go-common" crypto "github.com/tendermint/go-crypto" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) //-------------------------------------------------- @@ -126,7 +126,7 @@ func updateValidators(validators *types.ValidatorSet, changedValidators []*abci. power := int64(v.Power) // mind the overflow from uint64 if power < 0 { - return errors.New(Fmt("Power (%d) overflows int64", v.Power)) + return errors.New(cmn.Fmt("Power (%d) overflows int64", v.Power)) } _, val := validators.GetByAddress(address) @@ -134,20 +134,20 @@ func updateValidators(validators *types.ValidatorSet, changedValidators []*abci. // add val added := validators.Add(types.NewValidator(pubkey, power)) if !added { - return errors.New(Fmt("Failed to add new validator %X with voting power %d", address, power)) + return errors.New(cmn.Fmt("Failed to add new validator %X with voting power %d", address, power)) } } else if v.Power == 0 { // remove val _, removed := validators.Remove(address) if !removed { - return errors.New(Fmt("Failed to remove validator %X)")) + return errors.New(cmn.Fmt("Failed to remove validator %X)")) } } else { // update val val.VotingPower = power updated := validators.Update(val) if !updated { - return errors.New(Fmt("Failed to update validator %X with voting power %d", address, power)) + return errors.New(cmn.Fmt("Failed to update validator %X with voting power %d", address, power)) } } } @@ -156,8 +156,8 @@ func updateValidators(validators *types.ValidatorSet, changedValidators []*abci. // return a bit array of validators that signed the last commit // NOTE: assumes commits have already been authenticated -func commitBitArrayFromBlock(block *types.Block) *BitArray { - signed := NewBitArray(len(block.LastCommit.Precommits)) +func commitBitArrayFromBlock(block *types.Block) *cmn.BitArray { + signed := cmn.NewBitArray(len(block.LastCommit.Precommits)) for i, precommit := range block.LastCommit.Precommits { if precommit != nil { signed.SetIndex(i, true) // val_.LastCommitHeight = block.Height - 1 @@ -187,7 +187,7 @@ func (s *State) validateBlock(block *types.Block) error { } } else { if len(block.LastCommit.Precommits) != s.LastValidators.Size() { - return errors.New(Fmt("Invalid block commit size. Expected %v, got %v", + return errors.New(cmn.Fmt("Invalid block commit size. Expected %v, got %v", s.LastValidators.Size(), len(block.LastCommit.Precommits))) } err := s.LastValidators.VerifyCommit( diff --git a/state/execution_test.go b/state/execution_test.go index 299c6baa..40e4044a 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/abci/example/dummy" crypto "github.com/tendermint/go-crypto" - dbm "github.com/tendermint/go-db" + dbm "github.com/tendermint/tmlibs/db" cfg "github.com/tendermint/tendermint/config/tendermint_test" "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/proxy" diff --git a/state/log.go b/state/log.go index 5b102b57..0a235132 100644 --- a/state/log.go +++ b/state/log.go @@ -1,7 +1,7 @@ package state import ( - "github.com/tendermint/go-logger" + "github.com/tendermint/tmlibs/logger" ) var log = logger.New("module", "state") diff --git a/state/state.go b/state/state.go index 086b0e71..2625cbc7 100644 --- a/state/state.go +++ b/state/state.go @@ -6,10 +6,11 @@ import ( "sync" "time" + "github.com/spf13/viper" abci "github.com/tendermint/abci/types" - . "github.com/tendermint/go-common" - cfg "github.com/tendermint/go-config" - dbm "github.com/tendermint/go-db" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/go-wire" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/state/txindex/null" @@ -64,7 +65,7 @@ func loadState(db dbm.DB, key []byte) *State { wire.ReadBinaryPtr(&s, r, 0, n, err) if *err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED - Exit(Fmt("LoadState: Data has been corrupted or its spec has changed: %v\n", *err)) + cmn.Exit(cmn.Fmt("LoadState: Data has been corrupted or its spec has changed: %v\n", *err)) } // TODO: ensure that buf is completely read. } @@ -108,7 +109,7 @@ func (s *State) LoadABCIResponses() *ABCIResponses { wire.ReadBinaryPtr(abciResponses, r, 0, n, err) if *err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED - Exit(Fmt("LoadABCIResponses: Data has been corrupted or its spec has changed: %v\n", *err)) + cmn.Exit(cmn.Fmt("LoadABCIResponses: Data has been corrupted or its spec has changed: %v\n", *err)) } // TODO: ensure that buf is completely read. } @@ -123,7 +124,7 @@ func (s *State) Bytes() []byte { buf, n, err := new(bytes.Buffer), new(int), new(error) wire.WriteBinary(s, buf, n, err) if *err != nil { - PanicCrisis(*err) + cmn.PanicCrisis(*err) } return buf.Bytes() } @@ -168,7 +169,7 @@ func (s *State) GetValidators() (*types.ValidatorSet, *types.ValidatorSet) { // Load the most recent state from "state" db, // or create a new one (and save) from genesis. -func GetState(config cfg.Config, stateDB dbm.DB) *State { +func GetState(config *viper.Viper, stateDB dbm.DB) *State { state := LoadState(stateDB) if state == nil { state = MakeGenesisStateFromFile(stateDB, config.GetString("genesis_file")) @@ -203,7 +204,7 @@ func (a *ABCIResponses) Bytes() []byte { buf, n, err := new(bytes.Buffer), new(int), new(error) wire.WriteBinary(*a, buf, n, err) if *err != nil { - PanicCrisis(*err) + cmn.PanicCrisis(*err) } return buf.Bytes() } @@ -217,11 +218,11 @@ func (a *ABCIResponses) Bytes() []byte { func MakeGenesisStateFromFile(db dbm.DB, genDocFile string) *State { genDocJSON, err := ioutil.ReadFile(genDocFile) if err != nil { - Exit(Fmt("Couldn't read GenesisDoc file: %v", err)) + cmn.Exit(cmn.Fmt("Couldn't read GenesisDoc file: %v", err)) } genDoc, err := types.GenesisDocFromJSON(genDocJSON) if err != nil { - Exit(Fmt("Error reading GenesisDoc: %v", err)) + cmn.Exit(cmn.Fmt("Error reading GenesisDoc: %v", err)) } return MakeGenesisState(db, genDoc) } @@ -231,7 +232,7 @@ func MakeGenesisStateFromFile(db dbm.DB, genDocFile string) *State { // Used in tests. func MakeGenesisState(db dbm.DB, genDoc *types.GenesisDoc) *State { if len(genDoc.Validators) == 0 { - Exit(Fmt("The genesis file has no validators")) + cmn.Exit(cmn.Fmt("The genesis file has no validators")) } if genDoc.GenesisTime.IsZero() { diff --git a/state/state_test.go b/state/state_test.go index dca83e80..f80c08fe 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" abci "github.com/tendermint/abci/types" "github.com/tendermint/go-crypto" - dbm "github.com/tendermint/go-db" + dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tendermint/config/tendermint_test" ) diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index 03acc8da..8f684c4a 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -4,7 +4,7 @@ import ( "bytes" "fmt" - db "github.com/tendermint/go-db" + db "github.com/tendermint/tmlibs/db" "github.com/tendermint/go-wire" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index 9a1898d7..8de9b8cd 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/abci/types" - db "github.com/tendermint/go-db" + db "github.com/tendermint/tmlibs/db" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" ) diff --git a/test/app/grpc_client.go b/test/app/grpc_client.go index e43b8ae3..7d55dcd7 100644 --- a/test/app/grpc_client.go +++ b/test/app/grpc_client.go @@ -8,7 +8,7 @@ import ( "golang.org/x/net/context" "github.com/tendermint/go-wire" - "github.com/tendermint/tendermint/rpc/grpc" + "github.com/tendermint/tendermint/rpc/tendermint/grpc" ) var grpcAddr = "tcp://localhost:36656" diff --git a/test/p2p/data/chain_config.json b/test/p2p/data/chain_config.json index 54fcea52..4221ba8d 100644 --- a/test/p2p/data/chain_config.json +++ b/test/p2p/data/chain_config.json @@ -5,10 +5,10 @@ { "validator": { "id": "mach1", - "pub_key": [ - 1, - "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" - ] + "pub_key": { + "type": "ed25519", + "data": "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" + } }, "p2p_addr": "", "rpc_addr": "" @@ -16,10 +16,10 @@ { "validator": { "id": "mach2", - "pub_key": [ - 1, - "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" - ] + "pub_key": { + "type": "ed25519", + "data": "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" + } }, "p2p_addr": "", "rpc_addr": "", @@ -28,10 +28,10 @@ { "validator": { "id": "mach3", - "pub_key": [ - 1, - "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" - ] + "pub_key": { + "type": "ed25519", + "data": "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" + } }, "p2p_addr": "", "rpc_addr": "", @@ -40,14 +40,14 @@ { "validator": { "id": "mach4", - "pub_key": [ - 1, - "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" - ] + "pub_key": { + "type": "ed25519", + "data": "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" + } }, "p2p_addr": "", "rpc_addr": "", "index": 3 } ] -} \ No newline at end of file +} diff --git a/test/p2p/data/mach1/core/genesis.json b/test/p2p/data/mach1/core/genesis.json index 3f6fbe5a..522f9831 100644 --- a/test/p2p/data/mach1/core/genesis.json +++ b/test/p2p/data/mach1/core/genesis.json @@ -6,34 +6,34 @@ { "amount": 1, "name": "mach1", - "pub_key": [ - 1, - "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" - ] + "pub_key": { + "type": "ed25519", + "data": "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" + } }, { "amount": 1, "name": "mach2", - "pub_key": [ - 1, - "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" - ] + "pub_key": { + "type": "ed25519", + "data": "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" + } }, { "amount": 1, "name": "mach3", - "pub_key": [ - 1, - "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" - ] + "pub_key": { + "type": "ed25519", + "data": "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" + } }, { "amount": 1, "name": "mach4", - "pub_key": [ - 1, - "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" - ] + "pub_key": { + "type": "ed25519", + "data": "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" + } } ] -} \ No newline at end of file +} diff --git a/test/p2p/data/mach1/core/priv_validator.json b/test/p2p/data/mach1/core/priv_validator.json index 242c7b9f..6538281b 100644 --- a/test/p2p/data/mach1/core/priv_validator.json +++ b/test/p2p/data/mach1/core/priv_validator.json @@ -3,12 +3,12 @@ "last_height": 0, "last_round": 0, "last_step": 0, - "priv_key": [ - 1, - "547AA07C7A8CE16C5CB2A40C6C26D15B0A32960410A9F1EA6E50B636F1AB389ABE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" - ], - "pub_key": [ - 1, - "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" - ] -} \ No newline at end of file + "priv_key": { + "type": "ed25519", + "data": "547AA07C7A8CE16C5CB2A40C6C26D15B0A32960410A9F1EA6E50B636F1AB389ABE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" + }, + "pub_key": { + "type": "ed25519", + "data": "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" + } +} diff --git a/test/p2p/data/mach2/core/genesis.json b/test/p2p/data/mach2/core/genesis.json index 3f6fbe5a..522f9831 100644 --- a/test/p2p/data/mach2/core/genesis.json +++ b/test/p2p/data/mach2/core/genesis.json @@ -6,34 +6,34 @@ { "amount": 1, "name": "mach1", - "pub_key": [ - 1, - "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" - ] + "pub_key": { + "type": "ed25519", + "data": "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" + } }, { "amount": 1, "name": "mach2", - "pub_key": [ - 1, - "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" - ] + "pub_key": { + "type": "ed25519", + "data": "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" + } }, { "amount": 1, "name": "mach3", - "pub_key": [ - 1, - "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" - ] + "pub_key": { + "type": "ed25519", + "data": "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" + } }, { "amount": 1, "name": "mach4", - "pub_key": [ - 1, - "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" - ] + "pub_key": { + "type": "ed25519", + "data": "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" + } } ] -} \ No newline at end of file +} diff --git a/test/p2p/data/mach2/core/priv_validator.json b/test/p2p/data/mach2/core/priv_validator.json index ead45d5e..3602454f 100644 --- a/test/p2p/data/mach2/core/priv_validator.json +++ b/test/p2p/data/mach2/core/priv_validator.json @@ -3,12 +3,12 @@ "last_height": 0, "last_round": 0, "last_step": 0, - "priv_key": [ - 1, - "D047889E60502FC3129D0AB7F334B1838ED9ED1ECD99CBB96B71AD5ABF5A81436DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" - ], - "pub_key": [ - 1, - "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" - ] -} \ No newline at end of file + "priv_key": { + "type": "ed25519", + "data": "D047889E60502FC3129D0AB7F334B1838ED9ED1ECD99CBB96B71AD5ABF5A81436DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" + }, + "pub_key": { + "type": "ed25519", + "data": "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" + } +} diff --git a/test/p2p/data/mach3/core/genesis.json b/test/p2p/data/mach3/core/genesis.json index 3f6fbe5a..522f9831 100644 --- a/test/p2p/data/mach3/core/genesis.json +++ b/test/p2p/data/mach3/core/genesis.json @@ -6,34 +6,34 @@ { "amount": 1, "name": "mach1", - "pub_key": [ - 1, - "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" - ] + "pub_key": { + "type": "ed25519", + "data": "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" + } }, { "amount": 1, "name": "mach2", - "pub_key": [ - 1, - "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" - ] + "pub_key": { + "type": "ed25519", + "data": "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" + } }, { "amount": 1, "name": "mach3", - "pub_key": [ - 1, - "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" - ] + "pub_key": { + "type": "ed25519", + "data": "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" + } }, { "amount": 1, "name": "mach4", - "pub_key": [ - 1, - "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" - ] + "pub_key": { + "type": "ed25519", + "data": "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" + } } ] -} \ No newline at end of file +} diff --git a/test/p2p/data/mach3/core/priv_validator.json b/test/p2p/data/mach3/core/priv_validator.json index dd366d20..743a931f 100644 --- a/test/p2p/data/mach3/core/priv_validator.json +++ b/test/p2p/data/mach3/core/priv_validator.json @@ -3,12 +3,12 @@ "last_height": 0, "last_round": 0, "last_step": 0, - "priv_key": [ - 1, - "C1A4E47F349FC5F556F4A9A27BA776B94424C312BAA6CF6EE44B867348D7C3F2AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" - ], - "pub_key": [ - 1, - "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" - ] -} \ No newline at end of file + "priv_key": { + "type": "ed25519", + "data": "C1A4E47F349FC5F556F4A9A27BA776B94424C312BAA6CF6EE44B867348D7C3F2AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" + }, + "pub_key": { + "type": "ed25519", + "data": "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" + } +} diff --git a/test/p2p/data/mach4/core/genesis.json b/test/p2p/data/mach4/core/genesis.json index 3f6fbe5a..522f9831 100644 --- a/test/p2p/data/mach4/core/genesis.json +++ b/test/p2p/data/mach4/core/genesis.json @@ -6,34 +6,34 @@ { "amount": 1, "name": "mach1", - "pub_key": [ - 1, - "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" - ] + "pub_key": { + "type": "ed25519", + "data": "BE8933DFF1600C026E34718F1785A4CDEAB90C35698B394E38B6947AE91DE116" + } }, { "amount": 1, "name": "mach2", - "pub_key": [ - 1, - "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" - ] + "pub_key": { + "type": "ed25519", + "data": "6DC534465323126587D2A2A93B59D689B717073B1DE968A25A6EF13D595318AD" + } }, { "amount": 1, "name": "mach3", - "pub_key": [ - 1, - "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" - ] + "pub_key": { + "type": "ed25519", + "data": "AE67AC697D135AA0B4601EA57EAAB3FEBF4BAA4F229C45A598C2985B12FCD1A1" + } }, { "amount": 1, "name": "mach4", - "pub_key": [ - 1, - "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" - ] + "pub_key": { + "type": "ed25519", + "data": "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" + } } ] -} \ No newline at end of file +} diff --git a/test/p2p/data/mach4/core/priv_validator.json b/test/p2p/data/mach4/core/priv_validator.json index 4a73707e..1c10eb8a 100644 --- a/test/p2p/data/mach4/core/priv_validator.json +++ b/test/p2p/data/mach4/core/priv_validator.json @@ -3,12 +3,12 @@ "last_height": 0, "last_round": 0, "last_step": 0, - "priv_key": [ - 1, - "C4CC3ED28F020C2DBDA98BCDBF08C3CED370470E74F25E938D5D295E8E3D2B0C9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" - ], - "pub_key": [ - 1, - "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" - ] -} \ No newline at end of file + "priv_key": { + "type": "ed25519", + "data": "C4CC3ED28F020C2DBDA98BCDBF08C3CED370470E74F25E938D5D295E8E3D2B0C9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" + }, + "pub_key": { + "type": "ed25519", + "data": "9EBC8F58CED4B46DCD5AB8ABA591DD253CD7CB5037273FDA32BC0B6461C4EFD9" + } +} diff --git a/test/test_libs.sh b/test/test_libs.sh index d08a4659..0a848534 100644 --- a/test/test_libs.sh +++ b/test/test_libs.sh @@ -12,23 +12,8 @@ fi # libs we depend on #################### -# some libs are tested with go, others with make -# TODO: should be all make (post repo merge) -LIBS_GO_TEST=(go-clist go-common go-config go-crypto go-db go-events go-merkle go-p2p) -LIBS_MAKE_TEST=(go-rpc go-wire abci) - -for lib in "${LIBS_GO_TEST[@]}"; do - - # checkout vendored version of lib - bash scripts/glide/checkout.sh "$GLIDE" "$lib" - - echo "Testing $lib ..." - go test -v --race "github.com/tendermint/$lib/..." - if [[ "$?" != 0 ]]; then - echo "FAIL" - exit 1 - fi -done +# All libs should define `make test` and `make get_vendor_deps` +LIBS_TEST=(tmlibs go-wire go-crypto abci) DIR=$(pwd) for lib in "${LIBS_MAKE_TEST[@]}"; do @@ -38,6 +23,7 @@ for lib in "${LIBS_MAKE_TEST[@]}"; do echo "Testing $lib ..." cd "$GOPATH/src/github.com/tendermint/$lib" + make get_vendor_deps make test if [[ "$?" != 0 ]]; then echo "FAIL" diff --git a/types/block.go b/types/block.go index 61d25f6e..88288a2b 100644 --- a/types/block.go +++ b/types/block.go @@ -8,8 +8,8 @@ import ( "strings" "time" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-merkle" + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/merkle" "github.com/tendermint/go-wire" ) diff --git a/types/events.go b/types/events.go index 11497904..17f6fc6e 100644 --- a/types/events.go +++ b/types/events.go @@ -3,8 +3,8 @@ package types import ( // for registering TMEventData as events.EventData abci "github.com/tendermint/abci/types" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-events" + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/events" "github.com/tendermint/go-wire" ) diff --git a/types/genesis.go b/types/genesis.go index 3a039548..6a500664 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -1,11 +1,11 @@ package types import ( + "encoding/json" "time" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" ) //------------------------------------------------------------ @@ -31,14 +31,18 @@ type GenesisDoc struct { // Utility method for saving GenensisDoc as JSON file. func (genDoc *GenesisDoc) SaveAs(file string) error { - genDocBytes := wire.JSONBytesPretty(genDoc) + genDocBytes, err := json.Marshal(genDoc) + if err != nil { + return err + } return WriteFile(file, genDocBytes, 0644) } //------------------------------------------------------------ // Make genesis state from file -func GenesisDocFromJSON(jsonBlob []byte) (genDoc *GenesisDoc, err error) { - wire.ReadJSONPtr(&genDoc, jsonBlob, &err) - return +func GenesisDocFromJSON(jsonBlob []byte) (*GenesisDoc, error) { + genDoc := GenesisDoc{} + err := json.Unmarshal(jsonBlob, &genDoc) + return &genDoc, err } diff --git a/types/log.go b/types/log.go index dbe8a678..5a4b363b 100644 --- a/types/log.go +++ b/types/log.go @@ -1,7 +1,7 @@ package types import ( - "github.com/tendermint/go-logger" + "github.com/tendermint/tmlibs/logger" ) var log = logger.New("module", "types") diff --git a/types/part_set.go b/types/part_set.go index 3a5ee26a..96907aa5 100644 --- a/types/part_set.go +++ b/types/part_set.go @@ -9,8 +9,8 @@ import ( "golang.org/x/crypto/ripemd160" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-merkle" + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/merkle" "github.com/tendermint/go-wire" ) diff --git a/types/part_set_test.go b/types/part_set_test.go index 6e25752d..7088ef31 100644 --- a/types/part_set_test.go +++ b/types/part_set_test.go @@ -5,7 +5,7 @@ import ( "io/ioutil" "testing" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) const ( diff --git a/types/priv_validator.go b/types/priv_validator.go index c200dff3..b73591dd 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -2,17 +2,16 @@ package types import ( "bytes" + "encoding/json" "errors" "fmt" "io/ioutil" "os" "sync" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" - - "github.com/tendermint/ed25519" + data "github.com/tendermint/go-wire/data" ) const ( @@ -35,13 +34,13 @@ func voteToStep(vote *Vote) int8 { } type PrivValidator struct { - Address []byte `json:"address"` + Address data.Bytes `json:"address"` PubKey crypto.PubKey `json:"pub_key"` LastHeight int `json:"last_height"` LastRound int `json:"last_round"` LastStep int8 `json:"last_step"` - LastSignature crypto.Signature `json:"last_signature"` // so we dont lose signatures - LastSignBytes []byte `json:"last_signbytes"` // so we dont lose signatures + LastSignature crypto.Signature `json:"last_signature,omitempty"` // so we dont lose signatures + LastSignBytes data.Bytes `json:"last_signbytes,omitempty"` // so we dont lose signatures // PrivKey should be empty if a Signer other than the default is being used. PrivKey crypto.PrivKey `json:"priv_key"` @@ -81,22 +80,15 @@ func (privVal *PrivValidator) SetSigner(s Signer) { // Generates a new validator with private key. func GenPrivValidator() *PrivValidator { - privKeyBytes := new([64]byte) - copy(privKeyBytes[:32], crypto.CRandBytes(32)) - pubKeyBytes := ed25519.MakePublicKey(privKeyBytes) - pubKey := crypto.PubKeyEd25519(*pubKeyBytes) - privKey := crypto.PrivKeyEd25519(*privKeyBytes) + privKey := crypto.GenPrivKeyEd25519().Wrap() + pubKey := privKey.PubKey() return &PrivValidator{ - Address: pubKey.Address(), - PubKey: pubKey, - PrivKey: privKey, - LastHeight: 0, - LastRound: 0, - LastStep: stepNone, - LastSignature: nil, - LastSignBytes: nil, - filePath: "", - Signer: NewDefaultSigner(privKey), + Address: pubKey.Address(), + PubKey: pubKey, + PrivKey: privKey, + LastStep: stepNone, + filePath: "", + Signer: NewDefaultSigner(privKey), } } @@ -105,13 +97,14 @@ func LoadPrivValidator(filePath string) *PrivValidator { if err != nil { Exit(err.Error()) } - privVal := wire.ReadJSON(&PrivValidator{}, privValJSONBytes, &err).(*PrivValidator) + privVal := PrivValidator{} + err = json.Unmarshal(privValJSONBytes, &privVal) if err != nil { Exit(Fmt("Error reading PrivValidator from %v: %v\n", filePath, err)) } privVal.filePath = filePath privVal.Signer = NewDefaultSigner(privVal.PrivKey) - return privVal + return &privVal } func LoadOrGenPrivValidator(filePath string) *PrivValidator { @@ -145,8 +138,12 @@ func (privVal *PrivValidator) save() { if privVal.filePath == "" { PanicSanity("Cannot save PrivValidator: filePath not set") } - jsonBytes := wire.JSONBytesPretty(privVal) - err := WriteFileAtomic(privVal.filePath, jsonBytes, 0600) + jsonBytes, err := json.Marshal(privVal) + if err != nil { + // `@; BOOM!!! + PanicCrisis(err) + } + err = WriteFileAtomic(privVal.filePath, jsonBytes, 0600) if err != nil { // `@; BOOM!!! PanicCrisis(err) @@ -158,7 +155,7 @@ func (privVal *PrivValidator) Reset() { privVal.LastHeight = 0 privVal.LastRound = 0 privVal.LastStep = 0 - privVal.LastSignature = nil + privVal.LastSignature = crypto.Signature{} privVal.LastSignBytes = nil privVal.Save() } @@ -191,23 +188,24 @@ func (privVal *PrivValidator) SignProposal(chainID string, proposal *Proposal) e // check if there's a regression. Else sign and write the hrs+signature to disk func (privVal *PrivValidator) signBytesHRS(height, round int, step int8, signBytes []byte) (crypto.Signature, error) { + sig := crypto.Signature{} // If height regression, err if privVal.LastHeight > height { - return nil, errors.New("Height regression") + return sig, errors.New("Height regression") } // More cases for when the height matches if privVal.LastHeight == height { // If round regression, err if privVal.LastRound > round { - return nil, errors.New("Round regression") + return sig, errors.New("Round regression") } // If step regression, err if privVal.LastRound == round { if privVal.LastStep > step { - return nil, errors.New("Step regression") + return sig, errors.New("Step regression") } else if privVal.LastStep == step { if privVal.LastSignBytes != nil { - if privVal.LastSignature == nil { + if privVal.LastSignature.Empty() { PanicSanity("privVal: LastSignature is nil but LastSignBytes is not!") } // so we dont sign a conflicting vote or proposal @@ -218,23 +216,23 @@ func (privVal *PrivValidator) signBytesHRS(height, round int, step int8, signByt return privVal.LastSignature, nil } } - return nil, errors.New("Step regression") + return sig, errors.New("Step regression") } } } // Sign - signature := privVal.Sign(signBytes) + sig = privVal.Sign(signBytes) // Persist height/round/step privVal.LastHeight = height privVal.LastRound = round privVal.LastStep = step - privVal.LastSignature = signature + privVal.LastSignature = sig privVal.LastSignBytes = signBytes privVal.save() - return signature, nil + return sig, nil } diff --git a/types/priv_validator_test.go b/types/priv_validator_test.go new file mode 100644 index 00000000..1eb0b57d --- /dev/null +++ b/types/priv_validator_test.go @@ -0,0 +1,60 @@ +package types + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" +) + +func TestLoadValidator(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // create some fixed values + addrStr := "D028C9981F7A87F3093672BF0D5B0E2A1B3ED456" + pubStr := "3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" + privStr := "27F82582AEFAE7AB151CFB01C48BB6C1A0DA78F9BDDA979A9F70A84D074EB07D3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" + addrBytes, _ := hex.DecodeString(addrStr) + pubBytes, _ := hex.DecodeString(pubStr) + privBytes, _ := hex.DecodeString(privStr) + + // prepend type byte + pubKey, err := crypto.PubKeyFromBytes(append([]byte{1}, pubBytes...)) + require.Nil(err, "%+v", err) + privKey, err := crypto.PrivKeyFromBytes(append([]byte{1}, privBytes...)) + require.Nil(err, "%+v", err) + + serialized := fmt.Sprintf(`{ + "address": "%s", + "pub_key": { + "type": "ed25519", + "data": "%s" + }, + "priv_key": { + "type": "ed25519", + "data": "%s" + }, + "last_height": 0, + "last_round": 0, + "last_step": 0, + "last_signature": null +}`, addrStr, pubStr, privStr) + + val := PrivValidator{} + err = json.Unmarshal([]byte(serialized), &val) + require.Nil(err, "%+v", err) + + // make sure the values match + assert.EqualValues(addrBytes, val.Address) + assert.EqualValues(pubKey, val.PubKey) + assert.EqualValues(privKey, val.PrivKey) + + // export it and make sure it is the same + out, err := json.Marshal(val) + require.Nil(err, "%+v", err) + assert.JSONEq(serialized, string(out)) +} diff --git a/types/proposal.go b/types/proposal.go index 9852011f..8406403c 100644 --- a/types/proposal.go +++ b/types/proposal.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - //. "github.com/tendermint/go-common" + //. "github.com/tendermint/tmlibs/common" "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" ) diff --git a/types/signable.go b/types/signable.go index df94e43b..13389fef 100644 --- a/types/signable.go +++ b/types/signable.go @@ -4,8 +4,8 @@ import ( "bytes" "io" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-merkle" + . "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/merkle" ) // Signable is an interface for all signable things. diff --git a/types/tx.go b/types/tx.go index df7f0e71..e62b5f66 100644 --- a/types/tx.go +++ b/types/tx.go @@ -5,7 +5,7 @@ import ( "errors" abci "github.com/tendermint/abci/types" - "github.com/tendermint/go-merkle" + "github.com/tendermint/tmlibs/merkle" ) type Tx []byte @@ -22,7 +22,7 @@ type Txs []Tx func (txs Txs) Hash() []byte { // Recursive impl. - // Copied from go-merkle to avoid allocations + // Copied from tmlibs/merkle to avoid allocations switch len(txs) { case 0: return nil diff --git a/types/tx_test.go b/types/tx_test.go index 7688a9bf..3ed59e3c 100644 --- a/types/tx_test.go +++ b/types/tx_test.go @@ -5,9 +5,9 @@ import ( "testing" "github.com/stretchr/testify/assert" - cmn "github.com/tendermint/go-common" - ctest "github.com/tendermint/go-common/test" wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + ctest "github.com/tendermint/tmlibs/test" ) func makeTxs(cnt, size int) Txs { diff --git a/types/validator.go b/types/validator.go index c4ecef56..2a8795bf 100644 --- a/types/validator.go +++ b/types/validator.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" ) diff --git a/types/validator_set.go b/types/validator_set.go index b997b471..f6a8faf4 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -6,8 +6,8 @@ import ( "sort" "strings" - cmn "github.com/tendermint/go-common" - "github.com/tendermint/go-merkle" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/merkle" "github.com/tendermint/go-wire" ) diff --git a/types/validator_set_test.go b/types/validator_set_test.go index bc7fef79..71a1993e 100644 --- a/types/validator_set_test.go +++ b/types/validator_set_test.go @@ -5,14 +5,14 @@ import ( "strings" "testing" - cmn "github.com/tendermint/go-common" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/go-crypto" ) -func randPubKey() crypto.PubKeyEd25519 { +func randPubKey() crypto.PubKey { var pubKey [32]byte copy(pubKey[:], cmn.RandBytes(32)) - return crypto.PubKeyEd25519(pubKey) + return crypto.PubKeyEd25519(pubKey).Wrap() } func randValidator_() *Validator { @@ -194,7 +194,7 @@ func BenchmarkValidatorSetCopy(b *testing.B) { vset := NewValidatorSet([]*Validator{}) for i := 0; i < 1000; i++ { privKey := crypto.GenPrivKeyEd25519() - pubKey := privKey.PubKey().(crypto.PubKeyEd25519) + pubKey := privKey.PubKey() val := NewValidator(pubKey, 0) if !vset.Add(val) { panic("Failed to add validator") diff --git a/types/vote.go b/types/vote.go index af4f60fc..2ad9df0a 100644 --- a/types/vote.go +++ b/types/vote.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" ) diff --git a/types/vote_set.go b/types/vote_set.go index de853a5e..938dbcb6 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -6,7 +6,7 @@ import ( "strings" "sync" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) /* diff --git a/types/vote_set_test.go b/types/vote_set_test.go index 500daadf..6ef07592 100644 --- a/types/vote_set_test.go +++ b/types/vote_set_test.go @@ -3,9 +3,9 @@ package types import ( "bytes" - . "github.com/tendermint/go-common" - . "github.com/tendermint/go-common/test" "github.com/tendermint/go-crypto" + . "github.com/tendermint/tmlibs/common" + . "github.com/tendermint/tmlibs/test" "testing" )