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 <battledingo@gmail.com>
This commit is contained in:
bruce-riley 2023-12-01 08:56:59 -06:00 committed by GitHub
parent 08455a7770
commit e0fbd51f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 124 additions and 41 deletions

View File

@ -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)
}
}

View File

@ -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