diff --git a/changelog.md b/changelog.md index 473901a55b..7921ca10de 100644 --- a/changelog.md +++ b/changelog.md @@ -30,7 +30,7 @@ * [2339](https://github.com/zeta-chain/node/pull/2339) - add binaries related question to syncing issue form * [2366](https://github.com/zeta-chain/node/pull/2366) - add migration script for adding authorizations table * [2372](https://github.com/zeta-chain/node/pull/2372) - add queries for tss fund migration info -* [2416g](https://github.com/zeta-chain/node/pull/2416) - add Solana chain information +* [2416](https://github.com/zeta-chain/node/pull/2416) - add Solana chain information ### Refactor @@ -59,6 +59,7 @@ * [2380](https://github.com/zeta-chain/node/pull/2380) - use `ChainInfo` in `authority` to allow dynamically support new chains * [2395](https://github.com/zeta-chain/node/pull/2395) - converge AppContext with ZetaCoreContext in zetaclient * [2428](https://github.com/zeta-chain/node/pull/2428) - propagate context across codebase & refactor zetacore client +* [2464](https://github.com/zeta-chain/node/pull/2464) - move common voting logic to voting.go and add new function VoteOnBallot ### Tests @@ -95,6 +96,7 @@ * [2434](https://github.com/zeta-chain/node/pull/2434) - the default database when running `zetacored init` is now pebbledb ### CI + * [2388](https://github.com/zeta-chain/node/pull/2388) - added GitHub attestations of binaries produced in the release workflow. * [2285](https://github.com/zeta-chain/node/pull/2285) - added nightly EVM performance testing pipeline, modified localnet testing docker image to utilitze debian:bookworm, removed build-jet runners where applicable, removed deprecated/removed upgrade path testing pipeline * [2268](https://github.com/zeta-chain/node/pull/2268) - updated the publish-release pipeline to utilize the Github Actions Ubuntu 20.04 Runners @@ -214,7 +216,7 @@ * [1861](https://github.com/zeta-chain/node/pull/1861) - fix `ObserverSlashAmount` invalid read * [1880](https://github.com/zeta-chain/node/issues/1880) - lower the gas price multiplier for EVM chains * [1883](https://github.com/zeta-chain/node/issues/1883) - zetaclient should check 'IsSupported' flag to pause/unpause a specific chain -* * [2076](https://github.com/zeta-chain/node/pull/2076) - automatically deposit native zeta to an address if it doesn't exist on ZEVM +* [2076](https://github.com/zeta-chain/node/pull/2076) - automatically deposit native zeta to an address if it doesn't exist on ZEVM * [1633](https://github.com/zeta-chain/node/issues/1633) - zetaclient should be able to pick up new connector and erc20Custody addresses * [1944](https://github.com/zeta-chain/node/pull/1944) - fix evm signer unit tests * [1888](https://github.com/zeta-chain/node/issues/1888) - zetaclient should stop inbound/outbound txs according to cross-chain flags @@ -237,11 +239,12 @@ ## Version: v15.0.0 ### Features + * [1912](https://github.com/zeta-chain/node/pull/1912) - add reset chain nonces msg ## Version: v14.0.1 -- [1817](https://github.com/zeta-chain/node/pull/1817) - Add migration script to fix pending and chain nonces on testnet +* [1817](https://github.com/zeta-chain/node/pull/1817) - Add migration script to fix pending and chain nonces on testnet ## Version: v13.0.0 @@ -550,4 +553,4 @@ Getting the correct TSS address for Bitcoin now requires proviidng the Bitcoin c ### CI * [1218](https://github.com/zeta-chain/node/pull/1218) - cross-compile release binaries and simplify PR testings -* [1302](https://github.com/zeta-chain/node/pull/1302) - add mainnet builds to goreleaser +* [1302](https://github.com/zeta-chain/node/pull/1302) - add mainnet builds to goreleaser \ No newline at end of file diff --git a/x/crosschain/keeper/msg_server_vote_gas_price.go b/x/crosschain/keeper/msg_server_vote_gas_price.go index 040c85fd75..aa56a73e26 100644 --- a/x/crosschain/keeper/msg_server_vote_gas_price.go +++ b/x/crosschain/keeper/msg_server_vote_gas_price.go @@ -2,7 +2,6 @@ package keeper import ( "context" - "fmt" "math/big" "sort" "strconv" @@ -28,7 +27,7 @@ func (k msgServer) VoteGasPrice( chain, found := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, msg.ChainId) if !found { - return nil, cosmoserrors.Wrap(types.ErrUnsupportedChain, fmt.Sprintf("ChainID : %d ", msg.ChainId)) + return nil, cosmoserrors.Wrapf(types.ErrUnsupportedChain, "ChainID: %d ", msg.ChainId) } if ok := k.zetaObserverKeeper.IsNonTombstonedObserver(ctx, msg.Creator); !ok { return nil, observertypes.ErrNotObserver diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx.go b/x/crosschain/keeper/msg_server_vote_inbound_tx.go index e597509ab1..5aa925b363 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx.go @@ -2,14 +2,15 @@ package keeper import ( "context" - "fmt" - cosmoserrors "cosmossdk.io/errors" + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/zetacore/x/crosschain/types" ) +const voteInboundID = "Vote Inbound" + // FIXME: use more specific error types & codes // VoteInbound casts a vote on an inbound transaction observed on a connected chain. If this @@ -73,7 +74,7 @@ func (k msgServer) VoteInbound( msg.InboundHash, ) if err != nil { - return nil, err + return nil, sdkerrors.Wrap(err, voteInboundID) } // If it is a new ballot, check if an inbound with the same hash, sender chain and event index has already been finalized @@ -81,14 +82,13 @@ func (k msgServer) VoteInbound( // This check prevents double spending if isNew { if k.IsFinalizedInbound(tmpCtx, msg.InboundHash, msg.SenderChainId, msg.EventIndex) { - return nil, cosmoserrors.Wrap( + return nil, sdkerrors.Wrapf( types.ErrObservedTxAlreadyFinalized, - fmt.Sprintf( - "inboundHash:%s, SenderChainID:%d, EventIndex:%d", - msg.InboundHash, - msg.SenderChainId, - msg.EventIndex, - ), + "%s, InboundHash:%s, SenderChainID:%d, EventIndex:%d", + voteInboundID, + msg.InboundHash, + msg.SenderChainId, + msg.EventIndex, ) } } @@ -100,7 +100,7 @@ func (k msgServer) VoteInbound( cctx, err := k.ValidateInbound(ctx, msg, true) if err != nil { - return nil, err + return nil, sdkerrors.Wrap(err, voteInboundID) } // Save the inbound CCTX to the store. This is called irrespective of the status of the CCTX or the outcome of the process function. diff --git a/x/crosschain/keeper/msg_server_vote_outbound_tx.go b/x/crosschain/keeper/msg_server_vote_outbound_tx.go index 8cb4a2ce6d..61e1d9389e 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx.go @@ -14,6 +14,8 @@ import ( observerkeeper "github.com/zeta-chain/zetacore/x/observer/keeper" ) +const voteOutboundID = "Vote Outbound" + // VoteOutbound casts a vote on an outbound transaction observed on a connected chain (after // it has been broadcasted to and finalized on a connected chain). If this is // the first vote, a new ballot is created. When a threshold of votes is @@ -61,14 +63,13 @@ func (k msgServer) VoteOutbound( ) (*types.MsgVoteOutboundResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // Validate the message params to verify it against an existing cctx + // Validate the message params to verify it against an existing CCTX. cctx, err := k.ValidateOutboundMessage(ctx, *msg) if err != nil { - return nil, err + return nil, cosmoserrors.Wrap(err, voteOutboundID) } - // get ballot index + ballotIndex := msg.Digest() - // vote on outbound ballot isFinalizingVote, isNew, ballot, observationChain, err := k.zetaObserverKeeper.VoteOnOutboundBallot( ctx, ballotIndex, @@ -76,23 +77,26 @@ func (k msgServer) VoteOutbound( msg.Status, msg.Creator) if err != nil { - return nil, err + return nil, cosmoserrors.Wrap(err, voteOutboundID) } - // if the ballot is new, set the index to the CCTX + + // If the ballot is new, set the index to the CCTX. if isNew { observerkeeper.EmitEventBallotCreated(ctx, ballot, msg.ObservedOutboundHash, observationChain) } - // if not finalized commit state here + + // If not finalized commit state here. if !isFinalizingVote { return &types.MsgVoteOutboundResponse{}, nil } - // if ballot successful, the value received should be the out tx amount + // If ballot is successful, the value received should be the out tx amount. err = cctx.AddOutbound(ctx, *msg, ballot.BallotStatus) if err != nil { - return nil, err + return nil, cosmoserrors.Wrap(err, voteOutboundID) } - // Fund the gas stability pool with the remaining funds + + // Fund the gas stability pool with the remaining funds. k.FundStabilityPool(ctx, &cctx) err = k.ValidateOutboundObservers(ctx, &cctx, ballot.BallotStatus, msg.ValueReceived.String()) @@ -100,6 +104,7 @@ func (k msgServer) VoteOutbound( k.SaveFailedOutbound(ctx, &cctx, err.Error(), ballotIndex) return &types.MsgVoteOutboundResponse{}, nil } + k.SaveSuccessfulOutbound(ctx, &cctx, ballotIndex) return &types.MsgVoteOutboundResponse{}, nil } @@ -112,7 +117,7 @@ func (k Keeper) FundStabilityPool(ctx sdk.Context, cctx *types.CrossChainTx) { // Fund the gas stability pool with the remaining funds if err := k.FundGasStabilityPoolFromRemainingFees(ctx, *cctx.GetCurrentOutboundParam(), cctx.GetCurrentOutboundParam().ReceiverChainId); err != nil { ctx.Logger(). - Error(fmt.Sprintf("VoteOutbound: CCTX: %s Can't fund the gas stability pool with remaining fees %s", cctx.Index, err.Error())) + Error("%s: CCTX: %s Can't fund the gas stability pool with remaining fees %s", voteOutboundID, cctx.Index, err.Error()) } } @@ -136,16 +141,18 @@ func (k Keeper) FundGasStabilityPoolFromRemainingFees( remainingGas := gasLimit - gasUsed remainingFees := math.NewUint(remainingGas).Mul(gasPrice).BigInt() - // We fund the stability pool with a portion of the remaining fees + // We fund the stability pool with a portion of the remaining fees. remainingFees = percentOf(remainingFees, RemainingFeesToStabilityPoolPercent) - // Fund the gas stability pool + + // Fund the gas stability pool. if err := k.fungibleKeeper.FundGasStabilityPool(ctx, chainID, remainingFees); err != nil { return err } } else { - return fmt.Errorf("VoteOutbound: The gas limit %d is less than the gas used %d", gasLimit, gasUsed) + return fmt.Errorf("%s: The gas limit %d is less than the gas used %d", voteOutboundID, gasLimit, gasUsed) } } + return nil } @@ -167,7 +174,6 @@ SaveFailedOutbound saves a failed outbound transaction.It does the following thi func (k Keeper) SaveFailedOutbound(ctx sdk.Context, cctx *types.CrossChainTx, errMessage string, ballotIndex string) { cctx.SetAbort(errMessage) ctx.Logger().Error(errMessage) - k.SaveOutbound(ctx, cctx, ballotIndex) } @@ -195,48 +201,51 @@ func (k Keeper) SaveOutbound(ctx sdk.Context, cctx *types.CrossChainTx, ballotIn outTxTssNonce := cctx.GetCurrentOutboundParam().TssNonce cctx.GetCurrentOutboundParam().BallotIndex = ballotIndex + // #nosec G115 always in range k.GetObserverKeeper().RemoveFromPendingNonces(ctx, tssPubkey, receiverChain, int64(outTxTssNonce)) k.RemoveOutboundTrackerFromStore(ctx, receiverChain, outTxTssNonce) ctx.Logger(). - Info(fmt.Sprintf("Remove tracker %s: , Block Height : %d ", getOutboundTrackerIndex(receiverChain, outTxTssNonce), ctx.BlockHeight())) + Info("%s: Remove tracker %s: , Block Height : %d ", voteOutboundID, getOutboundTrackerIndex(receiverChain, outTxTssNonce), ctx.BlockHeight()) + // This should set nonce to cctx only if a new revert is created. k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, *cctx) } func (k Keeper) ValidateOutboundMessage(ctx sdk.Context, msg types.MsgVoteOutbound) (types.CrossChainTx, error) { - // check if CCTX exists and if the nonce matches + // Check if CCTX exists and if the nonce matches. cctx, found := k.GetCrossChainTx(ctx, msg.CctxHash) if !found { - return types.CrossChainTx{}, cosmoserrors.Wrap( + return types.CrossChainTx{}, cosmoserrors.Wrapf( sdkerrors.ErrInvalidRequest, - fmt.Sprintf("CCTX %s does not exist", msg.CctxHash), - ) + "%s, CCTX %s does not exist", voteOutboundID, msg.CctxHash) } + if cctx.GetCurrentOutboundParam().TssNonce != msg.OutboundTssNonce { - return types.CrossChainTx{}, cosmoserrors.Wrap( + return types.CrossChainTx{}, cosmoserrors.Wrapf( sdkerrors.ErrInvalidRequest, - fmt.Sprintf( - "OutboundTssNonce %d does not match CCTX OutboundTssNonce %d", - msg.OutboundTssNonce, - cctx.GetCurrentOutboundParam().TssNonce, - ), + "%s, OutboundTssNonce %d does not match CCTX OutboundTssNonce %d", + voteOutboundID, + msg.OutboundTssNonce, + cctx.GetCurrentOutboundParam().TssNonce, ) } - // do not process an outbound vote if TSS is not found + + // Do not process an outbound vote if TSS is not found. _, found = k.zetaObserverKeeper.GetTSS(ctx) if !found { - return types.CrossChainTx{}, types.ErrCannotFindTSSKeys + return types.CrossChainTx{}, cosmoserrors.Wrap(types.ErrCannotFindTSSKeys, voteOutboundID) } + if cctx.GetCurrentOutboundParam().ReceiverChainId != msg.OutboundChain { - return types.CrossChainTx{}, cosmoserrors.Wrap( + return types.CrossChainTx{}, cosmoserrors.Wrapf( sdkerrors.ErrInvalidRequest, - fmt.Sprintf( - "OutboundChain %d does not match CCTX OutboundChain %d", - msg.OutboundChain, - cctx.GetCurrentOutboundParam().ReceiverChainId, - ), + "%s, OutboundChain %d does not match CCTX OutboundChain %d", + voteOutboundID, + msg.OutboundChain, + cctx.GetCurrentOutboundParam().ReceiverChainId, ) } + return cctx, nil } diff --git a/x/observer/keeper/msg_server_update_observer.go b/x/observer/keeper/msg_server_update_observer.go index a1e4c134d1..f7686c4782 100644 --- a/x/observer/keeper/msg_server_update_observer.go +++ b/x/observer/keeper/msg_server_update_observer.go @@ -2,7 +2,6 @@ package keeper import ( "context" - "fmt" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -25,18 +24,16 @@ func (k msgServer) UpdateObserver( return nil, errorsmod.Wrap(types.ErrUpdateObserver, err.Error()) } if !ok { - return nil, errorsmod.Wrap( + return nil, errorsmod.Wrapf( types.ErrUpdateObserver, - fmt.Sprintf("Unable to update observer with update reason : %s", msg.UpdateReason), - ) + "Unable to update observer with update reason : %s", msg.UpdateReason) } // We do not use IsNonTombstonedObserver here because we want to allow tombstoned observers to be updated if !k.IsAddressPartOfObserverSet(ctx, msg.OldObserverAddress) { - return nil, errorsmod.Wrap( + return nil, errorsmod.Wrapf( types.ErrNotObserver, - fmt.Sprintf("Observer address is not authorized : %s", msg.OldObserverAddress), - ) + "Observer address is not authorized : %s", msg.OldObserverAddress) } err = k.IsValidator(ctx, msg.NewObserverAddress) @@ -53,10 +50,9 @@ func (k msgServer) UpdateObserver( // Update the node account with the new operator address nodeAccount, found := k.GetNodeAccount(ctx, msg.OldObserverAddress) if !found { - return nil, errorsmod.Wrap( + return nil, errorsmod.Wrapf( types.ErrNodeAccountNotFound, - fmt.Sprintf("Observer node account not found : %s", msg.OldObserverAddress), - ) + "Observer node account not found : %s", msg.OldObserverAddress) } newNodeAccount := nodeAccount newNodeAccount.Operator = msg.NewObserverAddress @@ -68,15 +64,15 @@ func (k msgServer) UpdateObserver( // Check LastBlockObserver count just to be safe observerSet, found := k.GetObserverSet(ctx) if !found { - return nil, errorsmod.Wrap(types.ErrObserverSetNotFound, fmt.Sprintf("Observer set not found")) + return nil, errorsmod.Wrap(types.ErrObserverSetNotFound, "Observer set not found") } totalObserverCountCurrentBlock := observerSet.LenUint() lastBlockCount, found := k.GetLastObserverCount(ctx) if !found { - return nil, errorsmod.Wrap(types.ErrLastObserverCountNotFound, fmt.Sprintf("Observer count not found")) + return nil, errorsmod.Wrap(types.ErrLastObserverCountNotFound, "Observer count not found") } if lastBlockCount.Count != totalObserverCountCurrentBlock { - return nil, errorsmod.Wrap(types.ErrUpdateObserver, fmt.Sprintf("Observer count mismatch")) + return nil, errorsmod.Wrap(types.ErrUpdateObserver, "Observer count mismatch") } return &types.MsgUpdateObserverResponse{}, nil } @@ -88,9 +84,7 @@ func (k Keeper) CheckUpdateReason(ctx sdk.Context, msg *types.MsgUpdateObserver) if msg.Creator != msg.OldObserverAddress { return false, errorsmod.Wrap( types.ErrUpdateObserver, - fmt.Sprintf( - "Creator address and old observer address need to be same for updating tombstoned observer", - ), + "Creator address and old observer address need to be same for updating tombstoned observer", ) } return k.IsOperatorTombstoned(ctx, msg.Creator) diff --git a/x/observer/keeper/msg_server_vote_blame.go b/x/observer/keeper/msg_server_vote_blame.go index 217d5e5912..61cbce01db 100644 --- a/x/observer/keeper/msg_server_vote_blame.go +++ b/x/observer/keeper/msg_server_vote_blame.go @@ -2,63 +2,59 @@ package keeper import ( "context" - "fmt" - cosmoserrors "cosmossdk.io/errors" + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" crosschainTypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/x/observer/types" ) +const voteBlameID = "Vote Blame" + func (k msgServer) VoteBlame( goCtx context.Context, - vote *types.MsgVoteBlame, + msg *types.MsgVoteBlame, ) (*types.MsgVoteBlameResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - observationType := types.ObservationType_TSSKeySign // GetChainFromChainID makes sure we are getting only supported chains , if a chain support has been turned on using gov proposal, this function returns nil - observationChain, found := k.GetSupportedChainFromChainID(ctx, vote.ChainId) + observationChain, found := k.GetSupportedChainFromChainID(ctx, msg.ChainId) if !found { - return nil, cosmoserrors.Wrap( + return nil, sdkerrors.Wrapf( crosschainTypes.ErrUnsupportedChain, - fmt.Sprintf("ChainID %d, Blame vote", vote.ChainId), - ) + "%s, ChainID %d", voteBlameID, msg.ChainId) } - if ok := k.IsNonTombstonedObserver(ctx, vote.Creator); !ok { - return nil, types.ErrNotObserver + if ok := k.IsNonTombstonedObserver(ctx, msg.Creator); !ok { + return nil, sdkerrors.Wrap( + types.ErrNotObserver, voteBlameID) } - index := vote.Digest() - // Add votes and Set Ballot - // GetBallot checks against the supported chains list before querying for Ballot - ballot, isNew, err := k.FindBallot(ctx, index, observationChain, observationType) + ballot, isFinalized, isNew, err := k.VoteOnBallot( + ctx, + observationChain, + msg.Digest(), + types.ObservationType_TSSKeySign, + msg.Creator, + types.VoteType_SuccessObservation, + ) if err != nil { - return nil, err + return nil, sdkerrors.Wrapf( + err, + "%s, BallotIdentifier %v", voteBlameID, ballot.BallotIdentifier) } if isNew { - EmitEventBallotCreated(ctx, ballot, vote.BlameInfo.Index, observationChain.String()) + EmitEventBallotCreated(ctx, ballot, msg.BlameInfo.Index, observationChain.String()) } - // AddVoteToBallot adds a vote and sets the ballot - ballot, err = k.AddVoteToBallot(ctx, ballot, vote.Creator, types.VoteType_SuccessObservation) - if err != nil { - return nil, err - } - - _, isFinalized := k.CheckIfFinalizingVote(ctx, ballot) if !isFinalized { - // Return nil here to add vote to ballot and commit state + // Return nil here to add vote to ballot and commit state. return &types.MsgVoteBlameResponse{}, nil } - // ****************************************************************************** - // below only happens when ballot is finalized: exactly when threshold vote is in - // ****************************************************************************** - - k.SetBlame(ctx, vote.BlameInfo) + // Ballot is finalized: exactly when threshold vote is in. + k.SetBlame(ctx, msg.BlameInfo) return &types.MsgVoteBlameResponse{}, nil } diff --git a/x/observer/keeper/msg_server_vote_block_header.go b/x/observer/keeper/msg_server_vote_block_header.go index 9ae7a35a56..b4e0d57d0d 100644 --- a/x/observer/keeper/msg_server_vote_block_header.go +++ b/x/observer/keeper/msg_server_vote_block_header.go @@ -3,13 +3,15 @@ package keeper import ( "context" - cosmoserrors "cosmossdk.io/errors" + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" "github.com/zeta-chain/zetacore/x/observer/types" ) +const voteBlockHeaderID = "Vote BlockHeader" + // VoteBlockHeader vote for a new block header to the storers func (k msgServer) VoteBlockHeader( goCtx context.Context, @@ -20,30 +22,36 @@ func (k msgServer) VoteBlockHeader( // check if the chain is enabled chain, found := k.GetSupportedChainFromChainID(ctx, msg.ChainId) if !found { - return nil, cosmoserrors.Wrapf(types.ErrSupportedChains, "chain id: %d", msg.ChainId) + return nil, sdkerrors.Wrapf( + types.ErrSupportedChains, + "%s, ChainID %d", voteBlockHeaderID, msg.ChainId) } // check if observer if ok := k.IsNonTombstonedObserver(ctx, msg.Creator); !ok { - return nil, types.ErrNotObserver + return nil, sdkerrors.Wrap(types.ErrNotObserver, voteBlockHeaderID) } // check the new block header is valid parentHash, err := k.lightclientKeeper.CheckNewBlockHeader(ctx, msg.ChainId, msg.BlockHash, msg.Height, msg.Header) if err != nil { - return nil, cosmoserrors.Wrap(lightclienttypes.ErrInvalidBlockHeader, err.Error()) + return nil, sdkerrors.Wrapf( + lightclienttypes.ErrInvalidBlockHeader, + "%s, parent hash %s", voteBlockHeaderID, parentHash) } - // add vote to ballot - ballot, isNew, err := k.FindBallot(ctx, msg.Digest(), chain, types.ObservationType_InboundTx) - if err != nil { - return nil, cosmoserrors.Wrap(err, "failed to find ballot") - } - ballot, err = k.AddVoteToBallot(ctx, ballot, msg.Creator, types.VoteType_SuccessObservation) + _, isFinalized, isNew, err := k.VoteOnBallot( + ctx, + chain, + msg.Digest(), + types.ObservationType_InboundTx, + msg.Creator, + types.VoteType_SuccessObservation, + ) if err != nil { - return nil, cosmoserrors.Wrap(err, "failed to add vote to ballot") + return nil, sdkerrors.Wrap(err, voteBlockHeaderID) } - _, isFinalized := k.CheckIfFinalizingVote(ctx, ballot) + if !isFinalized { return &types.MsgVoteBlockHeaderResponse{ BallotCreated: isNew, @@ -51,9 +59,8 @@ func (k msgServer) VoteBlockHeader( }, nil } - // add the new block header to the store + // Add the new block header to the store. k.lightclientKeeper.AddBlockHeader(ctx, msg.ChainId, msg.Height, msg.BlockHash, msg.Header, parentHash) - return &types.MsgVoteBlockHeaderResponse{ BallotCreated: isNew, VoteFinalized: true, diff --git a/x/observer/keeper/msg_server_vote_tss.go b/x/observer/keeper/msg_server_vote_tss.go index ae40c41444..617411d575 100644 --- a/x/observer/keeper/msg_server_vote_tss.go +++ b/x/observer/keeper/msg_server_vote_tss.go @@ -2,7 +2,6 @@ package keeper import ( "context" - "fmt" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -13,6 +12,8 @@ import ( "github.com/zeta-chain/zetacore/x/observer/types" ) +const voteTSSid = "Vote TSS" + // VoteTSS votes on creating a TSS key and recording the information about it (public // key, participant and operator addresses, finalized and keygen heights). // @@ -26,37 +27,38 @@ import ( func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types.MsgVoteTSSResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // checks whether a signer is authorized to sign , by checking their address against the observer mapper which contains the observer list for the chain and type + // Checks whether a signer is authorized to sign, by checking their address against the observer mapper + // which contains the observer list for the chain and type. _, found := k.GetNodeAccount(ctx, msg.Creator) if !found { - return nil, errorsmod.Wrap( + return nil, errorsmod.Wrapf( sdkerrors.ErrorInvalidSigner, - fmt.Sprintf("signer %s does not have a node account set", msg.Creator), - ) + "%s, signer %s does not have a node account set", voteTSSid, msg.Creator) } - // no need to create a ballot if keygen does not exist + + // No need to create a ballot if keygen does not exist. keygen, found := k.GetKeygen(ctx) if !found { - return &types.MsgVoteTSSResponse{}, types.ErrKeygenNotFound + return &types.MsgVoteTSSResponse{}, errorsmod.Wrap(types.ErrKeygenNotFound, voteTSSid) } - // use a separate transaction to update KEYGEN status to pending when trying to change the TSS address + + // Use a separate transaction to update keygen status to pending when trying to change the TSS address. if keygen.Status == types.KeygenStatus_KeyGenSuccess { - return &types.MsgVoteTSSResponse{}, types.ErrKeygenCompleted + return &types.MsgVoteTSSResponse{}, errorsmod.Wrap(types.ErrKeygenCompleted, voteTSSid) } - // add votes and set Ballot - // GetBallot checks against the supported chains list before querying for Ballot - // TODO : https://github.com/zeta-chain/node/issues/896 + // GetBallot checks against the supported chains list before querying for Ballot. ballotCreated := false index := msg.Digest() ballot, found := k.GetBallot(ctx, index) if !found { - // if ballot does not exist, create a new ballot + // If ballot does not exist, create a new ballot. var voterList []string for _, nodeAccount := range k.GetAllNodeAccount(ctx) { voterList = append(voterList, nodeAccount.Operator) } + ballot = types.Ballot{ Index: "", BallotIdentifier: index, @@ -73,29 +75,27 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types ballotCreated = true } - // vote the ballot - var err error vote := types.VoteType_SuccessObservation if msg.Status == chains.ReceiveStatus_failed { vote = types.VoteType_FailureObservation } - ballot, err = k.AddVoteToBallot(ctx, ballot, msg.Creator, vote) + + ballot, err := k.AddVoteToBallot(ctx, ballot, msg.Creator, vote) if err != nil { - return &types.MsgVoteTSSResponse{}, err + return &types.MsgVoteTSSResponse{}, errorsmod.Wrap(err, voteTSSid) } - // returns here if the ballot is not finalized ballot, isFinalized := k.CheckIfFinalizingVote(ctx, ballot) if !isFinalized { return &types.MsgVoteTSSResponse{ - VoteFinalized: false, + VoteFinalized: isFinalized, BallotCreated: ballotCreated, KeygenSuccess: false, }, nil } - // set TSS only on success, set Keygen either way. - // keygen block can be updated using a policy transaction if keygen fails + // Set TSS only on success, set keygen either way. + // Keygen block can be updated using a policy transaction if keygen fails. keygenSuccess := false if ballot.BallotStatus == types.BallotStatus_BallotFinalized_FailureObservation { keygen.Status = types.KeygenStatus_KeyGenFailed @@ -108,8 +108,9 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types FinalizedZetaHeight: ctx.BlockHeight(), KeyGenZetaHeight: msg.KeygenZetaHeight, } - // set TSS history only, current TSS is updated via admin transaction - // in Case this is the first TSS address update both current and history + + // Set TSS history only, current TSS is updated via admin transaction. + // In the case this is the first TSS address update both current and history. tssList := k.GetAllTSS(ctx) if len(tssList) == 0 { k.SetTssAndUpdateNonce(ctx, tss) @@ -123,7 +124,7 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types k.SetKeygen(ctx, keygen) return &types.MsgVoteTSSResponse{ - VoteFinalized: true, + VoteFinalized: isFinalized, BallotCreated: ballotCreated, KeygenSuccess: keygenSuccess, }, nil diff --git a/x/observer/keeper/vote_inbound.go b/x/observer/keeper/vote_inbound.go index 676e93aeaa..2a2bc8a66b 100644 --- a/x/observer/keeper/vote_inbound.go +++ b/x/observer/keeper/vote_inbound.go @@ -1,10 +1,8 @@ package keeper import ( - "fmt" - + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/observer/types" @@ -21,7 +19,7 @@ func (k Keeper) VoteOnInboundBallot( voter string, ballotIndex string, inboundHash string, -) (bool, bool, error) { +) (isFinalized bool, isNew bool, err error) { if !k.IsInboundEnabled(ctx) { return false, false, types.ErrInboundDisabled } @@ -31,11 +29,10 @@ func (k Keeper) VoteOnInboundBallot( // this function returns nil senderChain, found := k.GetSupportedChainFromChainID(ctx, senderChainID) if !found { - return false, false, sdkerrors.Wrap(types.ErrSupportedChains, fmt.Sprintf( + return false, false, sdkerrors.Wrapf(types.ErrSupportedChains, "ChainID %d, Observation %s", senderChainID, - types.ObservationType_InboundTx.String()), - ) + types.ObservationType_InboundTx.String()) } // checks the voter is authorized to vote on the observation chain @@ -46,11 +43,10 @@ func (k Keeper) VoteOnInboundBallot( // makes sure we are getting only supported chains receiverChain, found := k.GetSupportedChainFromChainID(ctx, receiverChainID) if !found { - return false, false, sdkerrors.Wrap(types.ErrSupportedChains, fmt.Sprintf( + return false, false, sdkerrors.Wrapf(types.ErrSupportedChains, "ChainID %d, Observation %s", receiverChainID, - types.ObservationType_InboundTx.String()), - ) + types.ObservationType_InboundTx.String()) } // check if we want to send ZETA to external chain, but there is no ZETA token. @@ -64,22 +60,21 @@ func (k Keeper) VoteOnInboundBallot( } } - // checks against the supported chains list before querying for Ballot - ballot, isNew, err := k.FindBallot(ctx, ballotIndex, senderChain, types.ObservationType_InboundTx) + ballot, isFinalized, isNew, err := k.VoteOnBallot( + ctx, + senderChain, + ballotIndex, + types.ObservationType_InboundTx, + voter, + types.VoteType_SuccessObservation, + ) if err != nil { - return false, false, err + return false, false, sdkerrors.Wrap(err, msgVoteOnBallot) } + if isNew { EmitEventBallotCreated(ctx, ballot, inboundHash, senderChain.String()) } - // adds a vote and sets the ballot - ballot, err = k.AddVoteToBallot(ctx, ballot, voter, types.VoteType_SuccessObservation) - if err != nil { - return false, isNew, err - } - - // checks if the ballot is finalized - _, isFinalized := k.CheckIfFinalizingVote(ctx, ballot) return isFinalized, isNew, nil } diff --git a/x/observer/keeper/vote_outbound.go b/x/observer/keeper/vote_outbound.go index a8212980ed..5713ee2bf1 100644 --- a/x/observer/keeper/vote_outbound.go +++ b/x/observer/keeper/vote_outbound.go @@ -1,6 +1,7 @@ package keeper import ( + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/zetacore/pkg/chains" @@ -36,18 +37,17 @@ func (k Keeper) VoteOnOutboundBallot( return false, false, ballot, "", observertypes.ErrNotObserver } - // fetch or create ballot - ballot, isNew, err = k.FindBallot(ctx, ballotIndex, observationChain, observertypes.ObservationType_OutboundTx) + ballot, isFinalized, isNew, err = k.VoteOnBallot( + ctx, + observationChain, + ballotIndex, + observertypes.ObservationType_OutboundTx, + voter, + observertypes.ConvertReceiveStatusToVoteType(receiveStatus), + ) if err != nil { - return false, false, ballot, "", err + return false, false, ballot, "", sdkerrors.Wrap(err, msgVoteOnBallot) } - // add vote to ballot - ballot, err = k.AddVoteToBallot(ctx, ballot, voter, observertypes.ConvertReceiveStatusToVoteType(receiveStatus)) - if err != nil { - return false, false, ballot, "", err - } - - ballot, isFinalizedInThisBlock := k.CheckIfFinalizingVote(ctx, ballot) - return isFinalizedInThisBlock, isNew, ballot, observationChain.String(), nil + return isFinalized, isNew, ballot, observationChain.String(), nil } diff --git a/x/observer/keeper/utils.go b/x/observer/keeper/voting.go similarity index 81% rename from x/observer/keeper/utils.go rename to x/observer/keeper/voting.go index 3215a86307..d340f0bf32 100644 --- a/x/observer/keeper/utils.go +++ b/x/observer/keeper/voting.go @@ -3,12 +3,17 @@ package keeper import ( "fmt" + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/x/observer/types" ) +const ( + msgVoteOnBallot = "error while voting on ballot" +) + func (k Keeper) AddVoteToBallot( ctx sdk.Context, ballot types.Ballot, @@ -147,3 +152,33 @@ func (k Keeper) CheckObserverSelfDelegation(ctx sdk.Context, accAddress string) } return nil } + +// VoteOnBallot finds a ballot or creates a new one if not found, +// and casts a vote on it. Then proceed to check if the vote has been finalized. +// This function holds generic logic for all types of votes. +func (k Keeper) VoteOnBallot( + ctx sdk.Context, + chain chains.Chain, + ballotIndex string, + observationType types.ObservationType, + voter string, + voteType types.VoteType, +) ( + ballot types.Ballot, + isFinalized bool, + isNew bool, + err error) { + ballot, isNew, err = k.FindBallot(ctx, ballotIndex, chain, observationType) + if err != nil { + return ballot, false, false, sdkerrors.Wrap(err, msgVoteOnBallot) + } + + ballot, err = k.AddVoteToBallot(ctx, ballot, voter, voteType) + if err != nil { + return ballot, false, isNew, sdkerrors.Wrap(err, msgVoteOnBallot) + } + + ballot, isFinalized = k.CheckIfFinalizingVote(ctx, ballot) + + return ballot, isFinalized, isNew, nil +} diff --git a/x/observer/keeper/utils_test.go b/x/observer/keeper/voting_test.go similarity index 63% rename from x/observer/keeper/utils_test.go rename to x/observer/keeper/voting_test.go index effd815ad5..d50e6e5f80 100644 --- a/x/observer/keeper/utils_test.go +++ b/x/observer/keeper/voting_test.go @@ -320,3 +320,242 @@ func TestKeeper_FindBallot(t *testing.T) { require.Error(t, err) }) } + +func TestKeeper_VoteOnBallot(t *testing.T) { + t.Run("fails if chain is not supported", func(t *testing.T) { + k, ctx, _, _ := keepertest.ObserverKeeper(t) + + k.SetChainParamsList(ctx, types.ChainParamsList{ + ChainParams: []*types.ChainParams{ + { + ChainId: getValidEthChainIDWithIndex(t, 0), + IsSupported: false, + }, + }, + }) + + chain, _ := k.GetSupportedChainFromChainID(ctx, 0) + index := sample.ZetaIndex(t) + _, _, _, err := k.VoteOnBallot( + ctx, + chain, + index, + types.ObservationType_InboundTx, + sample.AccAddress(), + types.VoteType_SuccessObservation) + + require.ErrorIs(t, err, types.ErrSupportedChains) + }) + + t.Run("fails if the user can not add a vote", func(t *testing.T) { + k, ctx, _, _ := keepertest.ObserverKeeper(t) + + k.SetChainParamsList(ctx, types.ChainParamsList{ + ChainParams: []*types.ChainParams{ + { + ChainId: getValidEthChainIDWithIndex(t, 0), + IsSupported: true, + }, + }, + }) + + chain, _ := k.GetSupportedChainFromChainID(ctx, getValidEthChainIDWithIndex(t, 0)) + index := sample.ZetaIndex(t) + _, _, _, err := k.VoteOnBallot( + ctx, + chain, + index, + types.ObservationType_InboundTx, + sample.AccAddress(), + types.VoteType_SuccessObservation) + + require.ErrorIs(t, err, types.ErrUnableToAddVote) + }) + + t.Run("user can create a ballot and add a vote", func(t *testing.T) { + k, ctx, _, _ := keepertest.ObserverKeeper(t) + + k.SetChainParamsList(ctx, types.ChainParamsList{ + ChainParams: []*types.ChainParams{ + { + ChainId: getValidEthChainIDWithIndex(t, 0), + IsSupported: true, + }, + }, + }) + + voter := sample.AccAddress() + k.SetObserverSet(ctx, types.ObserverSet{ + ObserverList: []string{voter}, + }) + + chain, _ := k.GetSupportedChainFromChainID(ctx, getValidEthChainIDWithIndex(t, 0)) + index := sample.ZetaIndex(t) + ballot, isFinalized, isNew, err := k.VoteOnBallot( + ctx, + chain, + index, + types.ObservationType_InboundTx, + voter, + types.VoteType_SuccessObservation) + + require.NoError(t, err) + require.True(t, isFinalized) + require.True(t, isNew) + expectedBallot, found := k.GetBallot(ctx, index) + require.True(t, found) + require.Equal(t, expectedBallot, ballot) + }) + + t.Run("user can create a ballot and add a vote, without finalizing ballot", func(t *testing.T) { + k, ctx, _, _ := keepertest.ObserverKeeper(t) + + threshold, err := sdk.NewDecFromStr("0.7") + require.NoError(t, err) + + k.SetChainParamsList(ctx, types.ChainParamsList{ + ChainParams: []*types.ChainParams{ + { + ChainId: getValidEthChainIDWithIndex(t, 0), + IsSupported: true, + BallotThreshold: threshold, + }, + }, + }) + + voter := sample.AccAddress() + k.SetObserverSet(ctx, types.ObserverSet{ + ObserverList: []string{ + voter, + sample.AccAddress(), + }, + }) + + chain, _ := k.GetSupportedChainFromChainID(ctx, getValidEthChainIDWithIndex(t, 0)) + index := sample.ZetaIndex(t) + ballot, isFinalized, isNew, err := k.VoteOnBallot( + ctx, + chain, + index, + types.ObservationType_InboundTx, + voter, + types.VoteType_SuccessObservation) + + require.NoError(t, err) + require.False(t, isFinalized) + require.True(t, isNew) + expectedBallot, found := k.GetBallot(ctx, index) + require.True(t, found) + require.Equal(t, expectedBallot, ballot) + }) + + t.Run("user can add a vote to an existing ballot", func(t *testing.T) { + k, ctx, _, _ := keepertest.ObserverKeeper(t) + + k.SetChainParamsList(ctx, types.ChainParamsList{ + ChainParams: []*types.ChainParams{ + { + ChainId: getValidEthChainIDWithIndex(t, 0), + IsSupported: true, + }, + }, + }) + + voter := sample.AccAddress() + k.SetObserverSet(ctx, types.ObserverSet{ + ObserverList: []string{voter}, + }) + + chain, _ := k.GetSupportedChainFromChainID(ctx, getValidEthChainIDWithIndex(t, 0)) + index := sample.ZetaIndex(t) + threshold, err := sdk.NewDecFromStr("0.7") + require.NoError(t, err) + + ballot := types.Ballot{ + Index: index, + BallotIdentifier: index, + VoterList: []string{ + sample.AccAddress(), + sample.AccAddress(), + voter, + sample.AccAddress(), + sample.AccAddress(), + }, + Votes: types.CreateVotes(5), + ObservationType: types.ObservationType_OutboundTx, + BallotThreshold: threshold, + BallotStatus: types.BallotStatus_BallotInProgress, + } + k.SetBallot(ctx, &ballot) + + ballot, isFinalized, isNew, err := k.VoteOnBallot( + ctx, + chain, + index, + types.ObservationType_InboundTx, + voter, + types.VoteType_SuccessObservation) + + require.NoError(t, err) + require.False(t, isFinalized) + require.False(t, isNew) + expectedBallot, found := k.GetBallot(ctx, index) + require.True(t, found) + require.Equal(t, expectedBallot, ballot) + }) + + t.Run("user can add a vote to an existing ballot, and finalize it", func(t *testing.T) { + k, ctx, _, _ := keepertest.ObserverKeeper(t) + + k.SetChainParamsList(ctx, types.ChainParamsList{ + ChainParams: []*types.ChainParams{ + { + ChainId: getValidEthChainIDWithIndex(t, 0), + IsSupported: true, + }, + }, + }) + + voter := sample.AccAddress() + k.SetObserverSet(ctx, types.ObserverSet{ + ObserverList: []string{voter}, + }) + + index := sample.ZetaIndex(t) + threshold, err := sdk.NewDecFromStr("0.1") + require.NoError(t, err) + + ballot := types.Ballot{ + Index: index, + BallotIdentifier: index, + VoterList: []string{ + sample.AccAddress(), + sample.AccAddress(), + voter, + sample.AccAddress(), + sample.AccAddress(), + }, + Votes: types.CreateVotes(5), + ObservationType: types.ObservationType_OutboundTx, + BallotThreshold: threshold, + BallotStatus: types.BallotStatus_BallotInProgress, + } + k.SetBallot(ctx, &ballot) + + chain, _ := k.GetSupportedChainFromChainID(ctx, getValidEthChainIDWithIndex(t, 0)) + ballot, isFinalized, isNew, err := k.VoteOnBallot( + ctx, + chain, + index, + types.ObservationType_InboundTx, + voter, + types.VoteType_SuccessObservation) + + require.NoError(t, err) + require.True(t, isFinalized) + require.False(t, isNew) + expectedBallot, found := k.GetBallot(ctx, index) + require.True(t, found) + require.Equal(t, expectedBallot, ballot) + }) +}