2021-03-08 12:25:13 -08:00
package solana
2021-01-21 02:31:32 -08:00
import (
2023-11-08 10:09:15 -08:00
"bytes"
2021-01-21 02:31:32 -08:00
"context"
2021-10-01 02:07:28 -07:00
"errors"
2021-07-29 07:09:40 -07:00
"fmt"
2023-09-14 07:30:32 -07:00
"strings"
2022-11-14 06:07:45 -08:00
"sync"
2022-06-21 12:18:16 -07:00
"time"
2023-01-18 08:24:55 -08:00
"encoding/base64"
"encoding/json"
2021-08-26 01:35:09 -07:00
"github.com/certusone/wormhole/node/pkg/common"
"github.com/certusone/wormhole/node/pkg/p2p"
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
"github.com/certusone/wormhole/node/pkg/readiness"
"github.com/certusone/wormhole/node/pkg/supervisor"
2021-01-21 02:31:32 -08:00
eth_common "github.com/ethereum/go-ethereum/common"
2021-07-28 06:56:19 -07:00
"github.com/gagliardetto/solana-go"
2023-09-11 10:01:58 -07:00
lookup "github.com/gagliardetto/solana-go/programs/address-lookup-table"
2021-07-28 06:56:19 -07:00
"github.com/gagliardetto/solana-go/rpc"
2021-10-01 02:07:28 -07:00
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
2023-01-18 08:24:55 -08:00
"github.com/google/uuid"
2021-02-03 04:01:51 -08:00
"github.com/mr-tron/base58"
2021-06-18 09:50:06 -07:00
"github.com/near/borsh-go"
2021-01-26 16:16:37 -08:00
"github.com/prometheus/client_golang/prometheus"
2021-07-23 07:06:35 -07:00
"github.com/prometheus/client_golang/prometheus/promauto"
2022-08-18 01:52:36 -07:00
"github.com/wormhole-foundation/wormhole/sdk/vaa"
2021-01-21 02:31:32 -08:00
"go.uber.org/zap"
2023-01-18 08:24:55 -08:00
"nhooyr.io/websocket"
2021-01-21 02:31:32 -08:00
)
2023-01-18 08:24:55 -08:00
type (
SolanaWatcher struct {
2023-01-20 13:15:13 -08:00
contract solana . PublicKey
rawContract string
rpcUrl string
wsUrl * string
commitment rpc . CommitmentType
msgC chan <- * common . MessagePublication
obsvReqC <- chan * gossipv1 . ObservationRequest
errC chan error
pumpData chan [ ] byte
rpcClient * rpc . Client
2023-01-18 08:24:55 -08:00
// Readiness component
2023-03-27 10:50:21 -07:00
readinessSync readiness . Component
2023-01-18 08:24:55 -08:00
// VAA ChainID of the network we're connecting to.
chainID vaa . ChainID
// Human readable name of network
networkName string
// The last slot processed by the watcher.
lastSlot uint64
// subscriber id
subId string
// latestFinalizedBlockNumber is the latest block processed by this watcher.
latestBlockNumber uint64
latestBlockNumberMu sync . Mutex
}
EventSubscriptionError struct {
Jsonrpc string ` json:"jsonrpc" `
Error struct {
Code int ` json:"code" `
Message * string ` json:"message" `
} ` json:"error" `
ID string ` json:"id" `
}
EventSubscriptionData struct {
Jsonrpc string ` json:"jsonrpc" `
Method string ` json:"method" `
Params * struct {
Result struct {
Context struct {
Slot int64 ` json:"slot" `
} ` json:"context" `
Value struct {
Pubkey string ` json:"pubkey" `
Account struct {
Lamports int64 ` json:"lamports" `
Data [ ] string ` json:"data" `
Owner string ` json:"owner" `
Executable bool ` json:"executable" `
RentEpoch int64 ` json:"rentEpoch" `
} ` json:"account" `
} ` json:"value" `
} ` json:"result" `
Subscription int ` json:"subscription" `
} ` json:"params" `
}
MessagePublicationAccount struct {
VaaVersion uint8
// Borsh does not seem to support booleans, so 0=false / 1=true
2023-11-08 10:09:15 -08:00
ConsistencyLevel uint8
EmitterAuthority vaa . Address
MessageStatus uint8
Gap [ 3 ] byte
SubmissionTime uint32
Nonce uint32
Sequence uint64
EmitterChain uint16
EmitterAddress vaa . Address
Payload [ ] byte
2023-01-18 08:24:55 -08:00
}
)
2021-01-21 02:31:32 -08:00
2023-11-08 10:09:15 -08:00
var (
emptyAddressBytes = vaa . Address { } . Bytes ( )
emptyGapBytes = [ ] byte { 0 , 0 , 0 }
)
2021-01-26 16:16:37 -08:00
var (
2021-07-23 07:06:35 -07:00
solanaConnectionErrors = promauto . NewCounterVec (
2021-01-26 16:16:37 -08:00
prometheus . CounterOpts {
Name : "wormhole_solana_connection_errors_total" ,
Help : "Total number of Solana connection errors" ,
2022-07-28 10:30:00 -07:00
} , [ ] string { "solana_network" , "commitment" , "reason" } )
2021-07-23 07:06:35 -07:00
solanaAccountSkips = promauto . NewCounterVec (
2021-01-26 16:16:37 -08:00
prometheus . CounterOpts {
Name : "wormhole_solana_account_updates_skipped_total" ,
Help : "Total number of account updates skipped due to invalid data" ,
2022-07-28 10:30:00 -07:00
} , [ ] string { "solana_network" , "reason" } )
solanaMessagesConfirmed = promauto . NewCounterVec (
2021-01-26 16:16:37 -08:00
prometheus . CounterOpts {
2021-07-21 10:46:10 -07:00
Name : "wormhole_solana_observations_confirmed_total" ,
Help : "Total number of verified Solana observations found" ,
2022-07-28 10:30:00 -07:00
} , [ ] string { "solana_network" } )
2021-07-29 05:24:35 -07:00
currentSolanaHeight = promauto . NewGaugeVec (
2021-01-26 16:16:37 -08:00
prometheus . GaugeOpts {
Name : "wormhole_solana_current_height" ,
2021-07-29 05:24:35 -07:00
Help : "Current Solana slot height" ,
2022-07-28 10:30:00 -07:00
} , [ ] string { "solana_network" , "commitment" } )
2021-07-23 07:06:35 -07:00
queryLatency = promauto . NewHistogramVec (
2021-01-26 16:16:37 -08:00
prometheus . HistogramOpts {
Name : "wormhole_solana_query_latency" ,
Help : "Latency histogram for Solana RPC calls" ,
2022-07-28 10:30:00 -07:00
} , [ ] string { "solana_network" , "operation" , "commitment" } )
2021-01-26 16:16:37 -08:00
)
2021-07-28 09:33:12 -07:00
const rpcTimeout = time . Second * 5
2021-09-18 01:46:48 -07:00
// Maximum retries for Solana fetching
2021-12-20 13:25:54 -08:00
const maxRetries = 10
2021-09-30 12:47:55 -07:00
const retryDelay = 5 * time . Second
2021-09-18 01:46:48 -07:00
2021-07-29 07:09:40 -07:00
type ConsistencyLevel uint8
2021-07-29 05:24:35 -07:00
// Mappings from consistency levels constants to commitment level.
const (
2021-07-29 07:09:40 -07:00
consistencyLevelConfirmed ConsistencyLevel = 0
consistencyLevelFinalized ConsistencyLevel = 1
)
func ( c ConsistencyLevel ) Commitment ( ) ( rpc . CommitmentType , error ) {
switch c {
case consistencyLevelConfirmed :
return rpc . CommitmentConfirmed , nil
case consistencyLevelFinalized :
return rpc . CommitmentFinalized , nil
default :
return "" , fmt . Errorf ( "unsupported consistency level: %d" , c )
}
}
const (
2023-09-11 08:55:06 -07:00
postMessageInstructionMinNumAccounts = 8
postMessageInstructionID = 0x01
postMessageUnreliableInstructionID = 0x08
accountPrefixReliable = "msg"
accountPrefixUnreliable = "msu"
2021-07-29 05:24:35 -07:00
)
2021-07-29 07:09:40 -07:00
// PostMessageData represents the user-supplied, untrusted instruction data
// for message publications. We use this to determine consistency level before fetching accounts.
type PostMessageData struct {
Nonce uint32
Payload [ ] byte
ConsistencyLevel ConsistencyLevel
}
2021-07-29 05:24:35 -07:00
func NewSolanaWatcher (
2022-09-26 06:56:39 -07:00
rpcUrl string ,
2023-01-18 08:24:55 -08:00
wsUrl * string ,
2021-08-30 07:19:00 -07:00
contractAddress solana . PublicKey ,
2023-01-18 08:24:55 -08:00
rawContract string ,
2023-01-20 13:15:13 -08:00
msgC chan <- * common . MessagePublication ,
obsvReqC <- chan * gossipv1 . ObservationRequest ,
2022-07-28 10:30:00 -07:00
commitment rpc . CommitmentType ,
chainID vaa . ChainID ) * SolanaWatcher {
2021-07-29 05:24:35 -07:00
return & SolanaWatcher {
2023-03-27 10:50:21 -07:00
rpcUrl : rpcUrl ,
wsUrl : wsUrl ,
contract : contractAddress ,
rawContract : rawContract ,
msgC : msgC ,
obsvReqC : obsvReqC ,
commitment : commitment ,
rpcClient : rpc . New ( rpcUrl ) ,
readinessSync : common . MustConvertChainIdToReadinessSyncing ( chainID ) ,
chainID : chainID ,
2023-05-03 15:33:20 -07:00
networkName : chainID . String ( ) ,
2021-07-29 05:24:35 -07:00
}
2021-01-21 02:31:32 -08:00
}
2023-01-18 08:24:55 -08:00
func ( s * SolanaWatcher ) SetupSubscription ( ctx context . Context ) ( error , * websocket . Conn ) {
logger := supervisor . Logger ( ctx )
logger . Info ( "Solana watcher connecting to WS node " , zap . String ( "url" , * s . wsUrl ) )
ws , _ , err := websocket . Dial ( ctx , * s . wsUrl , nil )
if err != nil {
return err , nil
}
s . subId = uuid . New ( ) . String ( )
s . pumpData = make ( chan [ ] byte )
const temp = ` { "jsonrpc": "2.0", "id": "%s", "method": "programSubscribe", "params": ["%s", { "encoding": "base64", "commitment": "%s", "filters": []}]} `
var p = fmt . Sprintf ( temp , s . subId , s . rawContract , string ( s . commitment ) )
logger . Info ( "Subscribing using" , zap . String ( "filter" , p ) )
if err := ws . Write ( ctx , websocket . MessageText , [ ] byte ( p ) ) ; err != nil {
logger . Error ( fmt . Sprintf ( "write: %s" , err . Error ( ) ) )
return err , nil
}
return nil , ws
}
func ( s * SolanaWatcher ) SetupWebSocket ( ctx context . Context ) error {
2023-05-03 15:33:20 -07:00
if s . chainID != vaa . ChainIDPythNet {
2023-01-18 08:24:55 -08:00
panic ( "unsupported chain id" )
}
logger := supervisor . Logger ( ctx )
err , ws := s . SetupSubscription ( ctx )
if err != nil {
return err
}
common . RunWithScissors ( ctx , s . errC , "SolanaDataPump" , func ( ctx context . Context ) error {
defer ws . Close ( websocket . StatusNormalClosure , "" )
for {
select {
case <- ctx . Done ( ) :
return nil
default :
2023-02-07 08:13:12 -08:00
if msg , err := s . readWebSocketWithTimeout ( ctx , ws ) ; err != nil {
logger . Error ( fmt . Sprintf ( "ReadMessage: '%s'" , err . Error ( ) ) )
2023-02-06 06:38:45 -08:00
return err
2023-02-07 08:13:12 -08:00
} else {
s . pumpData <- msg
2023-01-18 08:24:55 -08:00
}
}
}
} )
return nil
}
2023-02-07 08:13:12 -08:00
func ( s * SolanaWatcher ) readWebSocketWithTimeout ( ctx context . Context , ws * websocket . Conn ) ( [ ] byte , error ) {
rCtx , cancel := context . WithTimeout ( ctx , time . Second * 300 ) // 5 minute
defer cancel ( )
_ , msg , err := ws . Read ( rCtx )
return msg , err
}
2021-01-21 02:31:32 -08:00
func ( s * SolanaWatcher ) Run ( ctx context . Context ) error {
2021-02-03 04:01:51 -08:00
// Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing)
2021-08-30 07:19:00 -07:00
contractAddr := base58 . Encode ( s . contract [ : ] )
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . SetNetworkStats ( s . chainID , & gossipv1 . Heartbeat_Network {
2021-08-30 07:19:00 -07:00
ContractAddress : contractAddr ,
2021-02-03 04:01:51 -08:00
} )
2021-09-29 16:01:57 -07:00
logger := supervisor . Logger ( ctx )
2021-01-21 02:31:32 -08:00
2023-06-12 09:56:54 -07:00
wsUrl := ""
if s . wsUrl != nil {
wsUrl = * s . wsUrl
}
logger . Info ( "Starting watcher" ,
zap . String ( "watcher_name" , "solana" ) ,
zap . String ( "rpcUrl" , s . rpcUrl ) ,
zap . String ( "wsUrl" , wsUrl ) ,
zap . String ( "contract" , contractAddr ) ,
zap . String ( "rawContract" , s . rawContract ) ,
)
2023-01-18 08:24:55 -08:00
logger . Info ( "Solana watcher connecting to RPC node " , zap . String ( "url" , s . rpcUrl ) )
s . errC = make ( chan error )
s . pumpData = make ( chan [ ] byte )
2023-02-06 06:38:45 -08:00
useWs := false
if s . wsUrl != nil && * s . wsUrl != "" {
useWs = true
2023-01-18 08:24:55 -08:00
err := s . SetupWebSocket ( ctx )
if err != nil {
return err
}
}
common . RunWithScissors ( ctx , s . errC , "SolanaWatcher" , func ( ctx context . Context ) error {
2021-07-28 09:33:12 -07:00
timer := time . NewTicker ( time . Second * 1 )
2021-01-21 02:31:32 -08:00
defer timer . Stop ( )
for {
select {
case <- ctx . Done ( ) :
2023-01-18 08:24:55 -08:00
return nil
case msg := <- s . pumpData :
2023-09-29 08:42:44 -07:00
err := s . processAccountSubscriptionData ( ctx , logger , msg , false )
2023-01-18 08:24:55 -08:00
if err != nil {
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "account_subscription_data" ) . Inc ( )
s . errC <- err
return err
}
2021-12-20 13:23:45 -08:00
case m := <- s . obsvReqC :
2022-07-28 10:30:00 -07:00
if m . ChainId != uint32 ( s . chainID ) {
2021-12-20 13:23:45 -08:00
panic ( "unexpected chain id" )
}
acc := solana . PublicKeyFromBytes ( m . TxHash )
logger . Info ( "received observation request" , zap . String ( "account" , acc . String ( ) ) )
rCtx , cancel := context . WithTimeout ( ctx , rpcTimeout )
2023-09-29 08:42:44 -07:00
s . fetchMessageAccount ( rCtx , logger , acc , 0 , true )
2021-12-20 13:23:45 -08:00
cancel ( )
2021-01-21 02:31:32 -08:00
case <- timer . C :
2021-07-28 09:33:12 -07:00
// Get current slot height
rCtx , cancel := context . WithTimeout ( ctx , rpcTimeout )
start := time . Now ( )
2021-07-29 09:07:53 -07:00
slot , err := s . rpcClient . GetSlot ( rCtx , s . commitment )
2022-01-26 06:43:44 -08:00
cancel ( )
2022-07-28 10:30:00 -07:00
queryLatency . WithLabelValues ( s . networkName , "get_slot" , string ( s . commitment ) ) . Observe ( time . Since ( start ) . Seconds ( ) )
2021-07-28 09:33:12 -07:00
if err != nil {
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "get_slot_error" ) . Inc ( )
2023-01-18 08:24:55 -08:00
s . errC <- err
return err
2021-07-28 09:33:12 -07:00
}
2022-10-10 22:20:07 -07:00
lastSlot := s . lastSlot
2021-07-28 09:33:12 -07:00
if lastSlot == 0 {
lastSlot = slot - 1
}
2022-07-28 10:30:00 -07:00
currentSolanaHeight . WithLabelValues ( s . networkName , string ( s . commitment ) ) . Set ( float64 ( slot ) )
2023-03-27 10:50:21 -07:00
readiness . SetReady ( s . readinessSync )
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . SetNetworkStats ( s . chainID , & gossipv1 . Heartbeat_Network {
2021-08-30 07:19:00 -07:00
Height : int64 ( slot ) ,
ContractAddress : contractAddr ,
2021-07-28 09:33:12 -07:00
} )
2021-07-09 05:56:52 -07:00
2023-02-06 06:38:45 -08:00
if ! useWs {
2023-01-18 08:24:55 -08:00
rangeStart := lastSlot + 1
rangeEnd := slot
2021-07-28 09:33:12 -07:00
2023-01-18 08:24:55 -08:00
logger . Debug ( "fetched current Solana height" ,
zap . String ( "commitment" , string ( s . commitment ) ) ,
zap . Uint64 ( "slot" , slot ) ,
zap . Uint64 ( "lastSlot" , lastSlot ) ,
zap . Uint64 ( "pendingSlots" , slot - lastSlot ) ,
zap . Uint64 ( "from" , rangeStart ) , zap . Uint64 ( "to" , rangeEnd ) ,
zap . Duration ( "took" , time . Since ( start ) ) )
// Requesting each slot
for slot := rangeStart ; slot <= rangeEnd ; slot ++ {
_slot := slot
common . RunWithScissors ( ctx , s . errC , "SolanaWatcherSlotFetcher" , func ( ctx context . Context ) error {
2023-09-29 08:42:44 -07:00
s . retryFetchBlock ( ctx , logger , _slot , 0 , false )
2023-01-18 08:24:55 -08:00
return nil
} )
}
2021-07-28 09:33:12 -07:00
}
2022-10-10 22:20:07 -07:00
s . lastSlot = slot
2021-01-21 02:31:32 -08:00
}
}
2023-01-18 08:24:55 -08:00
} )
2021-01-21 02:31:32 -08:00
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
2023-01-18 08:24:55 -08:00
case err := <- s . errC :
2021-01-21 02:31:32 -08:00
return err
}
}
2023-09-29 08:42:44 -07:00
func ( s * SolanaWatcher ) retryFetchBlock ( ctx context . Context , logger * zap . Logger , slot uint64 , retry uint , isReobservation bool ) {
ok := s . fetchBlock ( ctx , logger , slot , 0 , isReobservation )
2021-09-18 01:46:48 -07:00
if ! ok {
if retry >= maxRetries {
2021-09-29 16:01:57 -07:00
logger . Error ( "max retries for block" ,
2021-09-18 01:46:48 -07:00
zap . Uint64 ( "slot" , slot ) ,
zap . String ( "commitment" , string ( s . commitment ) ) ,
zap . Uint ( "retry" , retry ) )
return
}
2021-09-30 12:47:55 -07:00
time . Sleep ( retryDelay )
2021-09-29 14:16:04 -07:00
2023-07-06 07:33:12 -07:00
logger . Debug ( "retrying block" ,
2021-09-18 01:46:48 -07:00
zap . Uint64 ( "slot" , slot ) ,
zap . String ( "commitment" , string ( s . commitment ) ) ,
zap . Uint ( "retry" , retry ) )
2023-01-18 08:24:55 -08:00
common . RunWithScissors ( ctx , s . errC , "retryFetchBlock" , func ( ctx context . Context ) error {
2023-09-29 08:42:44 -07:00
s . retryFetchBlock ( ctx , logger , slot , retry + 1 , isReobservation )
2023-01-18 08:24:55 -08:00
return nil
} )
2021-09-18 01:46:48 -07:00
}
}
2023-09-29 08:42:44 -07:00
func ( s * SolanaWatcher ) fetchBlock ( ctx context . Context , logger * zap . Logger , slot uint64 , emptyRetry uint , isReobservation bool ) ( ok bool ) {
2021-10-01 05:06:51 -07:00
logger . Debug ( "requesting block" ,
zap . Uint64 ( "slot" , slot ) ,
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-10-01 06:06:54 -07:00
zap . Uint ( "empty_retry" , emptyRetry ) )
2021-07-28 09:33:12 -07:00
rCtx , cancel := context . WithTimeout ( ctx , rpcTimeout )
defer cancel ( )
start := time . Now ( )
rewards := false
2022-10-24 10:22:56 -07:00
maxSupportedTransactionVersion := uint64 ( 0 )
out , err := s . rpcClient . GetBlockWithOpts ( rCtx , slot , & rpc . GetBlockOpts {
2022-10-27 09:14:42 -07:00
Encoding : solana . EncodingBase64 , // solana-go doesn't support json encoding.
2022-10-24 10:22:56 -07:00
TransactionDetails : "full" ,
Rewards : & rewards ,
Commitment : s . commitment ,
MaxSupportedTransactionVersion : & maxSupportedTransactionVersion ,
2021-07-28 09:33:12 -07:00
} )
2022-07-28 10:30:00 -07:00
queryLatency . WithLabelValues ( s . networkName , "get_confirmed_block" , string ( s . commitment ) ) . Observe ( time . Since ( start ) . Seconds ( ) )
2021-07-28 09:33:12 -07:00
if err != nil {
2021-10-01 02:07:28 -07:00
var rpcErr * jsonrpc . RPCError
if errors . As ( err , & rpcErr ) && ( rpcErr . Code == - 32007 /* SLOT_SKIPPED */ || rpcErr . Code == - 32004 /* BLOCK_NOT_AVAILABLE */ ) {
2023-01-31 06:24:33 -08:00
logger . Debug ( "empty slot" , zap . Uint64 ( "slot" , slot ) ,
2021-10-01 05:06:51 -07:00
zap . Int ( "code" , rpcErr . Code ) ,
2021-10-01 02:07:28 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) )
2021-10-01 05:06:51 -07:00
2021-10-01 06:06:54 -07:00
// TODO(leo): clean this up once we know what's happening
// https://github.com/solana-labs/solana/issues/20370
var maxEmptyRetry uint
if s . commitment == rpc . CommitmentFinalized {
maxEmptyRetry = 5
} else {
maxEmptyRetry = 1
}
2021-10-01 05:06:51 -07:00
// Schedule a single retry just in case the Solana node was confused about the block being missing.
2021-10-01 06:06:54 -07:00
if emptyRetry < maxEmptyRetry {
2023-01-18 08:24:55 -08:00
common . RunWithScissors ( ctx , s . errC , "delayedFetchBlock" , func ( ctx context . Context ) error {
2021-10-01 06:06:54 -07:00
time . Sleep ( retryDelay )
2023-09-29 08:42:44 -07:00
s . fetchBlock ( ctx , logger , slot , emptyRetry + 1 , isReobservation )
2023-01-18 08:24:55 -08:00
return nil
} )
2021-10-01 05:06:51 -07:00
}
return true
2021-10-01 02:07:28 -07:00
} else {
2023-07-06 07:33:12 -07:00
logger . Debug ( "failed to request block" , zap . Error ( err ) , zap . Uint64 ( "slot" , slot ) ,
2021-10-01 02:07:28 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) )
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "get_confirmed_block_error" ) . Inc ( )
2021-10-01 02:07:28 -07:00
}
2021-09-18 01:46:48 -07:00
return false
2021-07-28 09:33:12 -07:00
}
if out == nil {
2022-10-24 10:22:56 -07:00
// Per the API, nil just means the block is not confirmed.
logger . Info ( "block is not yet finalized" , zap . Uint64 ( "slot" , slot ) )
2021-09-18 01:46:48 -07:00
return false
2021-07-28 09:33:12 -07:00
}
2022-11-03 09:09:20 -07:00
logger . Debug ( "fetched block" ,
2021-07-28 09:33:12 -07:00
zap . Uint64 ( "slot" , slot ) ,
zap . Int ( "num_tx" , len ( out . Transactions ) ) ,
zap . Duration ( "took" , time . Since ( start ) ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) )
2021-07-28 09:33:12 -07:00
2022-11-14 06:07:45 -08:00
s . updateLatestBlock ( slot )
2021-07-28 09:33:12 -07:00
OUTER :
2022-10-27 09:14:42 -07:00
for txNum , txRpc := range out . Transactions {
if txRpc . Meta . Err != nil {
2022-11-01 08:13:00 -07:00
logger . Debug ( "Transaction failed, skipping it" ,
zap . Uint64 ( "slot" , slot ) ,
zap . Int ( "txNum" , txNum ) ,
zap . String ( "err" , fmt . Sprint ( txRpc . Meta . Err ) ) ,
)
2022-10-27 09:14:42 -07:00
continue
}
2023-09-14 07:30:32 -07:00
// If the logs don't contain the contract address, skip the transaction.
// ex: "Program 3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5 invoke [2]",
var (
possiblyWormhole bool
whLogPrefix = fmt . Sprintf ( "Program %s" , s . rawContract )
)
for i := 0 ; i < len ( txRpc . Meta . LogMessages ) && ! possiblyWormhole ; i ++ {
possiblyWormhole = strings . HasPrefix ( txRpc . Meta . LogMessages [ i ] , whLogPrefix )
}
if ! possiblyWormhole {
continue
}
2022-10-24 10:22:56 -07:00
tx , err := txRpc . GetTransaction ( )
if err != nil {
2022-10-27 09:14:42 -07:00
logger . Error ( "failed to unmarshal transaction" ,
zap . Uint64 ( "slot" , slot ) ,
zap . Int ( "txNum" , txNum ) ,
zap . Int ( "dataLen" , len ( txRpc . Transaction . GetBinary ( ) ) ) ,
zap . Error ( err ) ,
)
2022-10-24 10:22:56 -07:00
continue
}
2023-09-11 10:01:58 -07:00
err = s . populateLookupTableAccounts ( ctx , tx )
if err != nil {
logger . Error ( "failed to fetch lookup table accounts" ,
zap . Uint64 ( "slot" , slot ) ,
zap . Int ( "txNum" , txNum ) ,
zap . Error ( err ) ,
)
}
2022-10-24 10:22:56 -07:00
signature := tx . Signatures [ 0 ]
2021-07-28 09:33:12 -07:00
var programIndex uint16
2022-10-24 10:22:56 -07:00
for n , key := range tx . Message . AccountKeys {
2021-08-30 07:19:00 -07:00
if key . Equals ( s . contract ) {
2021-07-28 09:33:12 -07:00
programIndex = uint16 ( n )
}
}
if programIndex == 0 {
continue
}
2022-11-03 09:09:20 -07:00
logger . Debug ( "found Wormhole transaction" ,
2021-07-28 09:33:12 -07:00
zap . Stringer ( "signature" , signature ) ,
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) )
2021-07-28 09:33:12 -07:00
// Find top-level instructions
2022-10-24 10:22:56 -07:00
for i , inst := range tx . Message . Instructions {
2023-09-29 08:42:44 -07:00
found , err := s . processInstruction ( ctx , logger , slot , inst , programIndex , tx , signature , i , isReobservation )
2021-07-29 07:09:40 -07:00
if err != nil {
2021-09-29 16:01:57 -07:00
logger . Error ( "malformed Wormhole instruction" ,
2021-07-29 07:09:40 -07:00
zap . Error ( err ) ,
2021-10-01 00:27:56 -07:00
zap . Int ( "idx" , i ) ,
2021-07-29 07:09:40 -07:00
zap . Stringer ( "signature" , signature ) ,
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-07-29 07:09:40 -07:00
zap . Binary ( "data" , inst . Data ) )
continue OUTER
}
if found {
2021-07-28 09:33:12 -07:00
continue OUTER
}
}
// Call GetConfirmedTransaction to get at innerTransactions
rCtx , cancel := context . WithTimeout ( ctx , rpcTimeout )
start := time . Now ( )
2022-10-27 09:14:42 -07:00
maxSupportedTransactionVersion := uint64 ( 0 )
2022-10-24 10:22:56 -07:00
tr , err := s . rpcClient . GetTransaction ( rCtx , signature , & rpc . GetTransactionOpts {
2022-10-27 09:14:42 -07:00
Encoding : solana . EncodingBase64 , // solana-go doesn't support json encoding.
Commitment : s . commitment ,
MaxSupportedTransactionVersion : & maxSupportedTransactionVersion ,
2021-07-29 04:18:32 -07:00
} )
2022-01-26 06:43:44 -08:00
cancel ( )
2022-07-28 10:30:00 -07:00
queryLatency . WithLabelValues ( s . networkName , "get_confirmed_transaction" , string ( s . commitment ) ) . Observe ( time . Since ( start ) . Seconds ( ) )
2021-07-28 09:33:12 -07:00
if err != nil {
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "get_confirmed_transaction_error" ) . Inc ( )
2021-09-29 16:01:57 -07:00
logger . Error ( "failed to request transaction" ,
2021-07-28 09:33:12 -07:00
zap . Error ( err ) ,
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-07-28 09:33:12 -07:00
zap . Stringer ( "signature" , signature ) )
2021-09-18 01:46:48 -07:00
return false
2021-07-28 09:33:12 -07:00
}
2022-11-03 09:09:20 -07:00
logger . Debug ( "fetched transaction" ,
2021-07-28 09:33:12 -07:00
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-07-28 09:33:12 -07:00
zap . Stringer ( "signature" , signature ) ,
zap . Duration ( "took" , time . Since ( start ) ) )
for _ , inner := range tr . Meta . InnerInstructions {
2022-10-27 09:14:42 -07:00
for i , inst := range inner . Instructions {
2023-09-29 08:42:44 -07:00
_ , err = s . processInstruction ( ctx , logger , slot , inst , programIndex , tx , signature , i , isReobservation )
2021-07-29 07:09:40 -07:00
if err != nil {
2021-09-29 16:01:57 -07:00
logger . Error ( "malformed Wormhole instruction" ,
2021-07-29 07:09:40 -07:00
zap . Error ( err ) ,
2021-10-01 00:27:56 -07:00
zap . Int ( "idx" , i ) ,
2021-07-29 07:09:40 -07:00
zap . Stringer ( "signature" , signature ) ,
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) )
2021-07-28 09:33:12 -07:00
}
}
}
}
2021-09-18 01:46:48 -07:00
2021-10-01 06:06:54 -07:00
if emptyRetry > 0 {
logger . Warn ( "SOLANA BUG: skipped or unavailable block retrieved on retry attempt" ,
zap . Uint ( "empty_retry" , emptyRetry ) ,
2021-10-01 05:06:51 -07:00
zap . Uint64 ( "slot" , slot ) ,
zap . String ( "commitment" , string ( s . commitment ) ) )
}
2021-09-18 01:46:48 -07:00
return true
2021-07-28 09:33:12 -07:00
}
2023-09-29 08:42:44 -07:00
func ( s * SolanaWatcher ) processInstruction ( ctx context . Context , logger * zap . Logger , slot uint64 , inst solana . CompiledInstruction , programIndex uint16 , tx * solana . Transaction , signature solana . Signature , idx int , isReobservation bool ) ( bool , error ) {
2021-07-29 07:09:40 -07:00
if inst . ProgramIDIndex != programIndex {
return false , nil
}
2022-06-27 01:57:25 -07:00
if len ( inst . Data ) == 0 {
return false , nil
}
if inst . Data [ 0 ] != postMessageInstructionID && inst . Data [ 0 ] != postMessageUnreliableInstructionID {
2021-10-01 05:46:20 -07:00
return false , nil
}
2023-09-11 08:55:06 -07:00
if len ( inst . Accounts ) < postMessageInstructionMinNumAccounts {
return false , fmt . Errorf ( "invalid number of accounts: %d, must be at least %d" ,
len ( inst . Accounts ) , postMessageInstructionMinNumAccounts )
2021-07-29 07:09:40 -07:00
}
// Decode instruction data (UNTRUSTED)
var data PostMessageData
if err := borsh . Deserialize ( & data , inst . Data [ 1 : ] ) ; err != nil {
return false , fmt . Errorf ( "failed to deserialize instruction data: %w" , err )
}
2022-11-03 09:09:20 -07:00
logger . Debug ( "post message data" , zap . Any ( "deserialized_data" , data ) ,
2021-10-01 00:27:56 -07:00
zap . Stringer ( "signature" , signature ) , zap . Uint64 ( "slot" , slot ) , zap . Int ( "idx" , idx ) )
2021-07-29 07:09:40 -07:00
level , err := data . ConsistencyLevel . Commitment ( )
if err != nil {
return false , fmt . Errorf ( "failed to determine commitment: %w" , err )
}
2021-07-29 09:07:53 -07:00
if level != s . commitment {
2021-07-29 07:09:40 -07:00
return true , nil
}
// The second account in a well-formed Wormhole instruction is the VAA program account.
2022-10-24 10:22:56 -07:00
acc := tx . Message . AccountKeys [ inst . Accounts [ 1 ] ]
2021-10-01 00:27:56 -07:00
2022-11-03 09:09:20 -07:00
logger . Debug ( "fetching VAA account" , zap . Stringer ( "acc" , acc ) ,
2021-10-01 00:27:56 -07:00
zap . Stringer ( "signature" , signature ) , zap . Uint64 ( "slot" , slot ) , zap . Int ( "idx" , idx ) )
2023-01-18 08:24:55 -08:00
common . RunWithScissors ( ctx , s . errC , "retryFetchMessageAccount" , func ( ctx context . Context ) error {
2023-09-29 08:42:44 -07:00
s . retryFetchMessageAccount ( ctx , logger , acc , slot , 0 , isReobservation )
2023-01-18 08:24:55 -08:00
return nil
} )
2021-07-29 07:09:40 -07:00
return true , nil
}
2023-09-29 08:42:44 -07:00
func ( s * SolanaWatcher ) retryFetchMessageAccount ( ctx context . Context , logger * zap . Logger , acc solana . PublicKey , slot uint64 , retry uint , isReobservation bool ) {
retryable := s . fetchMessageAccount ( ctx , logger , acc , slot , isReobservation )
2021-09-30 12:47:55 -07:00
if retryable {
if retry >= maxRetries {
logger . Error ( "max retries for account" ,
zap . Uint64 ( "slot" , slot ) ,
zap . Stringer ( "account" , acc ) ,
zap . String ( "commitment" , string ( s . commitment ) ) ,
zap . Uint ( "retry" , retry ) )
return
}
time . Sleep ( retryDelay )
logger . Info ( "retrying account" ,
zap . Uint64 ( "slot" , slot ) ,
zap . Stringer ( "account" , acc ) ,
zap . String ( "commitment" , string ( s . commitment ) ) ,
zap . Uint ( "retry" , retry ) )
2023-01-18 08:24:55 -08:00
common . RunWithScissors ( ctx , s . errC , "retryFetchMessageAccount" , func ( ctx context . Context ) error {
2023-09-29 08:42:44 -07:00
s . retryFetchMessageAccount ( ctx , logger , acc , slot , retry + 1 , isReobservation )
2023-01-18 08:24:55 -08:00
return nil
} )
2021-09-30 12:47:55 -07:00
}
}
2023-09-29 08:42:44 -07:00
func ( s * SolanaWatcher ) fetchMessageAccount ( ctx context . Context , logger * zap . Logger , acc solana . PublicKey , slot uint64 , isReobservation bool ) ( retryable bool ) {
2021-07-28 09:33:12 -07:00
// Fetching account
rCtx , cancel := context . WithTimeout ( ctx , rpcTimeout )
defer cancel ( )
start := time . Now ( )
2021-07-29 09:07:53 -07:00
info , err := s . rpcClient . GetAccountInfoWithOpts ( rCtx , acc , & rpc . GetAccountInfoOpts {
2021-07-28 09:33:12 -07:00
Encoding : solana . EncodingBase64 ,
2021-07-29 09:07:53 -07:00
Commitment : s . commitment ,
2021-07-28 09:33:12 -07:00
} )
2022-07-28 10:30:00 -07:00
queryLatency . WithLabelValues ( s . networkName , "get_account_info" , string ( s . commitment ) ) . Observe ( time . Since ( start ) . Seconds ( ) )
2021-07-28 09:33:12 -07:00
if err != nil {
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "get_account_info_error" ) . Inc ( )
2021-09-29 16:01:57 -07:00
logger . Error ( "failed to request account" ,
2021-07-28 09:33:12 -07:00
zap . Error ( err ) ,
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-07-28 09:33:12 -07:00
zap . Stringer ( "account" , acc ) )
2021-09-30 12:47:55 -07:00
return true
2021-07-28 09:33:12 -07:00
}
2021-08-30 07:19:00 -07:00
if ! info . Value . Owner . Equals ( s . contract ) {
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "account_owner_mismatch" ) . Inc ( )
2021-09-29 16:01:57 -07:00
logger . Error ( "account has invalid owner" ,
2021-07-28 09:33:12 -07:00
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-07-28 09:33:12 -07:00
zap . Stringer ( "account" , acc ) ,
zap . Stringer ( "unexpected_owner" , info . Value . Owner ) )
2021-09-30 12:47:55 -07:00
return false
2021-07-28 09:33:12 -07:00
}
data := info . Value . Data . GetBinary ( )
2022-09-26 04:11:22 -07:00
if string ( data [ : 3 ] ) != accountPrefixReliable && string ( data [ : 3 ] ) != accountPrefixUnreliable {
2022-07-28 10:30:00 -07:00
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "bad_account_data" ) . Inc ( )
2021-09-29 16:01:57 -07:00
logger . Error ( "account is not a message account" ,
2021-07-28 09:33:12 -07:00
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-07-28 09:33:12 -07:00
zap . Stringer ( "account" , acc ) )
2021-09-30 12:47:55 -07:00
return false
2021-07-28 09:33:12 -07:00
}
2022-11-03 09:09:20 -07:00
logger . Debug ( "found valid VAA account" ,
2021-07-28 09:33:12 -07:00
zap . Uint64 ( "slot" , slot ) ,
2021-07-29 09:07:53 -07:00
zap . String ( "commitment" , string ( s . commitment ) ) ,
2021-07-28 09:33:12 -07:00
zap . Stringer ( "account" , acc ) ,
zap . Binary ( "data" , data ) )
2023-09-29 08:42:44 -07:00
s . processMessageAccount ( logger , data , acc , isReobservation )
2021-09-30 12:47:55 -07:00
return false
2021-07-28 09:33:12 -07:00
}
2023-09-29 08:42:44 -07:00
func ( s * SolanaWatcher ) processAccountSubscriptionData ( _ context . Context , logger * zap . Logger , data [ ] byte , isReobservation bool ) error {
2023-01-18 08:24:55 -08:00
// Do we have an error on the subscription?
var e EventSubscriptionError
err := json . Unmarshal ( data , & e )
if err != nil {
logger . Error ( * s . wsUrl , zap . Error ( err ) )
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
return err
}
if e . Error . Message != nil {
return errors . New ( * e . Error . Message )
}
var res EventSubscriptionData
err = json . Unmarshal ( data , & res )
if err != nil {
logger . Error ( * s . wsUrl , zap . Error ( err ) )
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
return err
}
if res . Params == nil {
return nil
}
value := ( * res . Params ) . Result . Value
if value . Account . Owner != s . rawContract {
// We got a message for the wrong contract on the websocket... uncomfortable...
solanaConnectionErrors . WithLabelValues ( s . networkName , string ( s . commitment ) , "invalid_websocket_account" ) . Inc ( )
return errors . New ( "Update for account with wrong owner" )
}
data , err = base64 . StdEncoding . DecodeString ( value . Account . Data [ 0 ] )
if err != nil {
logger . Error ( * s . wsUrl , zap . Error ( err ) )
p2p . DefaultRegistry . AddErrorCount ( s . chainID , 1 )
return err
}
// ignore truncated messages
if len ( data ) < 3 {
return nil
}
// Other accounts owned by the wormhole contract seem to send updates...
switch string ( data [ : 3 ] ) {
case accountPrefixReliable , accountPrefixUnreliable :
acc := solana . PublicKeyFromBytes ( [ ] byte ( value . Pubkey ) )
2023-09-29 08:42:44 -07:00
s . processMessageAccount ( logger , data , acc , isReobservation )
2023-01-18 08:24:55 -08:00
default :
break
}
return nil
}
2023-09-29 08:42:44 -07:00
func ( s * SolanaWatcher ) processMessageAccount ( logger * zap . Logger , data [ ] byte , acc solana . PublicKey , isReobservation bool ) {
2021-12-19 09:39:01 -08:00
proposal , err := ParseMessagePublicationAccount ( data )
2021-07-28 09:33:12 -07:00
if err != nil {
2022-07-28 10:30:00 -07:00
solanaAccountSkips . WithLabelValues ( s . networkName , "parse_transfer_out" ) . Inc ( )
2021-09-29 16:01:57 -07:00
logger . Error (
2021-07-28 09:33:12 -07:00
"failed to parse transfer proposal" ,
zap . Stringer ( "account" , acc ) ,
zap . Binary ( "data" , data ) ,
zap . Error ( err ) )
return
}
2023-11-09 09:10:07 -08:00
// 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
if ! bytes . Equal ( proposal . EmitterAuthority . Bytes ( ) , emptyAddressBytes ) || proposal . MessageStatus != 0 || ! bytes . Equal ( proposal . Gap [ : ] , emptyGapBytes ) {
solanaAccountSkips . WithLabelValues ( s . networkName , "unfinalized_account" ) . Inc ( )
logger . Error (
"account is not finalized" ,
zap . Stringer ( "account" , acc ) ,
zap . Binary ( "data" , data ) )
return
}
2023-11-08 10:09:15 -08:00
}
2021-07-28 09:33:12 -07:00
var txHash eth_common . Hash
copy ( txHash [ : ] , acc [ : ] )
2022-09-26 04:11:22 -07:00
var reliable bool
switch string ( data [ : 3 ] ) {
case accountPrefixReliable :
reliable = true
case accountPrefixUnreliable :
reliable = false
default :
panic ( "invalid prefix" )
}
2021-07-29 03:57:56 -07:00
observation := & common . MessagePublication {
2021-07-28 09:33:12 -07:00
TxHash : txHash ,
Timestamp : time . Unix ( int64 ( proposal . SubmissionTime ) , 0 ) ,
Nonce : proposal . Nonce ,
Sequence : proposal . Sequence ,
2022-07-28 10:30:00 -07:00
EmitterChain : s . chainID ,
2021-07-28 09:33:12 -07:00
EmitterAddress : proposal . EmitterAddress ,
Payload : proposal . Payload ,
ConsistencyLevel : proposal . ConsistencyLevel ,
2023-09-29 08:42:44 -07:00
IsReobservation : isReobservation ,
2022-09-26 04:11:22 -07:00
Unreliable : ! reliable ,
2021-07-28 09:33:12 -07:00
}
2022-07-28 10:30:00 -07:00
solanaMessagesConfirmed . WithLabelValues ( s . networkName ) . Inc ( )
2021-07-28 09:33:12 -07:00
2022-11-03 09:09:20 -07:00
logger . Debug ( "message observed" ,
2021-07-29 03:57:56 -07:00
zap . Stringer ( "account" , acc ) ,
zap . Time ( "timestamp" , observation . Timestamp ) ,
zap . Uint32 ( "nonce" , observation . Nonce ) ,
zap . Uint64 ( "sequence" , observation . Sequence ) ,
zap . Stringer ( "emitter_chain" , observation . EmitterChain ) ,
zap . Stringer ( "emitter_address" , observation . EmitterAddress ) ,
zap . Binary ( "payload" , observation . Payload ) ,
zap . Uint8 ( "consistency_level" , observation . ConsistencyLevel ) ,
)
2023-01-20 13:15:13 -08:00
s . msgC <- observation
2021-07-28 09:33:12 -07:00
}
2022-11-14 06:07:45 -08:00
// updateLatestBlock() updates the latest block number if the slot passed in is greater than the previous value.
// This check is necessary because blocks can be posted out of order, due to multi threading in this watcher.
func ( s * SolanaWatcher ) updateLatestBlock ( slot uint64 ) {
s . latestBlockNumberMu . Lock ( )
defer s . latestBlockNumberMu . Unlock ( )
if slot > s . latestBlockNumber {
s . latestBlockNumber = slot
}
}
// GetLatestFinalizedBlockNumber() returns the latest published block.
func ( s * SolanaWatcher ) GetLatestFinalizedBlockNumber ( ) uint64 {
s . latestBlockNumberMu . Lock ( )
defer s . latestBlockNumberMu . Unlock ( )
return s . latestBlockNumber
}
2021-12-19 09:39:01 -08:00
func ParseMessagePublicationAccount ( data [ ] byte ) ( * MessagePublicationAccount , error ) {
2021-04-15 02:36:29 -07:00
prop := & MessagePublicationAccount { }
2021-07-02 05:35:04 -07:00
// Skip the b"msg" prefix
if err := borsh . Deserialize ( prop , data [ 3 : ] ) ; err != nil {
2021-06-18 09:50:06 -07:00
return nil , err
2021-01-21 02:31:32 -08:00
}
return prop , nil
}
2023-09-11 10:01:58 -07:00
func ( s * SolanaWatcher ) populateLookupTableAccounts ( ctx context . Context , tx * solana . Transaction ) 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 {
info , err := s . rpcClient . GetAccountInfo ( ctx , key )
if err != nil {
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
}