diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1503598..2c3a0ba97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,19 @@ # Changelog -## Roadmap +## 0.19.3 (TBD) -BREAKING CHANGES: -- Better support for injecting randomness -- Upgrade consensus for more real-time use of evidence +FEATURES -FEATURES: -- Use the chain as its own CA for nodes and validators -- Tooling to run multiple blockchains/apps, possibly in a single process -- State syncing (without transaction replay) -- Add authentication and rate-limitting to the RPC +- [rpc] New `/consensus_state` returns just the votes seen at the current height -IMPROVEMENTS: -- Improve subtleties around mempool caching and logic -- Consensus optimizations: - - cache block parts for faster agreement after round changes - - propagate block parts rarest first -- Better testing of the consensus state machine (ie. use a DSL) -- Auto compiled serialization/deserialization code instead of go-wire reflection +IMPROVEMENTS -BUG FIXES: -- Graceful handling/recovery for apps that have non-determinism or fail to halt -- Graceful handling/recovery for violations of safety, or liveness +- [rpc] Add stringified votes and fraction of power voted to `/dump_consensus_state` +- [rpc] Add PeerStateStats to `/dump_consensus_state` + +BUG FIXES + +- [cmd] Set GenesisTime during `tendermint init` ## 0.19.2 (April 30th, 2018) diff --git a/consensus/state.go b/consensus/state.go index 40b8f16d5..4450c74e7 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -185,6 +185,14 @@ func (cs *ConsensusState) GetRoundStateJSON() ([]byte, error) { return cdc.MarshalJSON(cs.RoundState) } +// GetRoundStateSimpleJSON returns a json of RoundStateSimple, marshalled using go-amino. +func (cs *ConsensusState) GetRoundStateSimpleJSON() ([]byte, error) { + cs.mtx.Lock() + defer cs.mtx.Unlock() + + return cdc.MarshalJSON(cs.RoundState.RoundStateSimple()) +} + // GetValidators returns a copy of the current validators. func (cs *ConsensusState) GetValidators() (int64, []*types.Validator) { cs.mtx.Lock() @@ -1279,6 +1287,7 @@ func (cs *ConsensusState) defaultSetProposal(proposal *types.Proposal) error { cs.Proposal = proposal cs.ProposalBlockParts = types.NewPartSetFromHeader(proposal.BlockPartsHeader) + cs.Logger.Info("Received proposal", "proposal", proposal) return nil } diff --git a/consensus/types/height_vote_set.go b/consensus/types/height_vote_set.go index f65f365b8..3c9867940 100644 --- a/consensus/types/height_vote_set.go +++ b/consensus/types/height_vote_set.go @@ -174,6 +174,26 @@ func (hvs *HeightVoteSet) getVoteSet(round int, type_ byte) *types.VoteSet { } } +// If a peer claims that it has 2/3 majority for given blockKey, call this. +// NOTE: if there are too many peers, or too much peer churn, +// this can cause memory issues. +// TODO: implement ability to remove peers too +func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ byte, peerID p2p.ID, blockID types.BlockID) error { + hvs.mtx.Lock() + defer hvs.mtx.Unlock() + if !types.IsVoteTypeValid(type_) { + return fmt.Errorf("SetPeerMaj23: Invalid vote type %v", type_) + } + voteSet := hvs.getVoteSet(round, type_) + if voteSet == nil { + return nil // something we don't know about yet + } + return voteSet.SetPeerMaj23(types.P2PID(peerID), blockID) +} + +//--------------------------------------------------------- +// string and json + func (hvs *HeightVoteSet) String() string { return hvs.StringIndented("") } @@ -207,43 +227,35 @@ func (hvs *HeightVoteSet) StringIndented(indent string) string { indent) } -type roundVoteBitArrays struct { - Round int `json:"round"` - Prevotes *cmn.BitArray `json:"prevotes"` - Precommits *cmn.BitArray `json:"precommits"` -} - func (hvs *HeightVoteSet) MarshalJSON() ([]byte, error) { hvs.mtx.Lock() defer hvs.mtx.Unlock() + + allVotes := hvs.toAllRoundVotes() + return cdc.MarshalJSON(allVotes) +} + +func (hvs *HeightVoteSet) toAllRoundVotes() []roundVotes { totalRounds := hvs.round + 1 - roundsVotes := make([]roundVoteBitArrays, totalRounds) + allVotes := make([]roundVotes, totalRounds) // rounds 0 ~ hvs.round inclusive for round := 0; round < totalRounds; round++ { - roundsVotes[round] = roundVoteBitArrays{ - Round: round, - Prevotes: hvs.roundVoteSets[round].Prevotes.BitArray(), - Precommits: hvs.roundVoteSets[round].Precommits.BitArray(), + allVotes[round] = roundVotes{ + Round: round, + Prevotes: hvs.roundVoteSets[round].Prevotes.VoteStrings(), + PrevotesBitArray: hvs.roundVoteSets[round].Prevotes.BitArrayString(), + Precommits: hvs.roundVoteSets[round].Precommits.VoteStrings(), + PrecommitsBitArray: hvs.roundVoteSets[round].Precommits.BitArrayString(), } } // TODO: all other peer catchup rounds - - return cdc.MarshalJSON(roundsVotes) + return allVotes } -// If a peer claims that it has 2/3 majority for given blockKey, call this. -// NOTE: if there are too many peers, or too much peer churn, -// this can cause memory issues. -// TODO: implement ability to remove peers too -func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ byte, peerID p2p.ID, blockID types.BlockID) error { - hvs.mtx.Lock() - defer hvs.mtx.Unlock() - if !types.IsVoteTypeValid(type_) { - return fmt.Errorf("SetPeerMaj23: Invalid vote type %v", type_) - } - voteSet := hvs.getVoteSet(round, type_) - if voteSet == nil { - return nil // something we don't know about yet - } - return voteSet.SetPeerMaj23(types.P2PID(peerID), blockID) +type roundVotes struct { + Round int `json:"round"` + Prevotes []string `json:"prevotes"` + PrevotesBitArray string `json:"prevotes_bit_array"` + Precommits []string `json:"precommits"` + PrecommitsBitArray string `json:"precommits_bit_array"` } diff --git a/consensus/types/round_state.go b/consensus/types/round_state.go index 65c40781e..f69b8f39d 100644 --- a/consensus/types/round_state.go +++ b/consensus/types/round_state.go @@ -1,10 +1,12 @@ package types import ( + "encoding/json" "fmt" "time" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) //----------------------------------------------------------------------------- @@ -77,6 +79,32 @@ type RoundState struct { LastValidators *types.ValidatorSet `json:"last_validators"` } +// Compressed version of the RoundState for use in RPC +type RoundStateSimple struct { + HeightRoundStep string `json:"height/round/step"` + StartTime time.Time `json:"start_time"` + ProposalBlockHash cmn.HexBytes `json:"proposal_block_hash"` + LockedBlockHash cmn.HexBytes `json:"locked_block_hash"` + ValidBlockHash cmn.HexBytes `json:"valid_block_hash"` + Votes json.RawMessage `json:"height_vote_set"` +} + +// Compress the RoundState to RoundStateSimple +func (rs *RoundState) RoundStateSimple() RoundStateSimple { + votesJSON, err := rs.Votes.MarshalJSON() + if err != nil { + panic(err) + } + return RoundStateSimple{ + HeightRoundStep: fmt.Sprintf("%d/%d/%d", rs.Height, rs.Round, rs.Step), + StartTime: rs.StartTime, + ProposalBlockHash: rs.ProposalBlock.Hash(), + LockedBlockHash: rs.LockedBlock.Hash(), + ValidBlockHash: rs.ValidBlock.Hash(), + Votes: votesJSON, + } +} + // RoundStateEvent returns the H/R/S of the RoundState as an event. func (rs *RoundState) RoundStateEvent() types.EventDataRoundState { // XXX: copy the RoundState diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index 83207eb7b..89a1293af 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -126,6 +126,15 @@ func (c *HTTP) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { return result, nil } +func (c *HTTP) ConsensusState() (*ctypes.ResultConsensusState, error) { + result := new(ctypes.ResultConsensusState) + _, err := c.rpc.Call("consensus_state", map[string]interface{}{}, result) + if err != nil { + return nil, errors.Wrap(err, "ConsensusState") + } + return result, nil +} + func (c *HTTP) Health() (*ctypes.ResultHealth, error) { result := new(ctypes.ResultHealth) _, err := c.rpc.Call("health", map[string]interface{}{}, result) diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 010b6abf4..0cc9333ef 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -84,6 +84,7 @@ type Client interface { type NetworkClient interface { NetInfo() (*ctypes.ResultNetInfo, error) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) + ConsensusState() (*ctypes.ResultConsensusState, error) Health() (*ctypes.ResultHealth, error) } diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index 7e2f8fedc..0c47de830 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -84,6 +84,10 @@ func (Local) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { return core.DumpConsensusState() } +func (Local) ConsensusState() (*ctypes.ResultConsensusState, error) { + return core.ConsensusState() +} + func (Local) Health() (*ctypes.ResultHealth, error) { return core.Health() } diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 661ec717d..eb25b94ee 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -78,6 +78,17 @@ func TestDumpConsensusState(t *testing.T) { } } +func TestConsensusState(t *testing.T) { + for i, c := range GetClients() { + // FIXME: fix server so it doesn't panic on invalid input + nc, ok := c.(client.NetworkClient) + require.True(t, ok, "%d", i) + cons, err := nc.ConsensusState() + require.Nil(t, err, "%d: %+v", i, err) + assert.NotEmpty(t, cons.RoundState) + } +} + func TestHealth(t *testing.T) { for i, c := range GetClients() { nc, ok := c.(client.NetworkClient) diff --git a/rpc/core/consensus.go b/rpc/core/consensus.go index a747c600a..68e25c1e6 100644 --- a/rpc/core/consensus.go +++ b/rpc/core/consensus.go @@ -211,3 +211,51 @@ func DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { } return &ctypes.ResultDumpConsensusState{roundState, peerStates}, nil } + +// ConsensusState returns a concise summary of the consensus state. +// UNSTABLE +// +// ```shell +// curl 'localhost:46657/consensus_state' +// ``` +// +// ```go +// client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") +// state, err := client.ConsensusState() +// ``` +// +// The above command returns JSON structured like this: +// +// ```json +//{ +// "jsonrpc": "2.0", +// "id": "", +// "result": { +// "round_state": { +// "height/round/step": "9336/0/1", +// "start_time": "2018-05-14T10:25:45.72595357-04:00", +// "proposal_block_hash": "", +// "locked_block_hash": "", +// "valid_block_hash": "", +// "height_vote_set": [ +// { +// "round": 0, +// "prevotes": [ +// "nil-Vote" +// ], +// "prevotes_bit_array": "BA{1:_} 0/10 = 0.00", +// "precommits": [ +// "nil-Vote" +// ], +// "precommits_bit_array": "BA{1:_} 0/10 = 0.00" +// } +// ] +// } +// } +//} +//``` +func ConsensusState() (*ctypes.ResultConsensusState, error) { + // Get self round state. + bz, err := consensusState.GetRoundStateSimpleJSON() + return &ctypes.ResultConsensusState{bz}, err +} diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 0bbead943..e93ba2f80 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -23,6 +23,7 @@ type Consensus interface { GetState() sm.State GetValidators() (int64, []*types.Validator) GetRoundStateJSON() ([]byte, error) + GetRoundStateSimpleJSON() ([]byte, error) } type P2P interface { diff --git a/rpc/core/routes.go b/rpc/core/routes.go index be3881c36..bf90d6fbd 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -25,6 +25,7 @@ var Routes = map[string]*rpc.RPCFunc{ "tx_search": rpc.NewRPCFunc(TxSearch, "query,prove"), "validators": rpc.NewRPCFunc(Validators, "height"), "dump_consensus_state": rpc.NewRPCFunc(DumpConsensusState, ""), + "consensus_state": rpc.NewRPCFunc(ConsensusState, ""), "unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxs, ""), "num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxs, ""), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index eb0f46f97..18c545453 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -128,17 +128,23 @@ type ResultValidators struct { } // Info about the consensus state. -// Unstable +// UNSTABLE type ResultDumpConsensusState struct { RoundState json.RawMessage `json:"round_state"` Peers []PeerStateInfo `json:"peers"` } +// UNSTABLE type PeerStateInfo struct { NodeAddress string `json:"node_address"` PeerState json.RawMessage `json:"peer_state"` } +// UNSTABLE +type ResultConsensusState struct { + RoundState json.RawMessage `json:"round_state"` +} + // CheckTx result type ResultBroadcastTx struct { Code uint32 `json:"code"` diff --git a/types/vote_set.go b/types/vote_set.go index c5c72d435..8908f86f2 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -418,6 +418,9 @@ func (voteSet *VoteSet) TwoThirdsMajority() (blockID BlockID, ok bool) { return BlockID{}, false } +//-------------------------------------------------------------------------------- +// Strings and JSON + func (voteSet *VoteSet) String() string { if voteSet == nil { return "nil-VoteSet" @@ -454,6 +457,45 @@ func (voteSet *VoteSet) StringIndented(indent string) string { func (voteSet *VoteSet) MarshalJSON() ([]byte, error) { voteSet.mtx.Lock() defer voteSet.mtx.Unlock() + return cdc.MarshalJSON(VoteSetJSON{ + voteSet.voteStrings(), + voteSet.bitArrayString(), + voteSet.peerMaj23s, + }) +} + +// More human readable JSON of the vote set +// NOTE: insufficient for unmarshalling from (compressed votes) +// TODO: make the peerMaj23s nicer to read (eg just the block hash) +type VoteSetJSON struct { + Votes []string `json:"votes"` + VotesBitArray string `json:"votes_bit_array"` + PeerMaj23s map[P2PID]BlockID `json:"peer_maj_23s"` +} + +// Return the bit-array of votes including +// the fraction of power that has voted like: +// "BA{29:xx__x__x_x___x__x_______xxx__} 856/1304 = 0.66" +func (voteSet *VoteSet) BitArrayString() string { + voteSet.mtx.Lock() + defer voteSet.mtx.Unlock() + return voteSet.bitArrayString() +} + +func (voteSet *VoteSet) bitArrayString() string { + bAString := voteSet.votesBitArray.String() + voted, total, fracVoted := voteSet.sumTotalFrac() + return fmt.Sprintf("%s %d/%d = %.2f", bAString, voted, total, fracVoted) +} + +// Returns a list of votes compressed to more readable strings. +func (voteSet *VoteSet) VoteStrings() []string { + voteSet.mtx.Lock() + defer voteSet.mtx.Unlock() + return voteSet.voteStrings() +} + +func (voteSet *VoteSet) voteStrings() []string { voteStrings := make([]string, len(voteSet.votes)) for i, vote := range voteSet.votes { if vote == nil { @@ -462,13 +504,7 @@ func (voteSet *VoteSet) MarshalJSON() ([]byte, error) { voteStrings[i] = vote.String() } } - return cdc.MarshalJSON(struct { - Votes []string `json:"votes"` - VotesBitArray *cmn.BitArray `json:"votes_bit_array"` - PeerMaj23s map[P2PID]BlockID `json:"peer_maj_23s"` - }{ - voteStrings, voteSet.votesBitArray, voteSet.peerMaj23s, - }) + return voteStrings } func (voteSet *VoteSet) StringShort() string { @@ -477,8 +513,16 @@ func (voteSet *VoteSet) StringShort() string { } voteSet.mtx.Lock() defer voteSet.mtx.Unlock() - return fmt.Sprintf(`VoteSet{H:%v R:%v T:%v +2/3:%v %v %v}`, - voteSet.height, voteSet.round, voteSet.type_, voteSet.maj23, voteSet.votesBitArray, voteSet.peerMaj23s) + _, _, frac := voteSet.sumTotalFrac() + return fmt.Sprintf(`VoteSet{H:%v R:%v T:%v +2/3:%v(%v) %v %v}`, + voteSet.height, voteSet.round, voteSet.type_, voteSet.maj23, frac, voteSet.votesBitArray, voteSet.peerMaj23s) +} + +// return the power voted, the total, and the fraction +func (voteSet *VoteSet) sumTotalFrac() (int64, int64, float64) { + voted, total := voteSet.sum, voteSet.valSet.TotalVotingPower() + fracVoted := float64(voted) / float64(total) + return voted, total, fracVoted } //-------------------------------------------------------------------------------- diff --git a/version/version.go b/version/version.go index 1a2c64962..d877ee62a 100644 --- a/version/version.go +++ b/version/version.go @@ -4,13 +4,13 @@ package version const ( Maj = "0" Min = "19" - Fix = "2" + Fix = "3" ) var ( // Version is the current version of Tendermint // Must be a string because scripts like dist.sh read this file. - Version = "0.19.2" + Version = "0.19.3-dev" // GitCommit is the current HEAD set using ldflags. GitCommit string