Merge branch 'master' into marko/remove-apphash

This commit is contained in:
Marko
2021-04-06 06:50:25 +00:00
committed by GitHub
81 changed files with 1286 additions and 589 deletions

16
.github/workflows/janitor.yml vendored Normal file
View 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 }}

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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\"/" \

View File

@@ -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`:
```

View File

@@ -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) {

View File

@@ -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()
}
//----------------------------------------

View File

@@ -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

View File

@@ -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()
}
//----------------------------------------

View File

@@ -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
}

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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{

View 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)
})
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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},

View File

@@ -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 {

View File

@@ -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],

View File

@@ -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,

View File

@@ -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
View 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
View 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())
})
}

View File

@@ -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.

View File

@@ -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])
}
}

View File

@@ -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
View 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)
}

View File

@@ -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()

View File

@@ -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,

View File

@@ -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 }

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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())
})
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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=$!

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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 "$@"

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"]

View File

@@ -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.

View File

@@ -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
View 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
}

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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")
}