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  }