github.com/KYVENetwork/cometbft/v38@v38.0.3/evidence/verify.go (about) 1 package evidence 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "time" 8 9 "github.com/KYVENetwork/cometbft/v38/light" 10 "github.com/KYVENetwork/cometbft/v38/types" 11 ) 12 13 // verify verifies the evidence fully by checking: 14 // - It has not already been committed 15 // - it is sufficiently recent (MaxAge) 16 // - it is from a key who was a validator at the given height 17 // - it is internally consistent with state 18 // - it was properly signed by the alleged equivocator and meets the individual evidence verification requirements 19 func (evpool *Pool) verify(evidence types.Evidence) error { 20 var ( 21 state = evpool.State() 22 height = state.LastBlockHeight 23 evidenceParams = state.ConsensusParams.Evidence 24 ) 25 26 // verify the time of the evidence 27 blockMeta := evpool.blockStore.LoadBlockMeta(evidence.Height()) 28 if blockMeta == nil { 29 return fmt.Errorf("don't have header #%d", evidence.Height()) 30 } 31 evTime := blockMeta.Header.Time 32 if evidence.Time() != evTime { 33 return fmt.Errorf("evidence has a different time to the block it is associated with (%v != %v)", 34 evidence.Time(), evTime) 35 } 36 37 // checking if evidence is expired calculated using the block evidence time and height 38 if IsEvidenceExpired(height, state.LastBlockTime, evidence.Height(), evTime, evidenceParams) { 39 return fmt.Errorf( 40 "evidence from height %d (created at: %v) is too old; min height is %d and evidence can not be older than %v", 41 evidence.Height(), 42 evTime, 43 height-evidenceParams.MaxAgeNumBlocks, 44 state.LastBlockTime.Add(evidenceParams.MaxAgeDuration), 45 ) 46 } 47 48 // apply the evidence-specific verification logic 49 switch ev := evidence.(type) { 50 case *types.DuplicateVoteEvidence: 51 valSet, err := evpool.stateDB.LoadValidators(evidence.Height()) 52 if err != nil { 53 return err 54 } 55 return VerifyDuplicateVote(ev, state.ChainID, valSet) 56 57 case *types.LightClientAttackEvidence: 58 commonHeader, err := getSignedHeader(evpool.blockStore, evidence.Height()) 59 if err != nil { 60 return err 61 } 62 commonVals, err := evpool.stateDB.LoadValidators(evidence.Height()) 63 if err != nil { 64 return err 65 } 66 trustedHeader := commonHeader 67 // in the case of lunatic the trusted header is different to the common header 68 if evidence.Height() != ev.ConflictingBlock.Height { 69 trustedHeader, err = getSignedHeader(evpool.blockStore, ev.ConflictingBlock.Height) 70 if err != nil { 71 // FIXME: This multi step process is a bit unergonomic. We may want to consider a more efficient process 72 // that doesn't require as much io and is atomic. 73 74 // If the node doesn't have a block at the height of the conflicting block, then this could be 75 // a forward lunatic attack. Thus the node must get the latest height it has 76 latestHeight := evpool.blockStore.Height() 77 trustedHeader, err = getSignedHeader(evpool.blockStore, latestHeight) 78 if err != nil { 79 return err 80 } 81 if trustedHeader.Time.Before(ev.ConflictingBlock.Time) { 82 return fmt.Errorf("latest block time (%v) is before conflicting block time (%v)", 83 trustedHeader.Time, ev.ConflictingBlock.Time, 84 ) 85 } 86 } 87 } 88 89 err = VerifyLightClientAttack(ev, commonHeader, trustedHeader, commonVals, state.LastBlockTime, 90 state.ConsensusParams.Evidence.MaxAgeDuration) 91 if err != nil { 92 return err 93 } 94 return nil 95 default: 96 return fmt.Errorf("unrecognized evidence type: %T", evidence) 97 } 98 } 99 100 // VerifyLightClientAttack verifies LightClientAttackEvidence against the state of the full node. This involves 101 // the following checks: 102 // - the common header from the full node has at least 1/3 voting power which is also present in 103 // the conflicting header's commit 104 // - 2/3+ of the conflicting validator set correctly signed the conflicting block 105 // - the nodes trusted header at the same height as the conflicting header has a different hash 106 // - all signatures must be checked as this will be used as evidence 107 // 108 // CONTRACT: must run ValidateBasic() on the evidence before verifying 109 // 110 // must check that the evidence has not expired (i.e. is outside the maximum age threshold) 111 func VerifyLightClientAttack( 112 e *types.LightClientAttackEvidence, 113 commonHeader, trustedHeader *types.SignedHeader, 114 commonVals *types.ValidatorSet, 115 now time.Time, //nolint:revive 116 trustPeriod time.Duration, //nolint:revive 117 ) error { 118 // TODO: Should the current time and trust period be used in this method? 119 // If not, why were the parameters present? 120 121 // In the case of lunatic attack there will be a different commonHeader height. Therefore the node perform a single 122 // verification jump between the common header and the conflicting one 123 if commonHeader.Height != e.ConflictingBlock.Height { 124 err := commonVals.VerifyCommitLightTrustingAllSignatures(trustedHeader.ChainID, e.ConflictingBlock.Commit, light.DefaultTrustLevel) 125 if err != nil { 126 return fmt.Errorf("skipping verification of conflicting block failed: %w", err) 127 } 128 129 // In the case of equivocation and amnesia we expect all header hashes to be correctly derived 130 } else if e.ConflictingHeaderIsInvalid(trustedHeader.Header) { 131 return errors.New("common height is the same as conflicting block height so expected the conflicting" + 132 " block to be correctly derived yet it wasn't") 133 } 134 135 // Verify that the 2/3+ commits from the conflicting validator set were for the conflicting header 136 if err := e.ConflictingBlock.ValidatorSet.VerifyCommitLightAllSignatures(trustedHeader.ChainID, e.ConflictingBlock.Commit.BlockID, 137 e.ConflictingBlock.Height, e.ConflictingBlock.Commit); err != nil { 138 return fmt.Errorf("invalid commit from conflicting block: %w", err) 139 } 140 141 // Assert the correct amount of voting power of the validator set 142 if evTotal, valsTotal := e.TotalVotingPower, commonVals.TotalVotingPower(); evTotal != valsTotal { 143 return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)", 144 evTotal, valsTotal) 145 } 146 147 // check in the case of a forward lunatic attack that monotonically increasing time has been violated 148 if e.ConflictingBlock.Height > trustedHeader.Height && e.ConflictingBlock.Time.After(trustedHeader.Time) { 149 return fmt.Errorf("conflicting block doesn't violate monotonically increasing time (%v is after %v)", 150 e.ConflictingBlock.Time, trustedHeader.Time, 151 ) 152 153 // In all other cases check that the hashes of the conflicting header and the trusted header are different 154 } else if bytes.Equal(trustedHeader.Hash(), e.ConflictingBlock.Hash()) { 155 return fmt.Errorf("trusted header hash matches the evidence's conflicting header hash: %X", 156 trustedHeader.Hash()) 157 } 158 159 return validateABCIEvidence(e, commonVals, trustedHeader) 160 } 161 162 // VerifyDuplicateVote verifies DuplicateVoteEvidence against the state of full node. This involves the 163 // following checks: 164 // - the validator is in the validator set at the height of the evidence 165 // - the height, round, type and validator address of the votes must be the same 166 // - the block ID's must be different 167 // - The signatures must both be valid 168 func VerifyDuplicateVote(e *types.DuplicateVoteEvidence, chainID string, valSet *types.ValidatorSet) error { 169 _, val := valSet.GetByAddress(e.VoteA.ValidatorAddress) 170 if val == nil { 171 return fmt.Errorf("address %X was not a validator at height %d", e.VoteA.ValidatorAddress, e.Height()) 172 } 173 pubKey := val.PubKey 174 175 // H/R/S must be the same 176 if e.VoteA.Height != e.VoteB.Height || 177 e.VoteA.Round != e.VoteB.Round || 178 e.VoteA.Type != e.VoteB.Type { 179 return fmt.Errorf("h/r/s does not match: %d/%d/%v vs %d/%d/%v", 180 e.VoteA.Height, e.VoteA.Round, e.VoteA.Type, 181 e.VoteB.Height, e.VoteB.Round, e.VoteB.Type) 182 } 183 184 // Address must be the same 185 if !bytes.Equal(e.VoteA.ValidatorAddress, e.VoteB.ValidatorAddress) { 186 return fmt.Errorf("validator addresses do not match: %X vs %X", 187 e.VoteA.ValidatorAddress, 188 e.VoteB.ValidatorAddress, 189 ) 190 } 191 192 // BlockIDs must be different 193 if e.VoteA.BlockID.Equals(e.VoteB.BlockID) { 194 return fmt.Errorf( 195 "block IDs are the same (%v) - not a real duplicate vote", 196 e.VoteA.BlockID, 197 ) 198 } 199 200 // pubkey must match address (this should already be true, sanity check) 201 addr := e.VoteA.ValidatorAddress 202 if !bytes.Equal(pubKey.Address(), addr) { 203 return fmt.Errorf("address (%X) doesn't match pubkey (%v - %X)", 204 addr, pubKey, pubKey.Address()) 205 } 206 207 // validator voting power and total voting power must match 208 if val.VotingPower != e.ValidatorPower { 209 return fmt.Errorf("validator power from evidence and our validator set does not match (%d != %d)", 210 e.ValidatorPower, val.VotingPower) 211 } 212 if valSet.TotalVotingPower() != e.TotalVotingPower { 213 return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)", 214 e.TotalVotingPower, valSet.TotalVotingPower()) 215 } 216 217 va := e.VoteA.ToProto() 218 vb := e.VoteB.ToProto() 219 // Signatures must be valid 220 if !pubKey.VerifySignature(types.VoteSignBytes(chainID, va), e.VoteA.Signature) { 221 return fmt.Errorf("verifying VoteA: %w", types.ErrVoteInvalidSignature) 222 } 223 if !pubKey.VerifySignature(types.VoteSignBytes(chainID, vb), e.VoteB.Signature) { 224 return fmt.Errorf("verifying VoteB: %w", types.ErrVoteInvalidSignature) 225 } 226 227 return nil 228 } 229 230 // validateABCIEvidence validates the ABCI component of the light client attack 231 // evidence i.e voting power and byzantine validators 232 func validateABCIEvidence( 233 ev *types.LightClientAttackEvidence, 234 commonVals *types.ValidatorSet, 235 trustedHeader *types.SignedHeader, 236 ) error { 237 if evTotal, valsTotal := ev.TotalVotingPower, commonVals.TotalVotingPower(); evTotal != valsTotal { 238 return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)", 239 evTotal, valsTotal) 240 } 241 242 // Find out what type of attack this was and thus extract the malicious 243 // validators. Note, in the case of an Amnesia attack we don't have any 244 // malicious validators. 245 validators := ev.GetByzantineValidators(commonVals, trustedHeader) 246 247 // Ensure this matches the validators that are listed in the evidence. They 248 // should be ordered based on power. 249 if validators == nil && ev.ByzantineValidators != nil { 250 return fmt.Errorf( 251 "expected nil validators from an amnesia light client attack but got %d", 252 len(ev.ByzantineValidators), 253 ) 254 } 255 256 if exp, got := len(validators), len(ev.ByzantineValidators); exp != got { 257 return fmt.Errorf("expected %d byzantine validators from evidence but got %d", exp, got) 258 } 259 260 for idx, val := range validators { 261 if !bytes.Equal(ev.ByzantineValidators[idx].Address, val.Address) { 262 return fmt.Errorf( 263 "evidence contained an unexpected byzantine validator address; expected: %v, got: %v", 264 val.Address, ev.ByzantineValidators[idx].Address, 265 ) 266 } 267 268 if ev.ByzantineValidators[idx].VotingPower != val.VotingPower { 269 return fmt.Errorf( 270 "evidence contained unexpected byzantine validator power; expected %d, got %d", 271 val.VotingPower, ev.ByzantineValidators[idx].VotingPower, 272 ) 273 } 274 } 275 276 return nil 277 } 278 279 func getSignedHeader(blockStore BlockStore, height int64) (*types.SignedHeader, error) { 280 blockMeta := blockStore.LoadBlockMeta(height) 281 if blockMeta == nil { 282 return nil, fmt.Errorf("don't have header at height #%d", height) 283 } 284 commit := blockStore.LoadBlockCommit(height) 285 if commit == nil { 286 return nil, fmt.Errorf("don't have commit at height #%d", height) 287 } 288 return &types.SignedHeader{ 289 Header: &blockMeta.Header, 290 Commit: commit, 291 }, nil 292 } 293 294 // check that the evidence hasn't expired 295 func IsEvidenceExpired(heightNow int64, timeNow time.Time, heightEv int64, timeEv time.Time, evidenceParams types.EvidenceParams) bool { 296 ageDuration := timeNow.Sub(timeEv) 297 ageNumBlocks := heightNow - heightEv 298 299 if ageDuration > evidenceParams.MaxAgeDuration && ageNumBlocks > evidenceParams.MaxAgeNumBlocks { 300 return true 301 } 302 return false 303 }