Merge pull request #1564 from tendermint/bucky/dump-consensus
Improve consensus state RPC
This commit is contained in:
commit
5b9a1423ae
29
CHANGELOG.md
29
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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ type Consensus interface {
|
|||
GetState() sm.State
|
||||
GetValidators() (int64, []*types.Validator)
|
||||
GetRoundStateJSON() ([]byte, error)
|
||||
GetRoundStateSimpleJSON() ([]byte, error)
|
||||
}
|
||||
|
||||
type P2P interface {
|
||||
|
|
|
@ -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, ""),
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue