223 lines
5.5 KiB
Go
223 lines
5.5 KiB
Go
package guardiand
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
publicrpcv1 "github.com/certusone/wormhole/node/pkg/proto/publicrpc/v1"
|
|
"github.com/spf13/cobra"
|
|
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
)
|
|
|
|
// How to test in container:
|
|
// kubectl exec guardian-0 -- /guardiand admin list-nodes --socket /tmp/admin.sock
|
|
|
|
var (
|
|
showDetails bool
|
|
only []string
|
|
)
|
|
|
|
func init() {
|
|
AdminClientListNodes.Flags().BoolVar(&showDetails, "showDetails", false, "Show error counter and contract addresses")
|
|
AdminClientListNodes.Flags().StringSliceVar(&only, "only", nil, "Show only networks with the given name")
|
|
}
|
|
|
|
var AdminClientListNodes = &cobra.Command{
|
|
Use: "list-nodes",
|
|
Short: "Fetches an aggregated list of guardian nodes",
|
|
Run: runListNodes,
|
|
}
|
|
|
|
func runListNodes(cmd *cobra.Command, args []string) {
|
|
ctx := context.Background()
|
|
conn, c, err := getPublicRPCServiceClient(ctx, *clientSocketPath)
|
|
if err != nil {
|
|
log.Fatalf("failed to get publicrpc client: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
lastHeartbeats, err := c.GetLastHeartbeats(ctx, &publicrpcv1.GetLastHeartbeatsRequest{})
|
|
if err != nil {
|
|
log.Fatalf("failed to list nodes: %v", err)
|
|
}
|
|
|
|
gs, err := c.GetCurrentGuardianSet(ctx, &publicrpcv1.GetCurrentGuardianSetRequest{})
|
|
if err != nil {
|
|
log.Fatalf("failed to list current guardian get: %v", err)
|
|
}
|
|
|
|
log.Printf("current guardian set index: %d (%d guardians)",
|
|
gs.GuardianSet.Index, len(gs.GuardianSet.Addresses))
|
|
|
|
nodes := lastHeartbeats.Entries
|
|
|
|
sort.Slice(nodes, func(i, j int) bool {
|
|
if nodes[i].RawHeartbeat == nil || nodes[j].RawHeartbeat == nil {
|
|
return false
|
|
}
|
|
return nodes[i].RawHeartbeat.NodeName < nodes[j].RawHeartbeat.NodeName
|
|
})
|
|
|
|
log.Printf("%d nodes in guardian state set", len(nodes))
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', 0)
|
|
|
|
headers := []string{
|
|
"Node key",
|
|
"Guardian key",
|
|
"Node name",
|
|
"Version",
|
|
"Last seen",
|
|
}
|
|
|
|
if showDetails {
|
|
headers = append(headers, "Uptime")
|
|
}
|
|
|
|
type network struct {
|
|
string
|
|
vaa.ChainID
|
|
}
|
|
|
|
networks := []network{
|
|
{"Solana", vaa.ChainIDSolana},
|
|
{"Ethereum", vaa.ChainIDEthereum},
|
|
{"Terra", vaa.ChainIDTerra},
|
|
{"BSC", vaa.ChainIDBSC},
|
|
{"Polygon", vaa.ChainIDPolygon},
|
|
{"Avalanche", vaa.ChainIDAvalanche},
|
|
{"Algorand", vaa.ChainIDAlgorand},
|
|
{"Aptos", vaa.ChainIDAptos},
|
|
{"Sui", vaa.ChainIDSui},
|
|
{"Oasis", vaa.ChainIDOasis},
|
|
{"Aurora", vaa.ChainIDAurora},
|
|
{"Fantom", vaa.ChainIDFantom},
|
|
{"Karura", vaa.ChainIDKarura},
|
|
{"Acala", vaa.ChainIDAcala},
|
|
{"Klaytn", vaa.ChainIDKlaytn},
|
|
{"Celo", vaa.ChainIDCelo},
|
|
{"Near", vaa.ChainIDNear},
|
|
{"Terra2", vaa.ChainIDTerra2},
|
|
{"Pythnet", vaa.ChainIDPythNet},
|
|
{"Moonbeam", vaa.ChainIDMoonbeam},
|
|
{"Arbitrum", vaa.ChainIDArbitrum},
|
|
{"Optimism", vaa.ChainIDOptimism},
|
|
{"Xpla", vaa.ChainIDXpla},
|
|
{"Btc", vaa.ChainIDBtc},
|
|
{"Injective", vaa.ChainIDInjective},
|
|
{"Base", vaa.ChainIDBase},
|
|
{"Sei", vaa.ChainIDSei},
|
|
{"Scroll", vaa.ChainIDScroll},
|
|
{"Mantle", vaa.ChainIDMantle},
|
|
{"Blast", vaa.ChainIDBlast},
|
|
{"XLayer", vaa.ChainIDXLayer},
|
|
{"Linea", vaa.ChainIDLinea},
|
|
{"Berachain", vaa.ChainIDBerachain},
|
|
{"Snaxchain", vaa.ChainIDSnaxchain},
|
|
{"Unichain", vaa.ChainIDUnichain},
|
|
{"Worldchain", vaa.ChainIDWorldchain},
|
|
{"Wormchain", vaa.ChainIDWormchain},
|
|
{"Sepolia", vaa.ChainIDSepolia},
|
|
{"Holesky", vaa.ChainIDHolesky},
|
|
{"ArbitrumSepolia", vaa.ChainIDArbitrumSepolia},
|
|
{"BaseSepolia", vaa.ChainIDBaseSepolia},
|
|
{"OptimismSepolia", vaa.ChainIDOptimismSepolia},
|
|
{"PolygonSepolia", vaa.ChainIDPolygonSepolia},
|
|
{"MonadDevnet", vaa.ChainIDMonadDevnet},
|
|
}
|
|
|
|
if len(only) > 0 {
|
|
var filtered []network
|
|
for _, network := range networks {
|
|
for _, name := range only {
|
|
if strings.EqualFold(network.string, name) {
|
|
filtered = append(filtered, network)
|
|
}
|
|
}
|
|
}
|
|
networks = filtered
|
|
}
|
|
|
|
for _, k := range networks {
|
|
headers = append(headers, k.string)
|
|
}
|
|
|
|
for _, header := range headers {
|
|
_, _ = fmt.Fprintf(w, "%s\t", header)
|
|
}
|
|
_, _ = fmt.Fprintln(w)
|
|
|
|
for _, h := range nodes {
|
|
if h.RawHeartbeat == nil {
|
|
continue
|
|
}
|
|
|
|
last := time.Unix(0, h.RawHeartbeat.Timestamp)
|
|
boot := time.Unix(0, h.RawHeartbeat.BootTimestamp)
|
|
|
|
heights := map[vaa.ChainID]int64{}
|
|
truncAddrs := make(map[vaa.ChainID]string)
|
|
errors := map[vaa.ChainID]uint64{}
|
|
for _, n := range h.RawHeartbeat.Networks {
|
|
heights[vaa.ChainID(n.Id)] = n.Height
|
|
errors[vaa.ChainID(n.Id)] = n.ErrorCount
|
|
if len(n.ContractAddress) >= 16 {
|
|
truncAddrs[vaa.ChainID(n.Id)] = n.ContractAddress[:16]
|
|
} else {
|
|
truncAddrs[vaa.ChainID(n.Id)] = "INVALID"
|
|
}
|
|
}
|
|
|
|
fields := []string{
|
|
h.P2PNodeAddr,
|
|
h.RawHeartbeat.GuardianAddr,
|
|
h.RawHeartbeat.NodeName,
|
|
h.RawHeartbeat.Version,
|
|
time.Since(last).String(),
|
|
}
|
|
|
|
if showDetails {
|
|
fields = append(fields, time.Since(boot).String())
|
|
}
|
|
|
|
for _, n := range networks {
|
|
if showDetails {
|
|
fields = append(fields, fmt.Sprintf("%s %d (%d)",
|
|
truncAddrs[n.ChainID], heights[n.ChainID], errors[n.ChainID]))
|
|
} else {
|
|
fields = append(fields, fmt.Sprintf("%d", heights[n.ChainID]))
|
|
}
|
|
}
|
|
|
|
for _, field := range fields {
|
|
_, _ = fmt.Fprintf(w, "%s\t", field)
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(w)
|
|
}
|
|
|
|
w.Flush()
|
|
fmt.Print("\n")
|
|
|
|
for _, addr := range gs.GuardianSet.Addresses {
|
|
var found bool
|
|
for _, h := range nodes {
|
|
if h.VerifiedGuardianAddr == addr {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
fmt.Printf("Missing guardian: %s\n", addr)
|
|
}
|
|
}
|
|
|
|
fmt.Println("\n[do not parse - use the gRPC or REST API for scripting]")
|
|
}
|