Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/aggregator/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ func (a *Aggregator) GetUpgrade() (*types.Upgrade, error) {
func (a *Aggregator) GetBlockTime() (time.Duration, error) {
return a.TendermintClient.GetBlockTime()
}

func (a *Aggregator) GetAppHash() (string, error) {
return a.TendermintClient.GetProposedBlockAppHash()
}
10 changes: 9 additions & 1 deletion pkg/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,15 @@ func (a *App) RefreshConsensus() {
return
}

a.State.SetConsensusStateError(err)
// Fetch app_hash from the proposed block
appHash, err := a.Aggregator.GetAppHash()
if err != nil {
a.Logger.Debug().Err(err).Msg("Could not get app hash from proposed block")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add SetState with error to display that there was an error fetching data, the same way it's done for SetTendermintResponse?

} else if appHash != "" {
a.Logger.Debug().Str("app_hash", appHash).Msg("Got app_hash from proposed block")
a.State.SetAppHash(appHash)
}

a.DisplayWrapper.SetState(a.State)
}

Expand Down
16 changes: 16 additions & 0 deletions pkg/tendermint/tendermint.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,19 @@ func (rpc *RPC) GetBlockTime() (time.Duration, error) {
duration := time.Duration(int64(blockTime * float64(time.Second)))
return duration, nil
}

func (rpc *RPC) GetProposedBlockAppHash() (string, error) {
var response types.DumpConsensusStateResponse
if err := rpc.Client.Get("/dump_consensus_state", &response); err != nil {
return "", err
}

if response.Result == nil ||
response.Result.RoundState == nil ||
response.Result.RoundState.ProposalBlock == nil {
rpc.Logger.Debug().Msg("No proposed block available")
return "", nil
}

return response.Result.RoundState.ProposalBlock.Header.AppHash, nil
}
5 changes: 3 additions & 2 deletions pkg/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type TendermintBlock struct {
}

type TendermintBlockHeader struct {
Height string `json:"height"`
Time time.Time `json:"time"`
Height string `json:"height"`
Time time.Time `json:"time"`
AppHash string `json:"app_hash"`
}
68 changes: 54 additions & 14 deletions pkg/types/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ func ValidatorsWithLatestRoundFromTendermintResponse(
return nil, errors.New("error setting string")
}

prevoteVote, prevoteHash := VoteAndHashFromString(prevote)
precommitVote, precommitHash := VoteAndHashFromString(precommit)

validators[index] = ValidatorWithRoundVote{
Validator: Validator{
Address: validator.Address,
VotingPower: vp,
},
RoundVote: RoundVote{
Address: validator.Address,
Precommit: VoteFromString(precommit),
Prevote: VoteFromString(prevote),
IsProposer: validator.Address == consensus.Result.RoundState.Proposer.Address,
Address: validator.Address,
Prevote: prevoteVote,
PrevoteBlockHash: prevoteHash,
Precommit: precommitVote,
PrecommitBlockHash: precommitHash,
IsProposer: validator.Address == consensus.Result.RoundState.Proposer.Address,
},
}
}
Expand Down Expand Up @@ -91,11 +96,17 @@ func ValidatorsWithAllRoundsFromTendermintResponse(
for index, prevote := range roundHeightVoteSet.Prevotes {
precommit := roundHeightVoteSet.Precommits[index]
validator := tendermintValidators[index]

prevoteVote, prevoteHash := VoteAndHashFromString(prevote)
precommitVote, precommitHash := VoteAndHashFromString(precommit)

currentRoundVotes[index] = RoundVote{
Address: validator.Address,
Precommit: VoteFromString(precommit),
Prevote: VoteFromString(prevote),
IsProposer: validator.Address == consensus.Result.RoundState.Proposer.Address,
Address: validator.Address,
Prevote: prevoteVote,
PrevoteBlockHash: prevoteHash,
Precommit: precommitVote,
PrecommitBlockHash: precommitHash,
IsProposer: validator.Address == consensus.Result.RoundState.Proposer.Address,
}
}

Expand All @@ -108,14 +119,43 @@ func ValidatorsWithAllRoundsFromTendermintResponse(
}, nil
}

func VoteFromString(source ConsensusVote) Vote {
if source == "nil-Vote" {
return VotedNil
// VoteAndHashFromString parses a vote string and returns the vote type and block hash.
// Vote format: Vote{idx:addr height/round/type(typeStr) BLOCKHASH signature @ timestamp}
// Returns (vote type, block hash)
func VoteAndHashFromString(source ConsensusVote) (Vote, string) {
sourceStr := string(source)

if sourceStr == "nil-Vote" {
return VotedNil, ""
}

// Extract block hash: find the part after ") " and take next 12 chars
// Format: ...SIGNED_MSG_TYPE_PREVOTE(Prevote) BLOCKHASH SIGNATURE...
closingParenIdx := strings.Index(sourceStr, ") ")
if closingParenIdx == -1 {
// Malformed vote, return as voted with empty hash
return Voted, ""
}

if strings.Contains(string(source), "SIGNED_MSG_TYPE_PREVOTE(Prevote) 000000000000") {
return VotedZero
// Skip ") " to get to the block hash
hashStart := closingParenIdx + 2
if hashStart+12 > len(sourceStr) {
// Not enough characters for hash
return Voted, ""
}

return Voted
blockHash := sourceStr[hashStart : hashStart+12]

// Determine vote type
if blockHash == "000000000000" {
return VotedZero, blockHash
}

return Voted, blockHash
}

// VoteFromString returns just the vote type (for backward compatibility)
func VoteFromString(source ConsensusVote) Vote {
vote, _ := VoteAndHashFromString(source)
return vote
}
25 changes: 25 additions & 0 deletions pkg/types/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type State struct {
StartTime time.Time
Upgrade *Upgrade
BlockTime time.Duration
ProposedBlockHash string
AppHash string

ConsensusStateError error
ValidatorsError error
Expand Down Expand Up @@ -49,6 +51,12 @@ func (s *State) SetTendermintResponse(
s.Step = utils.MustParseInt64(hrsSplit[2])
s.StartTime = consensus.Result.RoundState.StartTime

// Extract proposed block hash if available
if consensus.Result.RoundState.Proposal != nil &&
consensus.Result.RoundState.Proposal.BlockID != nil {
s.ProposedBlockHash = consensus.Result.RoundState.Proposal.BlockID.Hash
}

validators, err := ValidatorsWithLatestRoundFromTendermintResponse(consensus, tendermintValidators, s.Round)
if err != nil {
return err
Expand Down Expand Up @@ -82,6 +90,10 @@ func (s *State) SetBlockTime(blockTime time.Duration) {
s.BlockTime = blockTime
}

func (s *State) SetAppHash(appHash string) {
s.AppHash = appHash
}

func (s *State) SetConsensusStateError(err error) {
s.ConsensusStateError = err
}
Expand Down Expand Up @@ -115,6 +127,19 @@ func (s *State) SerializeConsensus(timezone *time.Location) string {
utils.ZeroOrPositiveDuration(utils.SerializeDuration(time.Since(s.StartTime))),
utils.SerializeTime(s.StartTime.In(timezone)),
))

// Display app_hash (truncated)
appHashDisplay := "N/A"
if s.AppHash != "" {
// Truncate to show first 6 and last 6 characters
if len(s.AppHash) > 12 {
appHashDisplay = s.AppHash[:6] + "..." + s.AppHash[len(s.AppHash)-6:]
} else {
appHashDisplay = s.AppHash
}
}
sb.WriteString(fmt.Sprintf(" app_hash: %s\n", appHashDisplay))

sb.WriteString(fmt.Sprintf(
" prevote consensus (total/agreeing): %.2f / %.2f\n",
s.Validators.GetTotalVotingPowerPrevotedPercent(true),
Expand Down
17 changes: 17 additions & 0 deletions pkg/types/tendermint_consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ConsensusStateRoundState struct {
StartTime time.Time `json:"start_time"`
HeightVoteSet []ConsensusHeightVoteSet `json:"height_vote_set"`
Proposer ConsensusStateProposer `json:"proposer"`
Proposal *ConsensusProposal `json:"proposal"`
}

type ConsensusHeightVoteSet struct {
Expand All @@ -32,5 +33,21 @@ type ConsensusStateProposer struct {
Index int `json:"index"`
}

type ConsensusProposal struct {
Height string `json:"height"`
Round int `json:"round"`
BlockID *BlockID `json:"block_id"`
}

type BlockID struct {
Hash string `json:"hash"`
Parts *BlockIDParts `json:"parts"`
}

type BlockIDParts struct {
Total int `json:"total"`
Hash string `json:"hash"`
}

type ConsensusVote string
type ConsensusVoteBitArray string
13 changes: 12 additions & 1 deletion pkg/types/tendermint_dump_consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@ type DumpConsensusStateResult struct {
}

type DumpConsensusStateRoundState struct {
Validators DumpConsensusStateRoundStateValidators `json:"validators"`
Validators DumpConsensusStateRoundStateValidators `json:"validators"`
ProposalBlock *DumpConsensusProposalBlock `json:"proposal_block"`
}

type DumpConsensusStateRoundStateValidators struct {
Validators []TendermintValidator `json:"validators"`
}

type DumpConsensusProposalBlock struct {
Header DumpConsensusProposalBlockHeader `json:"header"`
}

type DumpConsensusProposalBlockHeader struct {
Height string `json:"height"`
AppHash string `json:"app_hash"`
}
40 changes: 35 additions & 5 deletions pkg/types/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ type Validator struct {
type Validators []Validator

type RoundVote struct {
Address string
Prevote Vote
Precommit Vote
IsProposer bool
Address string
Prevote Vote
PrevoteBlockHash string
Precommit Vote
PrecommitBlockHash string
IsProposer bool
}

func (v RoundVote) Equals(other RoundVote) bool {
Expand All @@ -32,10 +34,18 @@ func (v RoundVote) Equals(other RoundVote) bool {
return false
}

if v.PrevoteBlockHash != other.PrevoteBlockHash {
return false
}

if v.Precommit != other.Precommit {
return false
}

if v.PrecommitBlockHash != other.PrecommitBlockHash {
return false
}

if v.IsProposer != other.IsProposer {
return false
}
Expand Down Expand Up @@ -138,16 +148,36 @@ func (v ValidatorWithInfo) Serialize(disableEmojis bool) string {
}
}

// Format block hashes: show first 3 chars, "nil" for empty, "000" for nil block
prevoteHash := formatBlockHash(v.RoundVote.PrevoteBlockHash)
precommitHash := formatBlockHash(v.RoundVote.PrecommitBlockHash)

return fmt.Sprintf(
" %s %s %s %s%% %s ",
" %s%s %s%s %s %s%% %s ",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's maybe add another space between the emoji and the vote itself? otherwise it looks a bit ugly
image

v.RoundVote.Prevote.Serialize(disableEmojis),
prevoteHash,
v.RoundVote.Precommit.Serialize(disableEmojis),
precommitHash,
utils.RightPadAndTrim(strconv.Itoa(v.Validator.Index+1), 3),
utils.RightPadAndTrim(fmt.Sprintf("%.2f", v.Validator.VotingPowerPercent), 6),
utils.LeftPadAndTrim(name, 25),
)
}

// formatBlockHash truncates block hash to first 3 chars
func formatBlockHash(hash string) string {
if hash == "" {
return "nil"
}
if hash == "000000000000" {
return "000"
}
if len(hash) >= 3 {
return hash[:3]
}
return hash
}

type ValidatorsWithInfo []ValidatorWithInfo

type ValidatorWithChainValidator struct {
Expand Down