diff --git a/x/crosschain/keeper/cctx_utils_outbound.go b/x/crosschain/keeper/cctx_utils_outbound.go index 863feeca0b..e3ff2d984f 100644 --- a/x/crosschain/keeper/cctx_utils_outbound.go +++ b/x/crosschain/keeper/cctx_utils_outbound.go @@ -21,6 +21,8 @@ func SetRevertOutboundValues(cctx *types.CrossChainTx, gasLimit uint64) { OutboundTxGasLimit: gasLimit, TssPubkey: cctx.GetCurrentOutTxParam().TssPubkey, } + // The original outbound has been finalized, the new outbound is pending + cctx.GetCurrentOutTxParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed cctx.OutboundTxParams = append(cctx.OutboundTxParams, revertTxParams) } func SetOutboundValues(ctx sdk.Context, cctx *types.CrossChainTx, msg types.MsgVoteOnObservedOutboundTx, ballotStatus observertypes.BallotStatus) error { @@ -118,16 +120,7 @@ func (k Keeper) ProcessFailedOutbound(ctx sdk.Context, cctx *types.CrossChainTx, } // create new OutboundTxParams for the revert - revertTxParams := &types.OutboundTxParams{ - Receiver: cctx.InboundTxParams.Sender, - ReceiverChainId: cctx.InboundTxParams.SenderChainId, - Amount: cctx.InboundTxParams.Amount, - OutboundTxGasLimit: gasLimit, - TssPubkey: cctx.GetCurrentOutTxParam().TssPubkey, - } - // TODO : consider setting the first outbount to executed - // Move this creation to a separate function - cctx.OutboundTxParams = append(cctx.OutboundTxParams, revertTxParams) + SetRevertOutboundValues(cctx, gasLimit) err = k.PayGasAndUpdateCctx( ctx, @@ -173,21 +166,18 @@ func (k Keeper) ProcessOutbound(ctx sdk.Context, cctx *types.CrossChainTx, ballo return nil } -func (k Keeper) SaveFailedOutBound(ctx sdk.Context, cctx *types.CrossChainTx, errMessage string) { - receiverChain := cctx.GetCurrentOutTxParam().ReceiverChainId - tssPubkey := cctx.GetCurrentOutTxParam().TssPubkey - outTxTssNonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce - +func (k Keeper) SaveFailedOutBound(ctx sdk.Context, cctx *types.CrossChainTx, errMessage string, ballotIndex string) { cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, errMessage) - cctx.GetCurrentOutTxParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed ctx.Logger().Error(errMessage) - // #nosec G701 always in range - k.GetObserverKeeper().RemoveFromPendingNonces(ctx, tssPubkey, receiverChain, int64(outTxTssNonce)) - k.RemoveOutTxTracker(ctx, receiverChain, outTxTssNonce) - k.SetCctxAndNonceToCctxAndInTxHashToCctx(ctx, *cctx) + + k.SaveOutbound(ctx, cctx, ballotIndex) } func (k Keeper) SaveSuccessfulOutBound(ctx sdk.Context, cctx *types.CrossChainTx, ballotIndex string) { + k.SaveOutbound(ctx, cctx, ballotIndex) +} + +func (k Keeper) SaveOutbound(ctx sdk.Context, cctx *types.CrossChainTx, ballotIndex string) { receiverChain := cctx.GetCurrentOutTxParam().ReceiverChainId tssPubkey := cctx.GetCurrentOutTxParam().TssPubkey outTxTssNonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce diff --git a/x/crosschain/keeper/cctx_utils_outbound_test.go b/x/crosschain/keeper/cctx_utils_outbound_test.go index 72b061ad4b..163c118ee7 100644 --- a/x/crosschain/keeper/cctx_utils_outbound_test.go +++ b/x/crosschain/keeper/cctx_utils_outbound_test.go @@ -285,7 +285,7 @@ func TestKeeper_SaveFailedOutBound(t *testing.T) { HashList: nil, }) cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound - k.SaveFailedOutBound(ctx, cctx, sample.String()) + k.SaveFailedOutBound(ctx, cctx, sample.String(), sample.ZetaIndex(t)) require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_Aborted) require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_Executed) _, found := k.GetOutTxTracker(ctx, cctx.GetCurrentOutTxParam().ReceiverChainId, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) @@ -311,3 +311,58 @@ func TestKeeper_SaveSuccessfulOutBound(t *testing.T) { require.False(t, found) }) } + +func TestKeeper_SaveOutbound(t *testing.T) { + t.Run("successfully save outbound", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeper(t) + + // setup state for crosschain and observer modules + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + ballotIndex := sample.String() + k.SetOutTxTracker(ctx, types.OutTxTracker{ + Index: "", + ChainId: cctx.GetCurrentOutTxParam().ReceiverChainId, + Nonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + HashList: nil, + }) + + zk.ObserverKeeper.SetPendingNonces(ctx, observertypes.PendingNonces{ + NonceLow: int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) - 1, + NonceHigh: int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + 1, + ChainId: cctx.GetCurrentOutTxParam().ReceiverChainId, + Tss: cctx.GetCurrentOutTxParam().TssPubkey, + }) + zk.ObserverKeeper.SetTSS(ctx, observertypes.TSS{ + TssPubkey: cctx.GetCurrentOutTxParam().TssPubkey, + }) + + // Save outbound and assert all values are successfully saved + k.SaveOutbound(ctx, cctx, ballotIndex) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxBallotIndex, ballotIndex) + require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_Executed) + _, found := k.GetOutTxTracker(ctx, cctx.GetCurrentOutTxParam().ReceiverChainId, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + require.False(t, found) + pn, found := zk.ObserverKeeper.GetPendingNonces(ctx, cctx.GetCurrentOutTxParam().TssPubkey, cctx.GetCurrentOutTxParam().ReceiverChainId) + require.True(t, found) + require.Equal(t, pn.NonceLow, int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce)+1) + require.Equal(t, pn.NonceHigh, int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce)+1) + _, found = k.GetInTxHashToCctx(ctx, cctx.InboundTxParams.InboundTxObservedHash) + require.True(t, found) + _, found = zk.ObserverKeeper.GetNonceToCctx(ctx, cctx.GetCurrentOutTxParam().TssPubkey, cctx.GetCurrentOutTxParam().ReceiverChainId, int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce)) + require.True(t, found) + }) +} + +func Test_SetRevertOutboundValues(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + cctx.OutboundTxParams = cctx.OutboundTxParams[:1] + keeper.SetRevertOutboundValues(cctx, 100) + require.Len(t, cctx.OutboundTxParams, 2) + require.Equal(t, cctx.GetCurrentOutTxParam().Receiver, cctx.InboundTxParams.Sender) + require.Equal(t, cctx.GetCurrentOutTxParam().ReceiverChainId, cctx.InboundTxParams.SenderChainId) + require.Equal(t, cctx.GetCurrentOutTxParam().Amount, cctx.InboundTxParams.Amount) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxGasLimit, uint64(100)) + require.Equal(t, cctx.GetCurrentOutTxParam().TssPubkey, cctx.OutboundTxParams[0].TssPubkey) + require.Equal(t, types.TxFinalizationStatus_Executed, cctx.OutboundTxParams[0].TxFinalizationStatus) +} diff --git a/x/crosschain/keeper/msg_server_vote_outbound_tx.go b/x/crosschain/keeper/msg_server_vote_outbound_tx.go index bb37f83bef..9287264f8a 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx.go @@ -63,10 +63,14 @@ func (k msgServer) VoteOnObservedOutboundTx(goCtx context.Context, msg *types.Ms if cctx.GetCurrentOutTxParam().OutboundTxTssNonce != msg.OutTxTssNonce { return nil, cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("OutTxTssNonce %d does not match CCTX OutTxTssNonce %d", msg.OutTxTssNonce, cctx.GetCurrentOutTxParam().OutboundTxTssNonce)) } + // do not process outbound vote if TSS is not found + _, found = k.zetaObserverKeeper.GetTSS(ctx) + if !found { + return nil, types.ErrCannotFindTSSKeys + } // get ballot index ballotIndex := msg.Digest() - // vote on outbound ballot isFinalizingVote, isNew, ballot, observationChain, err := k.zetaObserverKeeper.VoteOnOutboundBallot( ctx, @@ -80,20 +84,12 @@ func (k msgServer) VoteOnObservedOutboundTx(goCtx context.Context, msg *types.Ms // if the ballot is new, set the index to the CCTX if isNew { observerkeeper.EmitEventBallotCreated(ctx, ballot, msg.ObservedOutTxHash, observationChain) - // Set this the first time when the ballot is created - // The ballot might change if there are more votes in a different outbound ballot for this cctx hash - cctx.GetCurrentOutTxParam().OutboundTxBallotIndex = ballotIndex } - // if not finalized commit state here if !isFinalizingVote { return &types.MsgVoteOnObservedOutboundTxResponse{}, nil } - _, found = k.zetaObserverKeeper.GetTSS(ctx) - if !found { - return nil, types.ErrCannotFindTSSKeys - } // if ballot successful, the value received should be the out tx amount err = SetOutboundValues(ctx, &cctx, *msg, ballot.BallotStatus) if err != nil { @@ -104,9 +100,9 @@ func (k msgServer) VoteOnObservedOutboundTx(goCtx context.Context, msg *types.Ms err = k.ProcessOutbound(ctx, &cctx, ballot.BallotStatus, msg.ValueReceived.String()) if err != nil { - k.SaveFailedOutBound(ctx, &cctx, msg.ValueReceived.String()) + k.SaveFailedOutBound(ctx, &cctx, err.Error(), ballotIndex) return &types.MsgVoteOnObservedOutboundTxResponse{}, nil } - k.SaveSuccessfulOutBound(ctx, &cctx, msg.ValueReceived.String()) + k.SaveSuccessfulOutBound(ctx, &cctx, ballotIndex) return &types.MsgVoteOnObservedOutboundTxResponse{}, nil } diff --git a/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go index 7ddfd4ea36..ec71cf626c 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go @@ -223,26 +223,27 @@ func TestKeeper_VoteOnObservedOutboundTx(t *testing.T) { }) t.Run("fail to finalize outbound if not a finalizing vote", func(t *testing.T) { - k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ - UseObserverMock: true, - }) + k, ctx, sk, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{}) // Setup mock data - observerMock := keepertest.GetCrosschainObserverMock(t, k) receiver := sample.EthAddress() amount := big.NewInt(42) senderChain := getValidEthChain(t) asset := "" - observer := sample.AccAddress() + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) tss := sample.Tss() - zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{observer}}) + + // set state to successfully vote on outbound tx + accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) + require.NoError(t, err) + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{accAddress.String(), sample.AccAddress(), sample.AccAddress()}}) + sk.StakingKeeper.SetValidator(ctx, validator) cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound k.SetCrossChainTx(ctx, *cctx) - - observerMock.On("VoteOnOutboundBallot", ctx, mock.Anything, cctx.GetCurrentOutTxParam().ReceiverChainId, common.ReceiveStatus_Success, observer). - Return(false, true, observertypes.Ballot{BallotStatus: observertypes.BallotStatus_BallotFinalized_SuccessObservation}, senderChain.ChainName.String(), nil).Once() + zk.ObserverKeeper.SetTSS(ctx, tss) msgServer := keeper.NewMsgServerImpl(*k) msg := &types.MsgVoteOnObservedOutboundTx{ @@ -250,7 +251,7 @@ func TestKeeper_VoteOnObservedOutboundTx(t *testing.T) { OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, Status: common.ReceiveStatus_Success, - Creator: observer, + Creator: accAddress.String(), ObservedOutTxHash: sample.Hash().String(), ValueReceived: cctx.GetCurrentOutTxParam().Amount, ObservedOutTxBlockHeight: 10, @@ -258,11 +259,59 @@ func TestKeeper_VoteOnObservedOutboundTx(t *testing.T) { ObservedOutTxGasUsed: 21, CoinType: cctx.CoinType, } - _, err := msgServer.VoteOnObservedOutboundTx(ctx, msg) + _, err = msgServer.VoteOnObservedOutboundTx(ctx, msg) + require.NoError(t, err) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + require.Equal(t, types.CctxStatus_PendingOutbound, c.CctxStatus.Status) + ballot, found := zk.ObserverKeeper.GetBallot(ctx, msg.Digest()) + require.True(t, found) + require.True(t, ballot.HasVoted(accAddress.String())) + }) + + t.Run("unable to add vote if tss is not present", func(t *testing.T) { + k, ctx, sk, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{}) + + // Setup mock data + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + tss := sample.Tss() + + // set state to successfully vote on outbound tx + accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) require.NoError(t, err) + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{accAddress.String()}}) + sk.StakingKeeper.SetValidator(ctx, validator) + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SetCrossChainTx(ctx, *cctx) + + msgServer := keeper.NewMsgServerImpl(*k) + msg := &types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + Status: common.ReceiveStatus_Success, + Creator: accAddress.String(), + ObservedOutTxHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxBlockHeight: 10, + ObservedOutTxEffectiveGasPrice: math.NewInt(21), + ObservedOutTxGasUsed: 21, + CoinType: cctx.CoinType, + } + _, err = msgServer.VoteOnObservedOutboundTx(ctx, msg) + require.ErrorIs(t, err, types.ErrCannotFindTSSKeys) c, found := k.GetCrossChainTx(ctx, cctx.Index) require.True(t, found) - // Status not changed if this is not the finalizing vote require.Equal(t, types.CctxStatus_PendingOutbound, c.CctxStatus.Status) + _, found = zk.ObserverKeeper.GetBallot(ctx, msg.Digest()) + require.False(t, found) + }) }