package light import ( "bytes" "errors" "fmt" "time" tmmath "github.com/tendermint/tendermint/libs/math" "github.com/tendermint/tendermint/types" ) var ( // DefaultTrustLevel - new header can be trusted if at least one correct // validator signed it. DefaultTrustLevel = tmmath.Fraction{Numerator: 1, Denominator: 3} ) // VerifyNonAdjacent verifies non-adjacent untrustedHeader against // trustedHeader. It ensures that: // // a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned) // b) untrustedHeader is valid (if not, ErrInvalidHeader is returned) // c) trustLevel ([1/3, 1]) of trustedHeaderVals (or trustedHeaderNextVals) // signed correctly (if not, ErrNewValSetCantBeTrusted is returned) // d) more than 2/3 of untrustedVals have signed h2 // (otherwise, ErrInvalidHeader is returned) // e) headers are non-adjacent. // // maxClockDrift defines how much untrustedHeader.Time can drift into the // future. // trustedHeader must have a ChainID, Height and Time func VerifyNonAdjacent( trustedHeader *types.SignedHeader, // height=X trustedVals *types.ValidatorSet, // height=X or height=X+1 untrustedHeader *types.SignedHeader, // height=Y untrustedVals *types.ValidatorSet, // height=Y trustingPeriod time.Duration, now time.Time, maxClockDrift time.Duration, trustLevel tmmath.Fraction) error { checkRequiredHeaderFields(trustedHeader) if untrustedHeader.Height == trustedHeader.Height+1 { return errors.New("headers must be non adjacent in height") } if HeaderExpired(trustedHeader, trustingPeriod, now) { return ErrOldHeaderExpired{trustedHeader.Time.Add(trustingPeriod), now} } if err := verifyNewHeaderAndVals( untrustedHeader, untrustedVals, trustedHeader, now, maxClockDrift); err != nil { return ErrInvalidHeader{err} } // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. err := trustedVals.VerifyCommitLightTrusting(trustedHeader.ChainID, untrustedHeader.Commit, trustLevel) if err != nil { switch e := err.(type) { case types.ErrNotEnoughVotingPowerSigned: return ErrNewValSetCantBeTrusted{e} default: return e } } // Ensure that +2/3 of new validators signed correctly. // // NOTE: this should always be the last check because untrustedVals can be // intentionally made very large to DOS the light client. not the case for // VerifyAdjacent, where validator set is known in advance. if err := untrustedVals.VerifyCommitLight(trustedHeader.ChainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, untrustedHeader.Commit); err != nil { return ErrInvalidHeader{err} } return nil } // VerifyAdjacent verifies directly adjacent untrustedHeader against // trustedHeader. It ensures that: // // a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned) // b) untrustedHeader is valid (if not, ErrInvalidHeader is returned) // c) untrustedHeader.ValidatorsHash equals trustedHeader.NextValidatorsHash // d) more than 2/3 of new validators (untrustedVals) have signed h2 // (otherwise, ErrInvalidHeader is returned) // e) headers are adjacent. // // maxClockDrift defines how much untrustedHeader.Time can drift into the // future. // trustedHeader must have a ChainID, Height, Time and NextValidatorsHash func VerifyAdjacent( trustedHeader *types.SignedHeader, // height=X untrustedHeader *types.SignedHeader, // height=X+1 untrustedVals *types.ValidatorSet, // height=X+1 trustingPeriod time.Duration, now time.Time, maxClockDrift time.Duration) error { checkRequiredHeaderFields(trustedHeader) if len(trustedHeader.NextValidatorsHash) == 0 { panic("next validators hash in trusted header is empty") } if untrustedHeader.Height != trustedHeader.Height+1 { return errors.New("headers must be adjacent in height") } if HeaderExpired(trustedHeader, trustingPeriod, now) { return ErrOldHeaderExpired{trustedHeader.Time.Add(trustingPeriod), now} } if err := verifyNewHeaderAndVals( untrustedHeader, untrustedVals, trustedHeader, now, maxClockDrift); err != nil { return ErrInvalidHeader{err} } // Check the validator hashes are the same if !bytes.Equal(untrustedHeader.ValidatorsHash, trustedHeader.NextValidatorsHash) { err := fmt.Errorf("expected old header next validators (%X) to match those from new header (%X)", trustedHeader.NextValidatorsHash, untrustedHeader.ValidatorsHash, ) return ErrInvalidHeader{err} } // Ensure that +2/3 of new validators signed correctly. if err := untrustedVals.VerifyCommitLight(trustedHeader.ChainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, untrustedHeader.Commit); err != nil { return ErrInvalidHeader{err} } return nil } // Verify combines both VerifyAdjacent and VerifyNonAdjacent functions. func Verify( trustedHeader *types.SignedHeader, // height=X trustedVals *types.ValidatorSet, // height=X or height=X+1 untrustedHeader *types.SignedHeader, // height=Y untrustedVals *types.ValidatorSet, // height=Y trustingPeriod time.Duration, now time.Time, maxClockDrift time.Duration, trustLevel tmmath.Fraction) error { if untrustedHeader.Height != trustedHeader.Height+1 { return VerifyNonAdjacent(trustedHeader, trustedVals, untrustedHeader, untrustedVals, trustingPeriod, now, maxClockDrift, trustLevel) } return VerifyAdjacent(trustedHeader, untrustedHeader, untrustedVals, trustingPeriod, now, maxClockDrift) } // ValidateTrustLevel checks that trustLevel is within the allowed range [1/3, // 1]. If not, it returns an error. 1/3 is the minimum amount of trust needed // which does not break the security model. func ValidateTrustLevel(lvl tmmath.Fraction) error { if lvl.Numerator*3 < lvl.Denominator || // < 1/3 lvl.Numerator > lvl.Denominator || // > 1 lvl.Denominator == 0 { return fmt.Errorf("trustLevel must be within [1/3, 1], given %v", lvl) } return nil } // HeaderExpired return true if the given header expired. func HeaderExpired(h *types.SignedHeader, trustingPeriod time.Duration, now time.Time) bool { expirationTime := h.Time.Add(trustingPeriod) return !expirationTime.After(now) } // VerifyBackwards verifies an untrusted header with a height one less than // that of an adjacent trusted header. It ensures that: // // a) untrusted header is valid // b) untrusted header has a time before the trusted header // c) that the LastBlockID hash of the trusted header is the same as the hash // of the trusted header // // For any of these cases ErrInvalidHeader is returned. func VerifyBackwards(untrustedHeader, trustedHeader *types.Header) error { if err := untrustedHeader.ValidateBasic(); err != nil { return ErrInvalidHeader{err} } if untrustedHeader.ChainID != trustedHeader.ChainID { return ErrInvalidHeader{errors.New("header belongs to another chain")} } if !untrustedHeader.Time.Before(trustedHeader.Time) { return ErrInvalidHeader{ fmt.Errorf("expected older header time %v to be before new header time %v", untrustedHeader.Time, trustedHeader.Time)} } if !bytes.Equal(untrustedHeader.Hash(), trustedHeader.LastBlockID.Hash) { return ErrInvalidHeader{ fmt.Errorf("older header hash %X does not match trusted header's last block %X", untrustedHeader.Hash(), trustedHeader.LastBlockID.Hash)} } return nil } func verifyNewHeaderAndVals( untrustedHeader *types.SignedHeader, untrustedVals *types.ValidatorSet, trustedHeader *types.SignedHeader, now time.Time, maxClockDrift time.Duration) error { if err := untrustedHeader.ValidateBasic(trustedHeader.ChainID); err != nil { return fmt.Errorf("untrustedHeader.ValidateBasic failed: %w", err) } if untrustedHeader.Height <= trustedHeader.Height { return fmt.Errorf("expected new header height %d to be greater than one of old header %d", untrustedHeader.Height, trustedHeader.Height) } if !untrustedHeader.Time.After(trustedHeader.Time) { return fmt.Errorf("expected new header time %v to be after old header time %v", untrustedHeader.Time, trustedHeader.Time) } if !untrustedHeader.Time.Before(now.Add(maxClockDrift)) { return fmt.Errorf("new header has a time from the future %v (now: %v; max clock drift: %v)", untrustedHeader.Time, now, maxClockDrift) } if !bytes.Equal(untrustedHeader.ValidatorsHash, untrustedVals.Hash()) { return fmt.Errorf("expected new header validators (%X) to match those that were supplied (%X) at height %d", untrustedHeader.ValidatorsHash, untrustedVals.Hash(), untrustedHeader.Height, ) } return nil } func checkRequiredHeaderFields(h *types.SignedHeader) { if h.Height == 0 { panic("height in trusted header must be set (non zero") } zeroTime := time.Time{} if h.Time == zeroTime { panic("time in trusted header must be set") } if h.ChainID == "" { panic("chain ID in trusted header must be set") } }