Skip to content

Commit

Permalink
Node/Solana: Process multiple wormhole instructions (#3527)
Browse files Browse the repository at this point in the history
* Node/Solana: Process multiple wormhole insts

* Better error logging

* node/solana: consistency defense-in-depth check

* Fix formatting

---------

Co-authored-by: Evan Gray <[email protected]>
  • Loading branch information
bruce-riley and evan-gray authored Dec 1, 2023
1 parent 08455a7 commit e0fbd51
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 41 deletions.
79 changes: 79 additions & 0 deletions node/hack/solana/account_lookup.go
Original file line number Diff line number Diff line change
@@ -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)
}

}
86 changes: 45 additions & 41 deletions node/pkg/watchers/solana/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -532,6 +541,7 @@ OUTER:
zap.Int("txNum", txNum),
zap.Error(err),
)
continue
}

signature := tx.Signatures[0]
Expand Down Expand Up @@ -561,51 +571,31 @@ 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),
zap.Int("idx", i),
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)))
}
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -918,25 +922,25 @@ 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
}

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
Expand Down

0 comments on commit e0fbd51

Please sign in to comment.