cosmos-sdk/x/ibc/07-tendermint/update.go

150 lines
5.8 KiB
Go

package tendermint
import (
"bytes"
"time"
lite "github.com/tendermint/tendermint/lite2"
tmtypes "github.com/tendermint/tendermint/types"
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 if valid it will:
// create the consensus state for the header.Height
// and update the client state if the header height is greater than the latest client state height
// It returns an error if:
// - the client or header provided are not parseable to tendermint types
// - the header is invalid
// - header height is less than or equal to the consensus state height
// - header valset commit verification fails
// - header timestamp is past the trusting period in relation to the consensus state
// - header timestamp is less than or equal to the consensus state timestamp
//
// UpdateClient may be used to either create a consensus state for:
// - a future height greater than the latest client state height
// - a past height that was skipped during bisection
// If we are updating to a past height, a consensus state is created for that height to be persisted in client store
// If we are updating to a future height, the consensus state is created and the client state is updated to reflect
// the new latest height
// 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, consState clientexported.ConsensusState,
header clientexported.Header, currentTimestamp time.Time,
) (clientexported.ClientState, clientexported.ConsensusState, error) {
tmClientState, ok := clientState.(*types.ClientState)
if !ok {
return nil, nil, sdkerrors.Wrapf(
clienttypes.ErrInvalidClientType, "expected type %T, got %T", types.ClientState{}, clientState,
)
}
tmConsState, ok := consState.(*types.ConsensusState)
if !ok {
return nil, nil, sdkerrors.Wrapf(
clienttypes.ErrInvalidConsensus, "expected type %T, got %T", types.ConsensusState{}, consState,
)
}
tmHeader, ok := header.(types.Header)
if !ok {
return nil, nil, sdkerrors.Wrapf(
clienttypes.ErrInvalidHeader, "expected type %T, got %T", types.Header{}, header,
)
}
if err := checkValidity(tmClientState, tmConsState, tmHeader, currentTimestamp); err != nil {
return nil, nil, err
}
tmClientState, consensusState := update(tmClientState, tmHeader)
return tmClientState, consensusState, nil
}
// checkTrustedHeader checks that consensus state matches trusted fields of Header
func checkTrustedHeader(header types.Header, consState *types.ConsensusState) error {
if header.TrustedHeight != consState.Height {
return sdkerrors.Wrapf(
types.ErrInvalidHeaderHeight,
"trusted header height %d does not match consensus state height %d",
header.TrustedHeight, consState.Height,
)
}
// assert that trustedVals is NextValidators of last trusted header
// to do this, we check that trustedVals.Hash() == consState.NextValidatorsHash
tvalHash := header.TrustedValidators.Hash()
if !bytes.Equal(consState.NextValidatorsHash, tvalHash) {
return sdkerrors.Wrapf(
types.ErrInvalidValidatorSet,
"trusted validators %s, does not hash to latest trusted validators. Expected: %X, got: %X",
header.TrustedValidators, consState.NextValidatorsHash, tvalHash,
)
}
return nil
}
// checkValidity checks if the Tendermint header is valid.
// CONTRACT: consState.Height == header.TrustedHeight
func checkValidity(
clientState *types.ClientState, consState *types.ConsensusState,
header types.Header, currentTimestamp time.Time,
) error {
if err := checkTrustedHeader(header, consState); err != nil {
return err
}
// assert header height is newer than consensus state
if header.GetHeight() <= consState.Height {
return sdkerrors.Wrapf(
clienttypes.ErrInvalidHeader,
"header height ≤ consensus state height (%d ≤ %d)", header.GetHeight(), consState.Height,
)
}
// Construct a trusted header using the fields in consensus state
// Only Height, Time, and NextValidatorsHash are necessary for verification
trustedHeader := tmtypes.Header{
Height: int64(consState.Height),
Time: consState.Timestamp,
NextValidatorsHash: consState.NextValidatorsHash,
}
signedHeader := tmtypes.SignedHeader{
Header: &trustedHeader,
}
// Verify next header with the passed-in trustedVals
// - asserts trusting period not passed
// - assert header timestamp is not past the trusting period
// - assert header timestamp is past latest stored consensus state timestamp
// - assert that a TrustLevel proportion of TrustedValidators signed new Commit
err := lite.Verify(
clientState.GetChainID(), &signedHeader,
header.TrustedValidators, &header.SignedHeader, header.ValidatorSet,
clientState.TrustingPeriod, currentTimestamp, clientState.MaxClockDrift, clientState.TrustLevel.ToTendermint(),
)
if err != nil {
return sdkerrors.Wrap(err, "failed to verify header")
}
return nil
}
// update the consensus state from a new header
func update(clientState *types.ClientState, header types.Header) (*types.ClientState, *types.ConsensusState) {
if uint64(header.Height) > clientState.LatestHeight {
clientState.LatestHeight = uint64(header.Height)
}
consensusState := &types.ConsensusState{
Height: uint64(header.Height),
Timestamp: header.Time,
Root: commitmenttypes.NewMerkleRoot(header.AppHash),
NextValidatorsHash: header.NextValidatorsHash,
}
return clientState, consensusState
}