package tendermint import ( "errors" "time" lite "github.com/tendermint/tendermint/lite2" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" ) // CheckValidityAndUpdateState checks if the provided header is valid and updates // the consensus state if appropriate. It returns an error if: // - the client or header provided are not parseable to tendermint types // - the header is invalid // - header height is lower than the latest client height // - header valset commit verification fails // // Tendermint client validity checking uses the bisection algorithm described // in the [Tendermint spec](https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md). func CheckValidityAndUpdateState( clientState clientexported.ClientState, header clientexported.Header, currentTimestamp time.Time, ) (clientexported.ClientState, clientexported.ConsensusState, error) { tmClientState, ok := clientState.(types.ClientState) if !ok { return nil, nil, sdkerrors.Wrap( clienttypes.ErrInvalidClientType, "light client is not from Tendermint", ) } tmHeader, ok := header.(types.Header) if !ok { return nil, nil, sdkerrors.Wrap( clienttypes.ErrInvalidHeader, "header is not from Tendermint", ) } if err := checkValidity(tmClientState, tmHeader, currentTimestamp); err != nil { return nil, nil, err } tmClientState, consensusState := update(tmClientState, tmHeader) return tmClientState, consensusState, nil } // checkValidity checks if the Tendermint header is valid. // // CONTRACT: assumes header.Height > consensusState.Height func checkValidity( clientState types.ClientState, header types.Header, currentTimestamp time.Time, ) error { // assert trusting period has not yet passed if currentTimestamp.Sub(clientState.GetLatestTimestamp()) >= clientState.TrustingPeriod { return errors.New("trusting period since last client timestamp already passed") } // assert header timestamp is not past the trusting period if header.Time.Sub(clientState.GetLatestTimestamp()) >= clientState.TrustingPeriod { return sdkerrors.Wrap( clienttypes.ErrInvalidHeader, "header blocktime is outside trusting period from last client timestamp", ) } // assert header timestamp is past latest clientstate timestamp if header.Time.Unix() <= clientState.GetLatestTimestamp().Unix() { return sdkerrors.Wrapf( clienttypes.ErrInvalidHeader, "header blocktime ≤ latest client state block time (%s ≤ %s)", header.Time.String(), clientState.GetLatestTimestamp().String(), ) } // assert header height is newer than any we know if header.GetHeight() <= clientState.GetLatestHeight() { return sdkerrors.Wrapf( clienttypes.ErrInvalidHeader, "header height ≤ latest client state height (%d ≤ %d)", header.GetHeight(), clientState.GetLatestHeight(), ) } // Verify next header with the last header's validatorset as trusted validatorset err := lite.Verify( clientState.GetChainID(), &clientState.LastHeader.SignedHeader, clientState.LastHeader.ValidatorSet, &header.SignedHeader, header.ValidatorSet, clientState.TrustingPeriod, currentTimestamp, clientState.MaxClockDrift, lite.DefaultTrustLevel, ) if err != nil { return sdkerrors.Wrap(clienttypes.ErrInvalidHeader, err.Error()) } return nil } // update the consensus state from a new header func update(clientState types.ClientState, header types.Header) (types.ClientState, types.ConsensusState) { clientState.LastHeader = header consensusState := types.ConsensusState{ Height: uint64(header.Height), Timestamp: header.Time, Root: commitmenttypes.NewMerkleRoot(header.AppHash), ValidatorSet: header.ValidatorSet, } return clientState, consensusState }