From 3c8109e3b0707be017d532c64f866c6cba033732 Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Fri, 1 Mar 2019 16:03:15 +0300 Subject: [PATCH 1/5] add readonly flag for validator switchable via SetReadonly unsafe RPC --- consensus/state.go | 71 ++++++++++++++++++++++++++++--------- rpc/core/pipe.go | 1 + rpc/core/routes.go | 2 ++ rpc/core/status.go | 7 ++++ rpc/core/types/responses.go | 4 +++ 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 83760754bea..55aebfc6249 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -8,6 +8,7 @@ import ( "runtime/debug" "sync" "time" + "strconv" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/fail" @@ -127,6 +128,8 @@ type ConsensusState struct { // for reporting metrics metrics *Metrics + + readonly bool } // StateOption sets an optional parameter on the ConsensusState. @@ -159,6 +162,7 @@ func NewConsensusState( evpool: evpool, evsw: tmevents.NewEventSwitch(), metrics: NopMetrics(), + readonly: false, } // set function defaults (may be overwritten before calling Start) cs.decideProposal = cs.defaultDecideProposal @@ -706,6 +710,7 @@ func (cs *ConsensusState) handleTimeout(ti timeoutInfo, rs cstypes.RoundState) { // XXX: should we fire timeout here (for timeout commit)? cs.enterNewRound(ti.Height, 0) case cstypes.RoundStepNewRound: + cs.Logger.Info("[handleTimeout] enterPropose") cs.enterPropose(ti.Height, 0) case cstypes.RoundStepPropose: cs.eventBus.PublishEventTimeoutPropose(cs.RoundStateEvent()) @@ -727,6 +732,7 @@ func (cs *ConsensusState) handleTxsAvailable() { cs.mtx.Lock() defer cs.mtx.Unlock() // we only need to do this for round 0 + cs.Logger.Info("[handleTxsAvailable] enterPropose") cs.enterPropose(cs.Height, 0) } @@ -793,6 +799,7 @@ func (cs *ConsensusState) enterNewRound(height int64, round int) { } go cs.proposalHeartbeat(height, round) } else { + cs.Logger.Info("[enterNewRound] enterPropose") cs.enterPropose(height, round) } } @@ -808,14 +815,33 @@ func (cs *ConsensusState) needProofBlock(height int64) bool { return !bytes.Equal(cs.state.AppHash, lastBlockMeta.Header.AppHash) } +func (cs *ConsensusState) isValidator() bool { + return cs.privValidator != nil && + cs.Validators.HasAddress(cs.privValidator.GetAddress()) +} + +func (cs *ConsensusState) SetReadonly(readonly bool) { + cs.readonly = readonly + cs.Logger.Info("[SetReadonly]: Set to " + strconv.FormatBool(readonly) + " " + strconv.FormatBool(cs.readonly)) +} + func (cs *ConsensusState) proposalHeartbeat(height int64, round int) { logger := cs.Logger.With("height", height, "round", round) addr := cs.privValidator.GetAddress() - if !cs.Validators.HasAddress(addr) { - logger.Debug("Not sending proposalHearbeat. This node is not a validator", "addr", addr, "vals", cs.Validators) + if !cs.isValidator() { + logger.Info("Not sending proposalHearbeat. This node is not a validator", "addr", addr, "vals", cs.Validators) return } + + cs.Logger.Info("proposalHeartbeat: I'm here") + + // validator is readonly, do nothing + if (cs.readonly) { + cs.Logger.Info("proposalHeartbeat: Validator is in readonly mode") + return + } + counter := 0 valIndex, _ := cs.Validators.GetByAddress(addr) chainID := cs.state.ChainID @@ -852,6 +878,25 @@ func (cs *ConsensusState) enterPropose(height int64, round int) { } logger.Info(fmt.Sprintf("enterPropose(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) + // Nothing more to do if we're not a validator + if cs.privValidator == nil { + logger.Debug("This node is not a validator 1") + return + } + + // if not a validator, we're done + if !cs.isValidator() { + logger.Debug("This node is not a validator 2", "addr", cs.privValidator.GetAddress(), "vals", cs.Validators) + return + } + logger.Debug("This node is a validator " + strconv.FormatBool(cs.readonly)) + + // validator is readonly, do nothing + if (cs.readonly) { + logger.Info("enterPropose: Validator is in readonly mode") + return + } + defer func() { // Done enterPropose: cs.updateRoundStep(round, cstypes.RoundStepPropose) @@ -868,19 +913,6 @@ func (cs *ConsensusState) enterPropose(height int64, round int) { // If we don't get the proposal and all block parts quick enough, enterPrevote cs.scheduleTimeout(cs.config.Propose(round), height, round, cstypes.RoundStepPropose) - // Nothing more to do if we're not a validator - if cs.privValidator == nil { - logger.Debug("This node is not a validator 1") - return - } - - // if not a validator, we're done - if !cs.Validators.HasAddress(cs.privValidator.GetAddress()) { - logger.Debug("This node is not a validator 2", "addr", cs.privValidator.GetAddress(), "vals", cs.Validators) - return - } - logger.Debug("This node is a validator") - if cs.isProposer() { logger.Info("enterPropose: Our turn to propose", "proposer", cs.Validators.GetProposer().Address, "privValidator", cs.privValidator) if height%32 == 0 { @@ -1736,9 +1768,16 @@ func (cs *ConsensusState) voteTime() time.Time { // sign the vote and publish on internalMsgQueue func (cs *ConsensusState) signAddVote(type_ types.SignedMsgType, hash []byte, header types.PartSetHeader) *types.Vote { // if we don't have a key or we're not in the validator set, do nothing - if cs.privValidator == nil || !cs.Validators.HasAddress(cs.privValidator.GetAddress()) { + if !cs.isValidator() { return nil } + + // validator is readonly, do nothing + if (cs.readonly) { + cs.Logger.Info("signAddVote: Validator is in readonly mode") + return nil + } + vote, err := cs.signVote(type_, hash, header) if err == nil { cs.sendInternalMessage(msgInfo{&VoteMessage{vote}, ""}) diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index ae8ae056a50..2974e6d0196 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -31,6 +31,7 @@ type Consensus interface { GetLastHeight() int64 GetRoundStateJSON() ([]byte, error) GetRoundStateSimpleJSON() ([]byte, error) + SetReadonly(readonly bool) } type transport interface { diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 736ded60787..f4c40006c0f 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -50,4 +50,6 @@ func AddUnsafeRoutes() { Routes["unsafe_start_cpu_profiler"] = rpc.NewRPCFunc(UnsafeStartCPUProfiler, "filename") Routes["unsafe_stop_cpu_profiler"] = rpc.NewRPCFunc(UnsafeStopCPUProfiler, "") Routes["unsafe_write_heap_profile"] = rpc.NewRPCFunc(UnsafeWriteHeapProfile, "filename") + + Routes["set_readonly"] = rpc.NewRPCFunc(SetReadonly, "readonly") } diff --git a/rpc/core/status.go b/rpc/core/status.go index 793e1ade7bd..9f56bbb2ff3 100644 --- a/rpc/core/status.go +++ b/rpc/core/status.go @@ -3,6 +3,7 @@ package core import ( "bytes" "time" + "strconv" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/p2p" @@ -136,3 +137,9 @@ func validatorAtHeight(h int64) *types.Validator { return nil } + +func SetReadonly(readonly bool) (*ctypes.ResultSetReadonly, error) { + consensusState.SetReadonly(readonly) + + return &ctypes.ResultSetReadonly{"Set validator as readonly: " + strconv.FormatBool(readonly)}, nil +} diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 07628d1c60e..da04de43b4a 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -105,6 +105,10 @@ type ResultDialPeers struct { Log string `json:"log"` } +type ResultSetReadonly struct { + Log string `json:"log"` +} + // A peer type Peer struct { NodeInfo p2p.DefaultNodeInfo `json:"node_info"` From 134ff0cffa76934cd180a7f24263346fe2c2bb5f Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Sat, 2 Mar 2019 16:02:26 +0300 Subject: [PATCH 2/5] add --consensus.readonly flag to start validator in readonly mode. Default: false --- cmd/tendermint/commands/run_node.go | 1 + config/config.go | 3 +++ config/toml.go | 3 +++ consensus/state.go | 6 +++--- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/tendermint/commands/run_node.go b/cmd/tendermint/commands/run_node.go index 6dabacb1f0a..026b5fc3553 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -43,6 +43,7 @@ func AddNodeFlags(cmd *cobra.Command) { // consensus flags cmd.Flags().Bool("consensus.create_empty_blocks", config.Consensus.CreateEmptyBlocks, "Set this to false to only produce blocks when there are txs or when the AppHash changes") + cmd.Flags().Bool("consensus.readonly", config.Consensus.Readonly, "Set this to true to make validator skip producing blocks") } // NewRunNodeCmd returns the command that allows the CLI to start a node. diff --git a/config/config.go b/config/config.go index 23b03399498..ac7e2354882 100644 --- a/config/config.go +++ b/config/config.go @@ -587,6 +587,8 @@ type ConsensusConfig struct { // Block time parameters. Corresponds to the minimum time increment between consecutive blocks. BlockTimeIota time.Duration `mapstructure:"blocktime_iota"` + + Readonly bool `mapstructure:"readonly"` } // DefaultConsensusConfig returns a default configuration for the consensus service @@ -606,6 +608,7 @@ func DefaultConsensusConfig() *ConsensusConfig { PeerGossipSleepDuration: 100 * time.Millisecond, PeerQueryMaj23SleepDuration: 2000 * time.Millisecond, BlockTimeIota: 1000 * time.Millisecond, + Readonly: false, } } diff --git a/config/toml.go b/config/toml.go index 21e017b455d..76878fe3c24 100644 --- a/config/toml.go +++ b/config/toml.go @@ -263,6 +263,9 @@ peer_query_maj23_sleep_duration = "{{ .Consensus.PeerQueryMaj23SleepDuration }}" # Block time parameters. Corresponds to the minimum time increment between consecutive blocks. blocktime_iota = "{{ .Consensus.BlockTimeIota }}" +# Do not produce blocks, just observe (for validator) +readonly = {{ .Consensus.Readonly }} + ##### transactions indexer configuration options ##### [tx_index] diff --git a/consensus/state.go b/consensus/state.go index 55aebfc6249..d62b5178132 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -162,7 +162,7 @@ func NewConsensusState( evpool: evpool, evsw: tmevents.NewEventSwitch(), metrics: NopMetrics(), - readonly: false, + readonly: config.Readonly, } // set function defaults (may be overwritten before calling Start) cs.decideProposal = cs.defaultDecideProposal @@ -893,10 +893,10 @@ func (cs *ConsensusState) enterPropose(height int64, round int) { // validator is readonly, do nothing if (cs.readonly) { - logger.Info("enterPropose: Validator is in readonly mode") + logger.Info("enterPropose: Validator is in readonly mode. Skipping..") return } - + defer func() { // Done enterPropose: cs.updateRoundStep(round, cstypes.RoundStepPropose) From 85221c8ec6f3954715112fdf667f2504a3bd7aa2 Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Sat, 2 Mar 2019 16:03:57 +0300 Subject: [PATCH 3/5] don't skip round step change in enterPropose if readonly --- consensus/state.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index d62b5178132..398a121b5cd 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -891,12 +891,6 @@ func (cs *ConsensusState) enterPropose(height int64, round int) { } logger.Debug("This node is a validator " + strconv.FormatBool(cs.readonly)) - // validator is readonly, do nothing - if (cs.readonly) { - logger.Info("enterPropose: Validator is in readonly mode. Skipping..") - return - } - defer func() { // Done enterPropose: cs.updateRoundStep(round, cstypes.RoundStepPropose) @@ -915,6 +909,13 @@ func (cs *ConsensusState) enterPropose(height int64, round int) { if cs.isProposer() { logger.Info("enterPropose: Our turn to propose", "proposer", cs.Validators.GetProposer().Address, "privValidator", cs.privValidator) + + // validator is readonly, do nothing + if (cs.readonly) { + logger.Info("enterPropose: Validator is in readonly mode. Skipping..") + return + } + if height%32 == 0 { rsp, err := cs.proxyApp.CheckBridgeSync(abci.RequestCheckBridge{Height: int32(round)}) if err != nil { From 228f547b29a89c2ad54be4b118345634232c3668 Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Mon, 11 Mar 2019 16:30:00 +0300 Subject: [PATCH 4/5] expose is_readonly in status RPC --- consensus/state.go | 4 ++++ rpc/core/pipe.go | 1 + rpc/core/status.go | 1 + rpc/core/types/responses.go | 1 + 4 files changed, 7 insertions(+) diff --git a/consensus/state.go b/consensus/state.go index 398a121b5cd..713a5be6aed 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -825,6 +825,10 @@ func (cs *ConsensusState) SetReadonly(readonly bool) { cs.Logger.Info("[SetReadonly]: Set to " + strconv.FormatBool(readonly) + " " + strconv.FormatBool(cs.readonly)) } +func (cs *ConsensusState) IsReadonly() bool { + return cs.readonly; +} + func (cs *ConsensusState) proposalHeartbeat(height int64, round int) { logger := cs.Logger.With("height", height, "round", round) addr := cs.privValidator.GetAddress() diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 2974e6d0196..492615c48ec 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -32,6 +32,7 @@ type Consensus interface { GetRoundStateJSON() ([]byte, error) GetRoundStateSimpleJSON() ([]byte, error) SetReadonly(readonly bool) + IsReadonly() bool } type transport interface { diff --git a/rpc/core/status.go b/rpc/core/status.go index 9f56bbb2ff3..130dff0090f 100644 --- a/rpc/core/status.go +++ b/rpc/core/status.go @@ -106,6 +106,7 @@ func Status() (*ctypes.ResultStatus, error) { Address: pubKey.Address(), PubKey: pubKey, VotingPower: votingPower, + IsReadonly: consensusState.IsReadonly(), }, } diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index da04de43b4a..37a5648d651 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -70,6 +70,7 @@ type ValidatorInfo struct { Address cmn.HexBytes `json:"address"` PubKey crypto.PubKey `json:"pub_key"` VotingPower int64 `json:"voting_power"` + IsReadonly bool `json:"is_readonly"` } // Node Status From f5f07b1bcca6ecbe69df9787c76ab57a9967a33b Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Mon, 11 Mar 2019 16:32:38 +0300 Subject: [PATCH 5/5] remove debug logging --- consensus/state.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 713a5be6aed..9c57d8e710e 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -710,7 +710,6 @@ func (cs *ConsensusState) handleTimeout(ti timeoutInfo, rs cstypes.RoundState) { // XXX: should we fire timeout here (for timeout commit)? cs.enterNewRound(ti.Height, 0) case cstypes.RoundStepNewRound: - cs.Logger.Info("[handleTimeout] enterPropose") cs.enterPropose(ti.Height, 0) case cstypes.RoundStepPropose: cs.eventBus.PublishEventTimeoutPropose(cs.RoundStateEvent()) @@ -732,7 +731,6 @@ func (cs *ConsensusState) handleTxsAvailable() { cs.mtx.Lock() defer cs.mtx.Unlock() // we only need to do this for round 0 - cs.Logger.Info("[handleTxsAvailable] enterPropose") cs.enterPropose(cs.Height, 0) } @@ -799,7 +797,6 @@ func (cs *ConsensusState) enterNewRound(height int64, round int) { } go cs.proposalHeartbeat(height, round) } else { - cs.Logger.Info("[enterNewRound] enterPropose") cs.enterPropose(height, round) } } @@ -838,8 +835,6 @@ func (cs *ConsensusState) proposalHeartbeat(height int64, round int) { return } - cs.Logger.Info("proposalHeartbeat: I'm here") - // validator is readonly, do nothing if (cs.readonly) { cs.Logger.Info("proposalHeartbeat: Validator is in readonly mode")