diff --git a/pkg/aggregator/aggregator.go b/pkg/aggregator/aggregator.go index 1614aae..4e754f4 100644 --- a/pkg/aggregator/aggregator.go +++ b/pkg/aggregator/aggregator.go @@ -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() +} diff --git a/pkg/app.go b/pkg/app.go index 91b7272..fcee740 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -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") + } 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) } diff --git a/pkg/tendermint/tendermint.go b/pkg/tendermint/tendermint.go index d58305d..36e7a77 100644 --- a/pkg/tendermint/tendermint.go +++ b/pkg/tendermint/tendermint.go @@ -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 +} diff --git a/pkg/types/block.go b/pkg/types/block.go index 394e6db..e178df3 100644 --- a/pkg/types/block.go +++ b/pkg/types/block.go @@ -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"` } diff --git a/pkg/types/converter.go b/pkg/types/converter.go index 2d4d22c..0794006 100644 --- a/pkg/types/converter.go +++ b/pkg/types/converter.go @@ -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, }, } } @@ -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, } } @@ -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 } diff --git a/pkg/types/state.go b/pkg/types/state.go index 7d89561..58b570e 100644 --- a/pkg/types/state.go +++ b/pkg/types/state.go @@ -18,6 +18,8 @@ type State struct { StartTime time.Time Upgrade *Upgrade BlockTime time.Duration + ProposedBlockHash string + AppHash string ConsensusStateError error ValidatorsError error @@ -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 @@ -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 } @@ -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), diff --git a/pkg/types/tendermint_consensus.go b/pkg/types/tendermint_consensus.go index 4684984..453f11c 100644 --- a/pkg/types/tendermint_consensus.go +++ b/pkg/types/tendermint_consensus.go @@ -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 { @@ -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 diff --git a/pkg/types/tendermint_dump_consensus.go b/pkg/types/tendermint_dump_consensus.go index 3bb4d94..d388e99 100644 --- a/pkg/types/tendermint_dump_consensus.go +++ b/pkg/types/tendermint_dump_consensus.go @@ -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"` +} diff --git a/pkg/types/validator.go b/pkg/types/validator.go index 12a0940..c268f9f 100644 --- a/pkg/types/validator.go +++ b/pkg/types/validator.go @@ -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 { @@ -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 } @@ -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 ", 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 {