|
| 1 | +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. |
| 2 | +// See the file LICENSE for licensing terms. |
| 3 | + |
| 4 | +package connecthandler |
| 5 | + |
| 6 | +import ( |
| 7 | + "context" |
| 8 | + "fmt" |
| 9 | + "time" |
| 10 | + |
| 11 | + "connectrpc.com/connect" |
| 12 | + "google.golang.org/protobuf/types/known/emptypb" |
| 13 | + "google.golang.org/protobuf/types/known/timestamppb" |
| 14 | + |
| 15 | + "github.com/ava-labs/avalanchego/api/info" |
| 16 | + "github.com/ava-labs/avalanchego/ids" |
| 17 | + "github.com/ava-labs/avalanchego/upgrade" |
| 18 | + |
| 19 | + v1 "github.com/ava-labs/avalanchego/proto/pb/info/v1" |
| 20 | +) |
| 21 | + |
| 22 | +// NewConnectInfoService returns a ConnectRPC-compatible InfoServiceHandler |
| 23 | +// that delegates calls to the existing Info implementation |
| 24 | +func NewConnectInfoService(info *info.Info) *ConnectInfoService { |
| 25 | + return &ConnectInfoService{ |
| 26 | + Info: info, |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +type ConnectInfoService struct { |
| 31 | + *info.Info |
| 32 | +} |
| 33 | + |
| 34 | +// GetNodeVersion returns the semantic version, database version, RPC protocol version, |
| 35 | +// Git commit hash, and the list of VM versions this node is running |
| 36 | +func (s *ConnectInfoService) GetNodeVersion( |
| 37 | + _ context.Context, |
| 38 | + _ *connect.Request[emptypb.Empty], |
| 39 | +) (*connect.Response[v1.GetNodeVersionReply], error) { |
| 40 | + var jsonReply info.GetNodeVersionReply |
| 41 | + if err := s.Info.GetNodeVersion(nil, nil, &jsonReply); err != nil { |
| 42 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 43 | + } |
| 44 | + |
| 45 | + // Convert VM versions map to protobuf format |
| 46 | + vmVersions := make(map[string]string) |
| 47 | + for id, version := range jsonReply.VMVersions { |
| 48 | + vmVersions[id] = version |
| 49 | + } |
| 50 | + |
| 51 | + reply := &v1.GetNodeVersionReply{ |
| 52 | + Version: jsonReply.Version, |
| 53 | + DatabaseVersion: jsonReply.DatabaseVersion, |
| 54 | + RpcProtocolVersion: uint32(jsonReply.RPCProtocolVersion), |
| 55 | + GitCommit: jsonReply.GitCommit, |
| 56 | + VmVersions: vmVersions, |
| 57 | + } |
| 58 | + |
| 59 | + return connect.NewResponse(reply), nil |
| 60 | +} |
| 61 | + |
| 62 | +// GetNodeID returns this node's unique identifier and proof-of-possession bytes |
| 63 | +func (s *ConnectInfoService) GetNodeID( |
| 64 | + _ context.Context, |
| 65 | + _ *connect.Request[emptypb.Empty], |
| 66 | +) (*connect.Response[v1.GetNodeIDReply], error) { |
| 67 | + var jsonReply info.GetNodeIDReply |
| 68 | + if err := s.Info.GetNodeID(nil, nil, &jsonReply); err != nil { |
| 69 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 70 | + } |
| 71 | + |
| 72 | + nodePOP := []byte{} |
| 73 | + if jsonReply.NodePOP != nil { |
| 74 | + // Use Marshal to serialize ProofOfPossession to bytes |
| 75 | + // MarshalJSON is not ideal here. Ideally, we would use a binary serialization method (MarshalBinary, Marshal, etc.) |
| 76 | + var err error |
| 77 | + nodePOP, err = jsonReply.NodePOP.MarshalJSON() |
| 78 | + if err != nil { |
| 79 | + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to marshal NodePOP: %w", err)) |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + reply := &v1.GetNodeIDReply{ |
| 84 | + NodeId: jsonReply.NodeID.String(), |
| 85 | + NodePop: nodePOP, |
| 86 | + } |
| 87 | + |
| 88 | + return connect.NewResponse(reply), nil |
| 89 | +} |
| 90 | + |
| 91 | +// GetNodeIP returns the primary IP address this node uses for P2P networking.\ |
| 92 | +func (s *ConnectInfoService) GetNodeIP( |
| 93 | + _ context.Context, |
| 94 | + _ *connect.Request[emptypb.Empty], |
| 95 | +) (*connect.Response[v1.GetNodeIPReply], error) { |
| 96 | + var jsonReply info.GetNodeIPReply |
| 97 | + if err := s.Info.GetNodeIP(nil, nil, &jsonReply); err != nil { |
| 98 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 99 | + } |
| 100 | + |
| 101 | + reply := &v1.GetNodeIPReply{ |
| 102 | + Ip: jsonReply.IP.String(), |
| 103 | + } |
| 104 | + |
| 105 | + return connect.NewResponse(reply), nil |
| 106 | +} |
| 107 | + |
| 108 | +// GetNetworkID returns the numeric ID of the Avalanche network this node is connected to |
| 109 | +func (s *ConnectInfoService) GetNetworkID( |
| 110 | + _ context.Context, |
| 111 | + _ *connect.Request[emptypb.Empty], |
| 112 | +) (*connect.Response[v1.GetNetworkIDReply], error) { |
| 113 | + var jsonReply info.GetNetworkIDReply |
| 114 | + if err := s.Info.GetNetworkID(nil, nil, &jsonReply); err != nil { |
| 115 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 116 | + } |
| 117 | + |
| 118 | + reply := &v1.GetNetworkIDReply{ |
| 119 | + NetworkId: uint32(jsonReply.NetworkID), |
| 120 | + } |
| 121 | + |
| 122 | + return connect.NewResponse(reply), nil |
| 123 | +} |
| 124 | + |
| 125 | +// GetNetworkName returns the name of the network |
| 126 | +func (s *ConnectInfoService) GetNetworkName( |
| 127 | + _ context.Context, |
| 128 | + _ *connect.Request[emptypb.Empty], |
| 129 | +) (*connect.Response[v1.GetNetworkNameReply], error) { |
| 130 | + var jsonReply info.GetNetworkNameReply |
| 131 | + if err := s.Info.GetNetworkName(nil, nil, &jsonReply); err != nil { |
| 132 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 133 | + } |
| 134 | + |
| 135 | + reply := &v1.GetNetworkNameReply{ |
| 136 | + NetworkName: jsonReply.NetworkName, |
| 137 | + } |
| 138 | + |
| 139 | + return connect.NewResponse(reply), nil |
| 140 | +} |
| 141 | + |
| 142 | +// GetBlockchainID maps an ID string to its canonical chain ID |
| 143 | +func (s *ConnectInfoService) GetBlockchainID( |
| 144 | + _ context.Context, |
| 145 | + req *connect.Request[v1.GetBlockchainIDArgs], |
| 146 | +) (*connect.Response[v1.GetBlockchainIDReply], error) { |
| 147 | + jsonArgs := info.GetBlockchainIDArgs{ |
| 148 | + Alias: req.Msg.Alias, |
| 149 | + } |
| 150 | + |
| 151 | + var jsonReply info.GetBlockchainIDReply |
| 152 | + if err := s.Info.GetBlockchainID(nil, &jsonArgs, &jsonReply); err != nil { |
| 153 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 154 | + } |
| 155 | + |
| 156 | + reply := &v1.GetBlockchainIDReply{ |
| 157 | + BlockchainId: jsonReply.BlockchainID.String(), |
| 158 | + } |
| 159 | + |
| 160 | + return connect.NewResponse(reply), nil |
| 161 | +} |
| 162 | + |
| 163 | +// Peers returns metadata (IP, nodeID, version, uptimes, subnets, etc.) for the given peer node IDs |
| 164 | +func (s *ConnectInfoService) Peers( |
| 165 | + _ context.Context, |
| 166 | + req *connect.Request[v1.PeersArgs], |
| 167 | +) (*connect.Response[v1.PeersReply], error) { |
| 168 | + nodeIDs := make([]ids.NodeID, 0, len(req.Msg.NodeIds)) |
| 169 | + for _, nodeIDStr := range req.Msg.NodeIds { |
| 170 | + nodeID, err := ids.NodeIDFromString(nodeIDStr) |
| 171 | + if err != nil { |
| 172 | + return nil, connect.NewError( |
| 173 | + connect.CodeInvalidArgument, fmt.Errorf("invalid nodeID %s: %w", nodeIDStr, err)) |
| 174 | + } |
| 175 | + nodeIDs = append(nodeIDs, nodeID) |
| 176 | + } |
| 177 | + |
| 178 | + jsonArgs := info.PeersArgs{ |
| 179 | + NodeIDs: nodeIDs, |
| 180 | + } |
| 181 | + |
| 182 | + var jsonReply info.PeersReply |
| 183 | + if err := s.Info.Peers(nil, &jsonArgs, &jsonReply); err != nil { |
| 184 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 185 | + } |
| 186 | + |
| 187 | + peers := make([]*v1.PeerInfo, 0, len(jsonReply.Peers)) |
| 188 | + for _, peer := range jsonReply.Peers { |
| 189 | + // Convert TrackedSubnets (set.Set[ids.ID]) to []string |
| 190 | + trackedSubnetsIDs := peer.TrackedSubnets.List() |
| 191 | + trackedSubnets := make([]string, len(trackedSubnetsIDs)) |
| 192 | + for i, id := range trackedSubnetsIDs { |
| 193 | + trackedSubnets[i] = id.String() |
| 194 | + } |
| 195 | + |
| 196 | + benched := make([]string, len(peer.Benched)) |
| 197 | + copy(benched, peer.Benched) |
| 198 | + |
| 199 | + peers = append(peers, &v1.PeerInfo{ |
| 200 | + Ip: peer.IP.String(), |
| 201 | + PublicIp: peer.PublicIP.String(), |
| 202 | + NodeId: peer.ID.String(), |
| 203 | + Version: peer.Version, |
| 204 | + LastSent: formatTime(peer.LastSent), |
| 205 | + LastReceived: formatTime(peer.LastReceived), |
| 206 | + Benched: benched, |
| 207 | + ObservedUptime: uint32(peer.ObservedUptime), |
| 208 | + TrackedSubnets: trackedSubnets, |
| 209 | + }) |
| 210 | + } |
| 211 | + |
| 212 | + reply := &v1.PeersReply{ |
| 213 | + NumPeers: uint32(jsonReply.NumPeers), |
| 214 | + Peers: peers, |
| 215 | + } |
| 216 | + |
| 217 | + return connect.NewResponse(reply), nil |
| 218 | +} |
| 219 | + |
| 220 | +// Helper function to format time |
| 221 | +func formatTime(t time.Time) string { |
| 222 | + return t.Format(time.RFC3339) |
| 223 | +} |
| 224 | + |
| 225 | +// IsBootstrapped returns whether the named chain has finished its bootstrap process on this node |
| 226 | +func (s *ConnectInfoService) IsBootstrapped( |
| 227 | + _ context.Context, |
| 228 | + req *connect.Request[v1.IsBootstrappedArgs], |
| 229 | +) (*connect.Response[v1.IsBootstrappedResponse], error) { |
| 230 | + // Use the chain from the request |
| 231 | + jsonArgs := info.IsBootstrappedArgs{ |
| 232 | + Chain: req.Msg.Chain, |
| 233 | + } |
| 234 | + |
| 235 | + var jsonReply info.IsBootstrappedResponse |
| 236 | + if err := s.Info.IsBootstrapped(nil, &jsonArgs, &jsonReply); err != nil { |
| 237 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 238 | + } |
| 239 | + |
| 240 | + reply := &v1.IsBootstrappedResponse{ |
| 241 | + IsBootstrapped: jsonReply.IsBootstrapped, |
| 242 | + } |
| 243 | + |
| 244 | + return connect.NewResponse(reply), nil |
| 245 | +} |
| 246 | + |
| 247 | +// Upgrades returns all the scheduled upgrade activation times and parameters for this node |
| 248 | +func (s *ConnectInfoService) Upgrades( |
| 249 | + _ context.Context, |
| 250 | + _ *connect.Request[emptypb.Empty], |
| 251 | +) (*connect.Response[v1.UpgradesReply], error) { |
| 252 | + var config upgrade.Config |
| 253 | + if err := s.Info.Upgrades(nil, nil, &config); err != nil { |
| 254 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 255 | + } |
| 256 | + |
| 257 | + reply := &v1.UpgradesReply{ |
| 258 | + ApricotPhase1Time: timestamppb.New(config.ApricotPhase1Time), |
| 259 | + ApricotPhase2Time: timestamppb.New(config.ApricotPhase2Time), |
| 260 | + ApricotPhase3Time: timestamppb.New(config.ApricotPhase3Time), |
| 261 | + ApricotPhase4Time: timestamppb.New(config.ApricotPhase4Time), |
| 262 | + ApricotPhase4MinPChainHeight: config.ApricotPhase4MinPChainHeight, |
| 263 | + ApricotPhase5Time: timestamppb.New(config.ApricotPhase5Time), |
| 264 | + ApricotPhasePre6Time: timestamppb.New(config.ApricotPhasePre6Time), |
| 265 | + ApricotPhase6Time: timestamppb.New(config.ApricotPhase6Time), |
| 266 | + ApricotPhasePost6Time: timestamppb.New(config.ApricotPhasePost6Time), |
| 267 | + BanffTime: timestamppb.New(config.BanffTime), |
| 268 | + CortinaTime: timestamppb.New(config.CortinaTime), |
| 269 | + CortinaXChainStopVertexId: config.CortinaXChainStopVertexID.String(), |
| 270 | + DurangoTime: timestamppb.New(config.DurangoTime), |
| 271 | + EtnaTime: timestamppb.New(config.EtnaTime), |
| 272 | + FortunaTime: timestamppb.New(config.FortunaTime), |
| 273 | + GraniteTime: timestamppb.New(config.GraniteTime), |
| 274 | + } |
| 275 | + |
| 276 | + return connect.NewResponse(reply), nil |
| 277 | +} |
| 278 | + |
| 279 | +// Uptime returns this node's uptime metrics (rewarding stake %, weighted average %, etc.) |
| 280 | +func (s *ConnectInfoService) Uptime( |
| 281 | + _ context.Context, |
| 282 | + _ *connect.Request[emptypb.Empty], |
| 283 | +) (*connect.Response[v1.UptimeResponse], error) { |
| 284 | + var jsonReply info.UptimeResponse |
| 285 | + if err := s.Info.Uptime(nil, nil, &jsonReply); err != nil { |
| 286 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 287 | + } |
| 288 | + |
| 289 | + reply := &v1.UptimeResponse{ |
| 290 | + RewardingStakePercentage: float64(jsonReply.RewardingStakePercentage), |
| 291 | + WeightedAveragePercentage: float64(jsonReply.WeightedAveragePercentage), |
| 292 | + } |
| 293 | + |
| 294 | + return connect.NewResponse(reply), nil |
| 295 | +} |
| 296 | + |
| 297 | +// GetVMs returns a map of VM IDs to their known aliases, plus FXs information |
| 298 | +func (s *ConnectInfoService) GetVMs( |
| 299 | + _ context.Context, |
| 300 | + _ *connect.Request[emptypb.Empty], |
| 301 | +) (*connect.Response[v1.GetVMsReply], error) { |
| 302 | + var jsonReply info.GetVMsReply |
| 303 | + if err := s.Info.GetVMs(nil, nil, &jsonReply); err != nil { |
| 304 | + return nil, connect.NewError(connect.CodeInternal, err) |
| 305 | + } |
| 306 | + |
| 307 | + // Convert the VM map from JSON-RPC format to protobuf format |
| 308 | + vms := make(map[string]*v1.VMAliases) |
| 309 | + for vmID, aliases := range jsonReply.VMs { |
| 310 | + vms[vmID.String()] = &v1.VMAliases{ |
| 311 | + Aliases: aliases, |
| 312 | + } |
| 313 | + } |
| 314 | + |
| 315 | + // Convert the FXs map from JSON-RPC format to protobuf format |
| 316 | + fxs := make(map[string]string) |
| 317 | + for fxID, name := range jsonReply.Fxs { |
| 318 | + fxs[fxID.String()] = name |
| 319 | + } |
| 320 | + |
| 321 | + reply := &v1.GetVMsReply{ |
| 322 | + Vms: vms, |
| 323 | + Fxs: fxs, |
| 324 | + } |
| 325 | + |
| 326 | + return connect.NewResponse(reply), nil |
| 327 | +} |
0 commit comments