Repository: cosmos/relayer
Pull request #806 (2022-06-22 23:36:15)

Event processor - MsgUpdateClient
### `MsgUpdateClient` assembly and necessary components

Introduces provider types `IBCHeader`, `ClientState`, and `ClientTrustedState`:

- `IBCHeader` is an interface type to be used for multiple chain implementations. For cosmos chains, the implementation is `CosmosIBCHeader`, which is composed of a `SignedHeader` and a `ValidatorSet` - the components needed at different block heights to update the clients.
- `ClientState` is the state of a client from one chain's perspective (`ClientID` and `ConsensusHeight`).
- `ClientTrustedState` is the state of a client from a chain coupled with the trusted `IBCHeader` for the client state's `ConsensusHeight` from the counterparty chain.

In order to construct a `MsgUpdateClient` message for cosmos chains when the counterparty chain is also a cosmos chain, we need to prove trust for the current consensus height and the new height we are updating to. This requires:
- The current client consensus height (trusted height of the counterparty chain). This is included in `ClientState` as `ConsensusHeight` when client updates are observed in the `CosmosChainProcessor`. This is shared with the `PathProcessor`, and later included in building the `ClientTrustedState` as `ClientState.ConsensusHeight`.
- The `ValidatorSet` of the counterparty chain at `ClientState.ConsensusHeight.RevisionHeight + 1`. The `PathProcessor` will correlate the `ClientState` updates from one chain against the `IBCHeader` updates of the counterparty chain. When it sees that the `IBCHeader` is available for the height + 1, it will construct the `ClientTrustedState`, which couples the `IBCHeader` (containing the `ValidatorSet`) with the `ClientState`. This coupled type is then passed to the `MsgUpdateClient` provider assembly function when ready to construct the update client message.
- The latest block `SignedHeader` and `ValidatorSet` of the counterparty chain. This is used to update the client's consensus height to the latest.

`IBCHeader` is left open to future provider implementations. The `CosmosProvider` currently only handles the `CosmosIBCHeader` of the counterparty. To make cosmos and non-cosmos chains interoperable in the future, cases will need to be added to the switch case in the `MsgUpdateClient` provider implementation for the different `IBCHeader` types.


Patch:
diff --git a/relayer/chains/cosmos/cosmos_chain_processor.go b/relayer/chains/cosmos/cosmos_chain_processor.go
index e0d522a..94104e3 100644
--- a/relayer/chains/cosmos/cosmos_chain_processor.go
+++ b/relayer/chains/cosmos/cosmos_chain_processor.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/avast/retry-go/v4"
 	cosmosClient "github.com/cosmos/cosmos-sdk/client"
+	clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
 	"github.com/cosmos/relayer/v2/relayer/processor"
 	"github.com/cosmos/relayer/v2/relayer/provider"
 	"github.com/cosmos/relayer/v2/relayer/provider/cosmos"
@@ -72,7 +73,7 @@ func NewCosmosChainProcessor(log *zap.Logger, provider *cosmos.CosmosProvider, r
 }
 
 const (
-	latestHeightQueryTimeout    = 5 * time.Second
+	queryTimeout                = 5 * time.Second
 	latestHeightQueryRetryDelay = 1 * time.Second
 	latestHeightQueryRetries    = 5
 
@@ -88,7 +89,7 @@ type msgHandlerParams struct {
 	ibcMessagesCache processor.IBCMessagesCache
 }
 
-var messageHandlers = map[string]func(*CosmosChainProcessor, msgHandlerParams) bool{
+var messageHandlers = map[string]func(*CosmosChainProcessor, msgHandlerParams){
 	processor.MsgTransfer:        (*CosmosChainProcessor).handleMsgTransfer,
 	processor.MsgRecvPacket:      (*CosmosChainProcessor).handleMsgRecvPacket,
 	processor.MsgAcknowledgement: (*CosmosChainProcessor).handleMsgAcknowledgement,
@@ -132,7 +133,7 @@ func (ccp *CosmosChainProcessor) SetPathProcessors(pathProcessors processor.Path
 // It will delay by latestHeightQueryRetryDelay between attempts, up to latestHeightQueryRetries.
 func (ccp *CosmosChainProcessor) latestHeightWithRetry(ctx context.Context) (latestHeight int64, err error) {
 	return latestHeight, retry.Do(func() error {
-		latestHeightQueryCtx, cancelLatestHeightQueryCtx := context.WithTimeout(ctx, latestHeightQueryTimeout)
+		latestHeightQueryCtx, cancelLatestHeightQueryCtx := context.WithTimeout(ctx, queryTimeout)
 		defer cancelLatestHeightQueryCtx()
 		var err error
 		latestHeight, err = ccp.chainProvider.QueryLatestHeight(latestHeightQueryCtx)
@@ -147,6 +148,25 @@ func (ccp *CosmosChainProcessor) latestHeightWithRetry(ctx context.Context) (lat
 	}))
 }
 
+// clientState will return the most recent client state if client messages
+// have already been observed for the clientID, otherwise it will query for it.
+func (ccp *CosmosChainProcessor) clientState(ctx context.Context, clientID string) (provider.ClientState, error) {
+	if state, ok := ccp.latestClientState[clientID]; ok {
+		return state, nil
+	}
+	cs, err := ccp.chainProvider.QueryClientState(ctx, int64(ccp.latestBlock.Height), clientID)
+	if err != nil {
+		return provider.ClientState{}, err
+	}
+	clientState := provider.ClientState{
+		ClientID:        clientID,
+		ConsensusHeight: cs.GetLatestHeight().(clienttypes.Height),
+	}
+	ccp.latestClientState[clientID] = clientState
+	return clientState, nil
+}
+
+// queryCyclePersistence hold the variables that should be retained across queryCycles.
 type queryCyclePersistence struct {
 	latestHeight         int64
 	latestQueriedBlock   int64
@@ -242,18 +262,26 @@ func (ccp *CosmosChainProcessor) queryCycle(ctx context.Context, persistence *qu
 
 	ibcMessagesCache := processor.NewIBCMessagesCache()
 
+	ibcHeaderCache := make(processor.IBCHeaderCache)
+
 	ppChanged := false
 
+	var latestHeader cosmos.CosmosIBCHeader
+
 	for i := persistence.latestQueriedBlock + 1; i <= persistence.latestHeight; i++ {
 		var eg errgroup.Group
 		var blockRes *ctypes.ResultBlockResults
 		var lightBlock *tmtypes.LightBlock
 		eg.Go(func() (err error) {
-			blockRes, err = ccp.cc.Client.BlockResults(ctx, &i)
+			queryCtx, cancelQueryCtx := context.WithTimeout(ctx, queryTimeout)
+			defer cancelQueryCtx()
+			blockRes, err = ccp.cc.Client.BlockResults(queryCtx, &i)
 			return err
 		})
 		eg.Go(func() (err error) {
-			lightBlock, err = ccp.lightProvider.LightBlock(ctx, i)
+			queryCtx, cancelQueryCtx := context.WithTimeout(ctx, queryTimeout)
+			defer cancelQueryCtx()
+			lightBlock, err = ccp.lightProvider.LightBlock(queryCtx, i)
 			return err
 		})
 
@@ -267,6 +295,14 @@ func (ccp *CosmosChainProcessor) queryCycle(ctx context.Context, persistence *qu
 			Time:   lightBlock.Header.Time,
 		}
 
+		latestHeader = cosmos.CosmosIBCHeader{
+			SignedHeader: lightBlock.SignedHeader,
+			ValidatorSet: lightBlock.ValidatorSet,
+		}
+
+		ibcHeaderCache[uint64(i)] = latestHeader
+		ppChanged = true
+
 		for _, tx := range blockRes.TxsResults {
 			if tx.Code != 0 {
 				// tx was not successful
@@ -282,11 +318,10 @@ func (ccp *CosmosChainProcessor) queryCycle(ctx context.Context, persistence *qu
 					continue
 				}
 				// call message handler for this ibc message type. can do things like cache things on the chain processor or retain ibc messages that should be sent to the PathProcessors.
-				changed := handler(ccp, msgHandlerParams{
+				handler(ccp, msgHandlerParams{
 					messageInfo:      m.messageInfo,
 					ibcMessagesCache: ibcMessagesCache,
 				})
-				ppChanged = ppChanged || changed
 			}
 		}
 	}
@@ -304,12 +339,25 @@ func (ccp *CosmosChainProcessor) queryCycle(ctx context.Context, persistence *qu
 	chainID := ccp.chainProvider.ChainId()
 
 	for _, pp := range ccp.pathProcessors {
+		clientID := pp.RelevantClientID(chainID)
+		clientState, err := ccp.clientState(ctx, clientID)
+		if err != nil {
+			ccp.log.Error("Error fetching client state",
+				zap.String("client_id", clientID),
+				zap.Error(err),
+			)
+			continue
+		}
+
 		pp.HandleNewData(chainID, processor.ChainProcessorCacheData{
 			LatestBlock:          ccp.latestBlock,
+			LatestHeader:         latestHeader,
 			IBCMessagesCache:     ibcMessagesCache,
 			InSync:               ccp.inSync,
-			ConnectionStateCache: ccp.connectionStateCache,
-			ChannelStateCache:    ccp.channelStateCache,
+			ClientState:          clientState,
+			ConnectionStateCache: ccp.connectionStateCache.Clone(),
+			ChannelStateCache:    ccp.channelStateCache.Clone(),
+			IBCHeaderCache:       ibcHeaderCache,
 		})
 	}
 
diff --git a/relayer/chains/cosmos/msg_handlers_channel.go b/relayer/chains/cosmos/msg_handlers_channel.go
index 6f55c8a..acef81a 100644
--- a/relayer/chains/cosmos/msg_handlers_channel.go
+++ b/relayer/chains/cosmos/msg_handlers_channel.go
@@ -13,7 +13,7 @@ import (
 // the counterparty chain, and a MsgChannelOpenAck is not detected yet on this chain,
 // a MsgChannelOpenTry will be sent to the counterparty chain using this information
 // with the channel open init proof from this chain added.
-func (ccp *CosmosChainProcessor) handleMsgChannelOpenInit(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgChannelOpenInit(p msgHandlerParams) {
 	ci := p.messageInfo.(*channelInfo)
 	k := ci.channelKey()
 	p.ibcMessagesCache.ChannelHandshake.Retain(k, processor.MsgChannelOpenInit, cosmos.NewCosmosMessage(&chantypes.MsgChannelOpenTry{
@@ -30,7 +30,6 @@ func (ccp *CosmosChainProcessor) handleMsgChannelOpenInit(p msgHandlerParams) bo
 	// Setting false for open state because channel is not open until MsgChannelOpenAck on this chain.
 	ccp.channelStateCache[k] = false
 	ccp.logChannelMessage("MsgChannelOpenInit", ci)
-	return true
 }
 
 // handleMsgChannelOpenTry will construct the start of the MsgChannelOpenAck
@@ -39,7 +38,7 @@ func (ccp *CosmosChainProcessor) handleMsgChannelOpenInit(p msgHandlerParams) bo
 // a MsgChannelOpenConfirm is not detected yet on this chain, a MsgChannelOpenAck
 // will be sent to the counterparty chain using this information with the
 // channel open try proof from this chain added.
-func (ccp *CosmosChainProcessor) handleMsgChannelOpenTry(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgChannelOpenTry(p msgHandlerParams) {
 	ci := p.messageInfo.(*channelInfo)
 	// using flipped counterparty since counterparty initialized this handshake
 	k := ci.channelKey().Counterparty()
@@ -51,7 +50,6 @@ func (ccp *CosmosChainProcessor) handleMsgChannelOpenTry(p msgHandlerParams) boo
 	// Setting false for open state because channel is not open until MsgChannelOpenConfirm on this chain.
 	ccp.channelStateCache[k] = false
 	ccp.logChannelMessage("MsgChannelOpenTry", ci)
-	return true
 }
 
 // handleMsgChannelOpenAck will construct the start of the MsgChannelOpenConfirm
@@ -59,7 +57,7 @@ func (ccp *CosmosChainProcessor) handleMsgChannelOpenTry(p msgHandlerParams) boo
 // For example, if a MsgChannelOpenConfirm is not detected on the counterparty chain,
 // a MsgChannelOpenConfirm will be sent to the counterparty chain
 // using this information with the channel open ack proof from this chain added.
-func (ccp *CosmosChainProcessor) handleMsgChannelOpenAck(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgChannelOpenAck(p msgHandlerParams) {
 	ci := p.messageInfo.(*channelInfo)
 	k := ci.channelKey()
 	p.ibcMessagesCache.ChannelHandshake.Retain(k, processor.MsgChannelOpenAck, cosmos.NewCosmosMessage(&chantypes.MsgChannelOpenConfirm{
@@ -68,21 +66,19 @@ func (ccp *CosmosChainProcessor) handleMsgChannelOpenAck(p msgHandlerParams) boo
 	}))
 	ccp.channelStateCache[k] = true
 	ccp.logChannelMessage("MsgChannelOpenAck", ci)
-	return true
 }
 
 // handleMsgChannelOpenConfirm will retain a nil message here because this is for
 // book-keeping in the PathProcessor cache only. A message does not need to be constructed
 // for the counterparty chain after the MsgChannelOpenConfirm is observed, but we want to
 // tell the PathProcessor that the channel handshake is complete for this channel.
-func (ccp *CosmosChainProcessor) handleMsgChannelOpenConfirm(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgChannelOpenConfirm(p msgHandlerParams) {
 	ci := p.messageInfo.(*channelInfo)
 	// using flipped counterparty since counterparty initialized this handshake
 	k := ci.channelKey().Counterparty()
 	p.ibcMessagesCache.ChannelHandshake.Retain(k, processor.MsgChannelOpenConfirm, nil)
 	ccp.channelStateCache[k] = true
 	ccp.logChannelMessage("MsgChannelOpenConfirm", ci)
-	return true
 }
 
 // handleMsgChannelCloseInit will construct the start of the MsgChannelCloseConfirm
@@ -90,7 +86,7 @@ func (ccp *CosmosChainProcessor) handleMsgChannelOpenConfirm(p msgHandlerParams)
 // For example, if a MsgChannelCloseConfirm is not detected on the counterparty chain,
 // a MsgChannelCloseConfirm will be sent to the counterparty chain
 // using this information with the channel close init proof from this chain added.
-func (ccp *CosmosChainProcessor) handleMsgChannelCloseInit(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgChannelCloseInit(p msgHandlerParams) {
 	ci := p.messageInfo.(*channelInfo)
 	k := ci.channelKey()
 	p.ibcMessagesCache.ChannelHandshake.Retain(k, processor.MsgChannelCloseInit, cosmos.NewCosmosMessage(&chantypes.MsgChannelCloseConfirm{
@@ -99,21 +95,19 @@ func (ccp *CosmosChainProcessor) handleMsgChannelCloseInit(p msgHandlerParams) b
 	}))
 	ccp.channelStateCache[k] = false
 	ccp.logChannelMessage("MsgChannelCloseInit", ci)
-	return true
 }
 
 // handleMsgChannelCloseConfirm will retain a nil message because this is for
 // book-keeping in the PathProcessor cache only. A message does not need to be constructed
 // for the counterparty chain after the MsgChannelCloseConfirm is observed, but we want to
 // tell the PathProcessor that the channel close is complete for this channel.
-func (ccp *CosmosChainProcessor) handleMsgChannelCloseConfirm(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgChannelCloseConfirm(p msgHandlerParams) {
 	ci := p.messageInfo.(*channelInfo)
 	// using flipped counterparty since counterparty initialized this channel close
 	k := ci.channelKey().Counterparty()
 	p.ibcMessagesCache.ChannelHandshake.Retain(k, processor.MsgChannelCloseConfirm, nil)
 	ccp.channelStateCache[k] = false
 	ccp.logChannelMessage("MsgChannelCloseConfirm", ci)
-	return true
 }
 
 func (ccp *CosmosChainProcessor) logChannelMessage(message string, channelInfo *channelInfo) {
diff --git a/relayer/chains/cosmos/msg_handlers_client.go b/relayer/chains/cosmos/msg_handlers_client.go
index f8d7218..e8c4f48 100644
--- a/relayer/chains/cosmos/msg_handlers_client.go
+++ b/relayer/chains/cosmos/msg_handlers_client.go
@@ -4,34 +4,30 @@ import (
 	"go.uber.org/zap"
 )
 
-func (ccp *CosmosChainProcessor) handleMsgCreateClient(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgCreateClient(p msgHandlerParams) {
 	clientInfo := p.messageInfo.(*clientInfo)
 	// save the latest consensus height and header for this client
 	ccp.latestClientState.UpdateLatestClientState(*clientInfo)
 	ccp.logObservedIBCMessage("MsgCreateClient", zap.String("client_id", clientInfo.clientID))
-	return false
 }
 
-func (ccp *CosmosChainProcessor) handleMsgUpdateClient(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgUpdateClient(p msgHandlerParams) {
 	clientInfo := p.messageInfo.(*clientInfo)
 	// save the latest consensus height and header for this client
 	ccp.latestClientState.UpdateLatestClientState(*clientInfo)
 	ccp.logObservedIBCMessage("MsgUpdateClient", zap.String("client_id", clientInfo.clientID))
-	return false
 }
 
-func (ccp *CosmosChainProcessor) handleMsgUpgradeClient(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgUpgradeClient(p msgHandlerParams) {
 	clientInfo := p.messageInfo.(*clientInfo)
 	// save the latest consensus height and header for this client
 	ccp.latestClientState.UpdateLatestClientState(*clientInfo)
 	ccp.logObservedIBCMessage("MsgUpgradeClient", zap.String("client_id", clientInfo.clientID))
-	return false
 }
 
-func (ccp *CosmosChainProcessor) handleMsgSubmitMisbehaviour(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgSubmitMisbehaviour(p msgHandlerParams) {
 	clientInfo := p.messageInfo.(*clientInfo)
 	// save the latest consensus height and header for this client
 	ccp.latestClientState.UpdateLatestClientState(*clientInfo)
 	ccp.logObservedIBCMessage("MsgSubmitMisbehaviour", zap.String("client_id", clientInfo.clientID))
-	return false
 }
diff --git a/relayer/chains/cosmos/msg_handlers_connection.go b/relayer/chains/cosmos/msg_handlers_connection.go
index fc6f8d1..3d331fb 100644
--- a/relayer/chains/cosmos/msg_handlers_connection.go
+++ b/relayer/chains/cosmos/msg_handlers_connection.go
@@ -13,7 +13,7 @@ import (
 // not detected on the counterparty chain, and a MsgConnectionOpenAck is not detected
 // yet on this chain, a MsgConnectionOpenTry will be sent to the counterparty chain
 // using this information with the connection init proof from this chain added.
-func (ccp *CosmosChainProcessor) handleMsgConnectionOpenInit(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgConnectionOpenInit(p msgHandlerParams) {
 	ci := p.messageInfo.(*connectionInfo)
 	k := ci.connectionKey()
 	p.ibcMessagesCache.ConnectionHandshake.Retain(k, processor.MsgConnectionOpenInit, cosmos.NewCosmosMessage(&conntypes.MsgConnectionOpenTry{
@@ -26,7 +26,6 @@ func (ccp *CosmosChainProcessor) handleMsgConnectionOpenInit(p msgHandlerParams)
 	}))
 	ccp.connectionStateCache[k] = false
 	ccp.logConnectionMessage("MsgConnectionOpenInit", ci)
-	return true
 }
 
 // handleMsgConnectionOpenTry will construct the start of the MsgConnectionOpenAck
@@ -35,7 +34,7 @@ func (ccp *CosmosChainProcessor) handleMsgConnectionOpenInit(p msgHandlerParams)
 // and a MsgConnectionOpenConfirm is not detected yet on this chain,
 // a MsgConnectionOpenAck will be sent to the counterparty chain
 // using this information with the connection try proof from this chain added.
-func (ccp *CosmosChainProcessor) handleMsgConnectionOpenTry(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgConnectionOpenTry(p msgHandlerParams) {
 	ci := p.messageInfo.(*connectionInfo)
 	k := ci.connectionKey().Counterparty()
 	p.ibcMessagesCache.ConnectionHandshake.Retain(k, processor.MsgConnectionOpenTry, cosmos.NewCosmosMessage(&conntypes.MsgConnectionOpenAck{
@@ -44,7 +43,6 @@ func (ccp *CosmosChainProcessor) handleMsgConnectionOpenTry(p msgHandlerParams)
 	}))
 	ccp.connectionStateCache[k] = false
 	ccp.logConnectionMessage("MsgConnectionOpenTry", ci)
-	return true
 }
 
 // handleMsgConnectionOpenAck will construct the start of the MsgConnectionOpenConfirm
@@ -52,7 +50,7 @@ func (ccp *CosmosChainProcessor) handleMsgConnectionOpenTry(p msgHandlerParams)
 // For example, if a MsgConnectionOpenConfirm is not detected on the counterparty chain,
 // a MsgConnectionOpenConfirm will be sent to the counterparty chain
 // using this information with the connection ack proof from this chain added.
-func (ccp *CosmosChainProcessor) handleMsgConnectionOpenAck(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgConnectionOpenAck(p msgHandlerParams) {
 	ci := p.messageInfo.(*connectionInfo)
 	k := ci.connectionKey()
 	p.ibcMessagesCache.ConnectionHandshake.Retain(k, processor.MsgConnectionOpenAck, cosmos.NewCosmosMessage(&conntypes.MsgConnectionOpenConfirm{
@@ -60,20 +58,18 @@ func (ccp *CosmosChainProcessor) handleMsgConnectionOpenAck(p msgHandlerParams)
 	}))
 	ccp.connectionStateCache[k] = true
 	ccp.logConnectionMessage("MsgConnectionOpenAck", ci)
-	return true
 }
 
 // handleMsgConnectionOpenConfirm will retaining a nil message here.
 // This is for book-keeping in the PathProcessor cache only.
 // A message does not need to be constructed for the counterparty chain after the MsgConnectionOpenConfirm is observed,
 // but we want to tell the PathProcessor that the connection handshake is complete for this sequence.
-func (ccp *CosmosChainProcessor) handleMsgConnectionOpenConfirm(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgConnectionOpenConfirm(p msgHandlerParams) {
 	ci := p.messageInfo.(*connectionInfo)
 	k := ci.connectionKey().Counterparty()
 	p.ibcMessagesCache.ConnectionHandshake.Retain(k, processor.MsgConnectionOpenConfirm, nil)
 	ccp.connectionStateCache[k] = true
 	ccp.logConnectionMessage("MsgConnectionOpenConfirm", ci)
-	return true
 }
 
 func (ccp *CosmosChainProcessor) logConnectionMessage(message string, connectionInfo *connectionInfo) {
diff --git a/relayer/chains/cosmos/msg_handlers_packet.go b/relayer/chains/cosmos/msg_handlers_packet.go
index 74ddda9..6ca057d 100644
--- a/relayer/chains/cosmos/msg_handlers_packet.go
+++ b/relayer/chains/cosmos/msg_handlers_packet.go
@@ -7,13 +7,13 @@ import (
 	"go.uber.org/zap"
 )
 
-func (ccp *CosmosChainProcessor) handleMsgTransfer(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgTransfer(p msgHandlerParams) {
 	pi := p.messageInfo.(*packetInfo)
 	// source chain processor will call this handler
 	// source channel used as key because MsgTransfer is sent to source chain
 	channelKey := pi.channelKey()
 	if !p.ibcMessagesCache.PacketFlow.ShouldRetainSequence(ccp.pathProcessors, channelKey, ccp.chainProvider.ChainId(), processor.MsgTransfer, pi.packet.Sequence) {
-		return false
+		return
 	}
 	// Construct the start of the MsgRecvPacket for the counterparty chain.
 	// PathProcessor will determine if this is needed.
@@ -38,16 +38,15 @@ func (ccp *CosmosChainProcessor) handleMsgTransfer(p msgHandlerParams) bool {
 		zap.Uint64("timeout_height_revision", pi.packet.TimeoutHeight.RevisionNumber),
 		zap.Uint64("timeout_timestamp", pi.packet.TimeoutTimestamp),
 	)
-	return true
 }
 
-func (ccp *CosmosChainProcessor) handleMsgRecvPacket(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgRecvPacket(p msgHandlerParams) {
 	pi := p.messageInfo.(*packetInfo)
 	// destination chain processor will call this handler
 	// destination channel used because MsgRecvPacket is sent to destination chain
 	channelKey := pi.channelKey().Counterparty()
 	if !p.ibcMessagesCache.PacketFlow.ShouldRetainSequence(ccp.pathProcessors, channelKey, ccp.chainProvider.ChainId(), processor.MsgRecvPacket, pi.packet.Sequence) {
-		return false
+		return
 	}
 	// Construct the start of the MsgAcknowledgement for the counterparty chain.
 	// PathProcessor will determine if this is needed.
@@ -68,54 +67,50 @@ func (ccp *CosmosChainProcessor) handleMsgRecvPacket(p msgHandlerParams) bool {
 		Acknowledgement: pi.ack,
 	}))
 	ccp.logPacketMessage("MsgRecvPacket", pi)
-	return true
 }
 
-func (ccp *CosmosChainProcessor) handleMsgAcknowledgement(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgAcknowledgement(p msgHandlerParams) {
 	pi := p.messageInfo.(*packetInfo)
 	// source chain processor will call this handler
 	// source channel used as key because MsgAcknowledgement is sent to source chain
 	channelKey := pi.channelKey()
 	if !p.ibcMessagesCache.PacketFlow.ShouldRetainSequence(ccp.pathProcessors, channelKey, ccp.chainProvider.ChainId(), processor.MsgAcknowledgement, pi.packet.Sequence) {
-		return false
+		return
 	}
 	// Retaining a nil message here because this is for book-keeping in the PathProcessor cache only.
 	// A message does not need to be constructed for the counterparty chain after the MsgAcknowledgement is observed,
 	// but we want to tell the PathProcessor that the packet flow is complete for this sequence.
 	p.ibcMessagesCache.PacketFlow.Retain(channelKey, processor.MsgAcknowledgement, pi.packet.Sequence, nil)
 	ccp.logPacketMessage("MsgAcknowledgement", pi)
-	return true
 }
 
-func (ccp *CosmosChainProcessor) handleMsgTimeout(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgTimeout(p msgHandlerParams) {
 	pi := p.messageInfo.(*packetInfo)
 	// source chain processor will call this handler
 	// source channel used as key because MsgTimeout is sent to source chain
 	channelKey := pi.channelKey()
 	if !p.ibcMessagesCache.PacketFlow.ShouldRetainSequence(ccp.pathProcessors, channelKey, ccp.chainProvider.ChainId(), processor.MsgTimeout, pi.packet.Sequence) {
-		return false
+		return
 	}
 	// Retaining a nil message here because this is for book-keeping in the PathProcessor cache only.
 	// A message does not need to be constructed for the counterparty chain after the MsgTimeout is observed,
 	// but we want to tell the PathProcessor that the packet flow is complete for this sequence.
 	p.ibcMessagesCache.PacketFlow.Retain(channelKey, processor.MsgTimeout, pi.packet.Sequence, nil)
 	ccp.logPacketMessage("MsgTimeout", pi)
-	return true
 }
 
-func (ccp *CosmosChainProcessor) handleMsgTimeoutOnClose(p msgHandlerParams) bool {
+func (ccp *CosmosChainProcessor) handleMsgTimeoutOnClose(p msgHandlerParams) {
 	pi := p.messageInfo.(*packetInfo)
 	// source channel used because timeout is sent to source chain
 	channelKey := pi.channelKey()
 	if !p.ibcMessagesCache.PacketFlow.ShouldRetainSequence(ccp.pathProcessors, channelKey, ccp.chainProvider.ChainId(), processor.MsgTimeoutOnClose, pi.packet.Sequence) {
-		return false
+		return
 	}
 	// Retaining a nil message here because this is for book-keeping in the PathProcessor cache only.
 	// A message does not need to be constructed for the counterparty chain after the MsgTimeoutOnClose is observed,
 	// but we want to tell the PathProcessor that the packet flow is complete for this sequence.
 	p.ibcMessagesCache.PacketFlow.Retain(channelKey, processor.MsgTimeoutOnClose, pi.packet.Sequence, nil)
 	ccp.logPacketMessage("MsgTimeoutOnClose", pi)
-	return true
 }
 
 func (ccp *CosmosChainProcessor) logPacketMessage(message string, pi *packetInfo, additionalFields ...zap.Field) {
diff --git a/relayer/chains/cosmos/types.go b/relayer/chains/cosmos/types.go
index 4af9b0c..79ce286 100644
--- a/relayer/chains/cosmos/types.go
+++ b/relayer/chains/cosmos/types.go
@@ -4,6 +4,7 @@ import (
 	clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
 	chantypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types"
 	"github.com/cosmos/relayer/v2/relayer/processor"
+	"github.com/cosmos/relayer/v2/relayer/provider"
 )
 
 // ibcMessage is the type used for parsing all possible properties of IBC messages
@@ -76,35 +77,23 @@ type clientInfo struct {
 	header          []byte
 }
 
+func (c clientInfo) ClientState() provider.ClientState {
+	return provider.ClientState{
+		ClientID:        c.clientID,
+		ConsensusHeight: c.consensusHeight,
+	}
+}
+
 // latestClientState is a map of clientID to the latest clientInfo for that client.
-type latestClientState map[string]clientInfo
+type latestClientState map[string]provider.ClientState
 
 func (l latestClientState) UpdateLatestClientState(clientInfo clientInfo) {
 	existingClientInfo, ok := l[clientInfo.clientID]
-	if ok && clientInfo.consensusHeight.LT(existingClientInfo.consensusHeight) {
+	if ok && clientInfo.consensusHeight.LT(existingClientInfo.ConsensusHeight) {
 		// height is less than latest, so no-op
 		return
 	}
 
 	// update latest if no existing state or provided consensus height is newer
-	l[clientInfo.clientID] = clientInfo
-}
-
-func (l latestClientState) Clone() latestClientState {
-	newLatestClientState := make(latestClientState, len(l))
-	for k, v := range l {
-		newLatestClientState[k] = v
-	}
-	return newLatestClientState
-}
-
-// channelOpenState is a map of channelKey to the latest open state, true/false, for a channel.
-type channelOpenState map[processor.ChannelKey]bool
-
-func (c channelOpenState) Clone() channelOpenState {
-	newChannelOpenState := make(channelOpenState, len(c))
-	for k, v := range c {
-		newChannelOpenState[k] = v
-	}
-	return newChannelOpenState
+	l[clientInfo.clientID] = clientInfo.ClientState()
 }
diff --git a/relayer/channel.go b/relayer/channel.go
index ac77cac..8541cbf 100644
--- a/relayer/channel.go
+++ b/relayer/channel.go
@@ -746,7 +746,7 @@ func (c *Chain) CloseChannelStep(ctx context.Context, dst *Chain, srcChanID, src
 				return nil, false, err
 			}
 
-			updateMsg, err := c.ChainProvider.UpdateClient(c.ClientID(), dstHeader)
+			updateMsg, err := c.ChainProvider.MsgUpdateClient(c.ClientID(), dstHeader)
 			if err != nil {
 				return nil, false, err
 			}
@@ -771,7 +771,7 @@ func (c *Chain) CloseChannelStep(ctx context.Context, dst *Chain, srcChanID, src
 				return nil, false, err
 			}
 
-			updateMsg, err := dst.ChainProvider.UpdateClient(dst.ClientID(), srcHeader)
+			updateMsg, err := dst.ChainProvider.MsgUpdateClient(dst.ClientID(), srcHeader)
 			if err != nil {
 				return nil, false, err
 			}
@@ -800,7 +800,7 @@ func (c *Chain) CloseChannelStep(ctx context.Context, dst *Chain, srcChanID, src
 				return nil, false, err
 			}
 
-			updateMsg, err := dst.ChainProvider.UpdateClient(dst.ClientID(), srcHeader)
+			updateMsg, err := dst.ChainProvider.MsgUpdateClient(dst.ClientID(), srcHeader)
 			if err != nil {
 				return nil, false, err
 			}
@@ -834,7 +834,7 @@ func (c *Chain) CloseChannelStep(ctx context.Context, dst *Chain, srcChanID, src
 				return nil, false, err
 			}
 
-			updateMsg, err := c.ChainProvider.UpdateClient(c.ClientID(), dstHeader)
+			updateMsg, err := c.ChainProvider.MsgUpdateClient(c.ClientID(), dstHeader)
 			if err != nil {
 				return nil, false, err
 			}
diff --git a/relayer/client.go b/relayer/client.go
index 2731206..8d9688d 100644
--- a/relayer/client.go
+++ b/relayer/client.go
@@ -279,7 +279,7 @@ func (c *Chain) UpdateClients(ctx context.Context, dst *Chain) (err error) {
 		return err
 	}
 
-	srcUpdateMsg, err := c.ChainProvider.UpdateClient(c.ClientID(), dstUpdateHeader)
+	srcUpdateMsg, err := c.ChainProvider.MsgUpdateClient(c.ClientID(), dstUpdateHeader)
 	if err != nil {
 		c.log.Debug(
 			"Failed to update source client",
@@ -289,7 +289,7 @@ func (c *Chain) UpdateClients(ctx context.Context, dst *Chain) (err error) {
 		return err
 	}
 
-	dstUpdateMsg, err := dst.ChainProvider.UpdateClient(dst.ClientID(), srcUpdateHeader)
+	dstUpdateMsg, err := dst.ChainProvider.MsgUpdateClient(dst.ClientID(), srcUpdateHeader)
 	if err != nil {
 		dst.log.Debug(
 			"Failed to update destination client",
@@ -342,7 +342,7 @@ func (c *Chain) UpgradeClients(ctx context.Context, dst *Chain, height int64) er
 	}
 
 	// updates off-chain light client
-	updateMsg, err := c.ChainProvider.UpdateClient(c.ClientID(), dstHeader)
+	updateMsg, err := c.ChainProvider.MsgUpdateClient(c.ClientID(), dstHeader)
 	if err != nil {
 		return err
 	}
diff --git a/relayer/naive-strategy.go b/relayer/naive-strategy.go
index d35b893..a90020a 100644
--- a/relayer/naive-strategy.go
+++ b/relayer/naive-strategy.go
@@ -419,11 +419,11 @@ func RelayAcknowledgements(ctx context.Context, log *zap.Logger, src, dst *Chain
 			return err
 		}
 
-		srcUpdateMsg, err := src.ChainProvider.UpdateClient(src.PathEnd.ClientID, dstHeader)
+		srcUpdateMsg, err := src.ChainProvider.MsgUpdateClient(src.PathEnd.ClientID, dstHeader)
 		if err != nil {
 			return err
 		}
-		dstUpdateMsg, err := dst.ChainProvider.UpdateClient(dst.PathEnd.ClientID, srcHeader)
+		dstUpdateMsg, err := dst.ChainProvider.MsgUpdateClient(dst.PathEnd.ClientID, srcHeader)
 		if err != nil {
 			return err
 		}
@@ -675,7 +675,7 @@ func PrependUpdateClientMsg(ctx context.Context, msgs *[]provider.RelayerMessage
 	var updateMsg provider.RelayerMessage
 	if err := retry.Do(func() error {
 		var err error
-		updateMsg, err = dst.ChainProvider.UpdateClient(dst.PathEnd.ClientID, srcHeader)
+		updateMsg, err = dst.ChainProvider.MsgUpdateClient(dst.PathEnd.ClientID, srcHeader)
 		return err
 	}, retry.Context(ctx), RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) {
 		dst.log.Info(
diff --git a/relayer/processor/path_processor.go b/relayer/processor/path_processor.go
index 89ee208..7beadb6 100644
--- a/relayer/processor/path_processor.go
+++ b/relayer/processor/path_processor.go
@@ -14,9 +14,11 @@ import (
 )
 
 const (
-	// DurationErrorRetry determines how long to wait before retrying
+	// durationErrorRetry determines how long to wait before retrying
 	// in the case of failure to send transactions with IBC messages.
-	DurationErrorRetry = 5 * time.Second
+	durationErrorRetry = 5 * time.Second
+
+	ibcHeadersToCache = 10
 )
 
 // PathProcessor is a process that handles incoming IBC messages from a pair of chains.
@@ -44,8 +46,8 @@ func (p PathProcessors) IsRelayedChannel(k ChannelKey, chainID string) bool {
 }
 
 // pathEndRuntime is used at runtime for each chain involved in the path.
-// It holds a channel for incoming messages from the ChainProcessors, which will
-// be processed during Run(ctx).
+// It holds a channel for incoming messages from the ChainProcessors, which are
+// processed during Run(ctx).
 type pathEndRuntime struct {
 	info PathEnd
 
@@ -54,8 +56,12 @@ type pathEndRuntime struct {
 	// cached data
 	latestBlock          provider.LatestBlock
 	messageCache         IBCMessagesCache
+	clientState          provider.ClientState
+	clientTrustedState   provider.ClientTrustedState
 	connectionStateCache ConnectionStateCache
 	channelStateCache    ChannelStateCache
+	latestHeader         provider.IBCHeader
+	ibcHeaderCache       IBCHeaderCache
 
 	// New messages and other data arriving from the handleNewMessagesForPathEnd method.
 	incomingCacheData chan ChainProcessorCacheData
@@ -67,10 +73,20 @@ type pathEndRuntime struct {
 func (pathEnd *pathEndRuntime) MergeCacheData(d ChainProcessorCacheData) {
 	pathEnd.inSync = d.InSync
 	pathEnd.latestBlock = d.LatestBlock
-	// TODO make sure passes channel filter for pathEnd1 before calling this
-	pathEnd.messageCache.Merge(d.IBCMessagesCache)             // Merge incoming packet IBC messages into the backlog
+	pathEnd.latestHeader = d.LatestHeader
+	pathEnd.clientState = d.ClientState
+
+	// TODO messageCache Merge will likely need to move into the PathProcessor, and make sure passes client, connection, or channel filter
+	pathEnd.messageCache.Merge(d.IBCMessagesCache) // Merge incoming packet IBC messages into the backlog
+
+	// TODO connectionStateCache Merge will likely need to move into the PathProcessor, and make sure passes connection filter
 	pathEnd.connectionStateCache.Merge(d.ConnectionStateCache) // Update latest connection open state for chain
-	pathEnd.channelStateCache.Merge(d.ChannelStateCache)       // Update latest channel open state for chain
+
+	// TODO channelStateCache Merge will likely need to move into the PathProcessor, and make sure passes connection filter
+	pathEnd.channelStateCache.Merge(d.ChannelStateCache) // Update latest channel open state for chain
+
+	pathEnd.ibcHeaderCache.Merge(d.IBCHeaderCache)  // Update latest IBC header state
+	pathEnd.ibcHeaderCache.Prune(ibcHeadersToCache) // Only keep most recent IBC headers
 }
 
 func NewPathProcessor(log *zap.Logger, pathEnd1 PathEnd, pathEnd2 PathEnd) *PathProcessor {
@@ -82,6 +98,7 @@ func NewPathProcessor(log *zap.Logger, pathEnd1 PathEnd, pathEnd2 PathEnd) *Path
 			connectionStateCache: make(ConnectionStateCache),
 			channelStateCache:    make(ChannelStateCache),
 			messageCache:         NewIBCMessagesCache(),
+			ibcHeaderCache:       make(IBCHeaderCache),
 		},
 		pathEnd2: &pathEndRuntime{
 			info:                 pathEnd2,
@@ -89,6 +106,7 @@ func NewPathProcessor(log *zap.Logger, pathEnd1 PathEnd, pathEnd2 PathEnd) *Path
 			connectionStateCache: make(ConnectionStateCache),
 			channelStateCache:    make(ChannelStateCache),
 			messageCache:         NewIBCMessagesCache(),
+			ibcHeaderCache:       make(IBCHeaderCache),
 		},
 		retryProcess: make(chan struct{}, 8),
 	}
@@ -109,6 +127,17 @@ type channelPair struct {
 	pathEnd2ChannelKey ChannelKey
 }
 
+// RelevantClientID returns the relevant client ID or panics
+func (pp *PathProcessor) RelevantClientID(chainID string) string {
+	if pp.pathEnd1.info.ChainID == chainID {
+		return pp.pathEnd1.info.ClientID
+	}
+	if pp.pathEnd2.info.ChainID == chainID {
+		return pp.pathEnd2.info.ClientID
+	}
+	panic(fmt.Errorf("no relevant client ID for chain ID: %s", chainID))
+}
+
 func (pp *PathProcessor) channelPairs() []channelPair {
 	// Channel keys are from pathEnd1's perspective
 	channels := make(map[ChannelKey]bool)
@@ -536,19 +565,76 @@ ChannelHandshakeLoop:
 	}
 }
 
-func (pp *PathProcessor) sendMessages(pathEnd *pathEndRuntime, messages []provider.RelayerMessage) error {
+func (pp *PathProcessor) prependMsgUpdateClient(src, dst *pathEndRuntime, messages *[]provider.RelayerMessage) error {
+	// If the client state trusted height is not equal to the client trusted state height, we cannot send a MsgUpdateClient
+	// until another block is observed. But we can send the rest without the MsgUpdateClient.
+	if !dst.clientTrustedState.ClientState.ConsensusHeight.EQ(dst.clientState.ConsensusHeight) {
+		return fmt.Errorf("observed client trusted height: %d does not equal latest client state height: %d",
+			dst.clientTrustedState.ClientState.ConsensusHeight.RevisionHeight, dst.clientState.ConsensusHeight.RevisionHeight)
+	}
+
+	msgUpdateClientHeader, err := src.chainProvider.MsgUpdateClientHeader(src.latestHeader, dst.clientTrustedState.ClientState.ConsensusHeight, dst.clientTrustedState.IBCHeader)
+	if err != nil {
+		return fmt.Errorf("error assembling new client header: %w", err)
+	}
+
+	msgUpdateClient, err := dst.chainProvider.MsgUpdateClient(dst.info.ClientID, msgUpdateClientHeader)
+	if err != nil {
+		return fmt.Errorf("error assembling MsgUpdateClient: %w", err)
+	}
+
+	*messages = append([]provider.RelayerMessage{msgUpdateClient}, *messages...)
+
+	return nil
+}
+
+func (pp *PathProcessor) sendMessages(src, dst *pathEndRuntime, messages []provider.RelayerMessage) error {
 	if len(messages) == 0 {
 		return nil
 	}
-	// TODO construct MsgUpdateClient for this pathEnd, using the latest trusted IBC header from other pathEnd, prepend messages with the MsgUpdateClient, then send the messages to this pathEnd
+
+	if err := pp.prependMsgUpdateClient(src, dst, &messages); err != nil {
+		return fmt.Errorf("error prepending MsgUpdateClient, chain_id: %s, client_id: %s, %w",
+			dst.info.ChainID,
+			dst.info.ClientID,
+			err,
+		)
+	}
 
 	pp.log.Debug("will send", zap.Any("messages", messages))
 
 	return nil
 }
 
+// updateClientTrustedState will combine the counterparty chains trusted IBC header
+// with the latest client state, which will be used for constructing MsgUpdateClient messages.
+func (pp *PathProcessor) updateClientTrustedState(src *pathEndRuntime, dst *pathEndRuntime) {
+	if src.clientTrustedState.ClientState.ConsensusHeight.GTE(src.clientState.ConsensusHeight) {
+		// current height already trusted
+		return
+	}
+	// need to assemble new trusted state
+	ibcHeader, ok := dst.ibcHeaderCache[src.clientState.ConsensusHeight.RevisionHeight+1]
+	if !ok {
+		pp.log.Warn("No IBC header for client trusted height",
+			zap.String("chain_id", src.info.ChainID),
+			zap.String("client_id", src.info.ClientID),
+			zap.Uint64("height", src.clientState.ConsensusHeight.RevisionHeight+1),
+		)
+		return
+	}
+	src.clientTrustedState = provider.ClientTrustedState{
+		ClientState: src.clientState,
+		IBCHeader:   ibcHeader,
+	}
+}
+
 // messages from both pathEnds are needed in order to determine what needs to be relayed for a single pathEnd
 func (pp *PathProcessor) processLatestMessages(ctx context.Context) error {
+	// Update trusted client state for both pathends
+	pp.updateClientTrustedState(pp.pathEnd1, pp.pathEnd2)
+	pp.updateClientTrustedState(pp.pathEnd2, pp.pathEnd1)
+
 	channelPairs := pp.channelPairs()
 
 	// process the packet flows for both packends to determine what needs to be relayed
@@ -664,10 +750,10 @@ func (pp *PathProcessor) processLatestMessages(ctx context.Context) error {
 	// now send messages in parallel
 	var eg errgroup.Group
 	eg.Go(func() error {
-		return pp.sendMessages(pp.pathEnd1, pathEnd1Messages)
+		return pp.sendMessages(pp.pathEnd1, pp.pathEnd2, pathEnd2Messages)
 	})
 	eg.Go(func() error {
-		return pp.sendMessages(pp.pathEnd2, pathEnd2Messages)
+		return pp.sendMessages(pp.pathEnd2, pp.pathEnd1, pathEnd1Messages)
 	})
 	return eg.Wait()
 }
@@ -697,8 +783,8 @@ func (pp *PathProcessor) Run(ctx context.Context) {
 
 		// process latest message cache state from both pathEnds
 		if err := pp.processLatestMessages(ctx); err != nil {
-			// in case of IBC message send errors, schedule retry after DurationErrorRetry
-			time.AfterFunc(DurationErrorRetry, pp.ProcessBacklogIfReady)
+			// in case of IBC message send errors, schedule retry after durationErrorRetry
+			time.AfterFunc(durationErrorRetry, pp.ProcessBacklogIfReady)
 		}
 	}
 }
diff --git a/relayer/processor/types.go b/relayer/processor/types.go
index 5da2080..5444b64 100644
--- a/relayer/processor/types.go
+++ b/relayer/processor/types.go
@@ -1,6 +1,8 @@
 package processor
 
 import (
+	"sort"
+
 	transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types"
 	clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
 	conntypes "github.com/cosmos/ibc-go/v3/modules/core/03-connection/types"
@@ -115,34 +117,55 @@ func (connectionKey ConnectionKey) Counterparty() ConnectionKey {
 // ChannelStateCache maintains channel open state for multiple channels.
 type ChannelStateCache map[ChannelKey]bool
 
-// Merge will merge another ChannelStateCache into this one, appending messages and updating the Open state.
+// Merge merges another ChannelStateCache into this one, appending messages and updating the Open state.
 func (c ChannelStateCache) Merge(other ChannelStateCache) {
 	for channelKey, newState := range other {
 		c[channelKey] = newState
 	}
 }
 
+// Clone makes a copy of the ChannelStateCache so it can be used by other threads.
+func (c ChannelStateCache) Clone() ChannelStateCache {
+	n := make(ChannelStateCache, len(c))
+	for k, v := range c {
+		n[k] = v
+	}
+	return n
+}
+
 // ConnectionStateCache maintains connection open state for multiple connections.
 type ConnectionStateCache map[ConnectionKey]bool
 
-// Merge will merge another ChannelStateCache into this one, appending messages and updating the Open state.
+// Merge merges another ChannelStateCache into this one, appending messages and updating the Open state.
 func (c ConnectionStateCache) Merge(other ConnectionStateCache) {
 	for channelKey, newState := range other {
 		c[channelKey] = newState
 	}
 }
 
+// Clone makes a copy of the ConnectionStateCache so it can be used by other threads.
+func (c ConnectionStateCache) Clone() ConnectionStateCache {
+	n := make(ConnectionStateCache, len(c))
+	for k, v := range c {
+		n[k] = v
+	}
+	return n
+}
+
 // ChainProcessorCacheData is the data sent from the ChainProcessors to the PathProcessors
 // to keep the PathProcessors up to date with the latest info from the chains.
 type ChainProcessorCacheData struct {
-	IBCMessagesCache
-	InSync bool
-	ConnectionStateCache
-	ChannelStateCache
-	LatestBlock provider.LatestBlock
+	IBCMessagesCache     IBCMessagesCache
+	InSync               bool
+	ClientState          provider.ClientState
+	ConnectionStateCache ConnectionStateCache
+	ChannelStateCache    ChannelStateCache
+	LatestBlock          provider.LatestBlock
+	LatestHeader         provider.IBCHeader
+	IBCHeaderCache       IBCHeaderCache
 }
 
-// Clone will create a deep copy of a PacketMessagesCache.
+// Clone creates a deep copy of a PacketMessagesCache.
 func (c PacketMessagesCache) Clone() PacketMessagesCache {
 	newPacketMessagesCache := make(PacketMessagesCache, len(c))
 	for mk, mv := range c {
@@ -171,7 +194,7 @@ func (c IBCMessagesCache) Merge(other IBCMessagesCache) {
 	c.PacketFlow.Merge(other.PacketFlow)
 }
 
-// Merge will merge another ChannelPacketMessagesCache into this one.
+// Merge merges another ChannelPacketMessagesCache into this one.
 func (c ChannelPacketMessagesCache) Merge(other ChannelPacketMessagesCache) {
 	for channelKey, messageCache := range other {
 		_, ok := c[channelKey]
@@ -218,7 +241,7 @@ func (c ChannelPacketMessagesCache) Retain(k ChannelKey, m string, seq uint64, i
 	c[k][m][seq] = ibcMsg
 }
 
-// Merge will merge another PacketMessagesCache into this one.
+// Merge merges another PacketMessagesCache into this one.
 func (c PacketMessagesCache) Merge(other PacketMessagesCache) {
 	for ibcMessage, messageCache := range other {
 		_, ok := c[ibcMessage]
@@ -230,14 +253,14 @@ func (c PacketMessagesCache) Merge(other PacketMessagesCache) {
 	}
 }
 
-// Merge will merge another PacketSequenceCache into this one.
+// Merge merges another PacketSequenceCache into this one.
 func (c PacketSequenceCache) Merge(other PacketSequenceCache) {
 	for k, v := range other {
 		c[k] = v
 	}
 }
 
-// Merge will merge another ConnectionMessagesCache into this one.
+// Merge merges another ConnectionMessagesCache into this one.
 func (c ConnectionMessagesCache) Merge(other ConnectionMessagesCache) {
 	for ibcMessage, messageCache := range other {
 		_, ok := c[ibcMessage]
@@ -267,14 +290,14 @@ func (c ConnectionMessagesCache) DeleteCachedMessages(toDelete ...map[string][]C
 	}
 }
 
-// Merge will merge another ConnectionMessageCache into this one.
+// Merge merges another ConnectionMessageCache into this one.
 func (c ConnectionMessageCache) Merge(other ConnectionMessageCache) {
 	for k, v := range other {
 		c[k] = v
 	}
 }
 
-// Merge will merge another ChannelMessagesCache into this one.
+// Merge merges another ChannelMessagesCache into this one.
 func (c ChannelMessagesCache) Merge(other ChannelMessagesCache) {
 	for ibcMessage, messageCache := range other {
 		_, ok := c[ibcMessage]
@@ -304,9 +327,36 @@ func (c ChannelMessagesCache) DeleteCachedMessages(toDelete ...map[string][]Chan
 	}
 }
 
-// Merge will merge another ChannelMessageCache into this one.
+// Merge merges another ChannelMessageCache into this one.
 func (c ChannelMessageCache) Merge(other ChannelMessageCache) {
 	for k, v := range other {
 		c[k] = v
 	}
 }
+
+// IBCHeaderCache holds a mapping of IBCHeaders for their block height.
+type IBCHeaderCache map[uint64]provider.IBCHeader
+
+// Merge merges another IBCHeaderCache into this one.
+func (c IBCHeaderCache) Merge(other IBCHeaderCache) {
+	for k, v := range other {
+		c[k] = v
+	}
+}
+
+// Prune deletes all map entries except for the most recent (keep).
+func (c IBCHeaderCache) Prune(keep int) {
+	keys := make([]uint64, len(c))
+	i := 0
+	for k := range c {
+		keys[i] = k
+		i++
+	}
+	sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
+	toRemove := len(keys) - keep - 1
+	if toRemove > 0 {
+		for i := 0; i <= toRemove; i++ {
+			delete(c, keys[i])
+		}
+	}
+}
diff --git a/relayer/provider/cosmos/provider.go b/relayer/provider/cosmos/provider.go
index 5fb91d2..b90442b 100644
--- a/relayer/provider/cosmos/provider.go
+++ b/relayer/provider/cosmos/provider.go
@@ -11,6 +11,7 @@ import (
 	"github.com/cosmos/relayer/v2/relayer/provider"
 	"github.com/gogo/protobuf/proto"
 	lens "github.com/strangelove-ventures/lens/client"
+	tmtypes "github.com/tendermint/tendermint/types"
 	"go.uber.org/zap"
 )
 
@@ -92,6 +93,14 @@ type CosmosProvider struct {
 	PCfg CosmosProviderConfig
 }
 
+type CosmosIBCHeader struct {
+	SignedHeader *tmtypes.SignedHeader
+	ValidatorSet *tmtypes.ValidatorSet
+}
+
+// noop to implement processor.IBCHeader
+func (h CosmosIBCHeader) IBCHeaderIndicator() {}
+
 func (cc *CosmosProvider) ProviderConfig() provider.ProviderConfig {
 	return cc.PCfg
 }
diff --git a/relayer/provider/cosmos/tx.go b/relayer/provider/cosmos/tx.go
index b63ff69..0603332 100644
--- a/relayer/provider/cosmos/tx.go
+++ b/relayer/provider/cosmos/tx.go
@@ -305,7 +305,7 @@ func (cc *CosmosProvider) SubmitMisbehavior( /*TBD*/ ) (provider.RelayerMessage,
 	return nil, nil
 }
 
-func (cc *CosmosProvider) UpdateClient(srcClientId string, dstHeader ibcexported.Header) (provider.RelayerMessage, error) {
+func (cc *CosmosProvider) MsgUpdateClient(srcClientId string, dstHeader ibcexported.Header) (provider.RelayerMessage, error) {
 	acc, err := cc.Address()
 	if err != nil {
 		return nil, err
@@ -331,7 +331,7 @@ func (cc *CosmosProvider) ConnectionOpenInit(srcClientId, dstClientId string, ds
 		err     error
 		version *conntypes.Version
 	)
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -361,7 +361,7 @@ func (cc *CosmosProvider) ConnectionOpenTry(ctx context.Context, dstQueryProvide
 		acc string
 		err error
 	)
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -427,7 +427,7 @@ func (cc *CosmosProvider) ConnectionOpenAck(ctx context.Context, dstQueryProvide
 		err error
 	)
 
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -475,7 +475,7 @@ func (cc *CosmosProvider) ConnectionOpenConfirm(ctx context.Context, dstQueryPro
 		acc string
 		err error
 	)
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -508,7 +508,7 @@ func (cc *CosmosProvider) ChannelOpenInit(srcClientId, srcConnId, srcPortId, src
 		acc string
 		err error
 	)
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -540,7 +540,7 @@ func (cc *CosmosProvider) ChannelOpenTry(ctx context.Context, dstQueryProvider p
 		acc string
 		err error
 	)
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -593,7 +593,7 @@ func (cc *CosmosProvider) ChannelOpenAck(ctx context.Context, dstQueryProvider p
 		acc string
 		err error
 	)
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -630,7 +630,7 @@ func (cc *CosmosProvider) ChannelOpenConfirm(ctx context.Context, dstQueryProvid
 		acc string
 		err error
 	)
-	updateMsg, err := cc.UpdateClient(srcClientId, dstHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstHeader)
 	if err != nil {
 		return nil, err
 	}
@@ -797,7 +797,7 @@ func (cc *CosmosProvider) AutoUpdateClient(ctx context.Context, dst provider.Cha
 		return 0, err
 	}
 
-	updateMsg, err := cc.UpdateClient(srcClientId, dstUpdateHeader)
+	updateMsg, err := cc.MsgUpdateClient(srcClientId, dstUpdateHeader)
 	if err != nil {
 		return 0, err
 	}
@@ -1178,6 +1178,37 @@ func (cc *CosmosProvider) MsgTimeoutOnClose(ctx context.Context, msgRecvPacket p
 	return NewCosmosMessage(msgTimeout), nil
 }
 
+func (cc *CosmosProvider) MsgUpdateClientHeader(latestHeader provider.IBCHeader, trustedHeight clienttypes.Height, trustedHeader provider.IBCHeader) (ibcexported.Header, error) {
+	trustedCosmosHeader, ok := trustedHeader.(CosmosIBCHeader)
+	if !ok {
+		return nil, fmt.Errorf("unsupported IBC trusted header type, expected: CosmosIBCHeader, actual: %T", trustedHeader)
+	}
+
+	latestCosmosHeader, ok := latestHeader.(CosmosIBCHeader)
+	if !ok {
+		return nil, fmt.Errorf("unsupported IBC header type, expected: CosmosIBCHeader, actual: %T", latestHeader)
+	}
+
+	trustedValidatorsProto, err := trustedCosmosHeader.ValidatorSet.ToProto()
+	if err != nil {
+		return nil, fmt.Errorf("error converting trusted validators to proto object: %w", err)
+	}
+
+	signedHeaderProto := latestCosmosHeader.SignedHeader.ToProto()
+
+	validatorSetProto, err := latestCosmosHeader.ValidatorSet.ToProto()
+	if err != nil {
+		return nil, fmt.Errorf("error converting validator set to proto object: %w", err)
+	}
+
+	return &tmclient.Header{
+		SignedHeader:      signedHeaderProto,
+		ValidatorSet:      validatorSetProto,
+		TrustedValidators: trustedValidatorsProto,
+		TrustedHeight:     trustedHeight,
+	}, nil
+}
+
 // RelayPacketFromSequence relays a packet with a given seq on src and returns recvPacket msgs, timeoutPacketmsgs and error
 func (cc *CosmosProvider) RelayPacketFromSequence(
 	ctx context.Context,
diff --git a/relayer/provider/provider.go b/relayer/provider/provider.go
index 01c7352..2fd173f 100644
--- a/relayer/provider/provider.go
+++ b/relayer/provider/provider.go
@@ -44,6 +44,23 @@ type LatestBlock struct {
 	Time   time.Time
 }
 
+type IBCHeader interface {
+	IBCHeaderIndicator()
+}
+
+// ClientState holds the current state of a client from a single chain's perspective
+type ClientState struct {
+	ClientID        string
+	ConsensusHeight clienttypes.Height
+}
+
+// ClientTrustedState holds the current state of a client from the perspective of both involved chains,
+// i.e. ClientState enriched with the trusted IBC header of the counterparty chain.
+type ClientTrustedState struct {
+	ClientState ClientState
+	IBCHeader   IBCHeader
+}
+
 // loggableEvents is an unexported wrapper type for a slice of RelayerEvent,
 // to satisfy the zapcore.ArrayMarshaler interface.
 type loggableEvents []RelayerEvent
@@ -94,7 +111,6 @@ type ChainProvider interface {
 	Init() error
 	CreateClient(clientState ibcexported.ClientState, dstHeader ibcexported.Header, signer string) (RelayerMessage, error)
 	SubmitMisbehavior( /*TODO TBD*/ ) (RelayerMessage, error)
-	UpdateClient(srcClientId string, dstHeader ibcexported.Header) (RelayerMessage, error)
 	ConnectionOpenInit(srcClientId, dstClientId string, dstHeader ibcexported.Header) ([]RelayerMessage, error)
 	ConnectionOpenTry(ctx context.Context, dstQueryProvider QueryProvider, dstHeader ibcexported.Header, srcClientId, dstClientId, srcConnId, dstConnId string) ([]RelayerMessage, error)
 	ConnectionOpenAck(ctx context.Context, dstQueryProvider QueryProvider, dstHeader ibcexported.Header, srcClientId, srcConnId, dstClientId, dstConnId string) ([]RelayerMessage, error)
@@ -113,7 +129,7 @@ type ChainProvider interface {
 
 	// [Begin] Packet flow IBC message assembly functions
 
-	// These functions will query the proof of the packet state on the source chain. The message
+	// These functions query the proof of the packet state on the source chain. The message
 	// indicated by the function name is assembled and returned to be written to the destination chain.
 
 	// MsgRecvPacket takes a partial MsgRecvPacket, queries the packet commitment,
@@ -138,6 +154,19 @@ type ChainProvider interface {
 
 	// [End] Packet flow IBC message assembly
 
+	// [Begin] Client IBC message assembly
+
+	// MsgUpdateClientHeader takes the latest chain header, in addition to the latest client trusted header
+	// and assembles a new header for updating the light client on the counterparty chain.
+	MsgUpdateClientHeader(latestHeader IBCHeader, trustedHeight clienttypes.Height, trustedHeader IBCHeader) (ibcexported.Header, error)
+
+	// MsgUpdateClient takes an update client header to prove trust for the previous
+	// consensus height and the new height, and assembles a MsgUpdateClient message
+	// formatted for this chain.
+	MsgUpdateClient(clientId string, counterpartyHeader ibcexported.Header) (RelayerMessage, error)
+
+	// [End] Client IBC message assembly
+
 	// TODO remove these message assembly functions in favor of the above.
 	MsgRelayAcknowledgement(ctx context.Context, dst ChainProvider, dstChanId, dstPortId, srcChanId, srcPortId string, dsth int64, packet RelayPacket) (RelayerMessage, error)
 	MsgTransfer(amount sdk.Coin, dstChainId, dstAddr, srcPortId, srcChanId string, timeoutHeight, timeoutTimestamp uint64) (RelayerMessage, error)


Test patch:
diff --git a/_test/relayer_chain_test.go b/_test/relayer_chain_test.go
index e5c6c1d..ef36f66 100644
--- a/_test/relayer_chain_test.go
+++ b/_test/relayer_chain_test.go
@@ -309,7 +309,7 @@ func TestGaiaMisbehaviourMonitoring(t *testing.T) {
 		tmHeader.GetTime().Add(time.Minute), valSet, valSet, signers, tmHeader)
 
 	// update client with duplicate header
-	updateMsg, err := src.ChainProvider.UpdateClient(src.PathEnd.ClientID, newHeader)
+	updateMsg, err := src.ChainProvider.MsgUpdateClient(src.PathEnd.ClientID, newHeader)
 	require.NoError(t, err)
 
 	res, success, err := src.ChainProvider.SendMessage(ctx, updateMsg)
diff --git a/relayer/chains/mock/mock_chain_processor_test.go b/relayer/chains/mock/mock_chain_processor_test.go
index f998d33..446dc7a 100644
--- a/relayer/chains/mock/mock_chain_processor_test.go
+++ b/relayer/chains/mock/mock_chain_processor_test.go
@@ -24,8 +24,8 @@ func TestMockChainAndPathProcessors(t *testing.T) {
 	mockChainID1 := "mock-chain-1"
 	mockChainID2 := "mock-chain-2"
 
-	pathEnd1 := processor.PathEnd{ChainID: mockChainID1}
-	pathEnd2 := processor.PathEnd{ChainID: mockChainID2}
+	pathEnd1 := processor.PathEnd{ChainID: mockChainID1, ClientID: "mock-client-1"}
+	pathEnd2 := processor.PathEnd{ChainID: mockChainID2, ClientID: "mock-client-2"}
 
 	mockSequence1 := uint64(0)
 	mockSequence2 := uint64(0)
diff --git a/relayer/processor/types_test.go b/relayer/processor/types_test.go
new file mode 100644
index 0000000..3cf6048
--- /dev/null
+++ b/relayer/processor/types_test.go
@@ -0,0 +1,41 @@
+package processor_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/cosmos/relayer/v2/relayer/processor"
+)
+
+type mockIBCHeader struct{}
+
+func (h mockIBCHeader) IBCHeaderIndicator() {}
+
+func TestIBCHeaderCachePrune(t *testing.T) {
+	cache := make(processor.IBCHeaderCache)
+
+	intermediaryCache1 := make(processor.IBCHeaderCache)
+	for i := uint64(0); i < 10; i++ {
+		intermediaryCache1[i] = mockIBCHeader{}
+	}
+
+	intermediaryCache2 := make(processor.IBCHeaderCache)
+	for i := uint64(10); i < 20; i++ {
+		intermediaryCache2[i] = mockIBCHeader{}
+	}
+
+	cache.Merge(intermediaryCache1)
+	require.Len(t, cache, 10)
+
+	// test pruning with keep greater than length
+	cache.Prune(15)
+	require.Len(t, cache, 10)
+
+	cache.Merge(intermediaryCache2)
+	require.Len(t, cache, 20)
+
+	cache.Prune(5)
+	require.Len(t, cache, 5)
+	require.NotNil(t, cache[uint64(15)], cache[uint64(16)], cache[uint64(17)], cache[uint64(18)], cache[uint64(19)])
+}