Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update pending nonces when aborting a cctx through MsgAbortStuckCCTX #3230

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

* [3206](https://github.com/zeta-chain/node/pull/3206) - skip Solana unsupported transaction version to not block inbound observation
* [3184](https://github.com/zeta-chain/node/pull/3184) - zetaclient should not retry if inbound vote message validation fails
* [3230](https://github.com/zeta-chain/node/pull/3230) - update pending nonces when aborting a cctx through MsgAbortStuckCCTX

## v23.0.0

Expand Down
10 changes: 2 additions & 8 deletions e2e/utils/zetacore.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func WaitCctxsMinedByInboundHash(
allFound := true
for j, cctx := range res.CrossChainTxs {
cctx := cctx
if !IsTerminalStatus(cctx.CctxStatus.Status) {
if !cctx.CctxStatus.Status.IsTerminalStatus() {
// prevent spamming logs
if i%20 == 0 {
logger.Info(
Expand Down Expand Up @@ -170,7 +170,7 @@ func WaitCCTXMinedByIndex(
}

cctx := res.CrossChainTx
if !IsTerminalStatus(cctx.CctxStatus.Status) {
if !cctx.CctxStatus.Status.IsTerminalStatus() {
// prevent spamming logs
if i%20 == 0 {
logger.Info(
Expand Down Expand Up @@ -299,12 +299,6 @@ func WaitCctxByInboundHash(
}
}

func IsTerminalStatus(status crosschaintypes.CctxStatus) bool {
return status == crosschaintypes.CctxStatus_OutboundMined ||
status == crosschaintypes.CctxStatus_Aborted ||
status == crosschaintypes.CctxStatus_Reverted
}

// WaitForBlockHeight waits until the block height reaches the given height
func WaitForBlockHeight(
ctx context.Context,
Expand Down
69 changes: 52 additions & 17 deletions x/crosschain/keeper/cctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,40 @@ import (
)

// SetCctxAndNonceToCctxAndInboundHashToCctx does the following things in one function:
// 1. set the cctx in the store
// 2. set the mapping inboundHash -> cctxIndex , one inboundHash can be connected to multiple cctxindex
// 3. set the mapping nonce => cctx

// 1. Set the Nonce to Cctx mapping
// A new mapping between a nonce and a cctx index should be created only when we add a new outbound to an existing cctx.
// When adding a new outbound , the only two conditions are
// - The cctx is in CctxStatus_PendingOutbound , which means the first outbound has been added, and we need to set the nonce for that
// - The cctx is in CctxStatus_PendingRevert , which means the second outbound has been added, and we need to set the nonce for that

// 2. Set the cctx in the store

// 3. Update the mapping inboundHash -> cctxIndex
// A new value is added to the mapping when a single inbound hash is connected to multiple cctx indexes
// If the inbound hash to cctx mapping does not exist, a new mapping is created and the cctx index is added to the list of cctx indexes

// 4. update the zeta accounting
// Zeta-accounting is updated aborted cctxs of cointtype zeta.When a cctx is aborted it means that `GetAbortedAmount`
//of zeta is locked and cannot be used.

func (k Keeper) SetCctxAndNonceToCctxAndInboundHashToCctx(
ctx sdk.Context,
cctx types.CrossChainTx,
tssPubkey string,
) {
// set mapping nonce => cctxIndex
if cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound ||
cctx.CctxStatus.Status == types.CctxStatus_PendingRevert {
k.GetObserverKeeper().SetNonceToCctx(ctx, observerTypes.NonceToCctx{
ChainId: cctx.GetCurrentOutboundParam().ReceiverChainId,
// #nosec G115 always in range
Nonce: int64(cctx.GetCurrentOutboundParam().TssNonce),
CctxIndex: cctx.Index,
Tss: tssPubkey,
})
}

k.SetNonceToCCTXMapping(ctx, cctx, tssPubkey)
k.SetCrossChainTx(ctx, cctx)
// set mapping inboundHash -> cctxIndex
k.UpdateInboundHashToCCTX(ctx, cctx)
k.UpdateZetaAccounting(ctx, cctx)
}

// UpdateInboundHashToCCTX updates the mapping between an inbound hash and a cctx index.
// A new index is added to the list of cctx indexes if it is not already present
func (k Keeper) UpdateInboundHashToCCTX(
ctx sdk.Context,
cctx types.CrossChainTx,
) {
in, _ := k.GetInboundHashToCctx(ctx, cctx.InboundParams.ObservedHash)
in.InboundHash = cctx.InboundParams.ObservedHash
found := false
Expand All @@ -48,15 +59,39 @@ func (k Keeper) SetCctxAndNonceToCctxAndInboundHashToCctx(
in.CctxIndex = append(in.CctxIndex, cctx.Index)
}
k.SetInboundHashToCctx(ctx, in)
}

func (k Keeper) UpdateZetaAccounting(
ctx sdk.Context,
cctx types.CrossChainTx,
) {
if cctx.CctxStatus.Status == types.CctxStatus_Aborted && cctx.InboundParams.CoinType == coin.CoinType_Zeta {
k.AddZetaAbortedAmount(ctx, GetAbortedAmount(cctx))
}
}

// SetNonceToCCTXMapping updates the mapping between a nonce and a cctx index if the cctx is in a PendingOutbound or PendingRevert state
func (k Keeper) SetNonceToCCTXMapping(
ctx sdk.Context,
cctx types.CrossChainTx,
tssPubkey string,
) {
// set mapping nonce => cctxIndex
if cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound ||
cctx.CctxStatus.Status == types.CctxStatus_PendingRevert {
k.GetObserverKeeper().SetNonceToCctx(ctx, observerTypes.NonceToCctx{
ChainId: cctx.GetCurrentOutboundParam().ReceiverChainId,
// #nosec G115 always in range
Nonce: int64(cctx.GetCurrentOutboundParam().TssNonce),
CctxIndex: cctx.Index,
Tss: tssPubkey,
})
}
}

// SetCrossChainTx set a specific cctx in the store from its index
func (k Keeper) SetCrossChainTx(ctx sdk.Context, cctx types.CrossChainTx) {
// only set the update timestamp if the block height is >0 to allow
// only set the updated timestamp if the block height is >0 to allow
// for a genesis import
if cctx.CctxStatus != nil && ctx.BlockHeight() > 0 {
cctx.CctxStatus.LastUpdateTimestamp = ctx.BlockHeader().Time.Unix()
Expand Down
203 changes: 203 additions & 0 deletions x/crosschain/keeper/cctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,206 @@ func Test_NewCCTX(t *testing.T) {
require.Equal(t, types.ProtocolContractVersion_V1, cctx.ProtocolContractVersion)
})
}

func TestKeeper_UpdateNonceToCCTX(t *testing.T) {
t.Run("should set nonce to cctx if status is PendingOutbound", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
chainID := chains.Ethereum.ChainId
nonce := uint64(10)

cctx := types.CrossChainTx{Index: "test",
OutboundParams: []*types.OutboundParams{{ReceiverChainId: chainID, TssNonce: nonce}},
CctxStatus: &types.Status{Status: types.CctxStatus_PendingOutbound},
}
tssPubkey := "test-tss-pubkey"

// Act
k.SetNonceToCCTXMapping(ctx, cctx, tssPubkey)

// Assert
nonceToCctx, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tssPubkey, chainID, int64(nonce))
require.True(t, found)
require.Equal(t, cctx.Index, nonceToCctx.CctxIndex)
require.Equal(t, tssPubkey, nonceToCctx.Tss)
require.Equal(t, chainID, nonceToCctx.ChainId)
})

t.Run("should set nonce to cctx if status is PendingRevert", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
chainID := chains.Ethereum.ChainId
nonce := uint64(10)

cctx := types.CrossChainTx{Index: "test",
OutboundParams: []*types.OutboundParams{{ReceiverChainId: chainID, TssNonce: nonce}},
CctxStatus: &types.Status{Status: types.CctxStatus_PendingRevert},
}
tssPubkey := "test-tss-pubkey"

// Act
k.SetNonceToCCTXMapping(ctx, cctx, tssPubkey)

// Assert
nonceToCctx, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tssPubkey, chainID, int64(nonce))
require.True(t, found)
require.Equal(t, cctx.Index, nonceToCctx.CctxIndex)
require.Equal(t, tssPubkey, nonceToCctx.Tss)
require.Equal(t, chainID, nonceToCctx.ChainId)
})

t.Run("should not set nonce to cctx if status is not PendingOutbound or PendingRevert", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
chainID := chains.Ethereum.ChainId
nonce := uint64(10)

cctx := types.CrossChainTx{Index: "test",
OutboundParams: []*types.OutboundParams{{ReceiverChainId: chainID, TssNonce: nonce}},
CctxStatus: &types.Status{Status: types.CctxStatus_Aborted},
}
tssPubkey := "test-tss-pubkey"

// Act
k.SetNonceToCCTXMapping(ctx, cctx, tssPubkey)

// Assert
_, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tssPubkey, chainID, int64(nonce))
require.False(t, found)
})
}

func TestKeeper_UpdateInboundHashToCCTX(t *testing.T) {
t.Run(
"should update inbound hash to cctx mapping if new cctx index is found for the same inbound hash",
func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
inboundHash := sample.Hash().String()
index1 := sample.ZetaIndex(t)
index2 := sample.ZetaIndex(t)

inboundHashToCctx := types.InboundHashToCctx{
InboundHash: inboundHash,
CctxIndex: []string{index1},
}
k.SetInboundHashToCctx(ctx, inboundHashToCctx)
cctx := types.CrossChainTx{Index: index2, InboundParams: &types.InboundParams{ObservedHash: inboundHash}}

// Act
k.UpdateInboundHashToCCTX(ctx, cctx)

// Assert
inboundHashToCctx, found := k.GetInboundHashToCctx(ctx, inboundHash)
require.True(t, found)
require.Equal(t, inboundHash, inboundHashToCctx.InboundHash)
require.Equal(t, 2, len(inboundHashToCctx.CctxIndex))
require.Contains(t, inboundHashToCctx.CctxIndex, index1)
require.Contains(t, inboundHashToCctx.CctxIndex, index2)
},
)

t.Run("should do nothing if the cctx index is already in the mapping", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
inboundHash := sample.Hash().String()
index := sample.ZetaIndex(t)

inboundHashToCctx := types.InboundHashToCctx{
InboundHash: inboundHash,
CctxIndex: []string{index},
}
k.SetInboundHashToCctx(ctx, inboundHashToCctx)
cctx := types.CrossChainTx{Index: index, InboundParams: &types.InboundParams{ObservedHash: inboundHash}}

// Act
k.UpdateInboundHashToCCTX(ctx, cctx)

// Assert
inboundHashToCctx, found := k.GetInboundHashToCctx(ctx, inboundHash)
require.True(t, found)
require.Equal(t, inboundHash, inboundHashToCctx.InboundHash)
require.Equal(t, 1, len(inboundHashToCctx.CctxIndex))
require.Contains(t, inboundHashToCctx.CctxIndex, index)
})

t.Run("should add cctx index to mapping if InboundHashToCctx is not found", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
inboundHash := sample.Hash().String()
index := sample.ZetaIndex(t)

cctx := types.CrossChainTx{Index: index, InboundParams: &types.InboundParams{ObservedHash: inboundHash}}

// Act
k.UpdateInboundHashToCCTX(ctx, cctx)

// Assert
inboundHashToCctx, found := k.GetInboundHashToCctx(ctx, inboundHash)
require.True(t, found)
require.Equal(t, inboundHash, inboundHashToCctx.InboundHash)
require.Equal(t, 1, len(inboundHashToCctx.CctxIndex))
require.Contains(t, inboundHashToCctx.CctxIndex, index)
})
}

func TestKeeper_UpdateZetaAccounting(t *testing.T) {
t.Run("should update zeta accounting if cctx is aborted and coin type is zeta", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
amount := sdkmath.NewUint(100)
cctx := types.CrossChainTx{
InboundParams: &types.InboundParams{CoinType: coin.CoinType_Zeta},
CctxStatus: &types.Status{Status: types.CctxStatus_Aborted},
OutboundParams: []*types.OutboundParams{{Amount: amount}},
}
k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: math.ZeroUint()})

// Act
k.UpdateZetaAccounting(ctx, cctx)

// Assert
zetaAccounting, found := k.GetZetaAccounting(ctx)
require.True(t, found)
require.Equal(t, amount, zetaAccounting.AbortedZetaAmount)
})

t.Run("should not update zeta accounting if cctx is not aborted", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
amount := sdkmath.NewUint(100)
cctx := types.CrossChainTx{
InboundParams: &types.InboundParams{CoinType: coin.CoinType_Zeta},
CctxStatus: &types.Status{Status: types.CctxStatus_PendingOutbound},
OutboundParams: []*types.OutboundParams{{Amount: amount}},
}
k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: math.ZeroUint()})

// Act
k.UpdateZetaAccounting(ctx, cctx)

// Assert
zetaAccounting, found := k.GetZetaAccounting(ctx)
require.True(t, found)
require.Equal(t, math.ZeroUint(), zetaAccounting.AbortedZetaAmount)
})

t.Run("should update to amount if zeta accounting is not set", func(t *testing.T) {
// Arrange
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
amount := sdkmath.NewUint(100)
cctx := types.CrossChainTx{
InboundParams: &types.InboundParams{CoinType: coin.CoinType_Zeta},
CctxStatus: &types.Status{Status: types.CctxStatus_Aborted},
OutboundParams: []*types.OutboundParams{{Amount: amount}},
}

// Act
k.UpdateZetaAccounting(ctx, cctx)

// Assert
zetaAccounting, found := k.GetZetaAccounting(ctx)
require.True(t, found)
require.Equal(t, amount, zetaAccounting.AbortedZetaAmount)
})
}
15 changes: 6 additions & 9 deletions x/crosschain/keeper/msg_server_abort_stuck_cctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,16 @@ func (k msgServer) AbortStuckCCTX(
}

// check if the cctx is pending
isPending := cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound ||
cctx.CctxStatus.Status == types.CctxStatus_PendingInbound ||
cctx.CctxStatus.Status == types.CctxStatus_PendingRevert
if !isPending {
if !cctx.CctxStatus.Status.IsPendingStatus() {
return nil, types.ErrStatusNotPending
}

cctx.CctxStatus = &types.Status{
Status: types.CctxStatus_Aborted,
StatusMessage: AbortMessage,
}
// update the status
cctx.CctxStatus.UpdateStatusAndErrorMessages(types.CctxStatus_Aborted, AbortMessage, "")

k.SetCrossChainTx(ctx, cctx)
// Save out outbound,
// We do not need to provide the tss-pubkey as NonceToCctx is not updated / New outbound is not added
k.SaveOutbound(ctx, &cctx, "")

return &types.MsgAbortStuckCCTXResponse{}, nil
}
Loading
Loading