From 771317f784db8f79861a8d1e1fa1479e690ef60d Mon Sep 17 00:00:00 2001 From: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:35:50 -0600 Subject: [PATCH 1/2] fix: skip solana unsupported transaction version (#3206) * fix solana testnet unsupported version 0 * add changelog entry * correct variable name; explicitly return a 'skip' flag to be more redable * fix unit test * Simplify RPC API * Comment test case * clean up commented test --------- Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- changelog.md | 4 ++ go.mod | 4 +- go.sum | 6 ++ pkg/crypto/privkey.go | 23 ------- pkg/crypto/privkey_test.go | 67 ------------------- zetaclient/chains/solana/observer/inbound.go | 35 ++++++---- zetaclient/chains/solana/rpc/rpc.go | 28 ++++++++ zetaclient/chains/solana/rpc/rpc_live_test.go | 19 ++++++ zetaclient/chains/solana/signer/signer.go | 4 +- zetaclient/keys/relayer_key.go | 5 +- 10 files changed, 86 insertions(+), 109 deletions(-) delete mode 100644 pkg/crypto/privkey.go delete mode 100644 pkg/crypto/privkey_test.go diff --git a/changelog.md b/changelog.md index 7a215bc637..6f2e09b9ac 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,10 @@ * [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient +### Fixes + +* [3206](https://github.com/zeta-chain/node/pull/3206) - skip Solana unsupported transaction version to not block inbound observation + ## v23.0.0 ### Features diff --git a/go.mod b/go.mod index 34a5ace653..05a7166c93 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/emicklei/proto v1.11.1 github.com/ethereum/go-ethereum v1.13.15 github.com/fatih/color v1.14.1 - github.com/gagliardetto/solana-go v1.10.0 + github.com/gagliardetto/solana-go v1.12.0 github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.4 github.com/gorilla/mux v1.8.0 @@ -282,7 +282,7 @@ require ( github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect - go.mongodb.org/mongo-driver v1.11.0 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect go.nhat.io/matcher/v2 v2.0.0 // indirect go.nhat.io/wait v0.1.0 // indirect go.opencensus.io v0.24.0 // indirect diff --git a/go.sum b/go.sum index f55f4033cf..5862cca9f4 100644 --- a/go.sum +++ b/go.sum @@ -599,6 +599,8 @@ github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvS github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= github.com/gagliardetto/solana-go v1.10.0 h1:lDuHGC+XLxw9j8fCHBZM9tv4trI0PVhev1m9NAMaIdM= github.com/gagliardetto/solana-go v1.10.0/go.mod h1:afBEcIRrDLJst3lvAahTr63m6W2Ns6dajZxe2irF7Jg= +github.com/gagliardetto/solana-go v1.12.0 h1:rzsbilDPj6p+/DOPXBMLhwMZeBgeRuXjm5zQFCoXgsg= +github.com/gagliardetto/solana-go v1.12.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= @@ -1501,7 +1503,9 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= @@ -1545,6 +1549,8 @@ go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/o go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE= go.mongodb.org/mongo-driver v1.11.0/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= go.nhat.io/aferomock v0.4.0 h1:gs3nJzIqAezglUuaPfautAmZwulwRWLcfSSzdK4YCC0= go.nhat.io/aferomock v0.4.0/go.mod h1:msi5MDOtJ/AroUa/lDc3jVGOILM4SKP//4yBRImOvkI= go.nhat.io/grpcmock v0.25.0 h1:zk03vvA60w7UrnurZbqL4wxnjmJz1Kuyb7ig2MF+n4c= diff --git a/pkg/crypto/privkey.go b/pkg/crypto/privkey.go deleted file mode 100644 index 2acbf1c609..0000000000 --- a/pkg/crypto/privkey.go +++ /dev/null @@ -1,23 +0,0 @@ -package crypto - -import ( - fmt "fmt" - - "github.com/gagliardetto/solana-go" - "github.com/pkg/errors" -) - -// SolanaPrivateKeyFromString converts a base58 encoded private key to a solana.PrivateKey -func SolanaPrivateKeyFromString(privKeyBase58 string) (*solana.PrivateKey, error) { - privateKey, err := solana.PrivateKeyFromBase58(privKeyBase58) - if err != nil { - return nil, errors.Wrap(err, "invalid base58 private key") - } - - // Solana private keys are 64 bytes long - if len(privateKey) != 64 { - return nil, fmt.Errorf("invalid private key length: %d", len(privateKey)) - } - - return &privateKey, nil -} diff --git a/pkg/crypto/privkey_test.go b/pkg/crypto/privkey_test.go deleted file mode 100644 index c342eccad5..0000000000 --- a/pkg/crypto/privkey_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package crypto_test - -import ( - "testing" - - "github.com/gagliardetto/solana-go" - "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/pkg/crypto" -) - -func Test_SolanaPrivateKeyFromString(t *testing.T) { - tests := []struct { - name string - input string - output *solana.PrivateKey - errMsg string - }{ - { - name: "valid private key", - input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", - output: func() *solana.PrivateKey { - privKey, _ := solana.PrivateKeyFromBase58( - "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", - ) - return &privKey - }(), - }, - { - name: "invalid private key - too short", - input: "oR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", - output: nil, - errMsg: "invalid private key length: 38", - }, - { - name: "invalid private key - too long", - input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQdJ", - output: nil, - errMsg: "invalid private key length: 66", - }, - { - name: "invalid private key - bad base58 encoding", - input: "!!!InvalidBase58!!!", - output: nil, - errMsg: "invalid base58 private key", - }, - { - name: "invalid private key - empty string", - input: "", - output: nil, - errMsg: "invalid base58 private key", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := crypto.SolanaPrivateKeyFromString(tt.input) - if tt.errMsg != "" { - require.ErrorContains(t, err, tt.errMsg) - require.Nil(t, result) - return - } - - require.NoError(t, err) - require.Equal(t, tt.output.String(), result.String()) - }) - } -} diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index bd0e9a98b7..fa3edde764 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -100,30 +100,39 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // process successfully signature only if sig.Err == nil { - txResult, err := ob.solClient.GetTransaction(ctx, sig.Signature, &rpc.GetTransactionOpts{}) - if err != nil { - // we have to re-scan this signature on next ticker - return errors.Wrapf(err, "error GetTransaction for chain %d sig %s", chainID, sigString) - } - - // filter inbound events and vote - err = ob.FilterInboundEventsAndVote(ctx, txResult) - if err != nil { + txResult, err := solanarpc.GetTransaction(ctx, ob.solClient, sig.Signature) + switch { + case errors.Is(err, solanarpc.ErrUnsupportedTxVersion): + ob.Logger().Inbound.Warn(). + Stringer("tx.signature", sig.Signature). + Msg("ObserveInbound: skip unsupported transaction") + // just save the sig to last scanned txs + case err != nil: // we have to re-scan this signature on next ticker - return errors.Wrapf(err, "error FilterInboundEventAndVote for chain %d sig %s", chainID, sigString) + return errors.Wrapf(err, "error GetTransaction for sig %s", sigString) + default: + // filter inbound events and vote + if err = ob.FilterInboundEventsAndVote(ctx, txResult); err != nil { + // we have to re-scan this signature on next ticker + return errors.Wrapf(err, "error FilterInboundEventAndVote for sig %s", sigString) + } } } // signature scanned; save last scanned signature to both memory and db, ignore db error - if err := ob.SaveLastTxScanned(sigString, sig.Slot); err != nil { + if err = ob.SaveLastTxScanned(sigString, sig.Slot); err != nil { ob.Logger(). Inbound.Error(). Err(err). - Msgf("ObserveInbound: error saving last sig %s for chain %d", sigString, chainID) + Str("tx.signature", sigString). + Msg("ObserveInbound: error saving last sig") } + ob.Logger(). Inbound.Info(). - Msgf("ObserveInbound: last scanned sig is %s for chain %d in slot %d", sigString, chainID, sig.Slot) + Str("tx.signature", sigString). + Uint64("tx.slot", sig.Slot). + Msg("ObserveInbound: last scanned sig") // take a rest if max signatures per ticker is reached if len(signatures)-i >= MaxSignaturesPerTicker { diff --git a/zetaclient/chains/solana/rpc/rpc.go b/zetaclient/chains/solana/rpc/rpc.go index c1fc0e1751..f523011c0d 100644 --- a/zetaclient/chains/solana/rpc/rpc.go +++ b/zetaclient/chains/solana/rpc/rpc.go @@ -2,6 +2,7 @@ package rpc import ( "context" + "strings" "time" "github.com/gagliardetto/solana-go" @@ -18,8 +19,14 @@ const ( // RPCAlertLatency is the default threshold for RPC latency to be considered unhealthy and trigger an alert. // The 'HEALTH_CHECK_SLOT_DISTANCE' is default to 150 slots, which is 150 * 0.4s = 60s RPCAlertLatency = time.Duration(60) * time.Second + + // see: https://github.com/solana-labs/solana/blob/master/rpc/src/rpc.rs#L7276 + errorCodeUnsupportedTransactionVersion = "-32015" ) +// ErrUnsupportedTxVersion is returned when the transaction version is not supported by zetaclient +var ErrUnsupportedTxVersion = errors.New("unsupported tx version") + // GetFirstSignatureForAddress searches the first signature for the given address. // Note: make sure that the rpc provider used has enough transaction history. func GetFirstSignatureForAddress( @@ -122,6 +129,27 @@ func GetSignaturesForAddressUntil( return allSignatures, nil } +// GetTransaction fetches a transaction with the given signature. +// Note that it might return ErrUnsupportedTxVersion (for tx that we don't support yet). +func GetTransaction( + ctx context.Context, + client interfaces.SolanaRPCClient, + sig solana.Signature, +) (*rpc.GetTransactionResult, error) { + txResult, err := client.GetTransaction(ctx, sig, &rpc.GetTransactionOpts{ + MaxSupportedTransactionVersion: &rpc.MaxSupportedTransactionVersion0, + }) + + switch { + case err != nil && strings.Contains(err.Error(), errorCodeUnsupportedTransactionVersion): + return nil, ErrUnsupportedTxVersion + case err != nil: + return nil, err + default: + return txResult, nil + } +} + // CheckRPCStatus checks the RPC status of the solana chain func CheckRPCStatus(ctx context.Context, client interfaces.SolanaRPCClient, privnet bool) (time.Time, error) { // query solana health (always return "ok" unless --trusted-validator is provided) diff --git a/zetaclient/chains/solana/rpc/rpc_live_test.go b/zetaclient/chains/solana/rpc/rpc_live_test.go index 7cbe98eeba..e5d47b7302 100644 --- a/zetaclient/chains/solana/rpc/rpc_live_test.go +++ b/zetaclient/chains/solana/rpc/rpc_live_test.go @@ -17,11 +17,30 @@ func Test_SolanaRPCLive(t *testing.T) { return } + LiveTest_GetTransactionWithVersion(t) LiveTest_GetFirstSignatureForAddress(t) LiveTest_GetSignaturesForAddressUntil(t) LiveTest_CheckRPCStatus(t) } +func LiveTest_GetTransactionWithVersion(t *testing.T) { + // create a Solana devnet RPC client + client := solanarpc.New(solanarpc.DevNet_RPC) + + // example transaction of version "0" + // https://explorer.solana.com/tx/Wqgj7hAaUUSfLzieN912G7GxyGHijzBZgY135NtuFtPRjevK8DnYjWwQZy7LAKFQZu582wsjuab2QP27VMUJzAi?cluster=devnet + txSig := solana.MustSignatureFromBase58( + "Wqgj7hAaUUSfLzieN912G7GxyGHijzBZgY135NtuFtPRjevK8DnYjWwQZy7LAKFQZu582wsjuab2QP27VMUJzAi", + ) + + t.Run("should get the transaction if the version is supported", func(t *testing.T) { + ctx := context.Background() + txResult, err := rpc.GetTransaction(ctx, client, txSig) + require.NoError(t, err) + require.NotNil(t, txResult) + }) +} + func LiveTest_GetFirstSignatureForAddress(t *testing.T) { // create a Solana devnet RPC client client := solanarpc.New(solanarpc.DevNet_RPC) diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 7405ddaf87..42b9855ab9 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -16,7 +16,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" contracts "github.com/zeta-chain/node/pkg/contracts/solana" - "github.com/zeta-chain/node/pkg/crypto" "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" @@ -86,10 +85,11 @@ func NewSigner( // construct Solana private key if present if relayerKey != nil { - signer.relayerKey, err = crypto.SolanaPrivateKeyFromString(relayerKey.PrivateKey) + privKey, err := solana.PrivateKeyFromBase58(relayerKey.PrivateKey) if err != nil { return nil, errors.Wrap(err, "unable to construct solana private key") } + signer.relayerKey = &privKey logger.Std.Info().Msgf("Solana relayer address: %s", signer.relayerKey.PublicKey()) } else { logger.Std.Info().Msg("Solana relayer key is not provided") diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go index c0b5ae29bc..af5918c634 100644 --- a/zetaclient/keys/relayer_key.go +++ b/zetaclient/keys/relayer_key.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" + "github.com/gagliardetto/solana-go" "github.com/pkg/errors" "github.com/zeta-chain/node/pkg/chains" @@ -23,7 +24,7 @@ func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, err switch network { case chains.Network_solana: - privKey, err := crypto.SolanaPrivateKeyFromString(rk.PrivateKey) + privKey, err := solana.PrivateKeyFromBase58(rk.PrivateKey) if err != nil { return "", "", errors.Wrap(err, "unable to construct solana private key") } @@ -128,7 +129,7 @@ func ReadRelayerKeyFromFile(fileName string) (*RelayerKey, error) { func IsRelayerPrivateKeyValid(privateKey string, network chains.Network) bool { switch network { case chains.Network_solana: - _, err := crypto.SolanaPrivateKeyFromString(privateKey) + _, err := solana.PrivateKeyFromBase58(privateKey) if err != nil { return false } From cc3bc8669e4ffe0a2daf34459769ea79bb791a9e Mon Sep 17 00:00:00 2001 From: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:58:52 -0600 Subject: [PATCH 2/2] fix: avoid endless tx rescan when the inbound vote message is invalid (#3184) * avoid endless rescan when the inbound vote message is invalid * add changelog entry * rename CheckEventProcessability as IsEventProcessable * rename InboundProcessability as InboundCategory * remove btc inbound duplidate log fields * remove duplicate log fields; add some function comments to improve readibality * move ValidateBasic checking right before posting the vote msg * cleanup changelog * fix unit test * added InboundCategoryUnknown; logs fields cleaning; renaming * remove uncessary log print * wrap a few log prints into fields * move zeta tx hash to log field --- changelog.md | 1 + testutil/sample/crosschain.go | 2 +- x/crosschain/types/message_vote_inbound.go | 2 +- zetaclient/chains/base/observer.go | 25 ++-- zetaclient/chains/base/observer_test.go | 19 +++ zetaclient/chains/bitcoin/observer/event.go | 48 +++---- .../chains/bitcoin/observer/event_test.go | 31 ++--- zetaclient/chains/bitcoin/observer/inbound.go | 25 +--- zetaclient/chains/bitcoin/signer/signer.go | 44 +++---- zetaclient/chains/evm/observer/v2_inbound.go | 10 +- .../chains/evm/observer/v2_inbound_tracker.go | 6 +- zetaclient/chains/solana/observer/inbound.go | 48 +++++-- .../chains/solana/observer/inbound_test.go | 76 ++++++++--- .../chains/ton/observer/observer_test.go | 4 +- zetaclient/compliance/compliance.go | 24 ---- zetaclient/logs/fields.go | 3 + zetaclient/types/event.go | 71 ++++++++++ zetaclient/types/event_test.go | 124 ++++++++++++++++++ 18 files changed, 400 insertions(+), 163 deletions(-) create mode 100644 zetaclient/types/event_test.go 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) + }) + } +}