diff --git a/changelog.md b/changelog.md index 6f2e09b9ac..a3a78e0d82 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ ### Fixes * [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 ## v23.0.0 diff --git a/testutil/sample/crosschain.go b/testutil/sample/crosschain.go index 4ac29ec697..e58c457073 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -292,7 +292,7 @@ func ZetaAccounting(t *testing.T, index string) types.ZetaAccounting { func InboundVote(coinType coin.CoinType, from, to int64) types.MsgVoteInbound { return types.MsgVoteInbound{ - Creator: "", + Creator: Bech32AccAddress().String(), Sender: EthAddress().String(), SenderChainId: Chain(from).ChainId, Receiver: EthAddress().String(), diff --git a/x/crosschain/types/message_vote_inbound.go b/x/crosschain/types/message_vote_inbound.go index 3db9fdde0f..e612fe582a 100644 --- a/x/crosschain/types/message_vote_inbound.go +++ b/x/crosschain/types/message_vote_inbound.go @@ -22,7 +22,7 @@ const MaxMessageLength = 10240 // InboundVoteOption is a function that sets some option on the inbound vote message type InboundVoteOption func(*MsgVoteInbound) -// WithMemoRevertOptions sets the revert options for inbound vote message +// WithRevertOptions sets the revert options for inbound vote message func WithRevertOptions(revertOptions RevertOptions) InboundVoteOption { return func(msg *MsgVoteInbound) { msg.RevertOptions = revertOptions diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 5e7eebaf30..af366faa5e 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -466,7 +466,7 @@ func (ob *Observer) ReadLastTxScannedFromDB() (string, error) { return lastTx.Hash, nil } -// PostVoteInbound posts a vote for the given vote message +// PostVoteInbound posts a vote for the given vote message and returns the ballot. func (ob *Observer) PostVoteInbound( ctx context.Context, msg *crosschaintypes.MsgVoteInbound, @@ -477,19 +477,26 @@ func (ob *Observer) PostVoteInbound( var ( txHash = msg.InboundHash coinType = msg.CoinType - chainID = ob.Chain().ChainId ) - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(ctx, gasLimit, retryGasLimit, msg) - + // prepare logger fields lf := map[string]any{ - "inbound.chain_id": chainID, - "inbound.coin_type": coinType.String(), - "inbound.external_tx_hash": txHash, - "inbound.ballot_index": ballot, - "inbound.zeta_tx_hash": zetaHash, + logs.FieldMethod: "PostVoteInbound", + logs.FieldTx: txHash, + logs.FieldCoinType: coinType.String(), + } + + // make sure the message is valid to avoid unnecessary retries + if err := msg.ValidateBasic(); err != nil { + ob.logger.Inbound.Warn().Err(err).Fields(lf).Msg("invalid inbound vote message") + return "", nil } + // post vote to zetacore + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(ctx, gasLimit, retryGasLimit, msg) + lf[logs.FieldZetaTx] = zetaHash + lf[logs.FieldBallot] = ballot + switch { case err != nil: ob.logger.Inbound.Error().Err(err).Fields(lf).Msg("inbound detected: error posting vote") diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index f3e90ded06..0ca1a6a147 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -15,6 +15,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/testutil/sample" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -633,6 +634,24 @@ func TestPostVoteInbound(t *testing.T) { require.NoError(t, err) require.Equal(t, "sampleBallotIndex", ballot) }) + + t.Run("should not post vote if message basic validation fails", func(t *testing.T) { + // create observer + ob := createObserver(t, chains.Ethereum, defaultAlertLatency) + + // create mock zetacore client + zetacoreClient := mocks.NewZetacoreClient(t) + ob = ob.WithZetacoreClient(zetacoreClient) + + // create sample message with long Message + msg := sample.InboundVote(coin.CoinType_Gas, chains.Ethereum.ChainId, chains.ZetaChainMainnet.ChainId) + msg.Message = strings.Repeat("1", crosschaintypes.MaxMessageLength+1) + + // post vote inbound + ballot, err := ob.PostVoteInbound(context.TODO(), &msg, 100000) + require.NoError(t, err) + require.Empty(t, ballot) + }) } func TestAlertOnRPCLatency(t *testing.T) { diff --git a/zetaclient/chains/bitcoin/observer/event.go b/zetaclient/chains/bitcoin/observer/event.go index 7cd581a1cc..1695a4d414 100644 --- a/zetaclient/chains/bitcoin/observer/event.go +++ b/zetaclient/chains/bitcoin/observer/event.go @@ -19,20 +19,7 @@ import ( "github.com/zeta-chain/node/zetaclient/compliance" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/logs" -) - -// InboundProcessability is an enum representing the processability of an inbound -type InboundProcessability int - -const ( - // InboundProcessabilityGood represents a processable inbound - InboundProcessabilityGood InboundProcessability = iota - - // InboundProcessabilityDonation represents a donation inbound - InboundProcessabilityDonation - - // InboundProcessabilityComplianceViolation represents a compliance violation - InboundProcessabilityComplianceViolation + clienttypes "github.com/zeta-chain/node/zetaclient/types" ) // BTCInboundEvent represents an incoming transaction event @@ -62,11 +49,11 @@ type BTCInboundEvent struct { TxHash string } -// Processability returns the processability of the inbound event -func (event *BTCInboundEvent) Processability() InboundProcessability { +// Category returns the category of the inbound event +func (event *BTCInboundEvent) Category() clienttypes.InboundCategory { // compliance check on sender and receiver addresses if config.ContainRestrictedAddress(event.FromAddress, event.ToAddress) { - return InboundProcessabilityComplianceViolation + return clienttypes.InboundCategoryRestricted } // compliance check on receiver, revert/abort addresses in standard memo @@ -76,16 +63,16 @@ func (event *BTCInboundEvent) Processability() InboundProcessability { event.MemoStd.RevertOptions.RevertAddress, event.MemoStd.RevertOptions.AbortAddress, ) { - return InboundProcessabilityComplianceViolation + return clienttypes.InboundCategoryRestricted } } // donation check if bytes.Equal(event.MemoBytes, []byte(constant.DonationMessage)) { - return InboundProcessabilityDonation + return clienttypes.InboundCategoryDonation } - return InboundProcessabilityGood + return clienttypes.InboundCategoryGood } // DecodeMemoBytes decodes the contained memo bytes as either standard or legacy memo @@ -164,25 +151,22 @@ func ValidateStandardMemo(memoStd memo.InboundMemo, chainID int64) error { return nil } -// CheckEventProcessability checks if the inbound event is processable -func (ob *Observer) CheckEventProcessability(event BTCInboundEvent) bool { - // check if the event is processable - switch result := event.Processability(); result { - case InboundProcessabilityGood: +// IsEventProcessable checks if the inbound event is processable +func (ob *Observer) IsEventProcessable(event BTCInboundEvent) bool { + logFields := map[string]any{logs.FieldTx: event.TxHash} + + switch category := event.Category(); category { + case clienttypes.InboundCategoryGood: return true - case InboundProcessabilityDonation: - logFields := map[string]any{ - logs.FieldChain: ob.Chain().ChainId, - logs.FieldTx: event.TxHash, - } + case clienttypes.InboundCategoryDonation: ob.Logger().Inbound.Info().Fields(logFields).Msgf("thank you rich folk for your donation!") return false - case InboundProcessabilityComplianceViolation: + case clienttypes.InboundCategoryRestricted: compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, false, ob.Chain().ChainId, event.TxHash, event.FromAddress, event.ToAddress, "BTC") return false default: - ob.Logger().Inbound.Error().Msgf("unreachable code got InboundProcessability: %v", result) + ob.Logger().Inbound.Error().Fields(logFields).Msgf("unreachable code got InboundCategory: %v", category) return false } } diff --git a/zetaclient/chains/bitcoin/observer/event_test.go b/zetaclient/chains/bitcoin/observer/event_test.go index 80566b55cd..9a73d28069 100644 --- a/zetaclient/chains/bitcoin/observer/event_test.go +++ b/zetaclient/chains/bitcoin/observer/event_test.go @@ -22,6 +22,7 @@ import ( "github.com/zeta-chain/node/zetaclient/keys" "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" + clienttypes "github.com/zeta-chain/node/zetaclient/types" ) // createTestBtcEvent creates a test BTC inbound event @@ -41,7 +42,7 @@ func createTestBtcEvent( } } -func Test_CheckProcessability(t *testing.T) { +func Test_Category(t *testing.T) { // setup compliance config cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), @@ -52,26 +53,26 @@ func Test_CheckProcessability(t *testing.T) { tests := []struct { name string event *observer.BTCInboundEvent - expected observer.InboundProcessability + expected clienttypes.InboundCategory }{ { - name: "should return InboundProcessabilityGood for a processable inbound event", + name: "should return InboundCategoryGood for a processable inbound event", event: &observer.BTCInboundEvent{ FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", ToAddress: testutils.TSSAddressBTCAthens3, }, - expected: observer.InboundProcessabilityGood, + expected: clienttypes.InboundCategoryGood, }, { - name: "should return InboundProcessabilityComplianceViolation for a restricted sender address", + name: "should return InboundCategoryRestricted for a restricted sender address", event: &observer.BTCInboundEvent{ FromAddress: sample.RestrictedBtcAddressTest, ToAddress: testutils.TSSAddressBTCAthens3, }, - expected: observer.InboundProcessabilityComplianceViolation, + expected: clienttypes.InboundCategoryRestricted, }, { - name: "should return InboundProcessabilityComplianceViolation for a restricted receiver address in standard memo", + name: "should return InboundCategoryRestricted for a restricted receiver address in standard memo", event: &observer.BTCInboundEvent{ FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", ToAddress: testutils.TSSAddressBTCAthens3, @@ -81,10 +82,10 @@ func Test_CheckProcessability(t *testing.T) { }, }, }, - expected: observer.InboundProcessabilityComplianceViolation, + expected: clienttypes.InboundCategoryRestricted, }, { - name: "should return InboundProcessabilityComplianceViolation for a restricted revert address in standard memo", + name: "should return InboundCategoryRestricted for a restricted revert address in standard memo", event: &observer.BTCInboundEvent{ FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", ToAddress: testutils.TSSAddressBTCAthens3, @@ -96,22 +97,22 @@ func Test_CheckProcessability(t *testing.T) { }, }, }, - expected: observer.InboundProcessabilityComplianceViolation, + expected: clienttypes.InboundCategoryRestricted, }, { - name: "should return InboundProcessabilityDonation for a donation inbound event", + name: "should return InboundCategoryDonation for a donation inbound event", event: &observer.BTCInboundEvent{ FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", ToAddress: testutils.TSSAddressBTCAthens3, MemoBytes: []byte(constant.DonationMessage), }, - expected: observer.InboundProcessabilityDonation, + expected: clienttypes.InboundCategoryDonation, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := tt.event.Processability() + result := tt.event.Category() require.Equal(t, tt.expected, result) }) } @@ -301,7 +302,7 @@ func Test_ValidateStandardMemo(t *testing.T) { } } -func Test_CheckEventProcessability(t *testing.T) { +func Test_IsEventProcessable(t *testing.T) { // can use any bitcoin chain for testing chain := chains.BitcoinMainnet params := mocks.MockChainParams(chain.ChainId, 10) @@ -344,7 +345,7 @@ func Test_CheckEventProcessability(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := ob.CheckEventProcessability(tt.event) + result := ob.IsEventProcessable(tt.event) require.Equal(t, tt.result, result) }) } diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index aa4f5667ad..27f0839856 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -282,21 +282,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, return msg.Digest(), nil } - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( - ctx, - zetacore.PostVoteInboundGasLimit, - zetacore.PostVoteInboundExecutionGasLimit, - msg, - ) - if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error posting to zetacore") - return "", err - } else if zetaHash != "" { - ob.logger.Inbound.Info().Msgf("BTC deposit detected and reported: PostVoteInbound zeta tx hash: %s inbound %s ballot %s fee %v", - zetaHash, txHash, ballot, event.DepositorFee) - } - - return msg.Digest(), nil + return ob.PostVoteInbound(ctx, msg, zetacore.PostVoteInboundExecutionGasLimit) } // FilterAndParseIncomingTx given txs list returned by the "getblock 2" RPC command, return the txs that are relevant to us @@ -332,12 +318,15 @@ func FilterAndParseIncomingTx( } // GetInboundVoteFromBtcEvent converts a BTCInboundEvent to a MsgVoteInbound to enable voting on the inbound on zetacore +// +// Returns: +// - a valid MsgVoteInbound message, or +// - nil if no valid message can be created for whatever reasons: +// invalid data, not processable, invalid amount, etc. func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *crosschaintypes.MsgVoteInbound { // prepare logger fields lf := map[string]any{ - logs.FieldModule: logs.ModNameInbound, logs.FieldMethod: "GetInboundVoteFromBtcEvent", - logs.FieldChain: ob.Chain().ChainId, logs.FieldTx: event.TxHash, } @@ -349,7 +338,7 @@ func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *crosscha } // check if the event is processable - if !ob.CheckEventProcessability(*event) { + if !ob.IsEventProcessable(*event) { return nil } diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index df00c18f81..1321b0c14f 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -30,6 +30,7 @@ import ( "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/compliance" "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/metrics" "github.com/zeta-chain/node/zetaclient/outboundprocessor" ) @@ -355,12 +356,13 @@ func (signer *Signer) TryProcessOutbound( // prepare logger params := cctx.GetCurrentOutboundParam() - logger := signer.Logger().Std.With(). - Str("method", "TryProcessOutbound"). - Int64("chain", signer.Chain().ChainId). - Uint64("nonce", params.TssNonce). - Str("cctx", cctx.Index). - Logger() + // prepare logger fields + lf := map[string]any{ + logs.FieldMethod: "TryProcessOutbound", + logs.FieldCctx: cctx.Index, + logs.FieldNonce: params.TssNonce, + } + logger := signer.Logger().Std.With().Fields(lf).Logger() // support gas token only for Bitcoin outbound coinType := cctx.InboundParams.CoinType @@ -383,6 +385,7 @@ func (signer *Signer) TryProcessOutbound( logger.Error().Err(err).Msg("cannot get signer address") return } + lf["signer"] = signerAddress.String() // get size limit and gas price sizelimit := params.CallOptions.GasLimit @@ -430,8 +433,6 @@ func (signer *Signer) TryProcessOutbound( cancelTx := restrictedCCTX || dustAmount if cancelTx { amount = 0.0 - } else { - logger.Info().Msgf("SignGasWithdraw: to %s, value %d sats", to.EncodeAddress(), params.Amount.Uint64()) } // sign withdraw tx @@ -448,25 +449,21 @@ func (signer *Signer) TryProcessOutbound( cancelTx, ) if err != nil { - logger.Warn(). - Err(err). - Msgf("SignConnectorOnReceive error: nonce %d chain %d", outboundTssNonce, params.ReceiverChainId) + logger.Warn().Err(err).Msg("SignWithdrawTx failed") return } - logger.Info(). - Msgf("Key-sign success: %d => %s, nonce %d", cctx.InboundParams.SenderChainId, chain.Name, outboundTssNonce) + logger.Info().Msg("Key-sign success") // FIXME: add prometheus metrics _, err = zetacoreClient.GetObserverList(ctx) if err != nil { logger.Warn(). - Err(err). - Msgf("unable to get observer list: chain %d observation %s", outboundTssNonce, observertypes.ObservationType_OutboundTx.String()) + Err(err).Stringer("observation_type", observertypes.ObservationType_OutboundTx). + Msg("unable to get observer list, observation") } if tx != nil { outboundHash := tx.TxHash().String() - logger.Info(). - Msgf("on chain %s nonce %d, outboundHash %s signer %s", chain.Name, outboundTssNonce, outboundHash, signerAddress) + lf[logs.FieldTx] = outboundHash // try broacasting tx with increasing backoff (1s, 2s, 4s, 8s, 16s) in case of RPC error backOff := broadcastBackoff @@ -474,14 +471,11 @@ func (signer *Signer) TryProcessOutbound( time.Sleep(backOff) err := signer.Broadcast(tx) if err != nil { - logger.Warn(). - Err(err). - Msgf("broadcasting tx %s to chain %s: nonce %d, retry %d", outboundHash, chain.Name, outboundTssNonce, i) + logger.Warn().Err(err).Fields(lf).Msgf("Broadcasting Bitcoin tx, retry %d", i) backOff *= 2 continue } - logger.Info(). - Msgf("Broadcast success: nonce %d to chain %s outboundHash %s", outboundTssNonce, chain.String(), outboundHash) + logger.Info().Fields(lf).Msgf("Broadcast Bitcoin tx successfully") zetaHash, err := zetacoreClient.PostOutboundTracker( ctx, chain.ChainId, @@ -489,10 +483,10 @@ func (signer *Signer) TryProcessOutbound( outboundHash, ) if err != nil { - logger.Err(err). - Msgf("Unable to add to tracker on zetacore: nonce %d chain %s outboundHash %s", outboundTssNonce, chain.Name, outboundHash) + logger.Err(err).Fields(lf).Msgf("Unable to add Bitcoin outbound tracker") } - logger.Info().Msgf("Broadcast to core successful %s", zetaHash) + lf[logs.FieldZetaTx] = zetaHash + logger.Info().Fields(lf).Msgf("Add Bitcoin outbound tracker successfully") // Save successfully broadcasted transaction to btc chain observer btcObserver.SaveBroadcastedTx(outboundHash, outboundTssNonce) diff --git a/zetaclient/chains/evm/observer/v2_inbound.go b/zetaclient/chains/evm/observer/v2_inbound.go index 9688851af6..6422034273 100644 --- a/zetaclient/chains/evm/observer/v2_inbound.go +++ b/zetaclient/chains/evm/observer/v2_inbound.go @@ -23,8 +23,8 @@ import ( "github.com/zeta-chain/node/zetaclient/zetacore" ) -// checkEventProcessability checks if the event is processable -func (ob *Observer) checkEventProcessability( +// isEventProcessable checks if the event is processable +func (ob *Observer) isEventProcessable( sender, receiver ethcommon.Address, txHash ethcommon.Hash, payload []byte, @@ -99,7 +99,7 @@ func (ob *Observer) ObserveGatewayDeposit(ctx context.Context, startBlock, toBlo } // check if the event is processable - if !ob.checkEventProcessability(event.Sender, event.Receiver, event.Raw.TxHash, event.Payload) { + if !ob.isEventProcessable(event.Sender, event.Receiver, event.Raw.TxHash, event.Payload) { continue } @@ -247,7 +247,7 @@ func (ob *Observer) ObserveGatewayCall(ctx context.Context, startBlock, toBlock } // check if the event is processable - if !ob.checkEventProcessability(event.Sender, event.Receiver, event.Raw.TxHash, event.Payload) { + if !ob.isEventProcessable(event.Sender, event.Receiver, event.Raw.TxHash, event.Payload) { continue } @@ -378,7 +378,7 @@ func (ob *Observer) ObserveGatewayDepositAndCall(ctx context.Context, startBlock } // check if the event is processable - if !ob.checkEventProcessability(event.Sender, event.Receiver, event.Raw.TxHash, event.Payload) { + if !ob.isEventProcessable(event.Sender, event.Receiver, event.Raw.TxHash, event.Payload) { continue } diff --git a/zetaclient/chains/evm/observer/v2_inbound_tracker.go b/zetaclient/chains/evm/observer/v2_inbound_tracker.go index 11f39fe2a5..d559e9e9d6 100644 --- a/zetaclient/chains/evm/observer/v2_inbound_tracker.go +++ b/zetaclient/chains/evm/observer/v2_inbound_tracker.go @@ -36,7 +36,7 @@ func (ob *Observer) ProcessInboundTrackerV2( eventDeposit, err := gateway.ParseDeposited(*log) if err == nil { // check if the event is processable - if !ob.checkEventProcessability( + if !ob.isEventProcessable( eventDeposit.Sender, eventDeposit.Receiver, eventDeposit.Raw.TxHash, @@ -53,7 +53,7 @@ func (ob *Observer) ProcessInboundTrackerV2( eventDepositAndCall, err := gateway.ParseDepositedAndCalled(*log) if err == nil { // check if the event is processable - if !ob.checkEventProcessability( + if !ob.isEventProcessable( eventDepositAndCall.Sender, eventDepositAndCall.Receiver, eventDepositAndCall.Raw.TxHash, @@ -70,7 +70,7 @@ func (ob *Observer) ProcessInboundTrackerV2( eventCall, err := gateway.ParseCalled(*log) if err == nil { // check if the event is processable - if !ob.checkEventProcessability( + if !ob.isEventProcessable( eventCall.Sender, eventCall.Receiver, eventCall.Raw.TxHash, diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index fa3edde764..2b30b5818d 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -1,7 +1,6 @@ package observer import ( - "bytes" "context" "encoding/hex" "fmt" @@ -13,12 +12,12 @@ import ( "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/pkg/constant" solanacontracts "github.com/zeta-chain/node/pkg/contracts/solana" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" solanarpc "github.com/zeta-chain/node/zetaclient/chains/solana/rpc" "github.com/zeta-chain/node/zetaclient/compliance" zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/logs" clienttypes "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) @@ -265,19 +264,27 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* // BuildInboundVoteMsgFromEvent builds a MsgVoteInbound from an inbound event func (ob *Observer) BuildInboundVoteMsgFromEvent(event *clienttypes.InboundEvent) *crosschaintypes.MsgVoteInbound { - // compliance check. Return nil if the inbound contains restricted addresses - if compliance.DoesInboundContainsRestrictedAddress(event, ob.Logger()) { + // prepare logger fields + lf := map[string]any{ + logs.FieldMethod: "BuildInboundVoteMsgFromEvent", + logs.FieldTx: event.TxHash, + } + + // decode event memo bytes to get the receiver + err := event.DecodeMemo() + if err != nil { + ob.Logger().Inbound.Info().Fields(lf).Msgf("invalid memo bytes: %s", hex.EncodeToString(event.Memo)) return nil } - // donation check - if bytes.Equal(event.Memo, []byte(constant.DonationMessage)) { - ob.Logger().Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", event.TxHash, event.SenderChainID) + // check if the event is processable + if !ob.IsEventProcessable(*event) { return nil } - return zetacore.GetInboundVoteMessage( + // create inbound vote message + return crosschaintypes.NewMsgVoteInbound( + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Sender, event.SenderChainID, event.Sender, @@ -290,7 +297,28 @@ func (ob *Observer) BuildInboundVoteMsgFromEvent(event *clienttypes.InboundEvent 0, event.CoinType, event.Asset, - ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, // not a smart contract call + crosschaintypes.ProtocolContractVersion_V1, + false, // not relevant for v1 ) } + +// IsEventProcessable checks if the inbound event is processable +func (ob *Observer) IsEventProcessable(event clienttypes.InboundEvent) bool { + logFields := map[string]any{logs.FieldTx: event.TxHash} + + switch category := event.Category(); category { + case clienttypes.InboundCategoryGood: + return true + case clienttypes.InboundCategoryDonation: + ob.Logger().Inbound.Info().Fields(logFields).Msgf("thank you rich folk for your donation!") + return false + case clienttypes.InboundCategoryRestricted: + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, event.TxHash, event.Sender, event.Receiver, event.CoinType.String()) + return false + default: + ob.Logger().Inbound.Error().Msgf("unreachable code got InboundCategory: %v", category) + return false + } +} diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index 28c31f04db..0b118ae55e 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -129,37 +129,75 @@ func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { msg := ob.BuildInboundVoteMsgFromEvent(event) require.NotNil(t, msg) }) - t.Run("should return nil msg if sender is restricted", func(t *testing.T) { - sender := sample.SolanaAddress(t) - receiver := sample.SolanaAddress(t) - event := sample.InboundEvent(chain.ChainId, sender, receiver, 1280, nil) - // restrict sender - cfg.ComplianceConfig.RestrictedAddresses = []string{sender} - config.LoadComplianceConfig(cfg) + t.Run("should return nil if failed to decode memo", func(t *testing.T) { + sender := sample.SolanaAddress(t) + memo := []byte("a memo too short") + event := sample.InboundEvent(chain.ChainId, sender, sender, 1280, memo) msg := ob.BuildInboundVoteMsgFromEvent(event) require.Nil(t, msg) }) - t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { + + t.Run("should return nil if event is not processable", func(t *testing.T) { sender := sample.SolanaAddress(t) receiver := sample.SolanaAddress(t) - memo := sample.EthAddress().Bytes() - event := sample.InboundEvent(chain.ChainId, sender, receiver, 1280, []byte(memo)) + event := sample.InboundEvent(chain.ChainId, sender, receiver, 1280, nil) - // restrict receiver - cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} + // restrict sender + cfg.ComplianceConfig.RestrictedAddresses = []string{sender} config.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgFromEvent(event) require.Nil(t, msg) }) - t.Run("should return nil msg on donation transaction", func(t *testing.T) { - // create event with donation memo - sender := sample.SolanaAddress(t) - event := sample.InboundEvent(chain.ChainId, sender, sender, 1280, []byte(constant.DonationMessage)) +} - msg := ob.BuildInboundVoteMsgFromEvent(event) - require.Nil(t, msg) - }) +func Test_IsEventProcessable(t *testing.T) { + // parepare params + chain := chains.SolanaDevnet + params := sample.ChainParams(chain.ChainId) + params.GatewayAddress = sample.SolanaAddress(t) + + // create test observer + ob := MockSolanaObserver(t, chain, nil, *params, nil, nil) + + // setup compliance config + cfg := config.Config{ + ComplianceConfig: sample.ComplianceConfig(), + } + config.LoadComplianceConfig(cfg) + + // test cases + tests := []struct { + name string + event clienttypes.InboundEvent + result bool + }{ + { + name: "should return true for processable event", + event: clienttypes.InboundEvent{Sender: sample.SolanaAddress(t), Receiver: sample.SolanaAddress(t)}, + result: true, + }, + { + name: "should return false on donation message", + event: clienttypes.InboundEvent{Memo: []byte(constant.DonationMessage)}, + result: false, + }, + { + name: "should return false on compliance violation", + event: clienttypes.InboundEvent{ + Sender: sample.RestrictedSolAddressTest, + Receiver: sample.EthAddress().Hex(), + }, + result: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ob.IsEventProcessable(tt.event) + require.Equal(t, tt.result, result) + }) + } } diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index ffbfeb1bd9..d08103ea63 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -64,7 +64,9 @@ func newTestSuite(t *testing.T) *testSuite { liteClient = mocks.NewLiteClient(t) tss = mocks.NewTSS(t) - zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) + zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{ + OperatorAddress: sample.Bech32AccAddress(), + }) testLogger = zerolog.New(zerolog.NewTestWriter(t)) logger = base.Logger{Std: testLogger, Compliance: testLogger} diff --git a/zetaclient/compliance/compliance.go b/zetaclient/compliance/compliance.go index f0135c3ad9..2a16dee6b6 100644 --- a/zetaclient/compliance/compliance.go +++ b/zetaclient/compliance/compliance.go @@ -2,16 +2,10 @@ package compliance import ( - "encoding/hex" - - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" - "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/config" - clienttypes "github.com/zeta-chain/node/zetaclient/types" ) // IsCctxRestricted returns true if the cctx involves restricted addresses @@ -61,21 +55,3 @@ func PrintComplianceLog( inboundLoggerWithFields.Warn().Msg(logMsg) complianceLoggerWithFields.Warn().Msg(logMsg) } - -// DoesInboundContainsRestrictedAddress returns true if the inbound event contains restricted addresses -func DoesInboundContainsRestrictedAddress(event *clienttypes.InboundEvent, logger *base.ObserverLogger) bool { - // parse memo-specified receiver - receiver := "" - parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(event.Memo)) - if err == nil && parsedAddress != (ethcommon.Address{}) { - receiver = parsedAddress.Hex() - } - - // check restricted addresses - if config.ContainRestrictedAddress(event.Sender, event.Receiver, receiver) { - PrintComplianceLog(logger.Inbound, logger.Compliance, - false, event.SenderChainID, event.TxHash, event.Sender, receiver, event.CoinType.String()) - return true - } - return false -} diff --git a/zetaclient/logs/fields.go b/zetaclient/logs/fields.go index 78b95fc7e0..58880543af 100644 --- a/zetaclient/logs/fields.go +++ b/zetaclient/logs/fields.go @@ -10,6 +10,9 @@ const ( FieldNonce = "nonce" FieldTx = "tx" FieldCctx = "cctx" + FieldZetaTx = "zeta_tx" + FieldBallot = "ballot" + FieldCoinType = "coin_type" // module names ModNameInbound = "inbound" diff --git a/zetaclient/types/event.go b/zetaclient/types/event.go index a0313236e6..fefdc39681 100644 --- a/zetaclient/types/event.go +++ b/zetaclient/types/event.go @@ -1,7 +1,34 @@ package types import ( + "bytes" + "encoding/hex" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/crypto" + "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/zetaclient/config" +) + +// InboundCategory is an enum representing the category of an inbound event +type InboundCategory int + +const ( + // InboundCategoryUnknown represents an unknown inbound + InboundCategoryUnknown InboundCategory = iota + + // InboundCategoryGood represents a processable inbound + InboundCategoryGood + + // InboundCategoryDonation represents a donation inbound + InboundCategoryDonation + + // InboundCategoryRestricted represents a restricted inbound + InboundCategoryRestricted ) // InboundEvent represents an inbound event @@ -41,3 +68,47 @@ type InboundEvent struct { // Asset is the asset of the inbound Asset string } + +// DecodeMemo decodes the receiver from the memo bytes +func (event *InboundEvent) DecodeMemo() error { + // skip decoding donation tx as it won't go through zetacore + if bytes.Equal(event.Memo, []byte(constant.DonationMessage)) { + return nil + } + + // decode receiver address from memo + parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(event.Memo)) + if err != nil { // unreachable code + return errors.Wrap(err, "invalid memo hex") + } + + // ensure the receiver is valid + if crypto.IsEmptyAddress(parsedAddress) { + return errors.New("got empty receiver address from memo") + } + event.Receiver = parsedAddress.Hex() + + return nil +} + +// Category returns the category of the inbound event +func (event *InboundEvent) Category() InboundCategory { + // parse memo-specified receiver + receiver := "" + parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(event.Memo)) + if err == nil && parsedAddress != (ethcommon.Address{}) { + receiver = parsedAddress.Hex() + } + + // check restricted addresses + if config.ContainRestrictedAddress(event.Sender, event.Receiver, event.TxOrigin, receiver) { + return InboundCategoryRestricted + } + + // donation check + if bytes.Equal(event.Memo, []byte(constant.DonationMessage)) { + return InboundCategoryDonation + } + + return InboundCategoryGood +} diff --git a/zetaclient/types/event_test.go b/zetaclient/types/event_test.go new file mode 100644 index 0000000000..3b6b1a50e3 --- /dev/null +++ b/zetaclient/types/event_test.go @@ -0,0 +1,124 @@ +package types_test + +import ( + "testing" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/types" +) + +func Test_DecodeMemo(t *testing.T) { + testReceiver := sample.EthAddress() + + // test cases + tests := []struct { + name string + event *types.InboundEvent + expectedReceiver string + errMsg string + }{ + { + name: "should decode receiver address successfully", + event: &types.InboundEvent{ + Memo: testReceiver.Bytes(), + }, + expectedReceiver: testReceiver.Hex(), + }, + { + name: "should skip decoding donation message", + event: &types.InboundEvent{ + Memo: []byte(constant.DonationMessage), + }, + expectedReceiver: "", + }, + { + name: "should return error if got an empty receiver address", + event: &types.InboundEvent{ + Memo: []byte(""), + }, + errMsg: "got empty receiver address from memo", + expectedReceiver: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.event.DecodeMemo() + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + require.Equal(t, tt.expectedReceiver, tt.event.Receiver) + }) + } +} + +func Test_Catetory(t *testing.T) { + // setup compliance config + cfg := config.Config{ + ComplianceConfig: sample.ComplianceConfig(), + } + config.LoadComplianceConfig(cfg) + + // test cases + tests := []struct { + name string + event *types.InboundEvent + expected types.InboundCategory + }{ + { + name: "should return InboundCategoryGood for a processable inbound event", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.EthAddress().Hex(), + }, + expected: types.InboundCategoryGood, + }, + { + name: "should return InboundCategoryRestricted for a restricted sender address", + event: &types.InboundEvent{ + Sender: sample.RestrictedSolAddressTest, + Receiver: sample.EthAddress().Hex(), + }, + expected: types.InboundCategoryRestricted, + }, + { + name: "should return InboundCategoryRestricted for a restricted receiver address", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.RestrictedSolAddressTest, + }, + expected: types.InboundCategoryRestricted, + }, + { + name: "should return InboundCategoryRestricted for a restricted receiver address in memo", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.EthAddress().Hex(), + Memo: ethcommon.HexToAddress(sample.RestrictedEVMAddressTest).Bytes(), + }, + expected: types.InboundCategoryRestricted, + }, + { + name: "should return InboundCategoryDonation for a donation inbound event", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.EthAddress().Hex(), + Memo: []byte(constant.DonationMessage), + }, + expected: types.InboundCategoryDonation, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.event.Category() + require.Equal(t, tt.expected, result) + }) + } +}