github.com/MetalBlockchain/metalgo@v1.11.9/tests/fixture/tmpnet/node.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package tmpnet 5 6 import ( 7 "context" 8 "encoding/base64" 9 "errors" 10 "fmt" 11 "io" 12 "net" 13 "net/http" 14 "os" 15 "path/filepath" 16 "strings" 17 "time" 18 19 "github.com/spf13/cast" 20 21 "github.com/MetalBlockchain/metalgo/config" 22 "github.com/MetalBlockchain/metalgo/ids" 23 "github.com/MetalBlockchain/metalgo/staking" 24 "github.com/MetalBlockchain/metalgo/utils/crypto/bls" 25 "github.com/MetalBlockchain/metalgo/vms/platformvm/signer" 26 ) 27 28 // The Node type is defined in this file (node.go - orchestration) and 29 // node_config.go (reading/writing configuration). 30 31 const ( 32 defaultNodeTickerInterval = 50 * time.Millisecond 33 ) 34 35 var ( 36 errMissingTLSKeyForNodeID = fmt.Errorf("failed to ensure node ID: missing value for %q", config.StakingTLSKeyContentKey) 37 errMissingCertForNodeID = fmt.Errorf("failed to ensure node ID: missing value for %q", config.StakingCertContentKey) 38 errInvalidKeypair = fmt.Errorf("%q and %q must be provided together or not at all", config.StakingTLSKeyContentKey, config.StakingCertContentKey) 39 ) 40 41 // NodeRuntime defines the methods required to support running a node. 42 type NodeRuntime interface { 43 readState() error 44 Start(w io.Writer) error 45 InitiateStop() error 46 WaitForStopped(ctx context.Context) error 47 IsHealthy(ctx context.Context) (bool, error) 48 } 49 50 // Configuration required to configure a node runtime. 51 type NodeRuntimeConfig struct { 52 AvalancheGoPath string 53 } 54 55 // Node supports configuring and running a node participating in a temporary network. 56 type Node struct { 57 // Uniquely identifies the network the node is part of to enable monitoring. 58 NetworkUUID string 59 60 // Identify the entity associated with this network. This is 61 // intended to be used to label metrics to enable filtering 62 // results for a test run between the primary/shared network used 63 // by the majority of tests and private networks used by 64 // individual tests. 65 NetworkOwner string 66 67 // Set by EnsureNodeID which is also called when the node is read. 68 NodeID ids.NodeID 69 70 // Flags that will be supplied to the node at startup 71 Flags FlagsMap 72 73 // An ephemeral node is not expected to be a persistent member of the network and 74 // should therefore not be used as for bootstrapping purposes. 75 IsEphemeral bool 76 77 // The configuration used to initialize the node runtime. 78 RuntimeConfig *NodeRuntimeConfig 79 80 // Runtime state, intended to be set by NodeRuntime 81 URI string 82 StakingAddress string 83 84 // Initialized on demand 85 runtime NodeRuntime 86 } 87 88 // Initializes a new node with only the data dir set 89 func NewNode(dataDir string) *Node { 90 return &Node{ 91 Flags: FlagsMap{ 92 config.DataDirKey: dataDir, 93 }, 94 } 95 } 96 97 // Initializes an ephemeral node using the provided config flags 98 func NewEphemeralNode(flags FlagsMap) *Node { 99 node := NewNode("") 100 node.Flags = flags 101 node.IsEphemeral = true 102 103 return node 104 } 105 106 // Initializes the specified number of nodes. 107 func NewNodesOrPanic(count int) []*Node { 108 nodes := make([]*Node, count) 109 for i := range nodes { 110 node := NewNode("") 111 if err := node.EnsureKeys(); err != nil { 112 panic(err) 113 } 114 nodes[i] = node 115 } 116 return nodes 117 } 118 119 // Reads a node's configuration from the specified directory. 120 func ReadNode(dataDir string) (*Node, error) { 121 node := NewNode(dataDir) 122 return node, node.Read() 123 } 124 125 // Reads nodes from the specified network directory. 126 func ReadNodes(networkDir string, includeEphemeral bool) ([]*Node, error) { 127 nodes := []*Node{} 128 129 // Node configuration is stored in child directories 130 entries, err := os.ReadDir(networkDir) 131 if err != nil { 132 return nil, fmt.Errorf("failed to read dir: %w", err) 133 } 134 for _, entry := range entries { 135 if !entry.IsDir() { 136 continue 137 } 138 139 nodeDir := filepath.Join(networkDir, entry.Name()) 140 node, err := ReadNode(nodeDir) 141 if errors.Is(err, os.ErrNotExist) { 142 // If no config file exists, assume this is not the path of a node 143 continue 144 } else if err != nil { 145 return nil, err 146 } 147 148 if !includeEphemeral && node.IsEphemeral { 149 continue 150 } 151 152 nodes = append(nodes, node) 153 } 154 155 return nodes, nil 156 } 157 158 // Retrieves the runtime for the node. 159 func (n *Node) getRuntime() NodeRuntime { 160 if n.runtime == nil { 161 n.runtime = &NodeProcess{ 162 node: n, 163 } 164 } 165 return n.runtime 166 } 167 168 // Runtime methods 169 170 func (n *Node) IsHealthy(ctx context.Context) (bool, error) { 171 return n.getRuntime().IsHealthy(ctx) 172 } 173 174 func (n *Node) Start(w io.Writer) error { 175 return n.getRuntime().Start(w) 176 } 177 178 func (n *Node) InitiateStop(ctx context.Context) error { 179 if err := n.SaveMetricsSnapshot(ctx); err != nil { 180 return err 181 } 182 return n.getRuntime().InitiateStop() 183 } 184 185 func (n *Node) WaitForStopped(ctx context.Context) error { 186 return n.getRuntime().WaitForStopped(ctx) 187 } 188 189 func (n *Node) readState() error { 190 return n.getRuntime().readState() 191 } 192 193 func (n *Node) GetDataDir() string { 194 return cast.ToString(n.Flags[config.DataDirKey]) 195 } 196 197 // Writes the current state of the metrics endpoint to disk 198 func (n *Node) SaveMetricsSnapshot(ctx context.Context) error { 199 if len(n.URI) == 0 { 200 // No URI to request metrics from 201 return nil 202 } 203 uri := n.URI + "/ext/metrics" 204 req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) 205 if err != nil { 206 return err 207 } 208 resp, err := http.DefaultClient.Do(req) 209 if err != nil { 210 return err 211 } 212 defer resp.Body.Close() 213 body, err := io.ReadAll(resp.Body) 214 if err != nil { 215 return err 216 } 217 return n.writeMetricsSnapshot(body) 218 } 219 220 // Initiates node shutdown and waits for the node to stop. 221 func (n *Node) Stop(ctx context.Context) error { 222 if err := n.InitiateStop(ctx); err != nil { 223 return err 224 } 225 return n.WaitForStopped(ctx) 226 } 227 228 // Sets networking configuration for the node. 229 // Convenience method for setting networking flags. 230 func (n *Node) SetNetworkingConfig(bootstrapIDs []string, bootstrapIPs []string) { 231 if _, ok := n.Flags[config.HTTPPortKey]; !ok { 232 // Default to dynamic port allocation 233 n.Flags[config.HTTPPortKey] = 0 234 } 235 if _, ok := n.Flags[config.StakingPortKey]; !ok { 236 // Default to dynamic port allocation 237 n.Flags[config.StakingPortKey] = 0 238 } 239 n.Flags[config.BootstrapIDsKey] = strings.Join(bootstrapIDs, ",") 240 n.Flags[config.BootstrapIPsKey] = strings.Join(bootstrapIPs, ",") 241 } 242 243 // Ensures staking and signing keys are generated if not already present and 244 // that the node ID (derived from the staking keypair) is set. 245 func (n *Node) EnsureKeys() error { 246 if err := n.EnsureBLSSigningKey(); err != nil { 247 return err 248 } 249 if err := n.EnsureStakingKeypair(); err != nil { 250 return err 251 } 252 return n.EnsureNodeID() 253 } 254 255 // Ensures a BLS signing key is generated if not already present. 256 func (n *Node) EnsureBLSSigningKey() error { 257 // Attempt to retrieve an existing key 258 existingKey, err := n.Flags.GetStringVal(config.StakingSignerKeyContentKey) 259 if err != nil { 260 return err 261 } 262 if len(existingKey) > 0 { 263 // Nothing to do 264 return nil 265 } 266 267 // Generate a new signing key 268 newKey, err := bls.NewSecretKey() 269 if err != nil { 270 return fmt.Errorf("failed to generate staking signer key: %w", err) 271 } 272 n.Flags[config.StakingSignerKeyContentKey] = base64.StdEncoding.EncodeToString(bls.SecretKeyToBytes(newKey)) 273 return nil 274 } 275 276 // Ensures a staking keypair is generated if not already present. 277 func (n *Node) EnsureStakingKeypair() error { 278 keyKey := config.StakingTLSKeyContentKey 279 certKey := config.StakingCertContentKey 280 281 key, err := n.Flags.GetStringVal(keyKey) 282 if err != nil { 283 return err 284 } 285 286 cert, err := n.Flags.GetStringVal(certKey) 287 if err != nil { 288 return err 289 } 290 291 if len(key) == 0 && len(cert) == 0 { 292 // Generate new keypair 293 tlsCertBytes, tlsKeyBytes, err := staking.NewCertAndKeyBytes() 294 if err != nil { 295 return fmt.Errorf("failed to generate staking keypair: %w", err) 296 } 297 n.Flags[keyKey] = base64.StdEncoding.EncodeToString(tlsKeyBytes) 298 n.Flags[certKey] = base64.StdEncoding.EncodeToString(tlsCertBytes) 299 } else if len(key) == 0 || len(cert) == 0 { 300 // Only one of key and cert was provided 301 return errInvalidKeypair 302 } 303 304 return nil 305 } 306 307 // Derives the nodes proof-of-possession. Requires the node to have a 308 // BLS signing key. 309 func (n *Node) GetProofOfPossession() (*signer.ProofOfPossession, error) { 310 signingKey, err := n.Flags.GetStringVal(config.StakingSignerKeyContentKey) 311 if err != nil { 312 return nil, err 313 } 314 signingKeyBytes, err := base64.StdEncoding.DecodeString(signingKey) 315 if err != nil { 316 return nil, err 317 } 318 secretKey, err := bls.SecretKeyFromBytes(signingKeyBytes) 319 if err != nil { 320 return nil, err 321 } 322 return signer.NewProofOfPossession(secretKey), nil 323 } 324 325 // Derives the node ID. Requires that a tls keypair is present. 326 func (n *Node) EnsureNodeID() error { 327 keyKey := config.StakingTLSKeyContentKey 328 certKey := config.StakingCertContentKey 329 330 key, err := n.Flags.GetStringVal(keyKey) 331 if err != nil { 332 return err 333 } 334 if len(key) == 0 { 335 return errMissingTLSKeyForNodeID 336 } 337 keyBytes, err := base64.StdEncoding.DecodeString(key) 338 if err != nil { 339 return fmt.Errorf("failed to ensure node ID: failed to base64 decode value for %q: %w", keyKey, err) 340 } 341 342 cert, err := n.Flags.GetStringVal(certKey) 343 if err != nil { 344 return err 345 } 346 if len(cert) == 0 { 347 return errMissingCertForNodeID 348 } 349 certBytes, err := base64.StdEncoding.DecodeString(cert) 350 if err != nil { 351 return fmt.Errorf("failed to ensure node ID: failed to base64 decode value for %q: %w", certKey, err) 352 } 353 354 tlsCert, err := staking.LoadTLSCertFromBytes(keyBytes, certBytes) 355 if err != nil { 356 return fmt.Errorf("failed to ensure node ID: failed to load tls cert: %w", err) 357 } 358 stakingCert, err := staking.ParseCertificate(tlsCert.Leaf.Raw) 359 if err != nil { 360 return fmt.Errorf("failed to ensure node ID: failed to parse staking cert: %w", err) 361 } 362 n.NodeID = ids.NodeIDFromCert(stakingCert) 363 364 return nil 365 } 366 367 // Saves the currently allocated API port to the node's configuration 368 // for use across restarts. Reusing the port ensures consistent 369 // labeling of metrics. 370 func (n *Node) SaveAPIPort() error { 371 hostPort := strings.TrimPrefix(n.URI, "http://") 372 if len(hostPort) == 0 { 373 // Without an API URI there is nothing to save 374 return nil 375 } 376 _, port, err := net.SplitHostPort(hostPort) 377 if err != nil { 378 return err 379 } 380 n.Flags[config.HTTPPortKey] = port 381 return nil 382 }