From a8ce6d8b6d50b260581b9f3158fce09f3d1c83d9 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] 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 | 12 ++++ go.mod | 4 +- go.sum | 13 ++-- 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, 94 insertions(+), 116 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 01b6f7bd66..7a0af20125 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,17 @@ # CHANGELOG +## v23.1.1 + +### Fixes + +* [3206](https://github.com/zeta-chain/node/pull/3206) - skip Solana unsupported transaction version to not block inbound observation + +## v23.1.0 + +### Chores + +- [3215](https://github.com/zeta-chain/node/pull/3215) - revert go-tss to v21/v22 version to avoid keysign failures + ## v23.0.0 ### Features diff --git a/go.mod b/go.mod index 79755063be..e26889026e 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 72767c59d3..ce9b9f0e5d 100644 --- a/go.sum +++ b/go.sum @@ -597,8 +597,8 @@ github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7 github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= 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= @@ -1460,7 +1460,6 @@ github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= @@ -1500,8 +1499,8 @@ github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 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/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +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= @@ -1543,8 +1542,8 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= 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 }