Browse Source

node: Log version on watcher start (#4472)

* Log version of EVM node

* Log version of Solana node

* Log version of Algorand node

* Log version of Aptos node

* Log version of Sui node

* Log version of NEAR node

* Log version of Cosmwasm node

* Mock the version response for NEAR tests
Dirk Brink 1 month ago
parent
commit
383481a382

+ 35 - 0
node/pkg/watchers/algorand/watcher.go

@@ -5,6 +5,7 @@ import (
 	"encoding/base32"
 	"encoding/base32"
 	"encoding/binary"
 	"encoding/binary"
 	"encoding/hex"
 	"encoding/hex"
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"math"
 	"math"
 	"time"
 	"time"
@@ -226,6 +227,9 @@ func (e *Watcher) Run(ctx context.Context) error {
 		return err
 		return err
 	}
 	}
 
 
+	// Get the node version for troubleshooting
+	e.logVersion(ctx, logger, algodClient)
+
 	status, err := algodClient.StatusAfterBlock(0).Do(ctx)
 	status, err := algodClient.StatusAfterBlock(0).Do(ctx)
 	if err != nil {
 	if err != nil {
 		logger.Error("StatusAfterBlock", zap.Error(err))
 		logger.Error("StatusAfterBlock", zap.Error(err))
@@ -322,3 +326,34 @@ func (e *Watcher) Run(ctx context.Context) error {
 		}
 		}
 	}
 	}
 }
 }
+
+// logVersion calls the versions rpc and logs the node version information.
+func (e *Watcher) logVersion(ctx context.Context, logger *zap.Logger, client *algod.Client) {
+	networkName := "algorand"
+
+	// From: https://developer.algorand.org/docs/rest-apis/algod/v2/#get-versions
+	versionRPC := client.Versions()
+	versionResult, err := versionRPC.Do(ctx)
+	if err != nil {
+		logger.Error("problem retrieving node version",
+			zap.Error(err),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	// Marshal the BuildVersions struct into raw json to log cleanly.
+	version, err := json.Marshal(&versionResult.Build)
+	if err != nil {
+		logger.Error("problem retrieving node version when marshalling build info",
+			zap.Error(err),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	logger.Info("node version",
+		zap.String("network", networkName),
+		zap.String("version", string(version)),
+	)
+}

+ 42 - 0
node/pkg/watchers/aptos/watcher.go

@@ -89,6 +89,9 @@ func (e *Watcher) Run(ctx context.Context) error {
 		zap.String("handle", e.aptosHandle),
 		zap.String("handle", e.aptosHandle),
 	)
 	)
 
 
+	// Get the node version for troubleshooting
+	e.logVersion(logger)
+
 	// SECURITY: the API guarantees that we only get the events from the right
 	// SECURITY: the API guarantees that we only get the events from the right
 	// contract
 	// contract
 	var eventsEndpoint = fmt.Sprintf(`%s/v1/accounts/%s/events/%s/event`, e.aptosRPC, e.aptosAccount, e.aptosHandle)
 	var eventsEndpoint = fmt.Sprintf(`%s/v1/accounts/%s/events/%s/event`, e.aptosRPC, e.aptosAccount, e.aptosHandle)
@@ -376,3 +379,42 @@ func (e *Watcher) observeData(logger *zap.Logger, data gjson.Result, nativeSeq u
 
 
 	e.msgC <- observation //nolint:channelcheck // The channel to the processor is buffered and shared across chains, if it backs up we should stop processing new observations
 	e.msgC <- observation //nolint:channelcheck // The channel to the processor is buffered and shared across chains, if it backs up we should stop processing new observations
 }
 }
+
+// logVersion retrieves the Aptos node version and logs it
+func (e *Watcher) logVersion(logger *zap.Logger) {
+	// From https://www.alchemy.com/docs/node/aptos/aptos-api-endpoints/aptos-api-endpoints/v-1
+	networkName := "aptos"
+	versionsEndpoint := fmt.Sprintf("%s/v1", e.aptosRPC)
+
+	body, err := e.retrievePayload(versionsEndpoint)
+	if err != nil {
+		logger.Error("problem retrieving node version",
+			zap.Error(err),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	if !gjson.Valid(string(body)) {
+		logger.Error("problem retrieving node version",
+			zap.String("invalid json", string(body)),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	version := gjson.GetBytes(body, "git_hash").String()
+
+	if version == "" {
+		logger.Error("problem retrieving node version",
+			zap.String("empty version", version),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	logger.Info("node version",
+		zap.String("network", networkName),
+		zap.String("version", version),
+	)
+}

+ 87 - 26
node/pkg/watchers/cosmwasm/watcher.go

@@ -62,6 +62,9 @@ type (
 
 
 		// b64Encoded indicates if transactions are base 64 encoded.
 		// b64Encoded indicates if transactions are base 64 encoded.
 		b64Encoded bool
 		b64Encoded bool
+
+		// Human readable chain name
+		networkName string
 	}
 	}
 )
 )
 
 
@@ -122,6 +125,9 @@ func NewWatcher(
 	// Terra2 no longer base64 encodes parameters.
 	// Terra2 no longer base64 encodes parameters.
 	b64Encoded := env == common.UnsafeDevNet || (chainID != vaa.ChainIDInjective && chainID != vaa.ChainIDTerra2 && chainID != vaa.ChainIDTerra)
 	b64Encoded := env == common.UnsafeDevNet || (chainID != vaa.ChainIDInjective && chainID != vaa.ChainIDTerra2 && chainID != vaa.ChainIDTerra)
 
 
+	// Human readable network name
+	networkName := vaa.ChainID(chainID).String()
+
 	return &Watcher{
 	return &Watcher{
 		urlWS:                    urlWS,
 		urlWS:                    urlWS,
 		urlLCD:                   urlLCD,
 		urlLCD:                   urlLCD,
@@ -134,12 +140,11 @@ func NewWatcher(
 		contractAddressLogKey:    contractAddressLogKey,
 		contractAddressLogKey:    contractAddressLogKey,
 		latestBlockURL:           latestBlockURL,
 		latestBlockURL:           latestBlockURL,
 		b64Encoded:               b64Encoded,
 		b64Encoded:               b64Encoded,
+		networkName:              networkName,
 	}
 	}
 }
 }
 
 
 func (e *Watcher) Run(ctx context.Context) error {
 func (e *Watcher) Run(ctx context.Context) error {
-	networkName := e.chainID.String()
-
 	p2p.DefaultRegistry.SetNetworkStats(e.chainID, &gossipv1.Heartbeat_Network{
 	p2p.DefaultRegistry.SetNetworkStats(e.chainID, &gossipv1.Heartbeat_Network{
 		ContractAddress: e.contract,
 		ContractAddress: e.contract,
 	})
 	})
@@ -155,17 +160,19 @@ func (e *Watcher) Run(ctx context.Context) error {
 		zap.String("chainID", e.chainID.String()),
 		zap.String("chainID", e.chainID.String()),
 	)
 	)
 
 
-	logger.Info("connecting to websocket", zap.String("network", networkName), zap.String("url", e.urlWS))
+	logger.Info("connecting to websocket", zap.String("network", e.networkName), zap.String("url", e.urlWS))
 
 
 	//nolint:bodyclose // The close is down below. The linter misses it.
 	//nolint:bodyclose // The close is down below. The linter misses it.
 	c, _, err := websocket.Dial(ctx, e.urlWS, nil)
 	c, _, err := websocket.Dial(ctx, e.urlWS, nil)
 	if err != nil {
 	if err != nil {
 		p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
 		p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
-		connectionErrors.WithLabelValues(networkName, "websocket_dial_error").Inc()
+		connectionErrors.WithLabelValues(e.networkName, "websocket_dial_error").Inc()
 		return fmt.Errorf("websocket dial failed: %w", err)
 		return fmt.Errorf("websocket dial failed: %w", err)
 	}
 	}
 	defer c.Close(websocket.StatusNormalClosure, "")
 	defer c.Close(websocket.StatusNormalClosure, "")
 
 
+	e.logVersion(ctx, logger, c)
+
 	c.SetReadLimit(ReadLimitSize)
 	c.SetReadLimit(ReadLimitSize)
 
 
 	// Subscribe to smart contract transactions
 	// Subscribe to smart contract transactions
@@ -179,7 +186,7 @@ func (e *Watcher) Run(ctx context.Context) error {
 	err = wsjson.Write(ctx, c, command)
 	err = wsjson.Write(ctx, c, command)
 	if err != nil {
 	if err != nil {
 		p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
 		p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
-		connectionErrors.WithLabelValues(networkName, "websocket_subscription_error").Inc()
+		connectionErrors.WithLabelValues(e.networkName, "websocket_subscription_error").Inc()
 		return fmt.Errorf("websocket subscription failed: %w", err)
 		return fmt.Errorf("websocket subscription failed: %w", err)
 	}
 	}
 
 
@@ -187,10 +194,10 @@ func (e *Watcher) Run(ctx context.Context) error {
 	_, _, err = c.Read(ctx)
 	_, _, err = c.Read(ctx)
 	if err != nil {
 	if err != nil {
 		p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
 		p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
-		connectionErrors.WithLabelValues(networkName, "event_subscription_error").Inc()
+		connectionErrors.WithLabelValues(e.networkName, "event_subscription_error").Inc()
 		return fmt.Errorf("event subscription failed: %w", err)
 		return fmt.Errorf("event subscription failed: %w", err)
 	}
 	}
-	logger.Info("subscribed to new transaction events", zap.String("network", networkName))
+	logger.Info("subscribed to new transaction events", zap.String("network", e.networkName))
 
 
 	readiness.SetReady(e.readinessSync)
 	readiness.SetReady(e.readinessSync)
 
 
@@ -209,12 +216,12 @@ func (e *Watcher) Run(ctx context.Context) error {
 				// Query and report height and set currentSlotHeight
 				// Query and report height and set currentSlotHeight
 				resp, err := client.Get(fmt.Sprintf("%s/%s", e.urlLCD, e.latestBlockURL)) //nolint:noctx // TODO FIXME we should propagate context with Deadline here.
 				resp, err := client.Get(fmt.Sprintf("%s/%s", e.urlLCD, e.latestBlockURL)) //nolint:noctx // TODO FIXME we should propagate context with Deadline here.
 				if err != nil {
 				if err != nil {
-					logger.Error("query latest block response error", zap.String("network", networkName), zap.Error(err))
+					logger.Error("query latest block response error", zap.String("network", e.networkName), zap.Error(err))
 					continue
 					continue
 				}
 				}
 				blocksBody, err := common.SafeRead(resp.Body)
 				blocksBody, err := common.SafeRead(resp.Body)
 				if err != nil {
 				if err != nil {
-					logger.Error("query latest block response read error", zap.String("network", networkName), zap.Error(err))
+					logger.Error("query latest block response read error", zap.String("network", e.networkName), zap.Error(err))
 					errC <- err //nolint:channelcheck // The watcher will exit anyway
 					errC <- err //nolint:channelcheck // The watcher will exit anyway
 					resp.Body.Close()
 					resp.Body.Close()
 					continue
 					continue
@@ -222,12 +229,12 @@ func (e *Watcher) Run(ctx context.Context) error {
 				resp.Body.Close()
 				resp.Body.Close()
 
 
 				// Update the prom metrics with how long the http request took to the rpc
 				// Update the prom metrics with how long the http request took to the rpc
-				queryLatency.WithLabelValues(networkName, "block_latest").Observe(time.Since(msm).Seconds())
+				queryLatency.WithLabelValues(e.networkName, "block_latest").Observe(time.Since(msm).Seconds())
 
 
 				blockJSON := string(blocksBody)
 				blockJSON := string(blocksBody)
 				latestBlock := gjson.Get(blockJSON, "block.header.height")
 				latestBlock := gjson.Get(blockJSON, "block.header.height")
-				logger.Debug("current height", zap.String("network", networkName), zap.Int64("block", latestBlock.Int()))
-				currentSlotHeight.WithLabelValues(networkName).Set(float64(latestBlock.Int()))
+				logger.Debug("current height", zap.String("network", e.networkName), zap.Int64("block", latestBlock.Int()))
+				currentSlotHeight.WithLabelValues(e.networkName).Set(float64(latestBlock.Int()))
 				p2p.DefaultRegistry.SetNetworkStats(e.chainID, &gossipv1.Heartbeat_Network{
 				p2p.DefaultRegistry.SetNetworkStats(e.chainID, &gossipv1.Heartbeat_Network{
 					Height:          latestBlock.Int(),
 					Height:          latestBlock.Int(),
 					ContractAddress: e.contract,
 					ContractAddress: e.contract,
@@ -253,7 +260,7 @@ func (e *Watcher) Run(ctx context.Context) error {
 
 
 				tx := hex.EncodeToString(r.TxHash)
 				tx := hex.EncodeToString(r.TxHash)
 
 
-				logger.Info("received observation request", zap.String("network", networkName), zap.String("tx_hash", tx))
+				logger.Info("received observation request", zap.String("network", e.networkName), zap.String("tx_hash", tx))
 
 
 				client := &http.Client{
 				client := &http.Client{
 					Timeout: time.Second * 5,
 					Timeout: time.Second * 5,
@@ -262,12 +269,12 @@ func (e *Watcher) Run(ctx context.Context) error {
 				// Query for tx by hash
 				// Query for tx by hash
 				resp, err := client.Get(fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", e.urlLCD, tx)) //nolint:noctx // TODO FIXME we should propagate context with Deadline here.
 				resp, err := client.Get(fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", e.urlLCD, tx)) //nolint:noctx // TODO FIXME we should propagate context with Deadline here.
 				if err != nil {
 				if err != nil {
-					logger.Error("query tx response error", zap.String("network", networkName), zap.Error(err))
+					logger.Error("query tx response error", zap.String("network", e.networkName), zap.Error(err))
 					continue
 					continue
 				}
 				}
 				txBody, err := common.SafeRead(resp.Body)
 				txBody, err := common.SafeRead(resp.Body)
 				if err != nil {
 				if err != nil {
-					logger.Error("query tx response read error", zap.String("network", networkName), zap.Error(err))
+					logger.Error("query tx response read error", zap.String("network", e.networkName), zap.Error(err))
 					resp.Body.Close()
 					resp.Body.Close()
 					continue
 					continue
 				}
 				}
@@ -277,14 +284,14 @@ func (e *Watcher) Run(ctx context.Context) error {
 
 
 				txHashRaw := gjson.Get(txJSON, "tx_response.txhash")
 				txHashRaw := gjson.Get(txJSON, "tx_response.txhash")
 				if !txHashRaw.Exists() {
 				if !txHashRaw.Exists() {
-					logger.Error("tx does not have tx hash", zap.String("network", networkName), zap.String("payload", txJSON))
+					logger.Error("tx does not have tx hash", zap.String("network", e.networkName), zap.String("payload", txJSON))
 					continue
 					continue
 				}
 				}
 				txHash := txHashRaw.String()
 				txHash := txHashRaw.String()
 
 
 				events := gjson.Get(txJSON, "tx_response.events")
 				events := gjson.Get(txJSON, "tx_response.events")
 				if !events.Exists() {
 				if !events.Exists() {
-					logger.Error("tx has no events", zap.String("network", networkName), zap.String("payload", txJSON))
+					logger.Error("tx has no events", zap.String("network", e.networkName), zap.String("payload", txJSON))
 					continue
 					continue
 				}
 				}
 
 
@@ -293,12 +300,12 @@ func (e *Watcher) Run(ctx context.Context) error {
 					// Terra Classic upgraded WASM versions starting at block 13215800. If this transaction is from before that, we need to use the old contract address format.
 					// Terra Classic upgraded WASM versions starting at block 13215800. If this transaction is from before that, we need to use the old contract address format.
 					blockHeightStr := gjson.Get(txJSON, "tx_response.height")
 					blockHeightStr := gjson.Get(txJSON, "tx_response.height")
 					if !blockHeightStr.Exists() {
 					if !blockHeightStr.Exists() {
-						logger.Error("failed to look up block height on old reobserved tx", zap.String("network", networkName), zap.String("txHash", txHash), zap.String("payload", txJSON))
+						logger.Error("failed to look up block height on old reobserved tx", zap.String("network", e.networkName), zap.String("txHash", txHash), zap.String("payload", txJSON))
 						continue
 						continue
 					}
 					}
 					blockHeight := blockHeightStr.Int()
 					blockHeight := blockHeightStr.Int()
 					if blockHeight < 13215800 {
 					if blockHeight < 13215800 {
-						logger.Info("doing look up of old tx", zap.String("network", networkName), zap.String("txHash", txHash), zap.Int64("blockHeight", blockHeight))
+						logger.Info("doing look up of old tx", zap.String("network", e.networkName), zap.String("txHash", txHash), zap.Int64("blockHeight", blockHeight))
 						contractAddressLogKey = "contract_address"
 						contractAddressLogKey = "contract_address"
 					}
 					}
 				}
 				}
@@ -307,8 +314,8 @@ func (e *Watcher) Run(ctx context.Context) error {
 				for _, msg := range msgs {
 				for _, msg := range msgs {
 					msg.IsReobservation = true
 					msg.IsReobservation = true
 					e.msgC <- msg //nolint:channelcheck // The channel to the processor is buffered and shared across chains, if it backs up we should stop processing new observations
 					e.msgC <- msg //nolint:channelcheck // The channel to the processor is buffered and shared across chains, if it backs up we should stop processing new observations
-					messagesConfirmed.WithLabelValues(networkName).Inc()
-					watchers.ReobservationsByChain.WithLabelValues(networkName, "std").Inc()
+					messagesConfirmed.WithLabelValues(e.networkName).Inc()
+					watchers.ReobservationsByChain.WithLabelValues(e.networkName, "std").Inc()
 				}
 				}
 			}
 			}
 		}
 		}
@@ -323,8 +330,8 @@ func (e *Watcher) Run(ctx context.Context) error {
 				_, message, err := c.Read(ctx)
 				_, message, err := c.Read(ctx)
 				if err != nil {
 				if err != nil {
 					p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
 					p2p.DefaultRegistry.AddErrorCount(e.chainID, 1)
-					connectionErrors.WithLabelValues(networkName, "channel_read_error").Inc()
-					logger.Error("error reading channel", zap.String("network", networkName), zap.Error(err))
+					connectionErrors.WithLabelValues(e.networkName, "channel_read_error").Inc()
+					logger.Error("error reading channel", zap.String("network", e.networkName), zap.Error(err))
 					errC <- err //nolint:channelcheck // The watcher will exit anyway
 					errC <- err //nolint:channelcheck // The watcher will exit anyway
 					return nil
 					return nil
 				}
 				}
@@ -334,21 +341,21 @@ func (e *Watcher) Run(ctx context.Context) error {
 
 
 				txHashRaw := gjson.Get(json, "result.events.tx\\.hash.0")
 				txHashRaw := gjson.Get(json, "result.events.tx\\.hash.0")
 				if !txHashRaw.Exists() {
 				if !txHashRaw.Exists() {
-					logger.Warn("message does not have tx hash", zap.String("network", networkName), zap.String("payload", json))
+					logger.Warn("message does not have tx hash", zap.String("network", e.networkName), zap.String("payload", json))
 					continue
 					continue
 				}
 				}
 				txHash := txHashRaw.String()
 				txHash := txHashRaw.String()
 
 
 				events := gjson.Get(json, "result.data.value.TxResult.result.events")
 				events := gjson.Get(json, "result.data.value.TxResult.result.events")
 				if !events.Exists() {
 				if !events.Exists() {
-					logger.Warn("message has no events", zap.String("network", networkName), zap.String("payload", json))
+					logger.Warn("message has no events", zap.String("network", e.networkName), zap.String("payload", json))
 					continue
 					continue
 				}
 				}
 
 
 				msgs := EventsToMessagePublications(e.contract, txHash, events.Array(), logger, e.chainID, e.contractAddressLogKey, e.b64Encoded)
 				msgs := EventsToMessagePublications(e.contract, txHash, events.Array(), logger, e.chainID, e.contractAddressLogKey, e.b64Encoded)
 				for _, msg := range msgs {
 				for _, msg := range msgs {
 					e.msgC <- msg //nolint:channelcheck // The channel to the processor is buffered and shared across chains, if it backs up we should stop processing new observations
 					e.msgC <- msg //nolint:channelcheck // The channel to the processor is buffered and shared across chains, if it backs up we should stop processing new observations
-					messagesConfirmed.WithLabelValues(networkName).Inc()
+					messagesConfirmed.WithLabelValues(e.networkName).Inc()
 				}
 				}
 
 
 				// We do not send guardian changes to the processor - ETH guardians are the source of truth.
 				// We do not send guardian changes to the processor - ETH guardians are the source of truth.
@@ -364,6 +371,60 @@ func (e *Watcher) Run(ctx context.Context) error {
 	}
 	}
 }
 }
 
 
+// logVersion uses the abci_info rpc to log node version information.
+func (e *Watcher) logVersion(ctx context.Context, logger *zap.Logger, c *websocket.Conn) {
+	// NOTE: This function is ugly because this watcher doesn't use a
+	//       client library. It can be rewritten in a followup change.
+	//
+	// Get information about the application (the /status endpoint returns the
+	// version of the tendermint or cometbft library, no the actual application
+	// version.
+	//
+	// From:
+	//    https://docs.cometbft.com/v0.34/rpc/#/ABCI/abci_info
+	//    https://docs.tendermint.com/v0.34/rpc/#/ABCI/abci_info
+	command := map[string]interface{}{
+		"jsonrpc": "2.0",
+		"method":  "abci_info",
+		"params":  []interface{}{},
+		"id":      1,
+	}
+
+	err := wsjson.Write(ctx, c, command)
+	if err != nil {
+		logger.Error("problem retrieving node version when building request",
+			zap.String("network", e.networkName),
+			zap.Error(err),
+		)
+		return
+	}
+
+	// Wait for the success response
+	_, data, err := c.Read(ctx)
+	if err != nil {
+		logger.Error("problem retrieving node version",
+			zap.String("network", e.networkName),
+			zap.Error(err),
+		)
+		return
+	}
+
+	version := gjson.GetBytes(data, "result.response.version").String()
+
+	if version == "" {
+		logger.Error("problem retrieving node version due to an empty response version ",
+			zap.String("network", e.networkName),
+			zap.String("response", string(data)),
+		)
+		return
+	}
+
+	logger.Info("node version",
+		zap.String("network", e.networkName),
+		zap.String("version", version),
+	)
+}
+
 func EventsToMessagePublications(contract string, txHash string, events []gjson.Result, logger *zap.Logger, chainID vaa.ChainID, contractAddressKey string, b64Encoded bool) []*common.MessagePublication {
 func EventsToMessagePublications(contract string, txHash string, events []gjson.Result, logger *zap.Logger, chainID vaa.ChainID, contractAddressKey string, b64Encoded bool) []*common.MessagePublication {
 	networkName := chainID.String()
 	networkName := chainID.String()
 	msgs := make([]*common.MessagePublication, 0, len(events))
 	msgs := make([]*common.MessagePublication, 0, len(events))

+ 21 - 0
node/pkg/watchers/evm/watcher.go

@@ -340,6 +340,9 @@ func (w *Watcher) Run(parentCtx context.Context) error {
 		w.ccqTimestampCache = NewBlocksByTimestamp(BTS_MAX_BLOCKS, (w.env == common.UnsafeDevNet))
 		w.ccqTimestampCache = NewBlocksByTimestamp(BTS_MAX_BLOCKS, (w.env == common.UnsafeDevNet))
 	}
 	}
 
 
+	// Get the node version for troubleshooting
+	w.logVersion(ctx, logger)
+
 	errC := make(chan error)
 	errC := make(chan error)
 
 
 	// Subscribe to new message publications. We don't use a timeout here because the LogPollConnector
 	// Subscribe to new message publications. We don't use a timeout here because the LogPollConnector
@@ -1005,6 +1008,24 @@ func (w *Watcher) waitForBlockTime(ctx context.Context, logger *zap.Logger, errC
 	}
 	}
 }
 }
 
 
+// logVersion runs the web3_clientVersion rpc and logs the node version
+func (w *Watcher) logVersion(ctx context.Context, logger *zap.Logger) {
+	// From: https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion
+	var version string
+	if err := w.ethConn.RawCallContext(ctx, &version, "web3_clientVersion"); err != nil {
+		logger.Error("problem retrieving node version",
+			zap.Error(err),
+			zap.String("network", w.networkName),
+		)
+		return
+	}
+
+	logger.Info("node version",
+		zap.String("version", version),
+		zap.String("network", w.networkName),
+	)
+}
+
 // msgIdFromLogEvent formats the message ID (chain/emitterAddress/seqNo) from a log event.
 // msgIdFromLogEvent formats the message ID (chain/emitterAddress/seqNo) from a log event.
 func msgIdFromLogEvent(chainID vaa.ChainID, ev *ethabi.AbiLogMessagePublished) string {
 func msgIdFromLogEvent(chainID vaa.ChainID, ev *ethabi.AbiLogMessagePublished) string {
 	return fmt.Sprintf("%v/%v/%v", uint16(chainID), PadAddress(ev.Sender), ev.Sequence)
 	return fmt.Sprintf("%v/%v/%v", uint16(chainID), PadAddress(ev.Sender), ev.Sequence)

+ 6 - 0
node/pkg/watchers/near/nearapi/mock/mock_server.go

@@ -125,6 +125,12 @@ func (s *ForwardingCachingServer) ServeHTTP(w http.ResponseWriter, req *http.Req
 		return
 		return
 	}
 	}
 
 
+	// Mock the status request to return a version
+	if bytes.Contains(origReqBody, []byte("\"method\": \"status\"")) {
+		_, _ = w.Write([]byte(`{"id": "dontcare", "jsonrpc": "2.0", "result": {"version": "1.0.0"}}`))
+		return
+	}
+
 	reqBody := s.RewriteReq(origReqBody)
 	reqBody := s.RewriteReq(origReqBody)
 	req.Body = io.NopCloser(bytes.NewReader(reqBody))
 	req.Body = io.NopCloser(bytes.NewReader(reqBody))
 
 

+ 16 - 0
node/pkg/watchers/near/nearapi/nearapi.go

@@ -42,6 +42,7 @@ type (
 		GetFinalBlock(ctx context.Context) (Block, error)
 		GetFinalBlock(ctx context.Context) (Block, error)
 		GetChunk(ctx context.Context, chunkHeader ChunkHeader) (Chunk, error)
 		GetChunk(ctx context.Context, chunkHeader ChunkHeader) (Chunk, error)
 		GetTxStatus(ctx context.Context, txHash string, senderAccountId string) ([]byte, error)
 		GetTxStatus(ctx context.Context, txHash string, senderAccountId string) ([]byte, error)
+		GetVersion(ctx context.Context) (string, error)
 	}
 	}
 	NearApiImpl struct {
 	NearApiImpl struct {
 		nearRPC NearRpc
 		nearRPC NearRpc
@@ -176,6 +177,21 @@ func (n NearApiImpl) GetTxStatus(ctx context.Context, txHash string, senderAccou
 	return n.nearRPC.Query(ctx, s)
 	return n.nearRPC.Query(ctx, s)
 }
 }
 
 
+func (n NearApiImpl) GetVersion(ctx context.Context) (string, error) {
+	s := `{"id": "dontcare", "jsonrpc": "2.0", "method": "status"}`
+	versionBytes, err := n.nearRPC.Query(ctx, s)
+	if err != nil {
+		return "", err
+	}
+
+	version, err := VersionFromBytes(versionBytes)
+	if err != nil {
+		return "", err
+	}
+
+	return version, nil
+}
+
 func IsWellFormedHash(hash string) error {
 func IsWellFormedHash(hash string) error {
 	hashBytes, err := base58.Decode(hash)
 	hashBytes, err := base58.Decode(hash)
 	if err != nil {
 	if err != nil {

+ 16 - 0
node/pkg/watchers/near/nearapi/types.go

@@ -82,6 +82,22 @@ func NewBlockFromBytes(bytes []byte) (Block, error) {
 	}, nil
 	}, nil
 }
 }
 
 
+func VersionFromBytes(bytes []byte) (string, error) {
+	if !gjson.ValidBytes(bytes) {
+		return "", errors.New("invalid json")
+	}
+
+	json := gjson.ParseBytes(bytes)
+
+	version := jsonGetString(json, "result.version")
+
+	if version == "" {
+		return "", errors.New("invalid json")
+	}
+
+	return version, nil
+}
+
 func (b Block) Timestamp() uint64 {
 func (b Block) Timestamp() uint64 {
 	ts_nanosec := jsonGetUint(b.json, "result.header.timestamp")
 	ts_nanosec := jsonGetUint(b.json, "result.header.timestamp")
 	return ts_nanosec / 1000000000
 	return ts_nanosec / 1000000000

+ 22 - 0
node/pkg/watchers/near/watcher.go

@@ -292,6 +292,9 @@ func (e *Watcher) Run(ctx context.Context) error {
 		ContractAddress: e.wormholeAccount,
 		ContractAddress: e.wormholeAccount,
 	})
 	})
 
 
+	// Get the node version for troubleshooting
+	e.logVersion(ctx, logger)
+
 	logger.Info("Near watcher connecting to RPC node ", zap.String("url", e.nearRPC))
 	logger.Info("Near watcher connecting to RPC node ", zap.String("url", e.nearRPC))
 
 
 	// start metrics reporter
 	// start metrics reporter
@@ -350,3 +353,22 @@ func (e *Watcher) schedule(ctx context.Context, job *transactionProcessingJob, d
 		})
 		})
 	return nil
 	return nil
 }
 }
+
+// logVersion retrieves the NEAR node version and logs it
+func (e *Watcher) logVersion(ctx context.Context, logger *zap.Logger) {
+	// From: https://www.quicknode.com/docs/near/status
+	networkName := "near"
+	version, err := e.nearAPI.GetVersion(ctx)
+	if err != nil {
+		logger.Error("problem retrieving node version",
+			zap.Error(err),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	logger.Info("node version",
+		zap.String("version", version),
+		zap.String("network", networkName),
+	)
+}

+ 21 - 0
node/pkg/watchers/solana/client.go

@@ -402,6 +402,9 @@ func (s *SolanaWatcher) Run(ctx context.Context) error {
 		}
 		}
 	}
 	}
 
 
+	// Get the node version for troubleshooting
+	s.logVersion(ctx, logger)
+
 	common.RunWithScissors(ctx, s.errC, "SolanaWatcher", func(ctx context.Context) error {
 	common.RunWithScissors(ctx, s.errC, "SolanaWatcher", func(ctx context.Context) error {
 		timer := time.NewTicker(pollInterval)
 		timer := time.NewTicker(pollInterval)
 		defer timer.Stop()
 		defer timer.Stop()
@@ -1170,6 +1173,24 @@ func (s *SolanaWatcher) checkCommitment(commitment rpc.CommitmentType, isReobser
 	return true
 	return true
 }
 }
 
 
+// logVersion runs the getVersion rpc and logs the node version.
+func (s *SolanaWatcher) logVersion(ctx context.Context, logger *zap.Logger) {
+	// From: https://docs.solana.com/api/http#getversion
+	v, err := s.rpcClient.GetVersion(ctx)
+	if err != nil {
+		logger.Error("problem retrieving node version",
+			zap.Error(err),
+			zap.String("network", s.networkName),
+		)
+		return
+	}
+	logger.Info("node version",
+		zap.String("network", s.networkName),
+		zap.Int64("feature_set", v.FeatureSet),
+		zap.String("version", v.SolanaCore),
+	)
+}
+
 // isPossibleWormholeMessage searches the logs on a transaction to see if it contains a Wormhole core PostMessage.
 // isPossibleWormholeMessage searches the logs on a transaction to see if it contains a Wormhole core PostMessage.
 // It looks for a log for the core contract, followed by one for the sequence number.
 // It looks for a log for the core contract, followed by one for the sequence number.
 func isPossibleWormholeMessage(whLogPrefix string, logMessages []string) bool {
 func isPossibleWormholeMessage(whLogPrefix string, logMessages []string) bool {

+ 43 - 0
node/pkg/watchers/sui/watcher.go

@@ -309,6 +309,9 @@ func (e *Watcher) Run(ctx context.Context) error {
 		zap.Bool("unsafeDevMode", e.unsafeDevMode),
 		zap.Bool("unsafeDevMode", e.unsafeDevMode),
 	)
 	)
 
 
+	// Get the node version for troubleshooting
+	e.logVersion(ctx, logger)
+
 	// Get the latest checkpoint sequence number.  This will be the starting point for the watcher.
 	// Get the latest checkpoint sequence number.  This will be the starting point for the watcher.
 	latest, err := e.getLatestCheckpointSN(ctx, logger)
 	latest, err := e.getLatestCheckpointSN(ctx, logger)
 	if err != nil {
 	if err != nil {
@@ -647,3 +650,43 @@ func (w *Watcher) createAndExecReq(ctx context.Context, payload string) ([]byte,
 	resp.Body.Close()
 	resp.Body.Close()
 	return body, nil
 	return body, nil
 }
 }
+
+// logVersion retrieves the Sui protocol version and logs it
+func (w *Watcher) logVersion(ctx context.Context, logger *zap.Logger) {
+	// We can't get the exact build, but we can get the protocol version.
+	// From: https://www.quicknode.com/docs/sui/suix_getLatestSuiSystemState
+	networkName := "sui"
+	payload := `{"jsonrpc":"2.0", "id": 1, "method": "suix_getLatestSuiSystemState", "params": []}`
+
+	type getLatestSuiSystemStateResponse struct {
+		Jsonrpc string `json:"jsonrpc"`
+		Result  struct {
+			ProtocolVersion string `json:"protocolVersion"`
+		} `json:"result"`
+		ID int `json:"id"`
+	}
+	var result getLatestSuiSystemStateResponse
+
+	body, err := w.createAndExecReq(ctx, payload)
+	if err != nil {
+		logger.Error("problem retrieving node version",
+			zap.Error(err),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	err = json.Unmarshal(body, &result)
+	if err != nil {
+		logger.Error("problem retrieving node version",
+			zap.Error(err),
+			zap.String("network", networkName),
+		)
+		return
+	}
+
+	logger.Info("node version",
+		zap.String("version", result.Result.ProtocolVersion),
+		zap.String("network", "sui"),
+	)
+}