github.com/aakash4dev/cometbft@v0.38.2/evidence/verify.go (about) 1 package evidence 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "time" 8 9 "github.com/aakash4dev/cometbft/light" 10 "github.com/aakash4dev/cometbft/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 // 107 // CONTRACT: must run ValidateBasic() on the evidence before verifying 108 // 109 // must check that the evidence has not expired (i.e. is outside the maximum age threshold) 110 func VerifyLightClientAttack( 111 e *types.LightClientAttackEvidence, 112 commonHeader, trustedHeader *types.SignedHeader, 113 commonVals *types.ValidatorSet, 114 now time.Time, //nolint:revive 115 trustPeriod time.Duration, //nolint:revive 116 ) error { 117 // TODO: Should the current time and trust period be used in this method? 118 // If not, why were the parameters present? 119 120 // In the case of lunatic attack there will be a different commonHeader height. Therefore the node perform a single 121 // verification jump between the common header and the conflicting one 122 if commonHeader.Height != e.ConflictingBlock.Height { 123 err := commonVals.VerifyCommitLightTrusting(trustedHeader.ChainID, e.ConflictingBlock.Commit, light.DefaultTrustLevel) 124 if err != nil { 125 return fmt.Errorf("skipping verification of conflicting block failed: %w", err) 126 } 127 128 // In the case of equivocation and amnesia we expect all header hashes to be correctly derived 129 } else if e.ConflictingHeaderIsInvalid(trustedHeader.Header) { 130 return errors.New("common height is the same as conflicting block height so expected the conflicting" + 131 " block to be correctly derived yet it wasn't") 132 } 133 134 // Verify that the 2/3+ commits from the conflicting validator set were for the conflicting header 135 if err := e.ConflictingBlock.ValidatorSet.VerifyCommitLight(trustedHeader.ChainID, e.ConflictingBlock.Commit.BlockID, 136 e.ConflictingBlock.Height, e.ConflictingBlock.Commit); err != nil { 137 return fmt.Errorf("invalid commit from conflicting block: %w", err) 138 } 139 140 // Assert the correct amount of voting power of the validator set 141 if evTotal, valsTotal := e.TotalVotingPower, commonVals.TotalVotingPower(); evTotal != valsTotal { 142 return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)", 143 evTotal, valsTotal) 144 } 145 146 // check in the case of a forward lunatic attack that monotonically increasing time has been violated 147 if e.ConflictingBlock.Height > trustedHeader.Height && e.ConflictingBlock.Time.After(trustedHeader.Time) { 148 return fmt.Errorf("conflicting block doesn't violate monotonically increasing time (%v is after %v)", 149 e.ConflictingBlock.Time, trustedHeader.Time, 150 ) 151 152 // In all other cases check that the hashes of the conflicting header and the trusted header are different 153 } else if bytes.Equal(trustedHeader.Hash(), e.ConflictingBlock.Hash()) { 154 return fmt.Errorf("trusted header hash matches the evidence's conflicting header hash: %X", 155 trustedHeader.Hash()) 156 } 157 158 return validateABCIEvidence(e, commonVals, trustedHeader) 159 } 160 161 // VerifyDuplicateVote verifies DuplicateVoteEvidence against the state of full node. This involves the 162 // following checks: 163 // - the validator is in the validator set at the height of the evidence 164 // - the height, round, type and validator address of the votes must be the same 165 // - the block ID's must be different 166 // - The signatures must both be valid 167 func VerifyDuplicateVote(e *types.DuplicateVoteEvidence, chainID string, valSet *types.ValidatorSet) error { 168 _, val := valSet.GetByAddress(e.VoteA.ValidatorAddress) 169 if val == nil { 170 return fmt.Errorf("address %X was not a validator at height %d", e.VoteA.ValidatorAddress, e.Height()) 171 } 172 pubKey := val.PubKey 173 174 // H/R/S must be the same 175 if e.VoteA.Height != e.VoteB.Height || 176 e.VoteA.Round != e.VoteB.Round || 177 e.VoteA.Type != e.VoteB.Type { 178 return fmt.Errorf("h/r/s does not match: %d/%d/%v vs %d/%d/%v", 179 e.VoteA.Height, e.VoteA.Round, e.VoteA.Type, 180 e.VoteB.Height, e.VoteB.Round, e.VoteB.Type) 181 } 182 183 // Address must be the same 184 if !bytes.Equal(e.VoteA.ValidatorAddress, e.VoteB.ValidatorAddress) { 185 return fmt.Errorf("validator addresses do not match: %X vs %X", 186 e.VoteA.ValidatorAddress, 187 e.VoteB.ValidatorAddress, 188 ) 189 } 190 191 // BlockIDs must be different 192 if e.VoteA.BlockID.Equals(e.VoteB.BlockID) { 193 return fmt.Errorf( 194 "block IDs are the same (%v) - not a real duplicate vote", 195 e.VoteA.BlockID, 196 ) 197 } 198 199 // pubkey must match address (this should already be true, sanity check) 200 addr := e.VoteA.ValidatorAddress 201 if !bytes.Equal(pubKey.Address(), addr) { 202 return fmt.Errorf("address (%X) doesn't match pubkey (%v - %X)", 203 addr, pubKey, pubKey.Address()) 204 } 205 206 // validator voting power and total voting power must match 207 if val.VotingPower != e.ValidatorPower { 208 return fmt.Errorf("validator power from evidence and our validator set does not match (%d != %d)", 209 e.ValidatorPower, val.VotingPower) 210 } 211 if valSet.TotalVotingPower() != e.TotalVotingPower { 212 return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)", 213 e.TotalVotingPower, valSet.TotalVotingPower()) 214 } 215 216 va := e.VoteA.ToProto() 217 vb := e.VoteB.ToProto() 218 // Signatures must be valid 219 if !pubKey.VerifySignature(types.VoteSignBytes(chainID, va), e.VoteA.Signature) { 220 return fmt.Errorf("verifying VoteA: %w", types.ErrVoteInvalidSignature) 221 } 222 if !pubKey.VerifySignature(types.VoteSignBytes(chainID, vb), e.VoteB.Signature) { 223 return fmt.Errorf("verifying VoteB: %w", types.ErrVoteInvalidSignature) 224 } 225 226 return nil 227 } 228 229 // validateABCIEvidence validates the ABCI component of the light client attack 230 // evidence i.e voting power and byzantine validators 231 func validateABCIEvidence( 232 ev *types.LightClientAttackEvidence, 233 commonVals *types.ValidatorSet, 234 trustedHeader *types.SignedHeader, 235 ) error { 236 if evTotal, valsTotal := ev.TotalVotingPower, commonVals.TotalVotingPower(); evTotal != valsTotal { 237 return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)", 238 evTotal, valsTotal) 239 } 240 241 // Find out what type of attack this was and thus extract the malicious 242 // validators. Note, in the case of an Amnesia attack we don't have any 243 // malicious validators. 244 validators := ev.GetByzantineValidators(commonVals, trustedHeader) 245 246 // Ensure this matches the validators that are listed in the evidence. They 247 // should be ordered based on power. 248 if validators == nil && ev.ByzantineValidators != nil { 249 return fmt.Errorf( 250 "expected nil validators from an amnesia light client attack but got %d", 251 len(ev.ByzantineValidators), 252 ) 253 } 254 255 if exp, got := len(validators), len(ev.ByzantineValidators); exp != got { 256 return fmt.Errorf("expected %d byzantine validators from evidence but got %d", exp, got) 257 } 258 259 for idx, val := range validators { 260 if !bytes.Equal(ev.ByzantineValidators[idx].Address, val.Address) { 261 return fmt.Errorf( 262 "evidence contained an unexpected byzantine validator address; expected: %v, got: %v", 263 val.Address, ev.ByzantineValidators[idx].Address, 264 ) 265 } 266 267 if ev.ByzantineValidators[idx].VotingPower != val.VotingPower { 268 return fmt.Errorf( 269 "evidence contained unexpected byzantine validator power; expected %d, got %d", 270 val.VotingPower, ev.ByzantineValidators[idx].VotingPower, 271 ) 272 } 273 } 274 275 return nil 276 } 277 278 func getSignedHeader(blockStore BlockStore, height int64) (*types.SignedHeader, error) { 279 blockMeta := blockStore.LoadBlockMeta(height) 280 if blockMeta == nil { 281 return nil, fmt.Errorf("don't have header at height #%d", height) 282 } 283 commit := blockStore.LoadBlockCommit(height) 284 if commit == nil { 285 return nil, fmt.Errorf("don't have commit at height #%d", height) 286 } 287 return &types.SignedHeader{ 288 Header: &blockMeta.Header, 289 Commit: commit, 290 }, nil 291 } 292 293 // check that the evidence hasn't expired 294 func IsEvidenceExpired(heightNow int64, timeNow time.Time, heightEv int64, timeEv time.Time, evidenceParams types.EvidenceParams) bool { 295 ageDuration := timeNow.Sub(timeEv) 296 ageNumBlocks := heightNow - heightEv 297 298 if ageDuration > evidenceParams.MaxAgeDuration && ageNumBlocks > evidenceParams.MaxAgeNumBlocks { 299 return true 300 } 301 return false 302 }