From e0fbd51f771b1739dd5c15dc58a878f812a806bd Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:56:59 -0600 Subject: [PATCH] Node/Solana: Process multiple wormhole instructions (#3527) * Node/Solana: Process multiple wormhole insts * Better error logging * node/solana: consistency defense-in-depth check * Fix formatting --------- Co-authored-by: Evan Gray --- node/hack/solana/account_lookup.go | 79 +++++++++++++++++++++++++++ node/pkg/watchers/solana/client.go | 86 ++++++++++++++++-------------- 2 files changed, 124 insertions(+), 41 deletions(-) create mode 100644 node/hack/solana/account_lookup.go diff --git a/node/hack/solana/account_lookup.go b/node/hack/solana/account_lookup.go new file mode 100644 index 0000000000..d4a3f8e810 --- /dev/null +++ b/node/hack/solana/account_lookup.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/gagliardetto/solana-go" + lookup "github.com/gagliardetto/solana-go/programs/address-lookup-table" + "github.com/gagliardetto/solana-go/rpc" +) + +const RPC = "https://api.devnet.solana.com" + +func populateLookupTableAccounts(ctx context.Context, tx *solana.Transaction, rpcClient *rpc.Client) error { + if !tx.Message.IsVersioned() { + return nil + } + + tblKeys := tx.Message.GetAddressTableLookups().GetTableIDs() + if len(tblKeys) == 0 { + return nil + } + + resolutions := make(map[solana.PublicKey]solana.PublicKeySlice) + for _, key := range tblKeys { + fmt.Println(key) + info, err := rpcClient.GetAccountInfo(ctx, key) + if err != nil { + fmt.Println("We errored here!") + return err + } + + tableContent, err := lookup.DecodeAddressLookupTableState(info.GetBinary()) + if err != nil { + return err + } + + resolutions[key] = tableContent.Addresses + } + + err := tx.Message.SetAddressTables(resolutions) + if err != nil { + return err + } + + err = tx.Message.ResolveLookups() + if err != nil { + return err + } + + return nil +} + +func main() { + ctx := context.Background() + testTx, err := solana.SignatureFromBase58("2Jr3bAuEKwYBKmaqSmmFQ2R7xxxQpmjY8g3N3gMH49C62kBaweUgc9UCEcFhqcewAVnDLcBGWUSQrKZ7vdxpBbq4") + if err != nil { + log.Fatal("SignatureFromBase58 errored", err) + } + rpcClient := rpc.New(RPC) + maxSupportedTransactionVersion := uint64(0) + tx, err := rpcClient.GetTransaction(ctx, testTx, &rpc.GetTransactionOpts{ + Encoding: solana.EncodingBase64, + MaxSupportedTransactionVersion: &maxSupportedTransactionVersion, + }) + if err != nil { + log.Fatal("getTransaction errored", err) + } + realTx, err := tx.Transaction.GetTransaction() + if err != nil { + log.Fatal("GetTransaction errored", err) + } + err = populateLookupTableAccounts(ctx, realTx, rpcClient) + if err != nil { + log.Fatal("populateLookupTableAccounts errored", err) + } + +} diff --git a/node/pkg/watchers/solana/client.go b/node/pkg/watchers/solana/client.go index 14b695cc7b..7cdcfaa274 100644 --- a/node/pkg/watchers/solana/client.go +++ b/node/pkg/watchers/solana/client.go @@ -93,8 +93,7 @@ type ( } MessagePublicationAccount struct { - VaaVersion uint8 - // Borsh does not seem to support booleans, so 0=false / 1=true + VaaVersion uint8 ConsistencyLevel uint8 EmitterAuthority vaa.Address MessageStatus uint8 @@ -166,6 +165,17 @@ func (c ConsistencyLevel) Commitment() (rpc.CommitmentType, error) { } } +func accountConsistencyLevelToCommitment(c uint8) (rpc.CommitmentType, error) { + switch c { + case 1: + return rpc.CommitmentConfirmed, nil + case 32: + return rpc.CommitmentFinalized, nil + default: + return "", fmt.Errorf("unsupported consistency level: %d", c) + } +} + const ( postMessageInstructionMinNumAccounts = 8 postMessageInstructionID = 0x01 @@ -490,7 +500,6 @@ func (s *SolanaWatcher) fetchBlock(ctx context.Context, logger *zap.Logger, slot s.updateLatestBlock(slot) -OUTER: for txNum, txRpc := range out.Transactions { if txRpc.Meta.Err != nil { logger.Debug("Transaction failed, skipping it", @@ -532,6 +541,7 @@ OUTER: zap.Int("txNum", txNum), zap.Error(err), ) + continue } signature := tx.Signatures[0] @@ -561,44 +571,18 @@ OUTER: zap.Uint64("slot", slot), zap.String("commitment", string(s.commitment)), zap.Binary("data", inst.Data)) - continue OUTER - } - if found { - continue OUTER + } else if found { + logger.Debug("found a top-level Wormhole instruction", + zap.Int("idx", i), + zap.Stringer("signature", signature), + zap.Uint64("slot", slot), + zap.String("commitment", string(s.commitment))) } } - // Call GetConfirmedTransaction to get at innerTransactions - rCtx, cancel := context.WithTimeout(ctx, rpcTimeout) - start := time.Now() - maxSupportedTransactionVersion := uint64(0) - tr, err := s.rpcClient.GetTransaction(rCtx, signature, &rpc.GetTransactionOpts{ - Encoding: solana.EncodingBase64, // solana-go doesn't support json encoding. - Commitment: s.commitment, - MaxSupportedTransactionVersion: &maxSupportedTransactionVersion, - }) - cancel() - queryLatency.WithLabelValues(s.networkName, "get_confirmed_transaction", string(s.commitment)).Observe(time.Since(start).Seconds()) - if err != nil { - p2p.DefaultRegistry.AddErrorCount(s.chainID, 1) - solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_confirmed_transaction_error").Inc() - logger.Error("failed to request transaction", - zap.Error(err), - zap.Uint64("slot", slot), - zap.String("commitment", string(s.commitment)), - zap.Stringer("signature", signature)) - return false - } - - logger.Debug("fetched transaction", - zap.Uint64("slot", slot), - zap.String("commitment", string(s.commitment)), - zap.Stringer("signature", signature), - zap.Duration("took", time.Since(start))) - - for _, inner := range tr.Meta.InnerInstructions { + for _, inner := range txRpc.Meta.InnerInstructions { for i, inst := range inner.Instructions { - _, err = s.processInstruction(ctx, logger, slot, inst, programIndex, tx, signature, i, isReobservation) + found, err := s.processInstruction(ctx, logger, slot, inst, programIndex, tx, signature, i, isReobservation) if err != nil { logger.Error("malformed Wormhole instruction", zap.Error(err), @@ -606,6 +590,12 @@ OUTER: zap.Stringer("signature", signature), zap.Uint64("slot", slot), zap.String("commitment", string(s.commitment))) + } else if found { + logger.Debug("found an inner Wormhole instruction", + zap.Int("idx", i), + zap.Stringer("signature", signature), + zap.Uint64("slot", slot), + zap.String("commitment", string(s.commitment))) } } } @@ -822,6 +812,20 @@ func (s *SolanaWatcher) processMessageAccount(logger *zap.Logger, data []byte, a return } + // SECURITY: defense-in-depth, ensure the consistency level in the account matches the consistency level of the watcher + commitment, err := accountConsistencyLevelToCommitment(proposal.ConsistencyLevel) + if err != nil { + logger.Error( + "failed to parse proposal consistency level", + zap.Any("proposal", proposal), + zap.Error(err)) + return + } + if commitment != s.commitment { + logger.Debug("skipping message which does not match the watcher commitment", zap.Stringer("account", acc), zap.String("message commitment", string(commitment)), zap.String("watcher commitment", string(s.commitment))) + return + } + // As of 2023-11-09, Pythnet has a bug which is not zeroing out these fields appropriately. This carve out should be removed after a fix is deployed. if s.chainID != vaa.ChainIDPythNet { // SECURITY: ensure these fields are zeroed out. in the legacy solana program they were always zero, and in the 2023 rewrite they are zeroed once the account is finalized @@ -918,12 +922,12 @@ func (s *SolanaWatcher) populateLookupTableAccounts(ctx context.Context, tx *sol for _, key := range tblKeys { info, err := s.rpcClient.GetAccountInfo(ctx, key) if err != nil { - return err + return fmt.Errorf("failed to get account info for key %s: %w", key, err) } tableContent, err := lookup.DecodeAddressLookupTableState(info.GetBinary()) if err != nil { - return err + return fmt.Errorf("failed to decode table content for key %s: %w", key, err) } resolutions[key] = tableContent.Addresses @@ -931,12 +935,12 @@ func (s *SolanaWatcher) populateLookupTableAccounts(ctx context.Context, tx *sol err := tx.Message.SetAddressTables(resolutions) if err != nil { - return err + return fmt.Errorf("failed to set address tables: %w", err) } err = tx.Message.ResolveLookups() if err != nil { - return err + return fmt.Errorf("failed to resolve lookups: %w", err) } return nil