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  }