adminnodes.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package guardiand
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "math"
  7. "os"
  8. "sort"
  9. "strings"
  10. "text/tabwriter"
  11. "time"
  12. publicrpcv1 "github.com/certusone/wormhole/node/pkg/proto/publicrpc/v1"
  13. "github.com/spf13/cobra"
  14. "github.com/wormhole-foundation/wormhole/sdk/vaa"
  15. )
  16. // How to test in container:
  17. // kubectl exec guardian-0 -- /guardiand admin list-nodes --socket /tmp/admin.sock
  18. var (
  19. showDetails bool
  20. only []string
  21. )
  22. func init() {
  23. AdminClientListNodes.Flags().BoolVar(&showDetails, "showDetails", false, "Show error counter and contract addresses")
  24. AdminClientListNodes.Flags().StringSliceVar(&only, "only", nil, "Show only networks with the given name")
  25. }
  26. var AdminClientListNodes = &cobra.Command{
  27. Use: "list-nodes",
  28. Short: "Fetches an aggregated list of guardian nodes",
  29. Run: runListNodes,
  30. }
  31. func runListNodes(cmd *cobra.Command, args []string) {
  32. ctx := context.Background()
  33. conn, c, err := getPublicRPCServiceClient(ctx, *clientSocketPath)
  34. if err != nil {
  35. log.Fatalf("failed to get publicrpc client: %v", err)
  36. }
  37. defer conn.Close()
  38. lastHeartbeats, err := c.GetLastHeartbeats(ctx, &publicrpcv1.GetLastHeartbeatsRequest{})
  39. if err != nil {
  40. log.Fatalf("failed to list nodes: %v", err)
  41. }
  42. gs, err := c.GetCurrentGuardianSet(ctx, &publicrpcv1.GetCurrentGuardianSetRequest{})
  43. if err != nil {
  44. log.Fatalf("failed to list current guardian get: %v", err)
  45. }
  46. log.Printf("current guardian set index: %d (%d guardians)",
  47. gs.GuardianSet.Index, len(gs.GuardianSet.Addresses))
  48. nodes := lastHeartbeats.Entries
  49. sort.Slice(nodes, func(i, j int) bool {
  50. if nodes[i].RawHeartbeat == nil || nodes[j].RawHeartbeat == nil {
  51. return false
  52. }
  53. return nodes[i].RawHeartbeat.NodeName < nodes[j].RawHeartbeat.NodeName
  54. })
  55. log.Printf("%d nodes in guardian state set", len(nodes))
  56. w := tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', 0)
  57. headers := []string{
  58. "Node key",
  59. "Guardian key",
  60. "Node name",
  61. "Version",
  62. "Last seen",
  63. }
  64. if showDetails {
  65. headers = append(headers, "Uptime")
  66. }
  67. type network struct {
  68. string
  69. vaa.ChainID
  70. }
  71. // NOTE: Please keep these in numerical order by chain ID.
  72. networks := []network{
  73. {"Solana", vaa.ChainIDSolana},
  74. {"Ethereum", vaa.ChainIDEthereum},
  75. {"BSC", vaa.ChainIDBSC},
  76. {"Polygon", vaa.ChainIDPolygon},
  77. {"Avalanche", vaa.ChainIDAvalanche},
  78. {"Algorand", vaa.ChainIDAlgorand},
  79. {"Fantom", vaa.ChainIDFantom},
  80. {"Klaytn", vaa.ChainIDKlaytn},
  81. {"Celo", vaa.ChainIDCelo},
  82. {"Near", vaa.ChainIDNear},
  83. {"Moonbeam", vaa.ChainIDMoonbeam},
  84. {"Injective", vaa.ChainIDInjective},
  85. // Osmosis is not supported in the guardian.
  86. {"Sui", vaa.ChainIDSui},
  87. {"Aptos", vaa.ChainIDAptos},
  88. {"Arbitrum", vaa.ChainIDArbitrum},
  89. {"Optimism", vaa.ChainIDOptimism},
  90. // Gnosis is not supported in the guardian.
  91. {"Pythnet", vaa.ChainIDPythNet},
  92. {"Btc", vaa.ChainIDBtc},
  93. {"Base", vaa.ChainIDBase},
  94. // Filecoin is not supported in the guardian.
  95. {"Sei", vaa.ChainIDSei},
  96. // Rootstock is not supported in the guardian.
  97. {"Scroll", vaa.ChainIDScroll},
  98. {"Mantle", vaa.ChainIDMantle},
  99. {"XLayer", vaa.ChainIDXLayer},
  100. {"Linea", vaa.ChainIDLinea},
  101. {"Berachain", vaa.ChainIDBerachain},
  102. {"SeiEVM", vaa.ChainIDSeiEVM},
  103. {"Unichain", vaa.ChainIDUnichain},
  104. {"Worldchain", vaa.ChainIDWorldchain},
  105. {"Ink", vaa.ChainIDInk},
  106. {"HyperEVM", vaa.ChainIDHyperEVM},
  107. {"Monad", vaa.ChainIDMonad},
  108. {"Movement", vaa.ChainIDMovement},
  109. {"Wormchain", vaa.ChainIDWormchain},
  110. {"Mezo", vaa.ChainIDMezo},
  111. {"Fogo", vaa.ChainIDFogo},
  112. {"Converge", vaa.ChainIDConverge},
  113. {"Plume", vaa.ChainIDPlume},
  114. {"XRPLEVM", vaa.ChainIDXRPLEVM},
  115. {"Plasma", vaa.ChainIDPlasma},
  116. {"CreditCoin", vaa.ChainIDCreditCoin},
  117. {"Moca", vaa.ChainIDMoca},
  118. // The IBC chains (4000 range) are not included here.
  119. {"Sepolia", vaa.ChainIDSepolia},
  120. {"ArbitrumSepolia", vaa.ChainIDArbitrumSepolia},
  121. {"BaseSepolia", vaa.ChainIDBaseSepolia},
  122. {"OptimismSepolia", vaa.ChainIDOptimismSepolia},
  123. {"Holesky", vaa.ChainIDHolesky},
  124. {"PolygonSepolia", vaa.ChainIDPolygonSepolia},
  125. }
  126. if len(only) > 0 {
  127. var filtered []network
  128. for _, network := range networks {
  129. for _, name := range only {
  130. if strings.EqualFold(network.string, name) {
  131. filtered = append(filtered, network)
  132. }
  133. }
  134. }
  135. networks = filtered
  136. }
  137. for _, k := range networks {
  138. headers = append(headers, k.string)
  139. }
  140. for _, header := range headers {
  141. _, _ = fmt.Fprintf(w, "%s\t", header)
  142. }
  143. _, _ = fmt.Fprintln(w)
  144. for _, h := range nodes {
  145. if h.RawHeartbeat == nil {
  146. continue
  147. }
  148. last := time.Unix(0, h.RawHeartbeat.Timestamp)
  149. boot := time.Unix(0, h.RawHeartbeat.BootTimestamp)
  150. heights := map[vaa.ChainID]int64{}
  151. truncAddrs := make(map[vaa.ChainID]string)
  152. errors := map[vaa.ChainID]uint64{}
  153. for _, n := range h.RawHeartbeat.Networks {
  154. if n.Id > math.MaxUint16 {
  155. log.Fatalf("heartbeat chain id is greater than MaxUint16: %v", n.Id)
  156. }
  157. heights[vaa.ChainID(n.Id)] = n.Height
  158. errors[vaa.ChainID(n.Id)] = n.ErrorCount
  159. if len(n.ContractAddress) >= 16 {
  160. truncAddrs[vaa.ChainID(n.Id)] = n.ContractAddress[:16]
  161. } else {
  162. truncAddrs[vaa.ChainID(n.Id)] = "INVALID"
  163. }
  164. }
  165. fields := []string{
  166. h.P2PNodeAddr,
  167. h.RawHeartbeat.GuardianAddr,
  168. h.RawHeartbeat.NodeName,
  169. h.RawHeartbeat.Version,
  170. time.Since(last).String(),
  171. }
  172. if showDetails {
  173. fields = append(fields, time.Since(boot).String())
  174. }
  175. for _, n := range networks {
  176. if showDetails {
  177. fields = append(fields, fmt.Sprintf("%s %d (%d)",
  178. truncAddrs[n.ChainID], heights[n.ChainID], errors[n.ChainID]))
  179. } else {
  180. fields = append(fields, fmt.Sprintf("%d", heights[n.ChainID]))
  181. }
  182. }
  183. for _, field := range fields {
  184. _, _ = fmt.Fprintf(w, "%s\t", field)
  185. }
  186. _, _ = fmt.Fprintln(w)
  187. }
  188. w.Flush()
  189. fmt.Print("\n")
  190. for _, addr := range gs.GuardianSet.Addresses {
  191. var found bool
  192. for _, h := range nodes {
  193. if h.VerifiedGuardianAddr == addr {
  194. found = true
  195. }
  196. }
  197. if !found {
  198. fmt.Printf("Missing guardian: %s\n", addr)
  199. }
  200. }
  201. fmt.Println("\n[do not parse - use the gRPC or REST API for scripting]")
  202. }