diff --git a/.github/workflows/janitor.yml b/.github/workflows/janitor.yml new file mode 100644 index 000000000..172ac07dd --- /dev/null +++ b/.github/workflows/janitor.yml @@ -0,0 +1,16 @@ +name: Janitor +# Janitor cleans up previous runs of various workflows +# To add more workflows to cancel visit https://api.github.com/repos/tendermint/tendermint/actions/workflows and find the actions name +on: + pull_request: + +jobs: + cancel: + name: "Cancel Previous Runs" + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - uses: styfle/cancel-workflow-action@0.8.0 + with: + workflow_id: 1041851,1401230,2837803 + access_token: ${{ github.token }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 8e4fcbdad..dc4263423 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -20,7 +20,7 @@ jobs: **/**.go go.mod go.sum - - uses: golangci/golangci-lint-action@v2.5.1 + - uses: golangci/golangci-lint-action@v2.5.2 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: v1.38 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index febdf33e9..5d978a3e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,14 +10,6 @@ on: - release/** jobs: - cleanup-runs: - runs-on: ubuntu-latest - steps: - - uses: rokroskar/workflow-run-cleanup-action@master - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/master'" - build: name: Build runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 40bfa28d8..6eb5b9675 100644 --- a/.gitignore +++ b/.gitignore @@ -35,10 +35,10 @@ shunit2 terraform.tfstate terraform.tfstate.backup terraform.tfstate.d +test/app/grpc_client test/e2e/build test/e2e/networks/*/ test/logs -test/maverick/maverick test/p2p/data/ vendor test/fuzz/**/corpus diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index da3fdd837..92af3dc30 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -15,6 +15,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi - [cli] \#5777 use hyphen-case instead of snake_case for all cli commands and config parameters (@cmwaters) - [rpc] \#6019 standardise RPC errors and return the correct status code (@bipulprasad & @cmwaters) - [rpc] \#6168 Change default sorting to desc for `/tx_search` results (@melekes) + - [cli] \#6282 User must specify the node mode when using `tendermint init` (@cmwaters) - Apps - [ABCI] \#5447 Remove `SetOption` method from `ABCI.Client` interface diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index acaa44867..c3cc05330 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -366,15 +366,6 @@ cd test/e2e && \ ./build/runner -f networks/ci.toml ``` -### Maverick - -**If you're changing the code in `consensus` package, please make sure to -replicate all the changes in `./test/maverick/consensus`**. Maverick is a -byzantine node used to assert that the validator gets punished for malicious -behavior. - -See [README](./test/maverick/README.md) for details. - ### Model-based tests (ADVANCED) *NOTE: if you're just submitting your first PR, you won't need to touch these diff --git a/DOCKER/README.md b/DOCKER/README.md index 2793fd0f6..2529f3282 100644 --- a/DOCKER/README.md +++ b/DOCKER/README.md @@ -32,7 +32,7 @@ A quick example of a built-in app and Tendermint core in one container. ```sh docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint init -docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint node --proxy-app=kvstore +docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint start --proxy-app=kvstore ``` ## Local cluster diff --git a/DOCKER/docker-entrypoint.sh b/DOCKER/docker-entrypoint.sh index d74511c17..e6442b485 100755 --- a/DOCKER/docker-entrypoint.sh +++ b/DOCKER/docker-entrypoint.sh @@ -3,7 +3,7 @@ set -e if [ ! -d "$TMHOME/config" ]; then echo "Running tendermint init to create (default) configuration for docker run." - tendermint init + tendermint init validator sed -i \ -e "s/^proxy-app\s*=.*/proxy-app = \"$PROXY_APP\"/" \ diff --git a/UPGRADING.md b/UPGRADING.md index ce8c2a2a6..e9170ffab 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -24,9 +24,11 @@ This guide provides instructions for upgrading to specific versions of Tendermin ### CLI Changes +* You must now specify the node mode (validator|full|seed) in `tendermint init [mode]` + * If you had previously used `tendermint gen_node_key` to generate a new node key, keep in mind that it no longer saves the output to a file. You can use - `tendermint init` or pipe the output of `tendermint gen_node_key` to + `tendermint init validator` or pipe the output of `tendermint gen_node_key` to `$TMHOME/config/node_key.json`: ``` diff --git a/abci/client/client.go b/abci/client/client.go index 4cc5dafff..bcba3bec3 100644 --- a/abci/client/client.go +++ b/abci/client/client.go @@ -87,7 +87,7 @@ type ReqRes struct { *sync.WaitGroup *types.Response // Not set atomically, so be sure to use WaitGroup. - mtx tmsync.Mutex + mtx tmsync.RWMutex done bool // Gets set to true once *after* WaitGroup.Done(). cb func(*types.Response) // A single callback that may be set. } @@ -137,16 +137,16 @@ func (r *ReqRes) InvokeCallback() { // // ref: https://github.com/tendermint/tendermint/issues/5439 func (r *ReqRes) GetCallback() func(*types.Response) { - r.mtx.Lock() - defer r.mtx.Unlock() + r.mtx.RLock() + defer r.mtx.RUnlock() return r.cb } // SetDone marks the ReqRes object as done. func (r *ReqRes) SetDone() { r.mtx.Lock() + defer r.mtx.Unlock() r.done = true - r.mtx.Unlock() } func waitGroup1() (wg *sync.WaitGroup) { diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index b10eebd9e..18d9cb168 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -24,7 +24,7 @@ type grpcClient struct { conn *grpc.ClientConn chReqRes chan *ReqRes // dispatches "async" responses to callbacks *in order*, needed by mempool - mtx tmsync.Mutex + mtx tmsync.RWMutex addr string err error resCb func(*types.Request, *types.Response) // listens to all callbacks @@ -149,8 +149,8 @@ func (cli *grpcClient) StopForError(err error) { } func (cli *grpcClient) Error() error { - cli.mtx.Lock() - defer cli.mtx.Unlock() + cli.mtx.RLock() + defer cli.mtx.RUnlock() return cli.err } @@ -158,8 +158,8 @@ func (cli *grpcClient) Error() error { // NOTE: callback may get internally generated flush responses. func (cli *grpcClient) SetResponseCallback(resCb Callback) { cli.mtx.Lock() + defer cli.mtx.Unlock() cli.resCb = resCb - cli.mtx.Unlock() } //---------------------------------------- diff --git a/abci/client/local_client.go b/abci/client/local_client.go index 92ca0804f..d25a98b4d 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -15,7 +15,7 @@ import ( type localClient struct { service.BaseService - mtx *tmsync.Mutex + mtx *tmsync.RWMutex types.Application Callback } @@ -26,22 +26,24 @@ var _ Client = (*localClient)(nil) // methods of the given app. // // Both Async and Sync methods ignore the given context.Context parameter. -func NewLocalClient(mtx *tmsync.Mutex, app types.Application) Client { +func NewLocalClient(mtx *tmsync.RWMutex, app types.Application) Client { if mtx == nil { - mtx = new(tmsync.Mutex) + mtx = &tmsync.RWMutex{} } + cli := &localClient{ mtx: mtx, Application: app, } + cli.BaseService = *service.NewBaseService(nil, "localClient", cli) return cli } func (app *localClient) SetResponseCallback(cb Callback) { app.mtx.Lock() + defer app.mtx.Unlock() app.Callback = cb - app.mtx.Unlock() } // TODO: change types.Application to include Error()? @@ -65,8 +67,8 @@ func (app *localClient) EchoAsync(ctx context.Context, msg string) (*ReqRes, err } func (app *localClient) InfoAsync(ctx context.Context, req types.RequestInfo) (*ReqRes, error) { - app.mtx.Lock() - defer app.mtx.Unlock() + app.mtx.RLock() + defer app.mtx.RUnlock() res := app.Application.Info(req) return app.callback( @@ -98,8 +100,8 @@ func (app *localClient) CheckTxAsync(ctx context.Context, req types.RequestCheck } func (app *localClient) QueryAsync(ctx context.Context, req types.RequestQuery) (*ReqRes, error) { - app.mtx.Lock() - defer app.mtx.Unlock() + app.mtx.RLock() + defer app.mtx.RUnlock() res := app.Application.Query(req) return app.callback( @@ -213,8 +215,8 @@ func (app *localClient) EchoSync(ctx context.Context, msg string) (*types.Respon } func (app *localClient) InfoSync(ctx context.Context, req types.RequestInfo) (*types.ResponseInfo, error) { - app.mtx.Lock() - defer app.mtx.Unlock() + app.mtx.RLock() + defer app.mtx.RUnlock() res := app.Application.Info(req) return &res, nil @@ -247,8 +249,8 @@ func (app *localClient) QuerySync( ctx context.Context, req types.RequestQuery, ) (*types.ResponseQuery, error) { - app.mtx.Lock() - defer app.mtx.Unlock() + app.mtx.RLock() + defer app.mtx.RUnlock() res := app.Application.Query(req) return &res, nil diff --git a/abci/client/socket_client.go b/abci/client/socket_client.go index 0569f4c6b..3e4034902 100644 --- a/abci/client/socket_client.go +++ b/abci/client/socket_client.go @@ -43,7 +43,7 @@ type socketClient struct { reqQueue chan *reqResWithContext flushTimer *timer.ThrottleTimer - mtx tmsync.Mutex + mtx tmsync.RWMutex err error reqSent *list.List // list of requests sent, waiting for response resCb func(*types.Request, *types.Response) // called on all requests, if set. @@ -108,8 +108,8 @@ func (cli *socketClient) OnStop() { // Error returns an error if the client was stopped abruptly. func (cli *socketClient) Error() error { - cli.mtx.Lock() - defer cli.mtx.Unlock() + cli.mtx.RLock() + defer cli.mtx.RUnlock() return cli.err } @@ -119,8 +119,8 @@ func (cli *socketClient) Error() error { // NOTE: callback may get internally generated flush responses. func (cli *socketClient) SetResponseCallback(resCb Callback) { cli.mtx.Lock() + defer cli.mtx.Unlock() cli.resCb = resCb - cli.mtx.Unlock() } //---------------------------------------- diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index d40983f85..0fcfcadf7 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -51,6 +51,10 @@ func NewPersistentKVStoreApplication(dbDir string) *PersistentKVStoreApplication } } +func (app *PersistentKVStoreApplication) Close() error { + return app.app.state.db.Close() +} + func (app *PersistentKVStoreApplication) SetLogger(l log.Logger) { app.logger = l } diff --git a/blockchain/v0/reactor_test.go b/blockchain/v0/reactor_test.go index a92d6fad1..64a66a6a0 100644 --- a/blockchain/v0/reactor_test.go +++ b/blockchain/v0/reactor_test.go @@ -152,7 +152,7 @@ func (rts *reactorTestSuite) addNode(t *testing.T, } rts.peerChans[nodeID] = make(chan p2p.PeerUpdate) - rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID]) + rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID], 1) rts.network.Nodes[nodeID].PeerManager.Register(rts.peerUpdates[nodeID]) rts.reactors[nodeID], err = NewReactor( rts.logger.With("nodeID", nodeID), diff --git a/cmd/tendermint/commands/init.go b/cmd/tendermint/commands/init.go index efe0f0244..99a59773c 100644 --- a/cmd/tendermint/commands/init.go +++ b/cmd/tendermint/commands/init.go @@ -2,6 +2,7 @@ package commands import ( "context" + "errors" "fmt" "github.com/spf13/cobra" @@ -17,9 +18,12 @@ import ( // InitFilesCmd initializes a fresh Tendermint Core instance. var InitFilesCmd = &cobra.Command{ - Use: "init", - Short: "Initialize Tendermint", - RunE: initFiles, + Use: "init [full|validator|seed]", + Short: "Initializes a Tendermint node", + ValidArgs: []string{"full", "validator", "seed"}, + // We allow for zero args so we can throw a more informative error + Args: cobra.MaximumNArgs(1), + RunE: initFiles, } var ( @@ -32,33 +36,40 @@ func init() { } func initFiles(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("must specify a node type: tendermint init [validator|full|seed]") + } + config.Mode = args[0] return initFilesWithConfig(config) } func initFilesWithConfig(config *cfg.Config) error { - // private validator - privValKeyFile := config.PrivValidatorKeyFile() - privValStateFile := config.PrivValidatorStateFile() var ( pv *privval.FilePV err error ) - if tmos.FileExists(privValKeyFile) { - pv, err = privval.LoadFilePV(privValKeyFile, privValStateFile) - if err != nil { - return err - } - logger.Info("Found private validator", "keyFile", privValKeyFile, - "stateFile", privValStateFile) - } else { - pv, err = privval.GenFilePV(privValKeyFile, privValStateFile, keyType) - if err != nil { - return err + if config.Mode == cfg.ModeValidator { + // private validator + privValKeyFile := config.PrivValidatorKeyFile() + privValStateFile := config.PrivValidatorStateFile() + if tmos.FileExists(privValKeyFile) { + pv, err = privval.LoadFilePV(privValKeyFile, privValStateFile) + if err != nil { + return err + } + + logger.Info("Found private validator", "keyFile", privValKeyFile, + "stateFile", privValStateFile) + } else { + pv, err = privval.GenFilePV(privValKeyFile, privValStateFile, keyType) + if err != nil { + return err + } + pv.Save() + logger.Info("Generated private validator", "keyFile", privValKeyFile, + "stateFile", privValStateFile) } - pv.Save() - logger.Info("Generated private validator", "keyFile", privValKeyFile, - "stateFile", privValStateFile) } nodeKeyFile := config.NodeKeyFile() @@ -91,15 +102,18 @@ func initFilesWithConfig(config *cfg.Config) error { ctx, cancel := context.WithTimeout(context.TODO(), ctxTimeout) defer cancel() - pubKey, err := pv.GetPubKey(ctx) - if err != nil { - return fmt.Errorf("can't get pubkey: %w", err) + // if this is a validator we add it to genesis + if pv != nil { + pubKey, err := pv.GetPubKey(ctx) + if err != nil { + return fmt.Errorf("can't get pubkey: %w", err) + } + genDoc.Validators = []types.GenesisValidator{{ + Address: pubKey.Address(), + PubKey: pubKey, + Power: 10, + }} } - genDoc.Validators = []types.GenesisValidator{{ - Address: pubKey.Address(), - PubKey: pubKey, - Power: 10, - }} if err := genDoc.SaveAs(genFile); err != nil { return err @@ -107,5 +121,9 @@ func initFilesWithConfig(config *cfg.Config) error { logger.Info("Generated genesis file", "path", genFile) } + // write config file + cfg.WriteConfigFile(config.RootDir, config) + logger.Info("Generated config", "mode", config.Mode) + return nil } diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index 6dcf17e1e..5d225413f 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -106,8 +106,7 @@ func testnetFiles(cmd *cobra.Command, args []string) error { } // set mode to validator for testnet - config := cfg.DefaultConfig() - config.Mode = cfg.ModeValidator + config := cfg.DefaultValidatorConfig() // overwrite default config if set and valid if configFile != "" { @@ -242,7 +241,7 @@ func testnetFiles(cmd *cobra.Command, args []string) error { } config.Moniker = moniker(i) - cfg.WriteConfigFile(filepath.Join(nodeDir, "config", "config.toml"), config) + cfg.WriteConfigFile(nodeDir, config) } fmt.Printf("Successfully initialized %v node directories\n", nValidators+nNonValidators) diff --git a/config/config.go b/config/config.go index 8e43e2bb5..79be27d11 100644 --- a/config/config.go +++ b/config/config.go @@ -93,6 +93,13 @@ func DefaultConfig() *Config { } } +// DefaultValidatorConfig returns default config with mode as validator +func DefaultValidatorConfig() *Config { + cfg := DefaultConfig() + cfg.Mode = ModeValidator + return cfg +} + // TestConfig returns a configuration that can be used for testing func TestConfig() *Config { return &Config{ @@ -167,13 +174,13 @@ type BaseConfig struct { //nolint: maligned // A custom human readable name for this node Moniker string `mapstructure:"moniker"` - // Mode of Node: full | validator | seed (default: "full") - // * full (default) - // - all reactors - // - No priv_validator_key.json, priv_validator_state.json + // Mode of Node: full | validator | seed // * validator // - all reactors // - with priv_validator_key.json, priv_validator_state.json + // * full + // - all reactors + // - No priv_validator_key.json, priv_validator_state.json // * seed // - only P2P, PEX Reactor // - No priv_validator_key.json, priv_validator_state.json @@ -346,6 +353,8 @@ func (cfg BaseConfig) ValidateBasic() error { } switch cfg.Mode { case ModeFull, ModeValidator, ModeSeed: + case "": + return errors.New("no mode has been set") default: return fmt.Errorf("unknown mode: %v", cfg.Mode) } diff --git a/config/toml.go b/config/toml.go index f98a35198..84d26935d 100644 --- a/config/toml.go +++ b/config/toml.go @@ -41,32 +41,29 @@ func EnsureRoot(rootDir string) { if err := tmos.EnsureDir(filepath.Join(rootDir, defaultDataDir), DefaultDirPerm); err != nil { panic(err.Error()) } - - configFilePath := filepath.Join(rootDir, defaultConfigFilePath) - - // Write default config file if missing. - if !tmos.FileExists(configFilePath) { - writeDefaultConfigFile(configFilePath) - } -} - -// XXX: this func should probably be called by cmd/tendermint/commands/init.go -// alongside the writing of the genesis.json and priv_validator.json -func writeDefaultConfigFile(configFilePath string) { - WriteConfigFile(configFilePath, DefaultConfig()) } // WriteConfigFile renders config using the template and writes it to configFilePath. -func WriteConfigFile(configFilePath string, config *Config) { +// This function is called by cmd/tendermint/commands/init.go +func WriteConfigFile(rootDir string, config *Config) { var buffer bytes.Buffer if err := configTemplate.Execute(&buffer, config); err != nil { panic(err) } + configFilePath := filepath.Join(rootDir, defaultConfigFilePath) + mustWriteFile(configFilePath, buffer.Bytes(), 0644) } +func writeDefaultConfigFileIfNone(rootDir string) { + configFilePath := filepath.Join(rootDir, defaultConfigFilePath) + if !tmos.FileExists(configFilePath) { + WriteConfigFile(rootDir, DefaultConfig()) + } +} + // Note: any changes to the comments/variables/mapstructure // must be reflected in the appropriate struct in config/config.go const defaultConfigTemplate = `# This is a TOML config file. @@ -88,14 +85,13 @@ proxy-app = "{{ .BaseConfig.ProxyApp }}" # A custom human readable name for this node moniker = "{{ .BaseConfig.Moniker }}" -# Mode of Node: full | validator | seed (default: "full") -# You will need to set it to "validator" if you want to run the node as a validator -# * full node (default) -# - all reactors -# - No priv_validator_key.json, priv_validator_state.json +# Mode of Node: full | validator | seed # * validator node # - all reactors # - with priv_validator_key.json, priv_validator_state.json +# * full node +# - all reactors +# - No priv_validator_key.json, priv_validator_state.json # * seed node # - only P2P, PEX Reactor # - No priv_validator_key.json, priv_validator_state.json @@ -502,15 +498,12 @@ func ResetTestRootWithChainID(testName string, chainID string) *Config { } baseConfig := DefaultBaseConfig() - configFilePath := filepath.Join(rootDir, defaultConfigFilePath) genesisFilePath := filepath.Join(rootDir, baseConfig.Genesis) privKeyFilePath := filepath.Join(rootDir, baseConfig.PrivValidatorKey) privStateFilePath := filepath.Join(rootDir, baseConfig.PrivValidatorState) // Write default config file if missing. - if !tmos.FileExists(configFilePath) { - writeDefaultConfigFile(configFilePath) - } + writeDefaultConfigFileIfNone(rootDir) if !tmos.FileExists(genesisFilePath) { if chainID == "" { chainID = "tendermint_test" diff --git a/config/toml_test.go b/config/toml_test.go index fa587d583..950d218ec 100644 --- a/config/toml_test.go +++ b/config/toml_test.go @@ -30,6 +30,8 @@ func TestEnsureRoot(t *testing.T) { // create root dir EnsureRoot(tmpDir) + WriteConfigFile(tmpDir, DefaultConfig()) + // make sure config is set properly data, err := ioutil.ReadFile(filepath.Join(tmpDir, defaultConfigFilePath)) require.Nil(err) diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index f0b65ee57..46b752d32 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -58,7 +58,7 @@ func TestByzantinePrevoteEquivocation(t *testing.T) { blockStore := store.NewBlockStore(blockDB) // one for mempool, one for consensus - mtx := new(tmsync.Mutex) + mtx := new(tmsync.RWMutex) proxyAppConnMem := abcicli.NewLocalClient(mtx, app) proxyAppConnCon := abcicli.NewLocalClient(mtx, app) diff --git a/consensus/common_test.go b/consensus/common_test.go index bd844982c..48a452de1 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "io/ioutil" "os" "path/filepath" @@ -392,7 +393,7 @@ func newStateWithConfigAndBlockStore( blockStore := store.NewBlockStore(blockDB) // one for mempool, one for consensus - mtx := new(tmsync.Mutex) + mtx := new(tmsync.RWMutex) proxyAppConnMem := abcicli.NewLocalClient(mtx, app) proxyAppConnCon := abcicli.NewLocalClient(mtx, app) @@ -704,7 +705,10 @@ func randConsensusState( css := make([]*State, nValidators) logger := consensusLogger() + closeFuncs := make([]func() error, 0, nValidators) + configRootDirs := make([]string, 0, nValidators) + for i := 0; i < nValidators; i++ { stateDB := dbm.NewMemDB() // each state needs its own db stateStore := sm.NewStore(stateDB) @@ -719,6 +723,11 @@ func randConsensusState( ensureDir(filepath.Dir(thisConfig.Consensus.WalFile()), 0700) // dir for wal app := appFunc() + + if appCloser, ok := app.(io.Closer); ok { + closeFuncs = append(closeFuncs, appCloser.Close) + } + vals := types.TM2PB.ValidatorUpdates(state.Validators) app.InitChain(abci.RequestInitChain{Validators: vals}) @@ -728,6 +737,9 @@ func randConsensusState( } return css, func() { + for _, closer := range closeFuncs { + _ = closer() + } for _, dir := range configRootDirs { os.RemoveAll(dir) } diff --git a/consensus/peer_state.go b/consensus/peer_state.go index 31406a025..1fb16d1be 100644 --- a/consensus/peer_state.go +++ b/consensus/peer_state.go @@ -89,7 +89,7 @@ func (ps *PeerState) GetRoundState() *cstypes.PeerRoundState { ps.mtx.Lock() defer ps.mtx.Unlock() - prs := ps.PRS // copy + prs := ps.PRS.Copy() return &prs } diff --git a/consensus/reactor.go b/consensus/reactor.go index 8185f4202..cf3f3e42d 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -1377,18 +1377,21 @@ func (r *Reactor) peerStatsRoutine() { switch msg.Msg.(type) { case *VoteMessage: - if numVotes := ps.RecordVote(); numVotes%votesToContributeToBecomeGoodPeer == 0 { // nolint: staticcheck - // TODO: Handle peer quality via the peer manager. - // r.Switch.MarkPeerAsGood(peer) + if numVotes := ps.RecordVote(); numVotes%votesToContributeToBecomeGoodPeer == 0 { + r.peerUpdates.SendUpdate(p2p.PeerUpdate{ + NodeID: msg.PeerID, + Status: p2p.PeerStatusGood, + }) } case *BlockPartMessage: - if numParts := ps.RecordBlockPart(); numParts%blocksToContributeToBecomeGoodPeer == 0 { // nolint: staticcheck - // TODO: Handle peer quality via the peer manager. - // r.Switch.MarkPeerAsGood(peer) + if numParts := ps.RecordBlockPart(); numParts%blocksToContributeToBecomeGoodPeer == 0 { + r.peerUpdates.SendUpdate(p2p.PeerUpdate{ + NodeID: msg.PeerID, + Status: p2p.PeerStatusGood, + }) } } - case <-r.closeCh: return } diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index a53561cff..3d14533c5 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -303,7 +303,7 @@ func TestReactorWithEvidence(t *testing.T) { blockStore := store.NewBlockStore(blockDB) // one for mempool, one for consensus - mtx := new(tmsync.Mutex) + mtx := new(tmsync.RWMutex) proxyAppConnMem := abcicli.NewLocalClient(mtx, app) proxyAppConnCon := abcicli.NewLocalClient(mtx, app) diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 4bf7466ab..2244d868e 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -308,7 +308,7 @@ func newConsensusStateForReplay(config cfg.BaseConfig, csConfig *cfg.ConsensusCo } // Create proxyAppConn connection (consensus, mempool, query) - clientCreator := proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()) + clientCreator, _ := proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()) proxyApp := proxy.NewAppConns(clientCreator) err = proxyApp.Start() if err != nil { diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 0a7430ab8..8175033ab 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -704,6 +704,7 @@ func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uin // make a new client creator kvstoreApp := kvstore.NewPersistentKVStoreApplication( filepath.Join(config.DBDir(), fmt.Sprintf("replay_test_%d_%d_a", nBlocks, mode))) + t.Cleanup(func() { require.NoError(t, kvstoreApp.Close()) }) clientCreator2 := proxy.NewLocalClientCreator(kvstoreApp) if nBlocks > 0 { @@ -835,9 +836,11 @@ func buildTMStateFromChain( nBlocks int, mode uint) sm.State { // run the whole chain against this client to build up the tendermint state - clientCreator := proxy.NewLocalClientCreator( - kvstore.NewPersistentKVStoreApplication( - filepath.Join(config.DBDir(), fmt.Sprintf("replay_test_%d_%d_t", nBlocks, mode)))) + kvstoreApp := kvstore.NewPersistentKVStoreApplication( + filepath.Join(config.DBDir(), fmt.Sprintf("replay_test_%d_%d_t", nBlocks, mode))) + defer kvstoreApp.Close() + clientCreator := proxy.NewLocalClientCreator(kvstoreApp) + proxyApp := proxy.NewAppConns(clientCreator) if err := proxyApp.Start(); err != nil { panic(err) diff --git a/consensus/types/peer_round_state.go b/consensus/types/peer_round_state.go index 07283c5b4..9d294d9af 100644 --- a/consensus/types/peer_round_state.go +++ b/consensus/types/peer_round_state.go @@ -46,6 +46,31 @@ func (prs PeerRoundState) String() string { return prs.StringIndented("") } +// Copy provides a deep copy operation. Because many of the fields in +// the PeerRound struct are pointers, we need an explicit deep copy +// operation to avoid a non-obvious shared data situation. +func (prs PeerRoundState) Copy() PeerRoundState { + // this works because it's not a pointer receiver so it's + // already, effectively a copy. + + headerHash := prs.ProposalBlockPartSetHeader.Hash.Bytes() + + hashCopy := make([]byte, len(headerHash)) + copy(hashCopy, headerHash) + prs.ProposalBlockPartSetHeader = types.PartSetHeader{ + Total: prs.ProposalBlockPartSetHeader.Total, + Hash: hashCopy, + } + prs.ProposalBlockParts = prs.ProposalBlockParts.Copy() + prs.ProposalPOL = prs.ProposalPOL.Copy() + prs.Prevotes = prs.Prevotes.Copy() + prs.Precommits = prs.Precommits.Copy() + prs.LastCommit = prs.LastCommit.Copy() + prs.CatchupCommit = prs.CatchupCommit.Copy() + + return prs +} + // StringIndented returns a string representation of the PeerRoundState func (prs PeerRoundState) StringIndented(indent string) string { return fmt.Sprintf(`PeerRoundState{ diff --git a/consensus/types/peer_round_state_test.go b/consensus/types/peer_round_state_test.go new file mode 100644 index 000000000..393fd2056 --- /dev/null +++ b/consensus/types/peer_round_state_test.go @@ -0,0 +1,29 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/bits" +) + +func TestCopy(t *testing.T) { + t.Run("VerifyShallowCopy", func(t *testing.T) { + prsOne := PeerRoundState{} + prsOne.Prevotes = bits.NewBitArray(12) + prsTwo := prsOne + + prsOne.Prevotes.SetIndex(1, true) + + require.Equal(t, prsOne.Prevotes, prsTwo.Prevotes) + }) + t.Run("DeepCopy", func(t *testing.T) { + prsOne := PeerRoundState{} + prsOne.Prevotes = bits.NewBitArray(12) + prsTwo := prsOne.Copy() + + prsOne.Prevotes.SetIndex(1, true) + + require.NotEqual(t, prsOne.Prevotes, prsTwo.Prevotes) + }) +} diff --git a/consensus/wal_generator.go b/consensus/wal_generator.go index e82bfe8a2..bbb31f5ad 100644 --- a/consensus/wal_generator.go +++ b/consensus/wal_generator.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" db "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/abci/example/kvstore" @@ -31,6 +32,7 @@ func WALGenerateNBlocks(t *testing.T, wr io.Writer, numBlocks int) (err error) { config := getConfig(t) app := kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), "wal_generator")) + t.Cleanup(func() { require.NoError(t, app.Close()) }) logger := log.TestingLogger().With("wal_generator", "wal_generator") logger.Info("generating WAL (last height msg excluded)", "numBlocks", numBlocks) diff --git a/docs/app-dev/getting-started.md b/docs/app-dev/getting-started.md index 9ba6bab26..7d261ae25 100644 --- a/docs/app-dev/getting-started.md +++ b/docs/app-dev/getting-started.md @@ -63,13 +63,13 @@ Tendermint binary installed. If not, follow the steps from before, use: ```sh -tendermint init -tendermint node +tendermint init validator +tendermint start ``` If you have used Tendermint, you may want to reset the data for a new blockchain by running `tendermint unsafe_reset_all`. Then you can run -`tendermint node` to start Tendermint, and connect to the app. For more +`tendermint start` to start Tendermint, and connect to the app. For more details, see [the guide on using Tendermint](../tendermint-core/using-tendermint.md). You should see Tendermint making blocks! We can get the status of our @@ -202,7 +202,7 @@ In another window, reset then start Tendermint: ```sh tendermint unsafe_reset_all -tendermint node +tendermint start ``` Once again, you can see the blocks streaming by. Let's send some @@ -277,7 +277,7 @@ In another window, reset and start `tendermint`: ```sh tendermint unsafe_reset_all -tendermint node +tendermint start ``` Once again, you should see blocks streaming by - but now, our diff --git a/docs/architecture/adr-008-priv-validator.md b/docs/architecture/adr-008-priv-validator.md index a8499465c..a3d31048a 100644 --- a/docs/architecture/adr-008-priv-validator.md +++ b/docs/architecture/adr-008-priv-validator.md @@ -4,7 +4,7 @@ Tendermint node's should support only two in-process PrivValidator implementations: - FilePV uses an unencrypted private key in a "priv_validator.json" file - no - configuration required (just `tendermint init`). + configuration required (just `tendermint init validator`). - TCPVal and IPCVal use TCP and Unix sockets respectively to send signing requests to another process - the user is responsible for starting that process themselves. diff --git a/docs/architecture/adr-052-tendermint-mode.md b/docs/architecture/adr-052-tendermint-mode.md index 344c68a5b..866f92278 100644 --- a/docs/architecture/adr-052-tendermint-mode.md +++ b/docs/architecture/adr-052-tendermint-mode.md @@ -4,6 +4,7 @@ * 27-11-2019: Initial draft from ADR-051 * 13-01-2020: Separate ADR Tendermint Mode from ADR-051 +* 29-03-2021: Update info regarding defaults ## Context @@ -16,7 +17,7 @@ We would like to suggest a simple Tendermint mode abstraction. These modes will live under one binary, and when initializing a node the user will be able to specify which node they would like to create. - Which reactor, component to include for each node - - full *(default)* + - full - switch, transport - reactors - mempool @@ -45,8 +46,9 @@ We would like to suggest a simple Tendermint mode abstraction. These modes will - Configuration, cli command - We would like to suggest by introducing `mode` parameter in `config.toml` and cli - `mode = "{{ .BaseConfig.Mode }}"` in `config.toml` - - `tendermint node --mode validator` in cli - - full | validator | seednode (default: "full") + - `tendermint start --mode validator` in cli + - full | validator | seednode + - There will be no default. Users will need to specify when they run `tendermint init` - RPC modification - `host:26657/status` - return empty `validator_info` when in full mode diff --git a/docs/introduction/install.md b/docs/introduction/install.md index a08182f51..42394b9d6 100644 --- a/docs/introduction/install.md +++ b/docs/introduction/install.md @@ -56,8 +56,8 @@ tendermint version To start a one-node blockchain with a simple in-process application: ```sh -tendermint init -tendermint node --proxy-app=kvstore +tendermint init validator +tendermint start --proxy-app=kvstore ``` ## Reinstall diff --git a/docs/introduction/quick-start.md b/docs/introduction/quick-start.md index 96d96a81c..bc6f36af0 100644 --- a/docs/introduction/quick-start.md +++ b/docs/introduction/quick-start.md @@ -34,7 +34,7 @@ For manual installation, see the [install instructions](install.md) Running: ```sh -tendermint init +tendermint init validator ``` will create the required files for a single, local node. @@ -59,7 +59,7 @@ Configuring a cluster is covered further below. Start Tendermint with a simple in-process application: ```sh -tendermint node --proxy-app=kvstore +tendermint start --proxy-app=kvstore ``` > Note: `kvstore` is a non persistent app, if you would like to run an application with persistence run `--proxy-app=persistent_kvstore` @@ -134,10 +134,10 @@ tendermint show_node_id --home ./mytestnet/node3 Finally, from each machine, run: ```sh -tendermint node --home ./mytestnet/node0 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" -tendermint node --home ./mytestnet/node1 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" -tendermint node --home ./mytestnet/node2 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" -tendermint node --home ./mytestnet/node3 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" +tendermint start --home ./mytestnet/node0 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" +tendermint start --home ./mytestnet/node1 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" +tendermint start --home ./mytestnet/node2 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" +tendermint start --home ./mytestnet/node3 --proxy-app=kvstore --p2p.persistent-peers="ID1@IP1:26656,ID2@IP2:26656,ID3@IP3:26656,ID4@IP4:26656" ``` Note that after the third node is started, blocks will start to stream in diff --git a/docs/introduction/what-is-tendermint.md b/docs/introduction/what-is-tendermint.md index 9cff48a2a..2386626ea 100644 --- a/docs/introduction/what-is-tendermint.md +++ b/docs/introduction/what-is-tendermint.md @@ -68,11 +68,11 @@ Tendermint is in essence similar software, but with two key differences: - It is Byzantine Fault Tolerant, meaning it can only tolerate up to a 1/3 of failures, but those failures can include arbitrary behaviour - - including hacking and malicious attacks. - It does not specify a - particular application, like a fancy key-value store. Instead, it - focuses on arbitrary state machine replication, so developers can build - the application logic that's right for them, from key-value store to - cryptocurrency to e-voting platform and beyond. + including hacking and malicious attacks. +- It does not specify a particular application, like a fancy key-value + store. Instead, it focuses on arbitrary state machine replication, + so developers can build the application logic that's right for them, + from key-value store to cryptocurrency to e-voting platform and beyond. ### Bitcoin, Ethereum, etc diff --git a/docs/nodes/configuration.md b/docs/nodes/configuration.md index eaf448c7e..35e639754 100644 --- a/docs/nodes/configuration.md +++ b/docs/nodes/configuration.md @@ -41,18 +41,17 @@ moniker = "anonymous" # and verifying their commits fast-sync = true -# Mode of Node: full | validator | seed -# You will need to set it to "validator" if you want to run the node as a validator -# * full node (default) -# - all reactors -# - No priv_validator_key.json, priv_validator_state.json -# * validator node +# Mode of Node: full | validator | seed (default: "validator") +# * validator node (default) # - all reactors # - with priv_validator_key.json, priv_validator_state.json +# * full node +# - all reactors +# - No priv_validator_key.json, priv_validator_state.json # * seed node # - only P2P, PEX Reactor # - No priv_validator_key.json, priv_validator_state.json -mode = "full" +mode = "validator" # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb # * goleveldb (github.com/syndtr/goleveldb - most popular implementation) diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 749c0dc4b..e6204c89e 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -21,7 +21,7 @@ this by setting the `TMHOME` environment variable. Initialize the root directory by running: ```sh -tendermint init +tendermint init validator ``` This will create a new private key (`priv_validator_key.json`), and a @@ -127,7 +127,7 @@ definition](https://github.com/tendermint/tendermint/blob/master/types/genesis.g To run a Tendermint node, use: ```bash -tendermint node +tendermint start ``` By default, Tendermint will try to connect to an ABCI application on @@ -136,7 +136,7 @@ another window. If you don't, kill Tendermint and run an in-process version of the `kvstore` app: ```bash -tendermint node --proxy-app=kvstore +tendermint start --proxy-app=kvstore ``` After a few seconds, you should see blocks start streaming in. Note that blocks @@ -150,10 +150,10 @@ Go, run it in another process, and use the `--proxy-app` flag to specify the address of the socket it is listening on, for instance: ```bash -tendermint node --proxy-app=/var/run/abci.sock +tendermint start --proxy-app=/var/run/abci.sock ``` -You can find out what flags are supported by running `tendermint node --help`. +You can find out what flags are supported by running `tendermint start --help`. ## Transactions @@ -270,7 +270,7 @@ transactions or the app hash changes, run Tendermint with this additional flag: ```sh -tendermint node --consensus.create_empty_blocks=false +tendermint start --consensus.create_empty_blocks=false ``` or set the configuration via the `config.toml` file: @@ -445,7 +445,7 @@ persistent connections with. For example, ```sh -tendermint node --p2p.seeds "f9baeaa15fedf5e1ef7448dd60f46c01f1a9e9c4@1.2.3.4:26656,0491d373a8e0fcf1023aaf18c51d6a1d0d4f31bd@5.6.7.8:26656" +tendermint start --p2p.seeds "f9baeaa15fedf5e1ef7448dd60f46c01f1a9e9c4@1.2.3.4:26656,0491d373a8e0fcf1023aaf18c51d6a1d0d4f31bd@5.6.7.8:26656" ``` Alternatively, you can use the `/dial_seeds` endpoint of the RPC to @@ -465,7 +465,7 @@ maintain a persistent connection with each, you can use the stopping Tendermint core instance. ```sh -tendermint node --p2p.persistent-peers "429fcf25974313b95673f58d77eacdd434402665@10.11.12.13:26656,96663a3dd0d7b9d17d4c8211b191af259621c693@10.11.12.14:26656" +tendermint start --p2p.persistent-peers "429fcf25974313b95673f58d77eacdd434402665@10.11.12.13:26656,96663a3dd0d7b9d17d4c8211b191af259621c693@10.11.12.14:26656" curl 'localhost:26657/dial_peers?persistent=true&peers=\["429fcf25974313b95673f58d77eacdd434402665@10.11.12.13:26656","96663a3dd0d7b9d17d4c8211b191af259621c693@10.11.12.14:26656"\]' ``` @@ -543,7 +543,7 @@ Update the `genesis.json` in `~/.tendermint/config`. Copy the genesis file and the new `priv_validator_key.json` to the `~/.tendermint/config` on a new machine. -Now run `tendermint node` on both machines, and use either +Now run `tendermint start` on both machines, and use either `--p2p.persistent-peers` or the `/dial_peers` to get them to peer up. They should start making blocks, and will only continue to do so as long as both of them are online. diff --git a/docs/tools/remote-signer-validation.md b/docs/tools/remote-signer-validation.md index 3099d7e4d..80a6a64bc 100644 --- a/docs/tools/remote-signer-validation.md +++ b/docs/tools/remote-signer-validation.md @@ -75,7 +75,7 @@ example, we will simply export a signing key from our local Tendermint instance. # Will generate all necessary Tendermint configuration files, including: # - ~/.tendermint/config/priv_validator_key.json # - ~/.tendermint/data/priv_validator_state.json -tendermint init +tendermint init validator # Extract the signing key from our local Tendermint instance tm-signer-harness extract_key \ # Use the "extract_key" command diff --git a/docs/tutorials/go-built-in.md b/docs/tutorials/go-built-in.md index f32a17eb7..040891637 100644 --- a/docs/tutorials/go-built-in.md +++ b/docs/tutorials/go-built-in.md @@ -393,7 +393,7 @@ func main() { func newTendermint(app abci.Application, configFile string) (*nm.Node, error) { // read config - config := cfg.DefaultConfig() + config := cfg.DefaultValidatorConfig() config.RootDir = filepath.Dir(filepath.Dir(configFile)) viper.SetConfigFile(configFile) if err := viper.ReadInConfig(); err != nil { @@ -503,7 +503,7 @@ of one communicating through a socket or gRPC. which we will generate later using the `tendermint init` command. ```go -config := cfg.DefaultConfig() +config := cfg.DefaultValidatorConfig() config.RootDir = filepath.Dir(filepath.Dir(configFile)) viper.SetConfigFile(configFile) if err := viper.ReadInConfig(); err != nil { @@ -604,7 +604,7 @@ go build ``` To create a default configuration, nodeKey and private validator files, let's -execute `tendermint init`. But before we do that, we will need to install +execute `tendermint init validator`. But before we do that, we will need to install Tendermint Core. Please refer to [the official guide](https://docs.tendermint.com/master/introduction/install.html). If you're installing from source, don't forget to checkout the latest release (`git @@ -613,11 +613,12 @@ major version. ```bash $ rm -rf /tmp/example -$ TMHOME="/tmp/example" tendermint init +$ TMHOME="/tmp/example" tendermint init validator I[2019-07-16|18:40:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json I[2019-07-16|18:40:36.481] Generated node key module=main path=/tmp/example/config/node_key.json I[2019-07-16|18:40:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +I[2019-07-16|18:40:36.483] Generated config module=main mode=validator ``` We are ready to start our application: diff --git a/docs/tutorials/go.md b/docs/tutorials/go.md index a01292112..02625291f 100644 --- a/docs/tutorials/go.md +++ b/docs/tutorials/go.md @@ -461,7 +461,7 @@ go build ``` To create a default configuration, nodeKey and private validator files, let's -execute `tendermint init`. But before we do that, we will need to install +execute `tendermint init validator`. But before we do that, we will need to install Tendermint Core. Please refer to [the official guide](https://docs.tendermint.com/master/introduction/install.html). If you're installing from source, don't forget to checkout the latest release (`git @@ -470,11 +470,12 @@ major version. ```bash rm -rf /tmp/example -TMHOME="/tmp/example" tendermint init +TMHOME="/tmp/example" tendermint init validator I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +I[2019-07-16|18:20:36.483] Generated config module=main mode=validator ``` Feel free to explore the generated files, which can be found at diff --git a/docs/tutorials/java.md b/docs/tutorials/java.md index f27ba1a9f..c36cafcf0 100644 --- a/docs/tutorials/java.md +++ b/docs/tutorials/java.md @@ -550,11 +550,12 @@ Tendermint Core. $ rm -rf /tmp/example $ cd $GOPATH/src/github.com/tendermint/tendermint $ make install -$ TMHOME="/tmp/example" tendermint init +$ TMHOME="/tmp/example" tendermint init validator I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +I[2019-07-16|18:20:36.483] Generated config module=main mode=validator ``` Feel free to explore the generated files, which can be found at diff --git a/docs/tutorials/kotlin.md b/docs/tutorials/kotlin.md index a46053427..6fa1d1894 100644 --- a/docs/tutorials/kotlin.md +++ b/docs/tutorials/kotlin.md @@ -517,18 +517,19 @@ class GrpcServer( ## 1.5 Getting Up and Running To create a default configuration, nodeKey and private validator files, let's -execute `tendermint init`. But before we do that, we will need to install +execute `tendermint init validator`. But before we do that, we will need to install Tendermint Core. ```bash rm -rf /tmp/example cd $GOPATH/src/github.com/tendermint/tendermint make install -TMHOME="/tmp/example" tendermint init +TMHOME="/tmp/example" tendermint init validator I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +I[2019-07-16|18:20:36.482] Generated config module=main mode=validator ``` Feel free to explore the generated files, which can be found at diff --git a/evidence/reactor_test.go b/evidence/reactor_test.go index 2170d7589..684a9f406 100644 --- a/evidence/reactor_test.go +++ b/evidence/reactor_test.go @@ -86,7 +86,7 @@ func setup(t *testing.T, stateStores []sm.Store, chBuf uint) *reactorTestSuite { require.NoError(t, err) rts.peerChans[nodeID] = make(chan p2p.PeerUpdate) - rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID]) + rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID], 1) rts.network.Nodes[nodeID].PeerManager.Register(rts.peerUpdates[nodeID]) rts.nodes = append(rts.nodes, rts.network.Nodes[nodeID]) diff --git a/go.mod b/go.mod index ae7e6738c..f29a4591d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 github.com/ChainSafe/go-schnorrkel v0.0.0-20210222182958-bd440c890782 - github.com/Workiva/go-datastructures v1.0.52 + github.com/Workiva/go-datastructures v1.0.53 github.com/btcsuite/btcd v0.21.0-beta github.com/btcsuite/btcutil v1.0.2 github.com/confio/ics23/go v0.6.3 @@ -14,7 +14,7 @@ require ( github.com/go-kit/kit v0.10.0 github.com/go-logfmt/logfmt v0.5.0 github.com/gogo/protobuf v1.3.2 - github.com/golang/protobuf v1.5.1 + github.com/golang/protobuf v1.5.2 github.com/google/orderedcode v0.0.1 github.com/gorilla/websocket v1.4.2 github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 @@ -22,7 +22,7 @@ require ( github.com/gtank/merlin v0.1.1 github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87 github.com/libp2p/go-buffer-pool v0.0.2 - github.com/minio/highwayhash v1.0.1 + github.com/minio/highwayhash v1.0.2 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.10.0 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 @@ -35,5 +35,5 @@ require ( github.com/tendermint/tm-db v0.6.4 golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 golang.org/x/net v0.0.0-20201021035429-f5854403a974 - google.golang.org/grpc v1.36.0 + google.golang.org/grpc v1.36.1 ) diff --git a/go.sum b/go.sum index 5d7a3346b..b5cd881cf 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,9 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/Workiva/go-datastructures v1.0.52 h1:PLSK6pwn8mYdaoaCZEMsXBpBotr4HHn9abU0yMQt0NI= github.com/Workiva/go-datastructures v1.0.52/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= +github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= +github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -187,8 +188,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1 h1:jAbXjIeW2ZSW2AwFxlGTDoc2CjI2XujLkV3ArsZFCvc= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -318,8 +319,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= -github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -377,6 +379,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -500,8 +503,10 @@ github.com/tendermint/tm-db v0.6.2/go.mod h1:GYtQ67SUvATOcoY8/+x6ylk8Qo02BQyLrAs github.com/tendermint/tm-db v0.6.3/go.mod h1:lfA1dL9/Y/Y8wwyPp2NMLyn5P5Ptr/gvDFNWtrCWSf8= github.com/tendermint/tm-db v0.6.4 h1:3N2jlnYQkXNQclQwd/eKV/NzlqPlfK21cpRRIx80XXQ= github.com/tendermint/tm-db v0.6.4/go.mod h1:dptYhIpJ2M5kUuenLr+Yyf3zQOv1SgBZcl8/BmWlMBw= +github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -685,6 +690,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -739,8 +745,8 @@ google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/libs/bits/bit_array.go b/libs/bits/bit_array.go index 1a41d87f9..3ebad38ce 100644 --- a/libs/bits/bit_array.go +++ b/libs/bits/bit_array.go @@ -422,21 +422,21 @@ func (bA *BitArray) UnmarshalJSON(bz []byte) error { // ToProto converts BitArray to protobuf. It returns nil if BitArray is // nil/empty. -// -// XXX: It does not copy the array. func (bA *BitArray) ToProto() *tmprotobits.BitArray { if bA == nil || (len(bA.Elems) == 0 && bA.Bits == 0) { // empty return nil } - return &tmprotobits.BitArray{Bits: int64(bA.Bits), Elems: bA.Elems} + bA.mtx.Lock() + defer bA.mtx.Unlock() + + bc := bA.copy() + return &tmprotobits.BitArray{Bits: int64(bc.Bits), Elems: bc.Elems} } // FromProto sets BitArray to the given protoBitArray. It returns an error if // protoBitArray is invalid. -// -// XXX: It does not copy the array. func (bA *BitArray) FromProto(protoBitArray *tmprotobits.BitArray) error { if protoBitArray == nil { return nil @@ -454,8 +454,14 @@ func (bA *BitArray) FromProto(protoBitArray *tmprotobits.BitArray) error { return fmt.Errorf("invalid number of Elems: got %d, but exp %d", got, exp) } + bA.mtx.Lock() + defer bA.mtx.Unlock() + + ec := make([]uint64, len(protoBitArray.Elems)) + copy(ec, protoBitArray.Elems) + bA.Bits = int(protoBitArray.Bits) - bA.Elems = protoBitArray.Elems + bA.Elems = ec return nil } diff --git a/libs/bits/bit_array_test.go b/libs/bits/bit_array_test.go index 10d607ef2..96f2e2257 100644 --- a/libs/bits/bit_array_test.go +++ b/libs/bits/bit_array_test.go @@ -299,7 +299,7 @@ func TestBitArrayFromProto(t *testing.T) { expErr bool }{ 0: {nil, &BitArray{}, false}, - 1: {&tmprotobits.BitArray{}, &BitArray{}, false}, + 1: {&tmprotobits.BitArray{}, &BitArray{Elems: []uint64{}}, false}, 2: {&tmprotobits.BitArray{Bits: 1, Elems: make([]uint64, 1)}, &BitArray{Bits: 1, Elems: make([]uint64, 1)}, false}, diff --git a/libs/clist/clist.go b/libs/clist/clist.go index 5579b1d0f..a32098774 100644 --- a/libs/clist/clist.go +++ b/libs/clist/clist.go @@ -229,18 +229,6 @@ type CList struct { maxLen int // max list length } -func (l *CList) Init() *CList { - l.mtx.Lock() - - l.wg = waitGroup1() - l.waitCh = make(chan struct{}) - l.head = nil - l.tail = nil - l.len = 0 - l.mtx.Unlock() - return l -} - // Return CList with MaxLength. CList will panic if it goes beyond MaxLength. func New() *CList { return newWithMax(MaxLength) } @@ -249,7 +237,14 @@ func New() *CList { return newWithMax(MaxLength) } func newWithMax(maxLength int) *CList { l := new(CList) l.maxLen = maxLength - return l.Init() + + l.wg = waitGroup1() + l.waitCh = make(chan struct{}) + l.head = nil + l.tail = nil + l.len = 0 + + return l } func (l *CList) Len() int { diff --git a/mempool/reactor_test.go b/mempool/reactor_test.go index d1d41209e..f9c609dac 100644 --- a/mempool/reactor_test.go +++ b/mempool/reactor_test.go @@ -60,13 +60,13 @@ func setup(t *testing.T, cfg *cfg.MempoolConfig, numNodes int, chBuf uint) *reac rts.mempools[nodeID] = mempool rts.peerChans[nodeID] = make(chan p2p.PeerUpdate) - rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID]) + rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID], 1) rts.network.Nodes[nodeID].PeerManager.Register(rts.peerUpdates[nodeID]) rts.reactors[nodeID] = NewReactor( rts.logger.With("nodeID", nodeID), cfg, - rts.network.RandomNode().PeerManager, + rts.network.Nodes[nodeID].PeerManager, mempool, rts.mempoolChnnels[nodeID], rts.peerUpdates[nodeID], diff --git a/node/node.go b/node/node.go index 2c3a713f0..477b4ca9a 100644 --- a/node/node.go +++ b/node/node.go @@ -126,10 +126,12 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { } else { pval = nil } + + appClient, _ := proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()) return NewNode(config, pval, nodeKey, - proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), + appClient, DefaultGenesisDocProviderFunc(config), DefaultDBProvider, DefaultMetricsProvider(config.Instrumentation), @@ -640,6 +642,7 @@ func createRouter( privKey crypto.PrivKey, peerManager *p2p.PeerManager, transport p2p.Transport, + options p2p.RouterOptions, ) (*p2p.Router, error) { return p2p.NewRouter( @@ -649,7 +652,7 @@ func createRouter( privKey, peerManager, []p2p.Transport{transport}, - p2p.RouterOptions{QueueType: p2pRouterQueueType}, + options, ) } @@ -915,7 +918,8 @@ func NewSeedNode(config *cfg.Config, return nil, fmt.Errorf("failed to create peer manager: %w", err) } - router, err := createRouter(p2pLogger, p2pMetrics, nodeInfo, nodeKey.PrivKey, peerManager, transport) + router, err := createRouter(p2pLogger, p2pMetrics, nodeInfo, nodeKey.PrivKey, + peerManager, transport, getRouterConfig(config, nil)) if err != nil { return nil, fmt.Errorf("failed to create router: %w", err) } @@ -1077,7 +1081,8 @@ func NewNode(config *cfg.Config, csMetrics, p2pMetrics, memplMetrics, smMetrics := metricsProvider(genDoc.ChainID) - router, err := createRouter(p2pLogger, p2pMetrics, nodeInfo, nodeKey.PrivKey, peerManager, transport) + router, err := createRouter(p2pLogger, p2pMetrics, nodeInfo, nodeKey.PrivKey, + peerManager, transport, getRouterConfig(config, proxyApp)) if err != nil { return nil, fmt.Errorf("failed to create router: %w", err) } @@ -1960,6 +1965,49 @@ func createAndStartPrivValidatorGRPCClient( return pvsc, nil } +func getRouterConfig(conf *cfg.Config, proxyApp proxy.AppConns) p2p.RouterOptions { + opts := p2p.RouterOptions{ + QueueType: p2pRouterQueueType, + } + + if conf.P2P.MaxNumInboundPeers > 0 { + opts.MaxIncommingConnectionsPerIP = uint(conf.P2P.MaxNumInboundPeers) + } + + if conf.FilterPeers && proxyApp != nil { + opts.FilterPeerByID = func(ctx context.Context, id p2p.NodeID) error { + res, err := proxyApp.Query().QuerySync(context.Background(), abci.RequestQuery{ + Path: fmt.Sprintf("/p2p/filter/id/%s", id), + }) + if err != nil { + return err + } + if res.IsErr() { + return fmt.Errorf("error querying abci app: %v", res) + } + + return nil + } + + opts.FilterPeerByIP = func(ctx context.Context, ip net.IP, port uint16) error { + res, err := proxyApp.Query().QuerySync(ctx, abci.RequestQuery{ + Path: fmt.Sprintf("/p2p/filter/addr/%s", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))), + }) + if err != nil { + return err + } + if res.IsErr() { + return fmt.Errorf("error querying abci app: %v", res) + } + + return nil + } + + } + + return opts +} + // FIXME: Temporary helper function, shims should be removed. func makeChannelsFromShims( router *p2p.Router, diff --git a/node/node_test.go b/node/node_test.go index 15554e001..b8b092be6 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -499,10 +499,12 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { pval, err := privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) require.NoError(t, err) + appClient, closer := proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()) + t.Cleanup(func() { closer.Close() }) n, err := NewNode(config, pval, nodeKey, - proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), + appClient, DefaultGenesisDocProviderFunc(config), DefaultDBProvider, DefaultMetricsProvider(config.Instrumentation), diff --git a/p2p/conn_tracker.go b/p2p/conn_tracker.go new file mode 100644 index 000000000..09673c093 --- /dev/null +++ b/p2p/conn_tracker.go @@ -0,0 +1,75 @@ +package p2p + +import ( + "fmt" + "net" + "sync" + "time" +) + +type connectionTracker interface { + AddConn(net.IP) error + RemoveConn(net.IP) + Len() int +} + +type connTrackerImpl struct { + cache map[string]uint + lastConnect map[string]time.Time + mutex sync.RWMutex + max uint + window time.Duration +} + +func newConnTracker(max uint, window time.Duration) connectionTracker { + return &connTrackerImpl{ + cache: make(map[string]uint), + lastConnect: make(map[string]time.Time), + max: max, + } +} + +func (rat *connTrackerImpl) Len() int { + rat.mutex.RLock() + defer rat.mutex.RUnlock() + return len(rat.cache) +} + +func (rat *connTrackerImpl) AddConn(addr net.IP) error { + address := addr.String() + rat.mutex.Lock() + defer rat.mutex.Unlock() + + if num := rat.cache[address]; num >= rat.max { + return fmt.Errorf("%q has %d connections [max=%d]", address, num, rat.max) + } else if num == 0 { + // if there is already at least connection, check to + // see if it was established before within the window, + // and error if so. + if last := rat.lastConnect[address]; time.Since(last) < rat.window { + return fmt.Errorf("%q tried to connect within window of last %s", address, rat.window) + } + } + + rat.cache[address]++ + rat.lastConnect[address] = time.Now() + + return nil +} + +func (rat *connTrackerImpl) RemoveConn(addr net.IP) { + address := addr.String() + rat.mutex.Lock() + defer rat.mutex.Unlock() + + if num := rat.cache[address]; num > 0 { + rat.cache[address]-- + } + if num := rat.cache[address]; num <= 0 { + delete(rat.cache, address) + } + + if last, ok := rat.lastConnect[address]; ok && time.Since(last) > rat.window { + delete(rat.lastConnect, address) + } +} diff --git a/p2p/conn_tracker_test.go b/p2p/conn_tracker_test.go new file mode 100644 index 000000000..66656e114 --- /dev/null +++ b/p2p/conn_tracker_test.go @@ -0,0 +1,73 @@ +package p2p + +import ( + "math" + "math/rand" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func randByte() byte { + return byte(rand.Intn(math.MaxUint8)) +} + +func randLocalIPv4() net.IP { + return net.IPv4(127, randByte(), randByte(), randByte()) +} + +func TestConnTracker(t *testing.T) { + for name, factory := range map[string]func() connectionTracker{ + "BaseSmall": func() connectionTracker { + return newConnTracker(10, time.Second) + }, + "BaseLarge": func() connectionTracker { + return newConnTracker(100, time.Hour) + }, + } { + t.Run(name, func(t *testing.T) { + factory := factory // nolint:scopelint + t.Run("Initialized", func(t *testing.T) { + ct := factory() + require.Equal(t, 0, ct.Len()) + }) + t.Run("RepeatedAdding", func(t *testing.T) { + ct := factory() + ip := randLocalIPv4() + require.NoError(t, ct.AddConn(ip)) + for i := 0; i < 100; i++ { + _ = ct.AddConn(ip) + } + require.Equal(t, 1, ct.Len()) + }) + t.Run("AddingMany", func(t *testing.T) { + ct := factory() + for i := 0; i < 100; i++ { + _ = ct.AddConn(randLocalIPv4()) + } + require.Equal(t, 100, ct.Len()) + }) + t.Run("Cycle", func(t *testing.T) { + ct := factory() + for i := 0; i < 100; i++ { + ip := randLocalIPv4() + require.NoError(t, ct.AddConn(ip)) + ct.RemoveConn(ip) + } + require.Equal(t, 0, ct.Len()) + }) + }) + } + t.Run("VeryShort", func(t *testing.T) { + ct := newConnTracker(10, time.Microsecond) + for i := 0; i < 10; i++ { + ip := randLocalIPv4() + require.NoError(t, ct.AddConn(ip)) + time.Sleep(2 * time.Microsecond) + require.NoError(t, ct.AddConn(ip)) + } + require.Equal(t, 10, ct.Len()) + }) +} diff --git a/p2p/peermanager.go b/p2p/peermanager.go index 3f80bf9d6..bbb1e31f9 100644 --- a/p2p/peermanager.go +++ b/p2p/peermanager.go @@ -33,13 +33,15 @@ type PeerStatus string const ( PeerStatusUp PeerStatus = "up" // connected and ready PeerStatusDown PeerStatus = "down" // disconnected + PeerStatusGood PeerStatus = "good" // peer observed as good + PeerStatusBad PeerStatus = "bad" // peer observed as bad ) // PeerScore is a numeric score assigned to a peer (higher is better). type PeerScore uint8 const ( - PeerScorePersistent PeerScore = 100 // persistent peers + PeerScorePersistent PeerScore = math.MaxUint8 // persistent peers ) // PeerUpdate is a peer update event sent via PeerUpdates. @@ -51,24 +53,35 @@ type PeerUpdate struct { // PeerUpdates is a peer update subscription with notifications about peer // events (currently just status changes). type PeerUpdates struct { - updatesCh chan PeerUpdate - closeCh chan struct{} - closeOnce sync.Once + routerUpdatesCh chan PeerUpdate + reactorUpdatesCh chan PeerUpdate + closeCh chan struct{} + closeOnce sync.Once } // NewPeerUpdates creates a new PeerUpdates subscription. It is primarily for // internal use, callers should typically use PeerManager.Subscribe(). The // subscriber must call Close() when done. -func NewPeerUpdates(updatesCh chan PeerUpdate) *PeerUpdates { +func NewPeerUpdates(updatesCh chan PeerUpdate, buf int) *PeerUpdates { return &PeerUpdates{ - updatesCh: updatesCh, - closeCh: make(chan struct{}), + reactorUpdatesCh: updatesCh, + routerUpdatesCh: make(chan PeerUpdate, buf), + closeCh: make(chan struct{}), } } // Updates returns a channel for consuming peer updates. func (pu *PeerUpdates) Updates() <-chan PeerUpdate { - return pu.updatesCh + return pu.reactorUpdatesCh +} + +// SendUpdate pushes information about a peer into the routing layer, +// presumably from a peer. +func (pu *PeerUpdates) SendUpdate(update PeerUpdate) { + select { + case <-pu.closeCh: + case pu.routerUpdatesCh <- update: + } } // Close closes the peer updates subscription. @@ -791,7 +804,7 @@ func (m *PeerManager) Subscribe() *PeerUpdates { // to the next subscriptions. This also prevents tail latencies from // compounding. Limiting it to 1 means that the subscribers are still // reasonably in sync. However, this should probably be benchmarked. - peerUpdates := NewPeerUpdates(make(chan PeerUpdate, 1)) + peerUpdates := NewPeerUpdates(make(chan PeerUpdate, 1), 1) m.Register(peerUpdates) return peerUpdates } @@ -809,6 +822,19 @@ func (m *PeerManager) Register(peerUpdates *PeerUpdates) { m.subscriptions[peerUpdates] = peerUpdates m.mtx.Unlock() + go func() { + for { + select { + case <-peerUpdates.closeCh: + return + case <-m.closeCh: + return + case pu := <-peerUpdates.routerUpdatesCh: + m.processPeerEvent(pu) + } + } + }() + go func() { select { case <-peerUpdates.Done(): @@ -820,6 +846,22 @@ func (m *PeerManager) Register(peerUpdates *PeerUpdates) { }() } +func (m *PeerManager) processPeerEvent(pu PeerUpdate) { + m.mtx.Lock() + defer m.mtx.Unlock() + + if _, ok := m.store.peers[pu.NodeID]; !ok { + m.store.peers[pu.NodeID] = &peerInfo{} + } + + switch pu.Status { + case PeerStatusBad: + m.store.peers[pu.NodeID].MutableScore-- + case PeerStatusGood: + m.store.peers[pu.NodeID].MutableScore++ + } +} + // broadcast broadcasts a peer update to all subscriptions. The caller must // already hold the mutex lock, to make sure updates are sent in the same order // as the PeerManager processes them, but this means subscribers must be @@ -837,7 +879,7 @@ func (m *PeerManager) broadcast(peerUpdate PeerUpdate) { default: } select { - case sub.updatesCh <- peerUpdate: + case sub.reactorUpdatesCh <- peerUpdate: case <-sub.closeCh: } } @@ -1149,6 +1191,8 @@ type peerInfo struct { Persistent bool Height int64 FixedScore PeerScore // mainly for tests + + MutableScore int64 // updated by router } // peerInfoFromProto converts a Protobuf PeerInfo message to a peerInfo, @@ -1205,14 +1249,22 @@ func (p *peerInfo) Copy() peerInfo { // Score calculates a score for the peer. Higher-scored peers will be // preferred over lower scores. func (p *peerInfo) Score() PeerScore { - var score PeerScore if p.FixedScore > 0 { return p.FixedScore } if p.Persistent { - score += PeerScorePersistent + return PeerScorePersistent } - return score + + if p.MutableScore <= 0 { + return 0 + } + + if p.MutableScore >= math.MaxUint8 { + return PeerScore(math.MaxUint8) + } + + return PeerScore(p.MutableScore) } // Validate validates the peer info. diff --git a/p2p/peermanager_test.go b/p2p/peermanager_test.go index d15d9f6d6..0c0e4b9bc 100644 --- a/p2p/peermanager_test.go +++ b/p2p/peermanager_test.go @@ -1615,3 +1615,39 @@ func TestPeerManager_SetHeight_GetHeight(t *testing.T) { require.Zero(t, peerManager.GetHeight(a.NodeID)) require.Zero(t, peerManager.GetHeight(b.NodeID)) } + +func TestPeerScoring(t *testing.T) { + // create a mock peer manager + db := dbm.NewMemDB() + peerManager, err := p2p.NewPeerManager(selfID, db, p2p.PeerManagerOptions{}) + require.NoError(t, err) + defer peerManager.Close() + + // create a fake node + id := p2p.NodeID(strings.Repeat("a1", 20)) + require.NoError(t, peerManager.Add(p2p.NodeAddress{NodeID: id, Protocol: "memory"})) + + // update the manager and make sure it's correct + pu := peerManager.Subscribe() + require.EqualValues(t, 0, peerManager.Scores()[id]) + + // add a bunch of good status updates and watch things increase. + for i := 1; i < 10; i++ { + pu.SendUpdate(p2p.PeerUpdate{ + NodeID: id, + Status: p2p.PeerStatusGood, + }) + time.Sleep(time.Millisecond) // force a context switch + require.EqualValues(t, i, peerManager.Scores()[id]) + } + + // watch the corresponding decreases respond to update + for i := 10; i == 0; i-- { + pu.SendUpdate(p2p.PeerUpdate{ + NodeID: id, + Status: p2p.PeerStatusBad, + }) + time.Sleep(time.Millisecond) // force a context switch + require.EqualValues(t, i, peerManager.Scores()[id]) + } +} diff --git a/p2p/router.go b/p2p/router.go index beb185a08..5c55164f3 100644 --- a/p2p/router.go +++ b/p2p/router.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "net" "sync" "time" @@ -130,6 +131,30 @@ type RouterOptions struct { // QueueType must be "wdrr" (Weighed Deficit Round Robin), // "priority", or FIFO. Defaults to FIFO. QueueType string + + // MaxIncommingConnectionsPerIP limits the number of incoming + // connections per IP address. Defaults to 100. + MaxIncommingConnectionsPerIP uint + + // IncomingConnectionWindow describes how often an IP address + // can attempt to create a new connection. Defaults to 10 + // milliseconds, and cannot be less than 1 millisecond. + IncomingConnectionWindow time.Duration + + // FilterPeerByIP is used by the router to inject filtering + // behavior for new incoming connections. The router passes + // the remote IP of the incoming connection the port number as + // arguments. Functions should return an error to reject the + // peer. + FilterPeerByIP func(context.Context, net.IP, uint16) error + + // FilterPeerByID is used by the router to inject filtering + // behavior for new incoming connections. The router passes + // the NodeID of the node before completing the connection, + // but this occurs after the handshake is complete. Filter by + // IP address to filter before the handshake. Functions should + // return an error to reject the peer. + FilterPeerByID func(context.Context, NodeID) error } const ( @@ -149,6 +174,18 @@ func (o *RouterOptions) Validate() error { return fmt.Errorf("queue type %q is not supported", o.QueueType) } + switch { + case o.IncomingConnectionWindow == 0: + o.IncomingConnectionWindow = 100 * time.Millisecond + case o.IncomingConnectionWindow < time.Millisecond: + return fmt.Errorf("incomming connection window must be grater than 1m [%s]", + o.IncomingConnectionWindow) + } + + if o.MaxIncommingConnectionsPerIP == 0 { + o.MaxIncommingConnectionsPerIP = 100 + } + return nil } @@ -202,6 +239,7 @@ type Router struct { peerManager *PeerManager chDescs []ChannelDescriptor transports []Transport + connTracker connectionTracker protocolTransports map[Protocol]Transport stopCh chan struct{} // signals Router shutdown @@ -235,10 +273,13 @@ func NewRouter( } router := &Router{ - logger: logger, - metrics: metrics, - nodeInfo: nodeInfo, - privKey: privKey, + logger: logger, + metrics: metrics, + nodeInfo: nodeInfo, + privKey: privKey, + connTracker: newConnTracker( + options.MaxIncommingConnectionsPerIP, + options.IncomingConnectionWindow), chDescs: make([]ChannelDescriptor, 0), transports: transports, protocolTransports: map[Protocol]Transport{}, @@ -446,29 +487,28 @@ func (r *Router) routeChannel( } } +func (r *Router) filterPeersIP(ctx context.Context, ip net.IP, port uint16) error { + if r.options.FilterPeerByIP == nil { + return nil + } + + return r.options.FilterPeerByIP(ctx, ip, port) +} + +func (r *Router) filterPeersID(ctx context.Context, id NodeID) error { + if r.options.FilterPeerByID == nil { + return nil + } + + return r.options.FilterPeerByID(ctx, id) +} + // acceptPeers accepts inbound connections from peers on the given transport, // and spawns goroutines that route messages to/from them. func (r *Router) acceptPeers(transport Transport) { r.logger.Debug("starting accept routine", "transport", transport) ctx := r.stopCtx() for { - // FIXME: We may need transports to enforce some sort of rate limiting - // here (e.g. by IP address), or alternatively have PeerManager.Accepted() - // do it for us. - // - // FIXME: Even though PeerManager enforces MaxConnected, we may want to - // limit the maximum number of active connections here too, since e.g. - // an adversary can open a ton of connections and then just hang during - // the handshake, taking up TCP socket descriptors. - // - // FIXME: The old P2P stack rejected multiple connections for the same IP - // unless P2PConfig.AllowDuplicateIP is true -- it's better to limit this - // by peer ID rather than IP address, so this hasn't been implemented and - // probably shouldn't (?). - // - // FIXME: The old P2P stack supported ABCI-based IP address filtering via - // /p2p/filter/addr/ queries, do we want to implement this here as well? - // Filtering by node ID is probably better. conn, err := transport.Accept() switch err { case nil: @@ -480,71 +520,102 @@ func (r *Router) acceptPeers(transport Transport) { return } + incomingIP := conn.RemoteEndpoint().IP + if err := r.connTracker.AddConn(incomingIP); err != nil { + closeErr := conn.Close() + r.logger.Debug("rate limiting incoming peer", + "err", err, + "ip", incomingIP.String(), + "closeErr", closeErr) + + return + } + // Spawn a goroutine for the handshake, to avoid head-of-line blocking. - go func() { - defer conn.Close() + go r.openConnection(ctx, conn) - // FIXME: The peer manager may reject the peer during Accepted() - // after we've handshaked with the peer (to find out which peer it - // is). However, because the handshake has no ack, the remote peer - // will think the handshake was successful and start sending us - // messages. - // - // This can cause problems in tests, where a disconnection can cause - // the local node to immediately redial, while the remote node may - // not have completed the disconnection yet and therefore reject the - // reconnection attempt (since it thinks we're still connected from - // before). - // - // The Router should do the handshake and have a final ack/fail - // message to make sure both ends have accepted the connection, such - // that it can be coordinated with the peer manager. - peerInfo, _, err := r.handshakePeer(ctx, conn, "") - switch { - case errors.Is(err, context.Canceled): - return - case err != nil: - r.logger.Error("peer handshake failed", "endpoint", conn, "err", err) - return - } - - if err := r.peerManager.Accepted(peerInfo.NodeID); err != nil { - r.logger.Error("failed to accept connection", "peer", peerInfo.NodeID, "err", err) - return - } - - r.metrics.Peers.Add(1) - - queue := r.queueFactory(queueBufferDefault) - - r.peerMtx.Lock() - r.peerQueues[peerInfo.NodeID] = queue - r.peerMtx.Unlock() - - defer func() { - r.peerMtx.Lock() - delete(r.peerQueues, peerInfo.NodeID) - r.peerMtx.Unlock() - - queue.close() - - if err := r.peerManager.Disconnected(peerInfo.NodeID); err != nil { - r.logger.Error("failed to disconnect peer", "peer", peerInfo.NodeID, "err", err) - } else { - r.metrics.Peers.Add(-1) - } - }() - - if err := r.peerManager.Ready(peerInfo.NodeID); err != nil { - r.logger.Error("failed to mark peer as ready", "peer", peerInfo.NodeID, "err", err) - return - } - - r.routePeer(peerInfo.NodeID, conn, queue) - }() } } +func (r *Router) openConnection(ctx context.Context, conn Connection) { + defer conn.Close() + defer r.connTracker.RemoveConn(conn.RemoteEndpoint().IP) + + re := conn.RemoteEndpoint() + incomingIP := re.IP + + if err := r.filterPeersIP(ctx, incomingIP, re.Port); err != nil { + r.logger.Debug("peer filtered by IP", + "ip", incomingIP.String(), + "err", err) + return + } + + // FIXME: The peer manager may reject the peer during Accepted() + // after we've handshaked with the peer (to find out which peer it + // is). However, because the handshake has no ack, the remote peer + // will think the handshake was successful and start sending us + // messages. + // + // This can cause problems in tests, where a disconnection can cause + // the local node to immediately redial, while the remote node may + // not have completed the disconnection yet and therefore reject the + // reconnection attempt (since it thinks we're still connected from + // before). + // + // The Router should do the handshake and have a final ack/fail + // message to make sure both ends have accepted the connection, such + // that it can be coordinated with the peer manager. + peerInfo, _, err := r.handshakePeer(ctx, conn, "") + switch { + case errors.Is(err, context.Canceled): + return + case err != nil: + r.logger.Error("peer handshake failed", "endpoint", conn, "err", err) + return + } + + if err := r.filterPeersID(ctx, peerInfo.NodeID); err != nil { + r.logger.Debug("peer filtered by node ID", + "node", peerInfo.NodeID, + "err", err) + return + } + + if err := r.peerManager.Accepted(peerInfo.NodeID); err != nil { + r.logger.Error("failed to accept connection", "peer", peerInfo.NodeID, "err", err) + return + } + + r.metrics.Peers.Add(1) + queue := r.queueFactory(queueBufferDefault) + + r.peerMtx.Lock() + r.peerQueues[peerInfo.NodeID] = queue + r.peerMtx.Unlock() + + defer func() { + r.peerMtx.Lock() + delete(r.peerQueues, peerInfo.NodeID) + r.peerMtx.Unlock() + + queue.close() + + if err := r.peerManager.Disconnected(peerInfo.NodeID); err != nil { + r.logger.Error("failed to disconnect peer", "peer", peerInfo.NodeID, "err", err) + } else { + r.metrics.Peers.Add(-1) + } + }() + + if err := r.peerManager.Ready(peerInfo.NodeID); err != nil { + r.logger.Error("failed to mark peer as ready", "peer", peerInfo.NodeID, "err", err) + return + } + + r.routePeer(peerInfo.NodeID, conn, queue) +} + // dialPeers maintains outbound connections to peers by dialing them. func (r *Router) dialPeers() { r.logger.Debug("starting dial routine") @@ -692,6 +763,7 @@ func (r *Router) handshakePeer(ctx context.Context, conn Connection, expectID No ctx, cancel = context.WithTimeout(ctx, r.options.HandshakeTimeout) defer cancel() } + peerInfo, peerKey, err := conn.Handshake(ctx, r.nodeInfo, r.privKey) if err != nil { return peerInfo, peerKey, err diff --git a/p2p/router_filter_test.go b/p2p/router_filter_test.go new file mode 100644 index 000000000..51c4947b4 --- /dev/null +++ b/p2p/router_filter_test.go @@ -0,0 +1,34 @@ +package p2p + +import ( + "context" + "errors" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/libs/sync" +) + +func TestConnectionFiltering(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + logger := log.TestingLogger() + + filterByIPCount := 0 + router := &Router{ + logger: logger, + connTracker: newConnTracker(1, time.Second), + options: RouterOptions{ + FilterPeerByIP: func(ctx context.Context, ip net.IP, port uint16) error { + filterByIPCount++ + return errors.New("mock") + }, + }, + } + require.Equal(t, 0, filterByIPCount) + router.openConnection(ctx, &MemoryConnection{logger: logger, closer: sync.NewCloser()}) + require.Equal(t, 1, filterByIPCount) +} diff --git a/p2p/router_test.go b/p2p/router_test.go index ac020a7d7..acc8fca05 100644 --- a/p2p/router_test.go +++ b/p2p/router_test.go @@ -97,6 +97,8 @@ func TestRouter_Channel(t *testing.T) { // Set up a router with no transports (so no peers). peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + router, err := p2p.NewRouter( log.TestingLogger(), p2p.NopMetrics(), @@ -334,7 +336,10 @@ func TestRouter_AcceptPeers(t *testing.T) { mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). Return(tc.peerInfo, tc.peerKey, nil) mockConnection.On("Close").Run(func(_ mock.Arguments) { closer.Close() }).Return(nil) + mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) if tc.ok { + // without the sleep after RequireUpdate this method isn't + // always called. Consider making this call optional. mockConnection.On("ReceiveMessage").Return(chID, nil, io.EOF) } @@ -348,6 +353,8 @@ func TestRouter_AcceptPeers(t *testing.T) { // Set up and start the router. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + sub := peerManager.Subscribe() defer sub.Close() @@ -368,6 +375,9 @@ func TestRouter_AcceptPeers(t *testing.T) { NodeID: tc.peerInfo.NodeID, Status: p2p.PeerStatusUp, }) + // force a context switch so that the + // connection is handled. + time.Sleep(time.Millisecond) sub.Close() } else { select { @@ -398,6 +408,8 @@ func TestRouter_AcceptPeers_Error(t *testing.T) { // Set up and start the router. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + router, err := p2p.NewRouter( log.TestingLogger(), p2p.NopMetrics(), @@ -430,6 +442,8 @@ func TestRouter_AcceptPeers_ErrorEOF(t *testing.T) { // Set up and start the router. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + router, err := p2p.NewRouter( log.TestingLogger(), p2p.NopMetrics(), @@ -462,6 +476,7 @@ func TestRouter_AcceptPeers_HeadOfLineBlocking(t *testing.T) { mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). WaitUntil(closeCh).Return(p2p.NodeInfo{}, nil, io.EOF) mockConnection.On("Close").Return(nil) + mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) mockTransport := &mocks.Transport{} mockTransport.On("String").Maybe().Return("mock") @@ -475,6 +490,8 @@ func TestRouter_AcceptPeers_HeadOfLineBlocking(t *testing.T) { // Set up and start the router. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + router, err := p2p.NewRouter( log.TestingLogger(), p2p.NopMetrics(), @@ -530,6 +547,8 @@ func TestRouter_DialPeers(t *testing.T) { mockConnection.On("Close").Run(func(_ mock.Arguments) { closer.Close() }).Return(nil) } if tc.ok { + // without the sleep after RequireUpdate this method isn't + // always called. Consider making this call optional. mockConnection.On("ReceiveMessage").Return(chID, nil, io.EOF) } @@ -552,6 +571,8 @@ func TestRouter_DialPeers(t *testing.T) { // Set up and start the router. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + require.NoError(t, peerManager.Add(address)) sub := peerManager.Subscribe() defer sub.Close() @@ -573,6 +594,9 @@ func TestRouter_DialPeers(t *testing.T) { NodeID: tc.peerInfo.NodeID, Status: p2p.PeerStatusUp, }) + // force a context switch so that the + // connection is handled. + time.Sleep(time.Millisecond) sub.Close() } else { select { @@ -622,6 +646,8 @@ func TestRouter_DialPeers_Parallel(t *testing.T) { // Set up and start the router. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + require.NoError(t, peerManager.Add(a)) require.NoError(t, peerManager.Add(b)) require.NoError(t, peerManager.Add(c)) @@ -661,6 +687,7 @@ func TestRouter_EvictPeers(t *testing.T) { mockConnection.On("Handshake", mock.Anything, selfInfo, selfKey). Return(peerInfo, peerKey.PubKey(), nil) mockConnection.On("ReceiveMessage").WaitUntil(closeCh).Return(chID, nil, io.EOF) + mockConnection.On("RemoteEndpoint").Return(p2p.Endpoint{}) mockConnection.On("Close").Run(func(_ mock.Arguments) { closeOnce.Do(func() { close(closeCh) @@ -677,6 +704,8 @@ func TestRouter_EvictPeers(t *testing.T) { // Set up and start the router. peerManager, err := p2p.NewPeerManager(selfID, dbm.NewMemDB(), p2p.PeerManagerOptions{}) require.NoError(t, err) + defer peerManager.Close() + sub := peerManager.Subscribe() defer sub.Close() diff --git a/p2p/shim.go b/p2p/shim.go index 5f20d06dd..5f5e8cede 100644 --- a/p2p/shim.go +++ b/p2p/shim.go @@ -64,7 +64,7 @@ func NewReactorShim(logger log.Logger, name string, descriptors map[ChannelID]*C rs := &ReactorShim{ Name: name, - PeerUpdates: NewPeerUpdates(make(chan PeerUpdate)), + PeerUpdates: NewPeerUpdates(make(chan PeerUpdate), 0), Channels: channels, } @@ -230,7 +230,7 @@ func (rs *ReactorShim) GetChannels() []*ChannelDescriptor { // handle adding a peer. func (rs *ReactorShim) AddPeer(peer Peer) { select { - case rs.PeerUpdates.updatesCh <- PeerUpdate{NodeID: peer.ID(), Status: PeerStatusUp}: + case rs.PeerUpdates.reactorUpdatesCh <- PeerUpdate{NodeID: peer.ID(), Status: PeerStatusUp}: rs.Logger.Debug("sent peer update", "reactor", rs.Name, "peer", peer.ID(), "status", PeerStatusUp) case <-rs.PeerUpdates.Done(): @@ -249,7 +249,7 @@ func (rs *ReactorShim) AddPeer(peer Peer) { // handle removing a peer. func (rs *ReactorShim) RemovePeer(peer Peer, reason interface{}) { select { - case rs.PeerUpdates.updatesCh <- PeerUpdate{NodeID: peer.ID(), Status: PeerStatusDown}: + case rs.PeerUpdates.reactorUpdatesCh <- PeerUpdate{NodeID: peer.ID(), Status: PeerStatusDown}: rs.Logger.Debug( "sent peer update", "reactor", rs.Name, diff --git a/proxy/client.go b/proxy/client.go index 1dc6d8853..f57c603ca 100644 --- a/proxy/client.go +++ b/proxy/client.go @@ -2,6 +2,7 @@ package proxy import ( "fmt" + "io" abcicli "github.com/tendermint/tendermint/abci/client" "github.com/tendermint/tendermint/abci/example/counter" @@ -20,7 +21,7 @@ type ClientCreator interface { // local proxy uses a mutex on an in-proc app type localClientCreator struct { - mtx *tmsync.Mutex + mtx *tmsync.RWMutex app types.Application } @@ -28,7 +29,7 @@ type localClientCreator struct { // which will be running locally. func NewLocalClientCreator(app types.Application) ClientCreator { return &localClientCreator{ - mtx: new(tmsync.Mutex), + mtx: new(tmsync.RWMutex), app: app, } } @@ -69,20 +70,28 @@ func (r *remoteClientCreator) NewABCIClient() (abcicli.Client, error) { // DefaultClientCreator returns a default ClientCreator, which will create a // local client if addr is one of: 'counter', 'counter_serial', 'kvstore', // 'persistent_kvstore' or 'noop', otherwise - a remote client. -func DefaultClientCreator(addr, transport, dbDir string) ClientCreator { +// +// The Closer is a noop except for persistent_kvstore applications, +// which will clean up the store. +func DefaultClientCreator(addr, transport, dbDir string) (ClientCreator, io.Closer) { switch addr { case "counter": - return NewLocalClientCreator(counter.NewApplication(false)) + return NewLocalClientCreator(counter.NewApplication(false)), noopCloser{} case "counter_serial": - return NewLocalClientCreator(counter.NewApplication(true)) + return NewLocalClientCreator(counter.NewApplication(true)), noopCloser{} case "kvstore": - return NewLocalClientCreator(kvstore.NewApplication()) + return NewLocalClientCreator(kvstore.NewApplication()), noopCloser{} case "persistent_kvstore": - return NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(dbDir)) + app := kvstore.NewPersistentKVStoreApplication(dbDir) + return NewLocalClientCreator(app), app case "noop": - return NewLocalClientCreator(types.NewBaseApplication()) + return NewLocalClientCreator(types.NewBaseApplication()), noopCloser{} default: mustConnect := false // loop retrying - return NewRemoteClientCreator(addr, transport, mustConnect) + return NewRemoteClientCreator(addr, transport, mustConnect), noopCloser{} } } + +type noopCloser struct{} + +func (noopCloser) Close() error { return nil } diff --git a/rpc/client/main_test.go b/rpc/client/main_test.go index c97311c81..cab8b7cdd 100644 --- a/rpc/client/main_test.go +++ b/rpc/client/main_test.go @@ -26,6 +26,7 @@ func TestMain(m *testing.M) { // and shut down proper at the end rpctest.StopTendermint(node) + app.Close() _ = os.RemoveAll(dir) os.Exit(code) } diff --git a/rpc/jsonrpc/client/http_json_client.go b/rpc/jsonrpc/client/http_json_client.go index 7f6a9a74a..295cdc24f 100644 --- a/rpc/jsonrpc/client/http_json_client.go +++ b/rpc/jsonrpc/client/http_json_client.go @@ -21,6 +21,7 @@ const ( protoWSS = "wss" protoWS = "ws" protoTCP = "tcp" + protoUNIX = "unix" ) //------------------------------------------------------------- @@ -28,6 +29,8 @@ const ( // Parsed URL structure type parsedURL struct { url.URL + + isUnixSocket bool } // Parse URL and set defaults @@ -42,7 +45,16 @@ func newParsedURL(remoteAddr string) (*parsedURL, error) { u.Scheme = protoTCP } - return &parsedURL{*u}, nil + pu := &parsedURL{ + URL: *u, + isUnixSocket: false, + } + + if u.Scheme == protoUNIX { + pu.isUnixSocket = true + } + + return pu, nil } // Change protocol to HTTP for unknown protocols and TCP protocol - useful for RPC connections @@ -65,10 +77,26 @@ func (u parsedURL) GetHostWithPath() string { // Get a trimmed address - useful for WS connections func (u parsedURL) GetTrimmedHostWithPath() string { - // replace / with . for http requests (kvstore domain) + // if it's not an unix socket we return the normal URL + if !u.isUnixSocket { + return u.GetHostWithPath() + } + // if it's a unix socket we replace the host slashes with a period + // this is because otherwise the http.Client would think that the + // domain is invalid. return strings.ReplaceAll(u.GetHostWithPath(), "/", ".") } +// GetDialAddress returns the endpoint to dial for the parsed URL +func (u parsedURL) GetDialAddress() string { + // if it's not a unix socket we return the host, example: localhost:443 + if !u.isUnixSocket { + return u.Host + } + // otherwise we return the path of the unix socket, ex /tmp/socket + return u.GetHostWithPath() +} + // Get a trimmed address with protocol - useful as address in RPC connections func (u parsedURL) GetTrimmedURL() string { return u.Scheme + "://" + u.GetTrimmedHostWithPath() @@ -350,7 +378,7 @@ func makeHTTPDialer(remoteAddr string) (func(string, string) (net.Conn, error), } dialFn := func(proto, addr string) (net.Conn, error) { - return net.Dial(protocol, u.GetHostWithPath()) + return net.Dial(protocol, u.GetDialAddress()) } return dialFn, nil diff --git a/rpc/jsonrpc/client/http_json_client_test.go b/rpc/jsonrpc/client/http_json_client_test.go index 5c8ef1a25..4b82ff1eb 100644 --- a/rpc/jsonrpc/client/http_json_client_test.go +++ b/rpc/jsonrpc/client/http_json_client_test.go @@ -34,3 +34,53 @@ func TestHTTPClientMakeHTTPDialer(t *testing.T) { require.NotNil(t, addr) } } + +func Test_parsedURL(t *testing.T) { + type test struct { + url string + expectedURL string + expectedHostWithPath string + expectedDialAddress string + } + + tests := map[string]test{ + "unix endpoint": { + url: "unix:///tmp/test", + expectedURL: "unix://.tmp.test", + expectedHostWithPath: "/tmp/test", + expectedDialAddress: "/tmp/test", + }, + + "http endpoint": { + url: "https://example.com", + expectedURL: "https://example.com", + expectedHostWithPath: "example.com", + expectedDialAddress: "example.com", + }, + + "http endpoint with port": { + url: "https://example.com:8080", + expectedURL: "https://example.com:8080", + expectedHostWithPath: "example.com:8080", + expectedDialAddress: "example.com:8080", + }, + + "http path routed endpoint": { + url: "https://example.com:8080/rpc", + expectedURL: "https://example.com:8080/rpc", + expectedHostWithPath: "example.com:8080/rpc", + expectedDialAddress: "example.com:8080", + }, + } + + for name, tt := range tests { + tt := tt // suppressing linter + t.Run(name, func(t *testing.T) { + parsed, err := newParsedURL(tt.url) + require.NoError(t, err) + require.Equal(t, tt.expectedDialAddress, parsed.GetDialAddress()) + require.Equal(t, tt.expectedURL, parsed.GetTrimmedURL()) + require.Equal(t, tt.expectedHostWithPath, parsed.GetHostWithPath()) + }) + } +} diff --git a/state/indexer/block/kv/kv.go b/state/indexer/block/kv/kv.go index 916c5f8ae..901f0e1d2 100644 --- a/state/indexer/block/kv/kv.go +++ b/state/indexer/block/kv/kv.go @@ -469,9 +469,10 @@ func (idx *BlockerIndexer) indexEvents(batch dbm.Batch, events []abci.Event, typ // index iff the event specified index:true and it's not a reserved event compositeKey := fmt.Sprintf("%s.%s", event.Type, string(attr.Key)) - if compositeKey == types.TxHashKey || compositeKey == types.TxHeightKey { + if compositeKey == types.BlockHeightKey { return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeKey) } + if attr.GetIndex() { key, err := eventKey(compositeKey, typ, string(attr.Value), height) if err != nil { diff --git a/state/txindex/indexer_service.go b/state/txindex/indexer_service.go index e72601252..9f2541b8b 100644 --- a/state/txindex/indexer_service.go +++ b/state/txindex/indexer_service.go @@ -79,7 +79,7 @@ func (is *IndexerService) OnStart() error { if err := is.blockIdxr.Index(eventDataHeader); err != nil { is.Logger.Error("failed to index block", "height", height, "err", err) } else { - is.Logger.Error("indexed block", "height", height) + is.Logger.Info("indexed block", "height", height) } if err = is.txIdxr.AddBatch(batch); err != nil { diff --git a/statesync/reactor_test.go b/statesync/reactor_test.go index 0760c1e54..14ffa9509 100644 --- a/statesync/reactor_test.go +++ b/statesync/reactor_test.go @@ -62,7 +62,7 @@ func setup( chunkInCh: make(chan p2p.Envelope, chBuf), chunkOutCh: make(chan p2p.Envelope, chBuf), chunkPeerErrCh: make(chan p2p.PeerError, chBuf), - peerUpdates: p2p.NewPeerUpdates(make(chan p2p.PeerUpdate)), + peerUpdates: p2p.NewPeerUpdates(make(chan p2p.PeerUpdate), int(chBuf)), conn: conn, connQuery: connQuery, stateProvider: stateProvider, diff --git a/test/app/test.sh b/test/app/test.sh index d415bc10e..67fb2d9b6 100755 --- a/test/app/test.sh +++ b/test/app/test.sh @@ -13,7 +13,7 @@ export TMHOME=$HOME/.tendermint_app function kvstore_over_socket(){ rm -rf $TMHOME - tendermint init + tendermint init validator echo "Starting kvstore_over_socket" abci-cli kvstore > /dev/null & pid_kvstore=$! @@ -30,7 +30,7 @@ function kvstore_over_socket(){ # start tendermint first function kvstore_over_socket_reorder(){ rm -rf $TMHOME - tendermint init + tendermint init validator echo "Starting kvstore_over_socket_reorder (ie. start tendermint first)" tendermint start --mode validator > tendermint.log & pid_tendermint=$! @@ -48,7 +48,7 @@ function kvstore_over_socket_reorder(){ function counter_over_socket() { rm -rf $TMHOME - tendermint init + tendermint init validator echo "Starting counter_over_socket" abci-cli counter --serial > /dev/null & pid_counter=$! @@ -64,7 +64,7 @@ function counter_over_socket() { function counter_over_grpc() { rm -rf $TMHOME - tendermint init + tendermint init validator echo "Starting counter_over_grpc" abci-cli counter --serial --abci grpc > /dev/null & pid_counter=$! @@ -80,7 +80,7 @@ function counter_over_grpc() { function counter_over_grpc_grpc() { rm -rf $TMHOME - tendermint init + tendermint init validator echo "Starting counter_over_grpc_grpc (ie. with grpc broadcast_tx)" abci-cli counter --serial --abci grpc > /dev/null & pid_counter=$! diff --git a/test/e2e/Makefile b/test/e2e/Makefile index c9eb8bc19..38ce809e6 100644 --- a/test/e2e/Makefile +++ b/test/e2e/Makefile @@ -8,11 +8,6 @@ docker: # ABCI testing). app: go build -o build/app -tags badgerdb,boltdb,cleveldb,rocksdb ./app - -# To be used primarily by the e2e docker instance. If you want to produce this binary -# elsewhere, then run go build in the maverick directory. -maverick: - go build -o build/maverick -tags badgerdb,boltdb,cleveldb,rocksdb ../maverick generator: go build -o build/generator ./generator @@ -20,4 +15,4 @@ generator: runner: go build -o build/runner ./runner -.PHONY: all app docker generator maverick runner +.PHONY: all app docker generator runner diff --git a/test/e2e/app/main.go b/test/e2e/app/main.go index 5141750ed..cb49b8303 100644 --- a/test/e2e/app/main.go +++ b/test/e2e/app/main.go @@ -83,10 +83,6 @@ func run(configFile string) error { default: err = startNode(cfg) } - // FIXME: Temporarily remove maverick until it is redesigned - // if len(cfg.Misbehaviors) == 0 { - // err = startMaverick(cfg) - // } default: err = fmt.Errorf("invalid protocol %q", cfg.Protocol) } @@ -227,43 +223,6 @@ func startLightNode(cfg *Config) error { return nil } -// FIXME: Temporarily disconnected maverick until it is redesigned -// startMaverick starts a Maverick node that runs the application directly. It assumes the Tendermint -// configuration is in $TMHOME/config/tendermint.toml. -// func startMaverick(cfg *Config) error { -// app, err := NewApplication(cfg) -// if err != nil { -// return err -// } - -// tmcfg, logger, nodeKey, err := setupNode() -// if err != nil { -// return fmt.Errorf("failed to setup config: %w", err) -// } - -// misbehaviors := make(map[int64]mcs.Misbehavior, len(cfg.Misbehaviors)) -// for heightString, misbehaviorString := range cfg.Misbehaviors { -// height, _ := strconv.ParseInt(heightString, 10, 64) -// misbehaviors[height] = mcs.MisbehaviorList[misbehaviorString] -// } - -// n, err := maverick.NewNode(tmcfg, -// maverick.LoadOrGenFilePV(tmcfg.PrivValidatorKeyFile(), tmcfg.PrivValidatorStateFile()), -// *nodeKey, -// proxy.NewLocalClientCreator(app), -// maverick.DefaultGenesisDocProviderFunc(tmcfg), -// maverick.DefaultDBProvider, -// maverick.DefaultMetricsProvider(tmcfg.Instrumentation), -// logger, -// misbehaviors, -// ) -// if err != nil { -// return err -// } - -// return n.Start() -// } - // startSigner starts a signer server connecting to the given endpoint. func startSigner(cfg *Config) error { filePV, err := privval.LoadFilePV(cfg.PrivValKey, cfg.PrivValState) diff --git a/test/e2e/docker/Dockerfile b/test/e2e/docker/Dockerfile index d0f9cd7a2..8c76f1d5e 100644 --- a/test/e2e/docker/Dockerfile +++ b/test/e2e/docker/Dockerfile @@ -19,8 +19,6 @@ COPY . . RUN make build && cp build/tendermint /usr/bin/tendermint COPY test/e2e/docker/entrypoint* /usr/bin/ -# FIXME: Temporarily disconnect maverick node until it is redesigned -# RUN cd test/e2e && make maverick && cp build/maverick /usr/bin/maverick RUN cd test/e2e && make app && cp build/app /usr/bin/app # Set up runtime directory. We don't use a separate runtime image since we need diff --git a/test/e2e/docker/entrypoint-maverick b/test/e2e/docker/entrypoint-maverick deleted file mode 100755 index 9469e2447..000000000 --- a/test/e2e/docker/entrypoint-maverick +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -# Forcibly remove any stray UNIX sockets left behind from previous runs -rm -rf /var/run/privval.sock /var/run/app.sock - -/usr/bin/app /tendermint/config/app.toml & - -sleep 1 - -/usr/bin/maverick "$@" diff --git a/test/e2e/generator/generate.go b/test/e2e/generator/generate.go index 29614fa3c..f71e71856 100644 --- a/test/e2e/generator/generate.go +++ b/test/e2e/generator/generate.go @@ -4,7 +4,6 @@ import ( "fmt" "math/rand" "sort" - "strconv" "strings" e2e "github.com/tendermint/tendermint/test/e2e/pkg" @@ -36,20 +35,14 @@ var ( nodeStateSyncs = uniformChoice{false, true} nodePersistIntervals = uniformChoice{0, 1, 5} nodeSnapshotIntervals = uniformChoice{0, 3} - nodeRetainBlocks = uniformChoice{0, 1, 5} + nodeRetainBlocks = uniformChoice{0, int(e2e.EvidenceAgeHeight), int(e2e.EvidenceAgeHeight) + 5} nodePerturbations = probSetChoice{ "disconnect": 0.1, "pause": 0.1, "kill": 0.1, "restart": 0.1, } - nodeMisbehaviors = weightedChoice{ - // FIXME: evidence disabled due to node panicing when not - // having sufficient block history to process evidence. - // https://github.com/tendermint/tendermint/issues/5617 - // misbehaviorOption{"double-prevote"}: 1, - misbehaviorOption{}: 9, - } + evidence = uniformChoice{0, 1, 10} ) // Generate generates random testnets using the given RNG. @@ -75,6 +68,7 @@ func generateTestnet(r *rand.Rand, opt map[string]interface{}) (e2e.Manifest, er ValidatorUpdates: map[string]map[string]int64{}, Nodes: map[string]*e2e.ManifestNode{}, KeyType: opt["keyType"].(string), + Evidence: evidence.Choose(r).(int), } var numSeeds, numValidators, numFulls, numLightClients int @@ -227,17 +221,6 @@ func generateNode( node.SnapshotInterval = 3 } - if node.Mode == string(e2e.ModeValidator) { - misbehaveAt := startAt + 5 + int64(r.Intn(10)) - if startAt == 0 { - misbehaveAt += initialHeight - 1 - } - node.Misbehaviors = nodeMisbehaviors.Choose(r).(misbehaviorOption).atHeight(misbehaveAt) - if len(node.Misbehaviors) != 0 { - node.PrivvalProtocol = "file" - } - } - // If a node which does not persist state also does not retain blocks, randomly // choose to either persist state or retain all blocks. if node.PersistInterval != nil && *node.PersistInterval == 0 && node.RetainBlocks > 0 { @@ -276,16 +259,3 @@ func generateLightNode(r *rand.Rand, startAt int64, providers []string) *e2e.Man func ptrUint64(i uint64) *uint64 { return &i } - -type misbehaviorOption struct { - misbehavior string -} - -func (m misbehaviorOption) atHeight(height int64) map[string]string { - misbehaviorMap := make(map[string]string) - if m.misbehavior == "" { - return misbehaviorMap - } - misbehaviorMap[strconv.Itoa(int(height))] = m.misbehavior - return misbehaviorMap -} diff --git a/test/e2e/generator/random.go b/test/e2e/generator/random.go index ec59a01b2..f21502118 100644 --- a/test/e2e/generator/random.go +++ b/test/e2e/generator/random.go @@ -56,28 +56,6 @@ func (uc uniformChoice) Choose(r *rand.Rand) interface{} { return uc[r.Intn(len(uc))] } -// weightedChoice chooses a single random key from a map of keys and weights. -type weightedChoice map[interface{}]uint - -func (wc weightedChoice) Choose(r *rand.Rand) interface{} { - total := 0 - choices := make([]interface{}, 0, len(wc)) - for choice, weight := range wc { - total += int(weight) - choices = append(choices, choice) - } - - rem := r.Intn(total) - for _, choice := range choices { - rem -= int(wc[choice]) - if rem <= 0 { - return choice - } - } - - return nil -} - // probSetChoice picks a set of strings based on each string's probability (0-1). type probSetChoice map[string]float64 diff --git a/test/e2e/networks/ci.toml b/test/e2e/networks/ci.toml index 879d0c22f..05875dace 100644 --- a/test/e2e/networks/ci.toml +++ b/test/e2e/networks/ci.toml @@ -2,6 +2,7 @@ # functionality with a single network. initial_height = 1000 +evidence = 0 initial_state = { initial01 = "a", initial02 = "b", initial03 = "c" } [validators] @@ -37,8 +38,6 @@ seeds = ["seed01"] seeds = ["seed01"] snapshot_interval = 5 perturb = ["disconnect"] -# FIXME: maverick has been disabled until it is redesigned (https://github.com/tendermint/tendermint/issues/5575) -# misbehaviors = { 1018 = "double-prevote" } [node.validator02] seeds = ["seed02"] @@ -80,7 +79,7 @@ mode = "full" # FIXME: should be v2, disabled due to flake fast_sync = "v0" persistent_peers = ["validator01", "validator02", "validator03", "validator04", "validator05"] -retain_blocks = 1 +retain_blocks = 3 perturb = ["restart"] [node.full02] @@ -94,10 +93,5 @@ perturb = ["restart"] [node.light01] mode= "light" -start_at= 1005 -persistent_peers = ["validator01", "validator02", "validator03"] - -[node.light02] -mode= "light" -start_at= 1015 -persistent_peers = ["validator04", "full01", "validator05"] \ No newline at end of file +start_at= 1010 +persistent_peers = ["validator01", "validator02", "validator03"] \ No newline at end of file diff --git a/test/e2e/pkg/manifest.go b/test/e2e/pkg/manifest.go index 8fbaea185..84628594f 100644 --- a/test/e2e/pkg/manifest.go +++ b/test/e2e/pkg/manifest.go @@ -51,6 +51,10 @@ type Manifest struct { // Options are ed25519 & secp256k1 KeyType string `toml:"key_type"` + // Evidence indicates the amount of evidence that will be injected into the + // testnet via the RPC endpoint of a random node. Default is 0 + Evidence int `toml:"evidence"` + // LogLevel sets the log level of the entire testnet. This can be overridden // by individual nodes. LogLevel string `toml:"log_level"` @@ -113,8 +117,8 @@ type ManifestNode struct { SnapshotInterval uint64 `toml:"snapshot_interval"` // RetainBlocks specifies the number of recent blocks to retain. Defaults to - // 0, which retains all blocks. Must be greater that PersistInterval and - // SnapshotInterval. + // 0, which retains all blocks. Must be greater that PersistInterval, + // SnapshotInterval and EvidenceAgeHeight. RetainBlocks uint64 `toml:"retain_blocks"` // Perturb lists perturbations to apply to the node after it has been @@ -126,16 +130,6 @@ type ManifestNode struct { // restart: restarts the node, shutting it down with SIGTERM Perturb []string `toml:"perturb"` - // Misbehaviors sets how a validator behaves during consensus at a - // certain height. Multiple misbehaviors at different heights can be used - // - // An example of misbehaviors - // { 10 = "double-prevote", 20 = "double-prevote"} - // - // For more information, look at the readme in the maverick folder. - // A list of all behaviors can be found in ../maverick/consensus/behavior.go - Misbehaviors map[string]string `toml:"misbehaviors"` - // Log level sets the log level of the specific node i.e. "consensus:info,*:error". // This is helpful when debugging a specific problem. This overrides the network // level. diff --git a/test/e2e/pkg/testnet.go b/test/e2e/pkg/testnet.go index b773a158a..1e3eede90 100644 --- a/test/e2e/pkg/testnet.go +++ b/test/e2e/pkg/testnet.go @@ -11,6 +11,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" @@ -46,6 +47,9 @@ const ( PerturbationKill Perturbation = "kill" PerturbationPause Perturbation = "pause" PerturbationRestart Perturbation = "restart" + + EvidenceAgeHeight int64 = 3 + EvidenceAgeTime time.Duration = 10 * time.Second ) // Testnet represents a single testnet. @@ -60,6 +64,7 @@ type Testnet struct { ValidatorUpdates map[int64]map[*Node]int64 Nodes []*Node KeyType string + Evidence int LogLevel string } @@ -84,7 +89,6 @@ type Node struct { Seeds []*Node PersistentPeers []*Node Perturbations []Perturbation - Misbehaviors map[int64]string LogLevel string } @@ -124,6 +128,7 @@ func LoadTestnet(file string) (*Testnet, error) { Validators: map[*Node]int64{}, ValidatorUpdates: map[int64]map[*Node]int64{}, Nodes: []*Node{}, + Evidence: manifest.Evidence, KeyType: "ed25519", LogLevel: manifest.LogLevel, } @@ -161,7 +166,6 @@ func LoadTestnet(file string) (*Testnet, error) { SnapshotInterval: nodeManifest.SnapshotInterval, RetainBlocks: nodeManifest.RetainBlocks, Perturbations: []Perturbation{}, - Misbehaviors: make(map[int64]string), LogLevel: manifest.LogLevel, } if node.StartAt == testnet.InitialHeight { @@ -185,13 +189,6 @@ func LoadTestnet(file string) (*Testnet, error) { for _, p := range nodeManifest.Perturb { node.Perturbations = append(node.Perturbations, Perturbation(p)) } - for heightString, misbehavior := range nodeManifest.Misbehaviors { - height, err := strconv.ParseInt(heightString, 10, 64) - if err != nil { - return nil, fmt.Errorf("unable to parse height %s to int64: %w", heightString, err) - } - node.Misbehaviors[height] = misbehavior - } if nodeManifest.LogLevel != "" { node.LogLevel = nodeManifest.LogLevel } @@ -344,6 +341,10 @@ func (n Node) Validate(testnet Testnet) error { if n.StateSync && n.StartAt == 0 { return errors.New("state synced nodes cannot start at the initial height") } + if n.RetainBlocks != 0 && n.RetainBlocks < uint64(EvidenceAgeHeight) { + return fmt.Errorf("retain_blocks must be greater or equal to max evidence age (%d)", + EvidenceAgeHeight) + } if n.PersistInterval == 0 && n.RetainBlocks > 0 { return errors.New("persist_interval=0 requires retain_blocks=0") } @@ -362,31 +363,6 @@ func (n Node) Validate(testnet Testnet) error { } } - if (n.PrivvalProtocol != "file" || n.Mode != "validator") && len(n.Misbehaviors) != 0 { - return errors.New("must be using \"file\" privval protocol to implement misbehaviors") - } - - for height, misbehavior := range n.Misbehaviors { - if height < n.StartAt { - return fmt.Errorf("misbehavior height %d is below node start height %d", - height, n.StartAt) - } - if height < testnet.InitialHeight { - return fmt.Errorf("misbehavior height %d is below network initial height %d", - height, testnet.InitialHeight) - } - exists := false - // FIXME: Maverick has been disabled until it is redesigned - // for possibleBehaviors := range mcs.MisbehaviorList { - // if possibleBehaviors == misbehavior { - // exists = true - // } - // } - if !exists { - return fmt.Errorf("misbehavior %s does not exist", misbehavior) - } - } - return nil } @@ -438,19 +414,6 @@ func (t Testnet) HasPerturbations() bool { return false } -// LastMisbehaviorHeight returns the height of the last misbehavior. -func (t Testnet) LastMisbehaviorHeight() int64 { - lastHeight := int64(0) - for _, node := range t.Nodes { - for height := range node.Misbehaviors { - if height > lastHeight { - lastHeight = height - } - } - } - return lastHeight -} - // Address returns a P2P endpoint address for the node. func (n Node) AddressP2P(withID bool) string { ip := n.IP.String() diff --git a/test/e2e/runner/evidence.go b/test/e2e/runner/evidence.go new file mode 100644 index 000000000..ece241cd2 --- /dev/null +++ b/test/e2e/runner/evidence.go @@ -0,0 +1,274 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "math/rand" + "path/filepath" + "time" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/tmhash" + tmjson "github.com/tendermint/tendermint/libs/json" + "github.com/tendermint/tendermint/privval" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + e2e "github.com/tendermint/tendermint/test/e2e/pkg" + "github.com/tendermint/tendermint/types" + "github.com/tendermint/tendermint/version" +) + +// 1 in 11 evidence is light client evidence, the rest is duplicate vote +// FIXME: Setting to 11 disables light client attack evidence since nodes +// don't follow a minimum retention height invariant. When we fix this we +// should use a ratio of 4. +const lightClientEvidenceRatio = 11 + +// InjectEvidence takes a running testnet and generates an amount of valid +// evidence and broadcasts it to a random node through the rpc endpoint `/broadcast_evidence`. +// Evidence is random and can be a mixture of LightClientAttackEvidence and +// DuplicateVoteEvidence. +func InjectEvidence(testnet *e2e.Testnet, amount int) error { + // select a random node + targetNode := testnet.RandomNode() + + logger.Info(fmt.Sprintf("Injecting evidence through %v (amount: %d)...", targetNode.Name, amount)) + + client, err := targetNode.Client() + if err != nil { + return err + } + + // request the latest block and validator set from the node + blockRes, err := client.Block(context.Background(), nil) + if err != nil { + return err + } + lightEvidenceCommonHeight := blockRes.Block.Height + waitHeight := blockRes.Block.Height + 3 + duplicateVoteHeight := waitHeight + + nValidators := 100 + valRes, err := client.Validators(context.Background(), &lightEvidenceCommonHeight, nil, &nValidators) + if err != nil { + return err + } + valSet, err := types.ValidatorSetFromExistingValidators(valRes.Validators) + if err != nil { + return err + } + + // get the private keys of all the validators in the network + privVals, err := getPrivateValidatorKeys(testnet) + if err != nil { + return err + } + + // wait for the node to reach the height above the forged height so that + // it is able to validate the evidence + status, err := waitForNode(targetNode, waitHeight, 10*time.Second) + if err != nil { + return err + } + duplicateVoteTime := status.SyncInfo.LatestBlockTime + + var ev types.Evidence + for i := 0; i < amount; i++ { + if i%lightClientEvidenceRatio == 0 { + ev, err = generateLightClientAttackEvidence( + privVals, lightEvidenceCommonHeight, valSet, testnet.Name, blockRes.Block.Time, + ) + } else { + ev, err = generateDuplicateVoteEvidence( + privVals, duplicateVoteHeight, valSet, testnet.Name, duplicateVoteTime, + ) + } + if err != nil { + return err + } + + _, err := client.BroadcastEvidence(context.Background(), ev) + if err != nil { + return err + } + } + + logger.Info(fmt.Sprintf("Finished sending evidence (height %d)", blockRes.Block.Height+2)) + + return nil +} + +func getPrivateValidatorKeys(testnet *e2e.Testnet) ([]types.MockPV, error) { + privVals := []types.MockPV{} + + for _, node := range testnet.Nodes { + if node.Mode == e2e.ModeValidator { + privKeyPath := filepath.Join(testnet.Dir, node.Name, PrivvalKeyFile) + privKey, err := readPrivKey(privKeyPath) + if err != nil { + return nil, err + } + // Create mock private validators from the validators private key. MockPV is + // stateless which means we can double vote and do other funky stuff + privVals = append(privVals, types.NewMockPVWithParams(privKey, false, false)) + } + } + + return privVals, nil +} + +// creates evidence of a lunatic attack. The height provided is the common height. +// The forged height happens 2 blocks later. +func generateLightClientAttackEvidence( + privVals []types.MockPV, + height int64, + vals *types.ValidatorSet, + chainID string, + evTime time.Time, +) (*types.LightClientAttackEvidence, error) { + // forge a random header + forgedHeight := height + 2 + forgedTime := evTime.Add(1 * time.Second) + header := makeHeaderRandom(chainID, forgedHeight) + header.Time = forgedTime + + // add a new bogus validator and remove an existing one to + // vary the validator set slightly + pv, conflictingVals, err := mutateValidatorSet(privVals, vals) + if err != nil { + return nil, err + } + + header.ValidatorsHash = conflictingVals.Hash() + + // create a commit for the forged header + blockID := makeBlockID(header.Hash(), 1000, []byte("partshash")) + voteSet := types.NewVoteSet(chainID, forgedHeight, 0, tmproto.SignedMsgType(2), conflictingVals) + commit, err := types.MakeCommit(blockID, forgedHeight, 0, voteSet, pv, forgedTime) + if err != nil { + return nil, err + } + + ev := &types.LightClientAttackEvidence{ + ConflictingBlock: &types.LightBlock{ + SignedHeader: &types.SignedHeader{ + Header: header, + Commit: commit, + }, + ValidatorSet: conflictingVals, + }, + CommonHeight: height, + TotalVotingPower: vals.TotalVotingPower(), + Timestamp: evTime, + } + ev.ByzantineValidators = ev.GetByzantineValidators(vals, &types.SignedHeader{ + Header: makeHeaderRandom(chainID, forgedHeight), + }) + return ev, nil +} + +// generateDuplicateVoteEvidence picks a random validator from the val set and +// returns duplicate vote evidence against the validator +func generateDuplicateVoteEvidence( + privVals []types.MockPV, + height int64, + vals *types.ValidatorSet, + chainID string, + time time.Time, +) (*types.DuplicateVoteEvidence, error) { + // nolint:gosec // G404: Use of weak random number generator + privVal := privVals[rand.Intn(len(privVals))] + voteA, err := types.MakeVote(height, makeRandomBlockID(), vals, privVal, chainID, time) + if err != nil { + return nil, err + } + voteB, err := types.MakeVote(height, makeRandomBlockID(), vals, privVal, chainID, time) + if err != nil { + return nil, err + } + return types.NewDuplicateVoteEvidence(voteA, voteB, time, vals), nil +} + +func readPrivKey(keyFilePath string) (crypto.PrivKey, error) { + keyJSONBytes, err := ioutil.ReadFile(keyFilePath) + if err != nil { + return nil, err + } + pvKey := privval.FilePVKey{} + err = tmjson.Unmarshal(keyJSONBytes, &pvKey) + if err != nil { + return nil, fmt.Errorf("error reading PrivValidator key from %v: %w", keyFilePath, err) + } + + return pvKey.PrivKey, nil +} + +func makeHeaderRandom(chainID string, height int64) *types.Header { + return &types.Header{ + Version: version.Consensus{Block: version.BlockProtocol, App: 1}, + ChainID: chainID, + Height: height, + Time: time.Now(), + LastBlockID: makeBlockID([]byte("headerhash"), 1000, []byte("partshash")), + LastCommitHash: crypto.CRandBytes(tmhash.Size), + DataHash: crypto.CRandBytes(tmhash.Size), + ValidatorsHash: crypto.CRandBytes(tmhash.Size), + NextValidatorsHash: crypto.CRandBytes(tmhash.Size), + ConsensusHash: crypto.CRandBytes(tmhash.Size), + AppHash: crypto.CRandBytes(tmhash.Size), + LastResultsHash: crypto.CRandBytes(tmhash.Size), + EvidenceHash: crypto.CRandBytes(tmhash.Size), + ProposerAddress: crypto.CRandBytes(crypto.AddressSize), + } +} + +func makeRandomBlockID() types.BlockID { + return makeBlockID(crypto.CRandBytes(tmhash.Size), 100, crypto.CRandBytes(tmhash.Size)) +} + +func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) types.BlockID { + var ( + h = make([]byte, tmhash.Size) + psH = make([]byte, tmhash.Size) + ) + copy(h, hash) + copy(psH, partSetHash) + return types.BlockID{ + Hash: h, + PartSetHeader: types.PartSetHeader{ + Total: partSetSize, + Hash: psH, + }, + } +} + +func mutateValidatorSet(privVals []types.MockPV, vals *types.ValidatorSet, +) ([]types.PrivValidator, *types.ValidatorSet, error) { + newVal, newPrivVal := types.RandValidator(false, 10) + + var newVals *types.ValidatorSet + if vals.Size() > 2 { + newVals = types.NewValidatorSet(append(vals.Copy().Validators[:vals.Size()-1], newVal)) + } else { + newVals = types.NewValidatorSet(append(vals.Copy().Validators, newVal)) + } + + // we need to sort the priv validators with the same index as the validator set + pv := make([]types.PrivValidator, newVals.Size()) + for idx, val := range newVals.Validators { + found := false + for _, p := range append(privVals, newPrivVal.(types.MockPV)) { + if bytes.Equal(p.PrivKey.PubKey().Address(), val.Address) { + pv[idx] = p + found = true + break + } + } + if !found { + return nil, nil, fmt.Errorf("missing priv validator for %v", val.Address) + } + } + + return pv, newVals, nil +} diff --git a/test/e2e/runner/main.go b/test/e2e/runner/main.go index 993df98ef..35b0d952f 100644 --- a/test/e2e/runner/main.go +++ b/test/e2e/runner/main.go @@ -71,14 +71,6 @@ func NewCLI() *CLI { return err } - if lastMisbehavior := cli.testnet.LastMisbehaviorHeight(); lastMisbehavior > 0 { - // wait for misbehaviors before starting perturbations. We do a separate - // wait for another 5 blocks, since the last misbehavior height may be - // in the past depending on network startup ordering. - if err := WaitUntil(cli.testnet, lastMisbehavior); err != nil { - return err - } - } if err := Wait(cli.testnet, 5); err != nil { // allow some txs to go through return err } @@ -92,6 +84,15 @@ func NewCLI() *CLI { } } + if cli.testnet.Evidence > 0 { + if err := InjectEvidence(cli.testnet, cli.testnet.Evidence); err != nil { + return err + } + if err := Wait(cli.testnet, 1); err != nil { // ensure chain progress + return err + } + } + loadCancel() if err := <-chLoadResult; err != nil { return err @@ -188,6 +189,24 @@ func NewCLI() *CLI { }, }) + cli.root.AddCommand(&cobra.Command{ + Use: "evidence [amount]", + Args: cobra.MaximumNArgs(1), + Short: "Generates and broadcasts evidence to a random node", + RunE: func(cmd *cobra.Command, args []string) (err error) { + amount := 1 + + if len(args) == 1 { + amount, err = strconv.Atoi(args[0]) + if err != nil { + return err + } + } + + return InjectEvidence(cli.testnet, amount) + }, + }) + cli.root.AddCommand(&cobra.Command{ Use: "test", Short: "Runs test cases against a running testnet", diff --git a/test/e2e/runner/setup.go b/test/e2e/runner/setup.go index 6045afbe4..b2dbfe5a9 100644 --- a/test/e2e/runner/setup.go +++ b/test/e2e/runner/setup.go @@ -12,7 +12,6 @@ import ( "path/filepath" "regexp" "sort" - "strconv" "strings" "text/template" "time" @@ -86,7 +85,7 @@ func Setup(testnet *e2e.Testnet) error { if err != nil { return err } - config.WriteConfigFile(filepath.Join(nodeDir, "config", "config.toml"), cfg) // panics + config.WriteConfigFile(nodeDir, cfg) // panics appCfg, err := MakeAppConfig(node) if err != nil { @@ -132,28 +131,6 @@ func Setup(testnet *e2e.Testnet) error { func MakeDockerCompose(testnet *e2e.Testnet) ([]byte, error) { // Must use version 2 Docker Compose format, to support IPv6. tmpl, err := template.New("docker-compose").Funcs(template.FuncMap{ - "startCommands": func(misbehaviors map[int64]string, logLevel string) string { - command := "start" - - // FIXME: Temporarily disable behaviors until maverick is redesigned - // misbehaviorString := "" - // for height, misbehavior := range misbehaviors { - // // after the first behavior set, a comma must be prepended - // if misbehaviorString != "" { - // misbehaviorString += "," - // } - // heightString := strconv.Itoa(int(height)) - // misbehaviorString += misbehavior + "," + heightString - // } - - // if misbehaviorString != "" { - // command += " --misbehaviors " + misbehaviorString - // } - if logLevel != "" && logLevel != config.DefaultLogLevel { - command += " --log-level " + logLevel - } - return command - }, "addUint32": func(x, y uint32) uint32 { return x + y }, @@ -181,8 +158,8 @@ services: image: tendermint/e2e-node {{- if eq .ABCIProtocol "builtin" }} entrypoint: /usr/bin/entrypoint-builtin -{{- else }} - command: {{ startCommands .Misbehaviors .LogLevel }} +{{- else if .LogLevel }} + command: start --log-level {{ .LogLevel }} {{- end }} init: true ports: @@ -223,6 +200,8 @@ func MakeGenesis(testnet *e2e.Testnet) (types.GenesisDoc, error) { default: return genesis, errors.New("unsupported KeyType") } + genesis.ConsensusParams.Evidence.MaxAgeNumBlocks = e2e.EvidenceAgeHeight + genesis.ConsensusParams.Evidence.MaxAgeDuration = e2e.EvidenceAgeTime for validator, power := range testnet.Validators { genesis.Validators = append(genesis.Validators, types.GenesisValidator{ Name: validator.Name, @@ -403,12 +382,6 @@ func MakeAppConfig(node *e2e.Node) ([]byte, error) { } } - misbehaviors := make(map[string]string) - for height, misbehavior := range node.Misbehaviors { - misbehaviors[strconv.Itoa(int(height))] = misbehavior - } - cfg["misbehaviors"] = misbehaviors - if len(node.Testnet.ValidatorUpdates) > 0 { validatorUpdates := map[string]map[string]int64{} for height, validators := range node.Testnet.ValidatorUpdates { diff --git a/test/e2e/tests/evidence_test.go b/test/e2e/tests/evidence_test.go index ea24b51e5..f7f2ede79 100644 --- a/test/e2e/tests/evidence_test.go +++ b/test/e2e/tests/evidence_test.go @@ -1,57 +1,22 @@ package e2e_test import ( - "bytes" "testing" "github.com/stretchr/testify/require" - - e2e "github.com/tendermint/tendermint/test/e2e/pkg" - "github.com/tendermint/tendermint/types" ) // assert that all nodes that have blocks at the height of a misbehavior has evidence // for that misbehavior func TestEvidence_Misbehavior(t *testing.T) { blocks := fetchBlockChain(t) - testNode(t, func(t *testing.T, node e2e.Node) { - seenEvidence := make(map[int64]struct{}) - for _, block := range blocks { - // Find any evidence blaming this node in this block - var nodeEvidence types.Evidence - for _, evidence := range block.Evidence.Evidence { - switch evidence := evidence.(type) { - case *types.DuplicateVoteEvidence: - if bytes.Equal(evidence.VoteA.ValidatorAddress, node.PrivvalKey.PubKey().Address()) { - nodeEvidence = evidence - } - default: - t.Fatalf("unexpected evidence type %T", evidence) - } - } - if nodeEvidence == nil { - continue // no evidence for the node at this height - } - - // Check that evidence was as expected - misbehavior, ok := node.Misbehaviors[nodeEvidence.Height()] - require.True(t, ok, "found unexpected evidence %v in height %v", - nodeEvidence, block.Height) - - switch misbehavior { - case "double-prevote": - require.IsType(t, &types.DuplicateVoteEvidence{}, nodeEvidence, "unexpected evidence type") - default: - t.Fatalf("unknown misbehavior %v", misbehavior) - } - - seenEvidence[nodeEvidence.Height()] = struct{}{} + testnet := loadTestnet(t) + seenEvidence := 0 + for _, block := range blocks { + if len(block.Evidence.Evidence) != 0 { + seenEvidence += len(block.Evidence.Evidence) } - // see if there is any evidence that we were expecting but didn't see - for height, misbehavior := range node.Misbehaviors { - _, ok := seenEvidence[height] - require.True(t, ok, "expected evidence for %v misbehavior at height %v by node but was never found", - misbehavior, height) - } - }) + } + require.Equal(t, testnet.Evidence, seenEvidence, + "difference between the amount of evidence produced and committed") }