mirror of
https://github.com/tendermint/tendermint.git
synced 2026-05-29 10:30:20 +00:00
Merge branch 'master' into marko/remove-apphash
This commit is contained in:
16
.github/workflows/janitor.yml
vendored
Normal file
16
.github/workflows/janitor.yml
vendored
Normal file
@@ -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 }}
|
||||
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\"/" \
|
||||
|
||||
@@ -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`:
|
||||
|
||||
```
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
29
consensus/types/peer_round_state_test.go
Normal file
29
consensus/types/peer_round_state_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
- <span v-pre>`mode = "{{ .BaseConfig.Mode }}"`</span> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
8
go.mod
8
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
|
||||
)
|
||||
|
||||
18
go.sum
18
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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
|
||||
56
node/node.go
56
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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
75
p2p/conn_tracker.go
Normal file
75
p2p/conn_tracker.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
73
p2p/conn_tracker_test.go
Normal file
73
p2p/conn_tracker_test.go
Normal file
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
234
p2p/router.go
234
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/<ip> 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
|
||||
|
||||
34
p2p/router_filter_test.go
Normal file
34
p2p/router_filter_test.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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=$!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
start_at= 1010
|
||||
persistent_peers = ["validator01", "validator02", "validator03"]
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
274
test/e2e/runner/evidence.go
Normal file
274
test/e2e/runner/evidence.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user