diff --git a/Makefile b/Makefile index 0df09b96..f53175f9 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ protoImage=$(DOCKER) run --user 0 --rm -v $(CURDIR):/workspace --workdir /worksp build: go build -o ./build/yrly . +TESTMOCKS = core/mock_chain_test.go .PHONY: test -test: +test: $(TESTMOCKS) go test -v ./... proto-gen: @@ -20,4 +21,7 @@ proto-update-deps: @echo "Updating Protobuf dependencies" $(DOCKER) run --user 0 --rm -v $(CURDIR)/proto:/workspace --workdir /workspace $(protoImageName) buf mod update +$(TESTMOCKS): + go generate ./... + .PHONY: proto-gen proto-update-deps diff --git a/chains/tendermint/query.go b/chains/tendermint/query.go index 1cb68650..aefccac8 100644 --- a/chains/tendermint/query.go +++ b/chains/tendermint/query.go @@ -101,6 +101,19 @@ func (c *Chain) queryChannel(ctx context.Context, height int64, prove bool) (cha return res, nil } +// QueryNextSequenceReceive returns a info about nextSequence +func (c *Chain) QueryNextSequenceReceive(ctx core.QueryContext) (res *chantypes.QueryNextSequenceReceiveResponse, err error) { + return c.queryNextSequenceReceive(ctx.Context(), int64(ctx.Height().GetRevisionHeight()), false) +} + +func (c *Chain) queryNextSequenceReceive(ctx context.Context, height int64, prove bool) (chanRes *chantypes.QueryNextSequenceReceiveResponse, err error) { + res, err := chanutils.QueryNextSequenceReceive(c.CLIContext(height).WithCmdContext(ctx), c.PathEnd.PortID, c.PathEnd.ChannelID, prove) + if err != nil { + return nil, err + } + return res, nil +} + // QueryClientConsensusState retrieves the latest consensus state for a client in state at a given height func (c *Chain) QueryClientConsensusState( ctx core.QueryContext, dstClientConsHeight ibcexported.Height) (*clienttypes.QueryConsensusStateResponse, error) { diff --git a/cmd/tx.go b/cmd/tx.go index 949dc5be..e80762bd 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -457,6 +457,11 @@ func relayMsgsCmd(ctx *config.Context) *cobra.Command { return err } + sp, err = st.ProcessTimeoutPackets(cmd.Context(), c[src], c[dst], sh, sp) + if err != nil { + return err + } + msgs := core.NewRelayMsgs() doExecuteRelaySrc := len(sp.Dst) > 0 diff --git a/core/chain.go b/core/chain.go index 43ecb033..0ba8a411 100644 --- a/core/chain.go +++ b/core/chain.go @@ -55,6 +55,7 @@ func (pc *ProvableChain) SetupForRelay(ctx context.Context) error { return nil } +//go:generate mockgen -source=chain.go -destination=mock_chain_test.go -package core // Chain represents a chain that supports sending transactions and querying the state type Chain interface { // GetAddress returns the address of relayer @@ -144,6 +145,9 @@ type ICS04Querier interface { // QueryChannel returns the channel associated with a channelID QueryChannel(ctx QueryContext) (chanRes *chantypes.QueryChannelResponse, err error) + // QueryNextSequenceReceive returns a info about nextSequence + QueryNextSequenceReceive(ctx QueryContext) (res *chantypes.QueryNextSequenceReceiveResponse, err error) + // QueryUnreceivedPackets returns a list of unrelayed packet commitments QueryUnreceivedPackets(ctx QueryContext, seqs []uint64) ([]uint64, error) diff --git a/core/naive-strategy.go b/core/naive-strategy.go index 54cbd1c5..dd0e5781 100644 --- a/core/naive-strategy.go +++ b/core/naive-strategy.go @@ -5,9 +5,11 @@ import ( "fmt" "log/slog" "time" + "encoding/binary" retry "github.com/avast/retry-go" sdk "github.com/cosmos/cosmos-sdk/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" chantypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" "github.com/hyperledger-labs/yui-relayer/metrics" @@ -199,6 +201,114 @@ func (st *NaiveStrategy) UnrelayedPackets(ctx context.Context, src, dst *Provabl }, nil } +func (st *NaiveStrategy) ProcessTimeoutPackets(ctx context.Context, src, dst *ProvableChain, sh SyncHeaders, rp *RelayPackets) (*RelayPackets, error) { + logger := GetChannelPairLogger(src, dst) + var ( + srcPackets PacketInfoList + dstPackets PacketInfoList + srcLatestHeight ibcexported.Height + srcLatestTimestamp uint64 + srcLatestFinalizedHeight ibcexported.Height + srcLatestFinalizedTimestamp uint64 + dstLatestHeight ibcexported.Height + dstLatestTimestamp uint64 + dstLatestFinalizedHeight ibcexported.Height + dstLatestFinalizedTimestamp uint64 + ) + + if 0 < len(rp.Src) { + if h, err := dst.LatestHeight(context.TODO()); err != nil { + logger.Error("fail to get dst.LatestHeight", err) + return nil, err + } else { + dstLatestHeight = h + } + + if t, err := dst.Timestamp(context.TODO(), dstLatestHeight); err != nil { + logger.Error("fail to get dst.Timestamp", err) + return nil, err + } else { + dstLatestTimestamp = uint64(t.UnixNano()) + } + + dstLatestFinalizedHeight = sh.GetLatestFinalizedHeader(dst.ChainID()).GetHeight() + if t, err := dst.Timestamp(context.TODO(), dstLatestFinalizedHeight); err != nil { + logger.Error("fail to get dst.Timestamp", err) + return nil, err + } else { + dstLatestFinalizedTimestamp = uint64(t.UnixNano()) + } + } + if 0 < len(rp.Dst) { + if h, err := src.LatestHeight(context.TODO()); err != nil { + logger.Error("fail to get src.LatestHeight", err) + return nil, err + } else { + srcLatestHeight = h + } + if t, err := src.Timestamp(context.TODO(), srcLatestHeight); err != nil { + logger.Error("fail to get src.Timestamp", err) + return nil, err + } else { + srcLatestTimestamp = uint64(t.UnixNano()) + } + + srcLatestFinalizedHeight = sh.GetLatestFinalizedHeader(src.ChainID()).GetHeight() + if t, err := src.Timestamp(context.TODO(), srcLatestFinalizedHeight); err != nil { + logger.Error("fail to get src.Timestamp", err) + return nil, err + } else { + srcLatestFinalizedTimestamp = uint64(t.UnixNano()) + } + } + + isTimeout := func(p *PacketInfo, height ibcexported.Height, timestamp uint64) (bool) { + return (!p.TimeoutHeight.IsZero() && p.TimeoutHeight.LTE(height)) || + (p.TimeoutTimestamp != 0 && p.TimeoutTimestamp <= timestamp) + } + + for i, p := range rp.Src { + if isTimeout(p, dstLatestFinalizedHeight, dstLatestFinalizedTimestamp) { + p.TimedOut = true + if src.Path().GetOrder() == chantypes.ORDERED { + if i == 0 { + dstPackets = append(dstPackets, p) + } + break + } else { + dstPackets = append(dstPackets, p) + } + } else if isTimeout(p, dstLatestHeight, dstLatestTimestamp) { + break + } else { + p.TimedOut = false + srcPackets = append(srcPackets, p) + } + } + for i, p := range rp.Dst { + if (isTimeout(p, srcLatestFinalizedHeight, srcLatestFinalizedTimestamp)) { + p.TimedOut = true + if dst.Path().GetOrder() == chantypes.ORDERED { + if i == 0 { + srcPackets = append(srcPackets, p) + } + break + } else { + srcPackets = append(srcPackets, p) + } + } else if (isTimeout(p, srcLatestHeight, srcLatestTimestamp)) { + break + } else { + p.TimedOut = false + dstPackets = append(dstPackets, p) + } + } + return &RelayPackets{ + Src: srcPackets, + Dst: dstPackets, + }, nil +} + func (st *NaiveStrategy) RelayPackets(ctx context.Context, src, dst *ProvableChain, rp *RelayPackets, sh SyncHeaders, doExecuteRelaySrc, doExecuteRelayDst bool) (*RelayMsgs, error) { logger := GetChannelPairLogger(src, dst) defer logger.TimeTrack(time.Now(), "RelayPackets", "num_src", len(rp.Src), "num_dst", len(rp.Dst)) @@ -392,20 +502,66 @@ func (st *NaiveStrategy) UnrelayedAcknowledgements(ctx context.Context, src, dst // TODO add packet-timeout support func collectPackets(ctx QueryContext, chain *ProvableChain, packets PacketInfoList, signer sdk.AccAddress) ([]sdk.Msg, error) { logger := GetChannelLogger(chain) + + var nextSequenceRecv uint64 + if chain.Path().GetOrder() == chantypes.ORDERED { + for _, p := range packets { + if p.TimedOut { + res, err := chain.QueryNextSequenceReceive(ctx) + if err != nil { + logger.Error("failed to QueryNextSequenceReceive", err, + "height", ctx.Height(), + ) + return nil, err + } + nextSequenceRecv = res.NextSequenceReceive + break + } + } + } else { + // nextSequenceRecv has no effect in unordered channel but ibc-go expect it is not zero. + nextSequenceRecv = 1 + } + var msgs []sdk.Msg for _, p := range packets { - commitment := chantypes.CommitPacket(chain.Codec(), &p.Packet) - path := host.PacketCommitmentPath(p.SourcePort, p.SourceChannel, p.Sequence) - proof, proofHeight, err := chain.ProveState(ctx, path, commitment) - if err != nil { - logger.Error("failed to ProveState", err, - "height", ctx.Height(), - "path", path, - "commitment", commitment, - ) - return nil, err + var msg sdk.Msg + if p.TimedOut { + // make path of original packet's destination port and channel + var path string + var commitment []byte + if chain.Path().GetOrder() == chantypes.ORDERED { + path = host.NextSequenceRecvPath(p.SourcePort, p.SourceChannel) + commitment = make([]byte, 8) + binary.BigEndian.PutUint64(commitment[0:], nextSequenceRecv) + } else { + path = host.PacketReceiptPath(p.SourcePort, p.SourceChannel, p.Sequence) + commitment = []byte{} //ABSENSE + } + proof, proofHeight, err := chain.ProveState(ctx, path, commitment) + if err != nil { + logger.Error("failed to ProveState", err, + "height", ctx.Height(), + "path", path, + "commitment", commitment, + ) + return nil, err + } + msg = chantypes.NewMsgTimeout(p.Packet, nextSequenceRecv, proof, proofHeight, signer.String()) + } else { + path := host.PacketCommitmentPath(p.SourcePort, p.SourceChannel, p.Sequence) + commitment := chantypes.CommitPacket(chain.Codec(), &p.Packet) + proof, proofHeight, err := chain.ProveState(ctx, path, commitment) + if err != nil { + logger.Error("failed to ProveState", err, + "height", ctx.Height(), + "path", path, + "commitment", commitment, + ) + return nil, err + } + msg = chantypes.NewMsgRecvPacket(p.Packet, proof, proofHeight, signer.String()) } - msg := chantypes.NewMsgRecvPacket(p.Packet, proof, proofHeight, signer.String()) msgs = append(msgs, msg) } return msgs, nil diff --git a/core/provers.go b/core/provers.go index db8cec38..e2762191 100644 --- a/core/provers.go +++ b/core/provers.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" - "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" ) // Prover represents a prover that supports generating a commitment proof @@ -31,7 +31,7 @@ type StateProver interface { // ProveHostConsensusState returns an existence proof of the consensus state at `height` // This proof would be ignored in ibc-go, but it is required to `getSelfConsensusState` of ibc-solidity. - ProveHostConsensusState(ctx QueryContext, height exported.Height, consensusState exported.ConsensusState) (proof []byte, err error) + ProveHostConsensusState(ctx QueryContext, height ibcexported.Height, consensusState ibcexported.ConsensusState) (proof []byte, err error) } // LightClient provides functions for creating and updating on-chain light clients on the counterparty chain @@ -41,7 +41,7 @@ type LightClient interface { // CreateInitialLightClientState returns a pair of ClientState and ConsensusState based on the state of the self chain at `height`. // These states will be submitted to the counterparty chain as MsgCreateClient. // If `height` is nil, the latest finalized height is selected automatically. - CreateInitialLightClientState(ctx context.Context, height exported.Height) (exported.ClientState, exported.ConsensusState, error) + CreateInitialLightClientState(ctx context.Context, height ibcexported.Height) (ibcexported.ClientState, ibcexported.ConsensusState, error) // SetupHeadersForUpdate returns the finalized header and any intermediate headers needed to apply it to the client on the counterpaty chain // The order of the returned header slice should be as: [..., ] diff --git a/core/service.go b/core/service.go index 12a62bdb..a04df28e 100644 --- a/core/service.go +++ b/core/service.go @@ -119,6 +119,12 @@ func (srv *RelayService) Serve(ctx context.Context) error { return err } + pseqs2, err := srv.st.ProcessTimeoutPackets(ctx, srv.src, srv.dst, srv.sh, pseqs) + if err != nil { + logger.Error("failed to process timeout packets", err) + return err + } + // get unrelayed acks aseqs, err := srv.st.UnrelayedAcknowledgements(ctx, srv.src, srv.dst, srv.sh, false) if err != nil { @@ -128,7 +134,7 @@ func (srv *RelayService) Serve(ctx context.Context) error { msgs := NewRelayMsgs() - doExecuteRelaySrc, doExecuteRelayDst := srv.shouldExecuteRelay(ctx, pseqs) + doExecuteRelaySrc, doExecuteRelayDst := srv.shouldExecuteRelay(ctx, pseqs2) doExecuteAckSrc, doExecuteAckDst := srv.shouldExecuteRelay(ctx, aseqs) // update clients if m, err := srv.st.UpdateClients(ctx, srv.src, srv.dst, doExecuteRelaySrc, doExecuteRelayDst, doExecuteAckSrc, doExecuteAckDst, srv.sh, true); err != nil { @@ -139,7 +145,7 @@ func (srv *RelayService) Serve(ctx context.Context) error { } // relay packets if unrelayed seqs exist - if m, err := srv.st.RelayPackets(ctx, srv.src, srv.dst, pseqs, srv.sh, doExecuteRelaySrc, doExecuteRelayDst); err != nil { + if m, err := srv.st.RelayPackets(ctx, srv.src, srv.dst, pseqs2, srv.sh, doExecuteRelaySrc, doExecuteRelayDst); err != nil { logger.Error("failed to relay packets", err) return err } else { diff --git a/core/service_test.go b/core/service_test.go new file mode 100644 index 00000000..ce62e06d --- /dev/null +++ b/core/service_test.go @@ -0,0 +1,328 @@ +package core_test + +import ( + "testing" + "go.uber.org/mock/gomock" + "github.com/stretchr/testify/assert" + + "time" + "context" + "os" + "fmt" + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + chantypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + mocktypes "github.com/datachainlab/ibc-mock-client/modules/light-clients/xx-mock/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" + + "github.com/hyperledger-labs/yui-relayer/core" + "github.com/hyperledger-labs/yui-relayer/log" + "github.com/hyperledger-labs/yui-relayer/provers/mock" + "github.com/hyperledger-labs/yui-relayer/metrics" + "github.com/hyperledger-labs/yui-relayer/chains/tendermint" +) + +type NaiveStrategyWrap struct { + Inner *core.NaiveStrategy + + UnrelayedPacketsOut *core.RelayPackets + ProcessTimeoutPacketsOut *core.RelayPackets + UnrelayedAcknowledgementsOut *core.RelayPackets + RelayPacketsOut *core.RelayMsgs + RelayAcknowledgementsOut *core.RelayMsgs + UpdateClientsOut *core.RelayMsgs + SendInSrc []string + SendInDst []string +} +func (s *NaiveStrategyWrap) GetType() string { return s.Inner.GetType() } +func (s *NaiveStrategyWrap) SetupRelay(ctx context.Context, src, dst *core.ProvableChain) error { return s.Inner.SetupRelay(ctx, src, dst) } +func (s *NaiveStrategyWrap) UnrelayedPackets(ctx context.Context, src, dst *core.ProvableChain, sh core.SyncHeaders, includeRelayedButUnfinalized bool) (*core.RelayPackets, error) { + ret, err := s.Inner.UnrelayedPackets(ctx, src, dst, sh, includeRelayedButUnfinalized) + s.UnrelayedPacketsOut = ret + return ret, err +} + +func (s *NaiveStrategyWrap) ProcessTimeoutPackets(ctx context.Context, src, dst *core.ProvableChain, sh core.SyncHeaders, rp *core.RelayPackets) (*core.RelayPackets, error) { + ret, err := s.Inner.ProcessTimeoutPackets(ctx, src, dst, sh, rp) + s.ProcessTimeoutPacketsOut = ret + return ret, err +} + +func (s *NaiveStrategyWrap) RelayPackets(ctx context.Context, src, dst *core.ProvableChain, rp *core.RelayPackets, sh core.SyncHeaders, doExecuteRelaySrc, doExecuteRelayDst bool) (*core.RelayMsgs, error) { + ret, err := s.Inner.RelayPackets(ctx, src, dst, rp, sh, doExecuteRelaySrc, doExecuteRelayDst) + s.RelayPacketsOut = ret + return ret, err +} +func (s *NaiveStrategyWrap) UnrelayedAcknowledgements(ctx context.Context, src, dst *core.ProvableChain, sh core.SyncHeaders, includeRelayedButUnfinalized bool) (*core.RelayPackets, error) { + ret, err := s.Inner.UnrelayedAcknowledgements(ctx, src, dst, sh, includeRelayedButUnfinalized) + s.UnrelayedAcknowledgementsOut = ret + return ret, err +} +func (s *NaiveStrategyWrap) RelayAcknowledgements(ctx context.Context, src, dst *core.ProvableChain, rp *core.RelayPackets, sh core.SyncHeaders, doExecuteAckSrc, doExecuteAckDst bool) (*core.RelayMsgs, error) { + ret, err := s.Inner.RelayAcknowledgements(ctx, src, dst, rp, sh, doExecuteAckSrc, doExecuteAckDst) + s.RelayAcknowledgementsOut = ret + return ret, err +} +func (s *NaiveStrategyWrap) UpdateClients(ctx context.Context, src, dst *core.ProvableChain, doExecuteRelaySrc, doExecuteRelayDst, doExecuteAckSrc, doExecuteAckDst bool, sh core.SyncHeaders, doRefresh bool) (*core.RelayMsgs, error) { + ret, err := s.Inner.UpdateClients(ctx, src, dst, doExecuteRelaySrc, doExecuteRelayDst, doExecuteAckSrc, doExecuteAckDst, sh, doRefresh) + s.UpdateClientsOut = ret + return ret, err +} +func (s *NaiveStrategyWrap) Send(ctx context.Context, src, dst core.Chain, msgs *core.RelayMsgs) { + // format message object as string to be easily comparable + format := func(msgs []sdk.Msg) ([]string) { + ret := []string{} + for _, msg := range msgs { + typeof := reflect.TypeOf(msg).Elem().Name() + var desc string + switch typeof { + case "MsgUpdateClient": + m := msg.(*clienttypes.MsgUpdateClient) + desc = fmt.Sprintf("%s(%s)", typeof, m.ClientId) + case "MsgRecvPacket": + m := msg.(*chantypes.MsgRecvPacket) + desc = fmt.Sprintf("%s(%v)", typeof, m.Packet.GetSequence()) + case "MsgTimeout": + m := msg.(*chantypes.MsgTimeout) + desc = fmt.Sprintf("%s(%v)", typeof, m.Packet.GetSequence()) + default: + desc = fmt.Sprintf("%s()", typeof) + } + ret = append(ret, desc) + } + return ret + } + s.SendInSrc = format(msgs.Src) + s.SendInDst = format(msgs.Dst) + s.Inner.Send(ctx, src, dst, msgs) +} + +/** + * create mock ProvableChain with our MockProver and gomock's MockChain. + * about height: + * LatestHeight: returns header that NewMockProvableChain is given + * LatestFinalizedHeight: LatestHeight - 10 + * Timestamp: height + 10000 + */ +func NewMockProvableChain( + ctrl *gomock.Controller, + name, order string, + header mocktypes.Header, + unfinalizedRelayPackets core.PacketInfoList, + unreceivedPackets []uint64, +) *core.ProvableChain { + chain := core.NewMockChain(ctrl) + prover := mock.NewProver(chain, mock.ProverConfig{ FinalityDelay: 10 }) + + chain.EXPECT().ChainID().Return(name + "Chain").AnyTimes() + chain.EXPECT().Codec().Return(nil).AnyTimes() + chain.EXPECT().GetAddress().Return(sdk.AccAddress{}, nil).AnyTimes() + chain.EXPECT().Path().Return(&core.PathEnd{ + ChainID: name + "Chain", + ClientID: name + "Client", + ConnectionID: name + "Conn", + ChannelID: name + "Chan", + PortID: name + "Port", + Order: order, + Version: name + "Version", + }).AnyTimes() + chain.EXPECT().LatestHeight(gomock.Any()).Return(header.Height, nil).AnyTimes() + chain.EXPECT().Timestamp(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, h ibcexported.Height) (time.Time, error) { + return time.Unix(0, int64(10000 + h.GetRevisionHeight())), nil + }).AnyTimes() + chain.EXPECT().QueryNextSequenceReceive(gomock.Any()).DoAndReturn( + func(ctx core.QueryContext) (*chantypes.QueryNextSequenceReceiveResponse, error) { + height := ctx.Height().(clienttypes.Height) + return &chantypes.QueryNextSequenceReceiveResponse{ 1, []byte{}, height }, nil + }).AnyTimes() + chain.EXPECT().QueryUnfinalizedRelayPackets(gomock.Any(), gomock.Any()).Return(unfinalizedRelayPackets, nil) + chain.EXPECT().QueryUnreceivedPackets(gomock.Any(), gomock.Any()).Return(unreceivedPackets, nil).AnyTimes() + chain.EXPECT().QueryUnreceivedAcknowledgements(gomock.Any(), gomock.Any()).Return([]uint64{}, nil).AnyTimes() + chain.EXPECT().QueryUnfinalizedRelayAcknowledgements(gomock.Any(), gomock.Any()).Return([]*core.PacketInfo{}, nil).AnyTimes() + chain.EXPECT().SendMsgs(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, msgs []sdk.Msg) ([]core.MsgID, error) { + var msgIDs []core.MsgID + for _, _ = range msgs { + msgIDs = append(msgIDs, &tendermint.MsgID{TxHash:"", MsgIndex:0}) + } + return msgIDs, nil + }).AnyTimes() + return core.NewProvableChain(chain, prover) +} + +type testCase struct { + Order string + optimizeCount uint64 + UnfinalizedRelayPackets core.PacketInfoList + ExpectSendSrc []string + ExpectSendDst []string +} + +func newPacketInfo(seq uint64, timeoutHeight uint64) (*core.PacketInfo) { + return &core.PacketInfo{ + Packet: chantypes.NewPacket( + []byte{}, + seq, + "srcPort", + "srcChannel", + "dstPort", + "dstChannel", + clienttypes.NewHeight(1, timeoutHeight), + 0, // timeoutTimestamp + ), + EventHeight: clienttypes.NewHeight(1, 1), + } +} + +func TestServe(t *testing.T) { + cases := map[string]testCase{ + "empty": { + "ORDERED", + 1, + []*core.PacketInfo{}, + []string{ }, + []string{ }, + }, + "single": { + "ORDERED", + 1, + []*core.PacketInfo{ + newPacketInfo(1, 9999), + }, + []string{ }, + []string{ + "MsgUpdateClient(dstClient)", + "MsgRecvPacket(1)", + }, + }, + "multi": { + "ORDERED", + 1, + []*core.PacketInfo{ + newPacketInfo(1, 9999), + newPacketInfo(2, 9999), + newPacketInfo(3, 9999), + }, + []string{ }, + []string{ + "MsgUpdateClient(dstClient)", + "MsgRecvPacket(1)", + "MsgRecvPacket(2)", + "MsgRecvPacket(3)", + }, + }, + "queued": { + "ORDERED", + 9, + []*core.PacketInfo{ + newPacketInfo(1, 9999), + }, + []string{ }, + []string{ }, + }, + "@not timeout(at border height)": { + "ORDERED", + 1, + []*core.PacketInfo{ + newPacketInfo(1, 101), + }, + []string{ }, + []string{ + "MsgUpdateClient(dstClient)", + "MsgRecvPacket(1)", + }, + }, + "timeout": { + "ORDERED", + 1, + []*core.PacketInfo{ + newPacketInfo(1, 90), + }, + []string{ "MsgUpdateClient(srcClient)", "MsgTimeout(1)" }, + []string{ }, + }, + "timeout at latest block but not at finalized block(at lower border)": { + "ORDERED", + 1, + []*core.PacketInfo{ + newPacketInfo(1, 91), + }, + []string{ }, + []string{ }, + }, + "timeout at latest block but not at finalized block(at heigher border)": { + "ORDERED", + 1, + []*core.PacketInfo{ + newPacketInfo(1, 100), + }, + []string{ }, + []string{ }, + }, + "only packets precede timeout packet": { + "ORDERED", + 1, + []*core.PacketInfo{ + newPacketInfo(1, 9999), + newPacketInfo(2, 9999), + newPacketInfo(3, 9), + }, + []string{ }, + []string{ + "MsgUpdateClient(dstClient)", + "MsgRecvPacket(1)", + "MsgRecvPacket(2)", + }, + }, + } + for n, c := range cases { + if n[0] == '_' { continue } + t.Run(n, func (t2 *testing.T) { testServe(t2, c) }) + } +} + +func testServe(t *testing.T, tc testCase) { + log.InitLoggerWithWriter("debug", "text", os.Stdout) + metrics.InitializeMetrics(metrics.ExporterNull{}) + + srcLatestHeader := mocktypes.Header{ + Height: clienttypes.NewHeight(1, 100), + Timestamp: uint64(10100), + } + dstLatestHeader := mocktypes.Header{ + Height: clienttypes.NewHeight(1, 100), + Timestamp: uint64(10100), + } + + ctrl := gomock.NewController(t) + + var unreceivedPackets []uint64 + for _, p := range tc.UnfinalizedRelayPackets { + unreceivedPackets = append(unreceivedPackets, p.Sequence) + } + src := NewMockProvableChain(ctrl, "src", tc.Order, srcLatestHeader, tc.UnfinalizedRelayPackets, []uint64{}) + dst := NewMockProvableChain(ctrl, "dst", tc.Order, dstLatestHeader, []*core.PacketInfo{}, unreceivedPackets) + + st := &NaiveStrategyWrap{ Inner: core.NewNaiveStrategy(false, false) } + sh, err := core.NewSyncHeaders(context.TODO(), src, dst) + if err != nil { + fmt.Printf("NewSyncHeders: %v\n", err) + } + var forever time.Duration = 1 << 63 - 1 + srv := core.NewRelayService(st, src, dst, sh, time.Minute, forever, tc.optimizeCount, forever, tc.optimizeCount) + + srv.Serve(context.TODO()) + /* for debug + fmt.Printf("UnrelayedPackets: %v\n", st.UnrelayedPacketsOut) + fmt.Printf("UnrelayedAcknowledgementsOut: %v\n", st.UnrelayedAcknowledgementsOut) + fmt.Printf("RelayPacketsOut: %v\n", st.RelayPacketsOut) + fmt.Printf("RelayAcknowledgementsOut: %v\n", st.RelayAcknowledgementsOut) + fmt.Printf("UpdateClientsOut: %v\n", st.UpdateClientsOut) + fmt.Printf("Send.Src: %v\n", st.SendInSrc) + fmt.Printf("Send.Dst: %v\n", st.SendInDst) + */ + assert.Equal(t, tc.ExpectSendSrc, st.SendInSrc, "Send.Src") + assert.Equal(t, tc.ExpectSendDst, st.SendInDst, "Send.Dst") +} diff --git a/core/strategies.go b/core/strategies.go index e070ab9a..cbd5195a 100644 --- a/core/strategies.go +++ b/core/strategies.go @@ -24,6 +24,9 @@ type StrategyI interface { // `includeRelayedButUnfinalized` decides if the result includes packets of which acknowledgePacket has been executed but not finalized UnrelayedAcknowledgements(ctx context.Context, src, dst *ProvableChain, sh SyncHeaders, includeRelayedButUnfinalized bool) (*RelayPackets, error) + // ProcessTimeoutPackets process timeout packets in given RelayPackets and returns sorted RelayPackets. Note that input Packet object may be modified. + ProcessTimeoutPackets(ctx context.Context, src, dst *ProvableChain, sh SyncHeaders, rp *RelayPackets) (*RelayPackets, error) + // RelayAcknowledgements executes AcknowledgePacket to the packets contained in `rp` on both chains (`src` and `dst`). RelayAcknowledgements(ctx context.Context, src, dst *ProvableChain, rp *RelayPackets, sh SyncHeaders, doExecuteAckSrc, doExecuteAckDst bool) (*RelayMsgs, error) diff --git a/core/types.go b/core/types.go index d5a520ed..e24ae5ff 100644 --- a/core/types.go +++ b/core/types.go @@ -13,6 +13,7 @@ type PacketInfo struct { chantypes.Packet Acknowledgement []byte `json:"acknowledgement"` EventHeight clienttypes.Height `json:"event_height"` + TimedOut bool `json:"timed_out"` } // PacketInfoList represents a list of PacketInfo that is sorted in the order in which diff --git a/go.mod b/go.mod index bcc2bc8c..221bde1d 100644 --- a/go.mod +++ b/go.mod @@ -16,16 +16,19 @@ require ( github.com/cosmos/gogoproto v1.4.11 github.com/cosmos/ibc-go/modules/capability v1.0.0 github.com/cosmos/ibc-go/v8 v8.2.1 - github.com/datachainlab/ibc-mock-client v0.4.2 + github.com/datachainlab/ibc-mock-client v0.4.3 github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.33.0 go.opentelemetry.io/otel/exporters/prometheus v0.55.0 go.opentelemetry.io/otel/metric v1.33.0 go.opentelemetry.io/otel/sdk/metric v1.33.0 + go.uber.org/mock v0.5.0 golang.org/x/sync v0.10.0 + golang.org/x/sys v0.28.0 google.golang.org/grpc v1.62.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -161,7 +164,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect @@ -181,7 +183,6 @@ require ( golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/net v0.32.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum index 8b009e9f..398a735d 100644 --- a/go.sum +++ b/go.sum @@ -377,8 +377,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= -github.com/datachainlab/ibc-mock-client v0.4.2 h1:0BbQFwLUUbKknCsUO6m80VogRbJop5kA0u9/3Hma9n0= -github.com/datachainlab/ibc-mock-client v0.4.2/go.mod h1:Fn37FzeevLp5gmla4TSoDY56Jm2tBcqz+p0lIyRCOsg= +github.com/datachainlab/ibc-mock-client v0.4.3 h1:vFl8P4lx0aAgvnZIMfmwhDcj8atps1aP+sthzKdVNo8= +github.com/datachainlab/ibc-mock-client v0.4.3/go.mod h1:Fn37FzeevLp5gmla4TSoDY56Jm2tBcqz+p0lIyRCOsg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -1077,6 +1077,8 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=