github.com/cgcardona/r-subnet-evm@v0.1.5/tests/utils/runner/network_manager.go (about)

     1  // Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package runner
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"time"
    11  
    12  	runner_sdk "github.com/ava-labs/avalanche-network-runner/client"
    13  	"github.com/ava-labs/avalanche-network-runner/rpcpb"
    14  	runner_server "github.com/ava-labs/avalanche-network-runner/server"
    15  	"github.com/ava-labs/avalanchego/ids"
    16  	"github.com/ava-labs/avalanchego/utils/logging"
    17  	"github.com/ava-labs/avalanchego/utils/wrappers"
    18  	"github.com/cgcardona/r-subnet-evm/plugin/evm"
    19  	"github.com/ethereum/go-ethereum/log"
    20  	"github.com/onsi/ginkgo/v2"
    21  	"github.com/onsi/gomega"
    22  )
    23  
    24  // Subnet provides the basic details of a created subnet
    25  // Note: currently assumes one blockchain per subnet
    26  type Subnet struct {
    27  	// SubnetID is the txID of the transaction that created the subnet
    28  	SubnetID ids.ID
    29  	// Current ANR assumes one blockchain per subnet, so we have a single blockchainID here
    30  	BlockchainID ids.ID
    31  	// ValidatorURIs is the base URIs for each participant of the Subnet
    32  	ValidatorURIs []string
    33  }
    34  
    35  type ANRConfig struct {
    36  	LogLevel            string
    37  	AvalancheGoExecPath string
    38  	PluginDir           string
    39  	GlobalNodeConfig    string
    40  }
    41  
    42  // NetworkManager is a wrapper around the ANR to simplify the setup and teardown code
    43  // of tests that rely on the ANR.
    44  type NetworkManager struct {
    45  	ANRConfig ANRConfig
    46  
    47  	subnets []*Subnet
    48  
    49  	logFactory      logging.Factory
    50  	anrClient       runner_sdk.Client
    51  	anrServer       runner_server.Server
    52  	done            chan struct{}
    53  	serverCtxCancel context.CancelFunc
    54  }
    55  
    56  // NewDefaultANRConfig returns a default config for launching the avalanche-network-runner manager
    57  // with both a server and client.
    58  // By default, it expands $GOPATH/src/github.com/ava-labs/avalanchego/build/ directory to extract
    59  // the AvalancheGoExecPath and PluginDir arguments.
    60  // If the AVALANCHEGO_BUILD_PATH environment variable is set, it overrides the default location for
    61  // the AvalancheGoExecPath and PluginDir arguments.
    62  func NewDefaultANRConfig() ANRConfig {
    63  	defaultConfig := ANRConfig{
    64  		LogLevel:            "info",
    65  		AvalancheGoExecPath: os.ExpandEnv("$GOPATH/src/github.com/ava-labs/avalanchego/build/avalanchego"),
    66  		PluginDir:           os.ExpandEnv("$GOPATH/src/github.com/ava-labs/avalanchego/build/plugins"),
    67  		GlobalNodeConfig: `{
    68  			"log-display-level":"info",
    69  			"proposervm-use-current-height":true
    70  		}`,
    71  	}
    72  	// If AVALANCHEGO_BUILD_PATH is populated, override location set by GOPATH
    73  	if envBuildPath, exists := os.LookupEnv("AVALANCHEGO_BUILD_PATH"); exists {
    74  		defaultConfig.AvalancheGoExecPath = fmt.Sprintf("%s/avalanchego", envBuildPath)
    75  		defaultConfig.PluginDir = fmt.Sprintf("%s/plugins", envBuildPath)
    76  	}
    77  	return defaultConfig
    78  }
    79  
    80  // NewNetworkManager constructs a new instance of a network manager
    81  func NewNetworkManager(config ANRConfig) *NetworkManager {
    82  	manager := &NetworkManager{
    83  		ANRConfig: config,
    84  	}
    85  
    86  	logLevel, err := logging.ToLevel(config.LogLevel)
    87  	if err != nil {
    88  		panic(fmt.Errorf("invalid ANR log level: %w", err))
    89  	}
    90  	manager.logFactory = logging.NewFactory(logging.Config{
    91  		DisplayLevel: logLevel,
    92  		LogLevel:     logLevel,
    93  	})
    94  
    95  	return manager
    96  }
    97  
    98  // startServer starts a new ANR server and sets/overwrites the anrServer, done channel, and serverCtxCancel function.
    99  func (n *NetworkManager) startServer(ctx context.Context) (<-chan struct{}, error) {
   100  	done := make(chan struct{})
   101  	zapServerLog, err := n.logFactory.Make("server")
   102  	if err != nil {
   103  		return nil, fmt.Errorf("failed to make server log: %w", err)
   104  	}
   105  
   106  	n.anrServer, err = runner_server.New(
   107  		runner_server.Config{
   108  			Port:                ":12352",
   109  			GwPort:              ":12353",
   110  			GwDisabled:          false,
   111  			DialTimeout:         10 * time.Second,
   112  			RedirectNodesOutput: true,
   113  			SnapshotsDir:        "",
   114  		},
   115  		zapServerLog,
   116  	)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("failed to start ANR server: %w", err)
   119  	}
   120  	n.done = done
   121  
   122  	// Use a separate background context here, since the server should only be canceled by explicit shutdown
   123  	serverCtx, serverCtxCancel := context.WithCancel(context.Background())
   124  	n.serverCtxCancel = serverCtxCancel
   125  	go func() {
   126  		if err := n.anrServer.Run(serverCtx); err != nil {
   127  			log.Error("Error shutting down ANR server", "err", err)
   128  		} else {
   129  			log.Info("Terminating ANR Server")
   130  		}
   131  		close(done)
   132  	}()
   133  
   134  	return done, nil
   135  }
   136  
   137  // startClient starts an ANR Client dialing the ANR server at the expected endpoint.
   138  // Note: will overwrite client if it already exists.
   139  func (n *NetworkManager) startClient() error {
   140  	logLevel, err := logging.ToLevel(n.ANRConfig.LogLevel)
   141  	if err != nil {
   142  		return fmt.Errorf("failed to parse ANR log level: %w", err)
   143  	}
   144  	logFactory := logging.NewFactory(logging.Config{
   145  		DisplayLevel: logLevel,
   146  		LogLevel:     logLevel,
   147  	})
   148  	zapLog, err := logFactory.Make("main")
   149  	if err != nil {
   150  		return fmt.Errorf("failed to make client log: %w", err)
   151  	}
   152  
   153  	n.anrClient, err = runner_sdk.New(runner_sdk.Config{
   154  		Endpoint:    "0.0.0.0:12352",
   155  		DialTimeout: 10 * time.Second,
   156  	}, zapLog)
   157  	if err != nil {
   158  		return fmt.Errorf("failed to start ANR client: %w", err)
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  // initServer starts the ANR server if it is not populated
   165  func (n *NetworkManager) initServer() error {
   166  	if n.anrServer != nil {
   167  		return nil
   168  	}
   169  
   170  	_, err := n.startServer(context.Background())
   171  	return err
   172  }
   173  
   174  // initClient starts an ANR client if it not populated
   175  func (n *NetworkManager) initClient() error {
   176  	if n.anrClient != nil {
   177  		return nil
   178  	}
   179  
   180  	return n.startClient()
   181  }
   182  
   183  // init starts the ANR server and client if they are not yet populated
   184  func (n *NetworkManager) init() error {
   185  	if err := n.initServer(); err != nil {
   186  		return err
   187  	}
   188  	return n.initClient()
   189  }
   190  
   191  // StartDefaultNetwork constructs a default 5 node network.
   192  func (n *NetworkManager) StartDefaultNetwork(ctx context.Context) (<-chan struct{}, error) {
   193  	if err := n.init(); err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	log.Info("Sending 'start'", "AvalancheGoExecPath", n.ANRConfig.AvalancheGoExecPath)
   198  
   199  	// Start cluster
   200  	resp, err := n.anrClient.Start(
   201  		ctx,
   202  		n.ANRConfig.AvalancheGoExecPath,
   203  		runner_sdk.WithPluginDir(n.ANRConfig.PluginDir),
   204  		runner_sdk.WithGlobalNodeConfig(n.ANRConfig.GlobalNodeConfig),
   205  	)
   206  	if err != nil {
   207  		return nil, fmt.Errorf("failed to start ANR network: %w", err)
   208  	}
   209  	log.Info("successfully started cluster", "RootDataDir", resp.ClusterInfo.RootDataDir, "Subnets", resp.GetClusterInfo().GetSubnets())
   210  	return n.done, nil
   211  }
   212  
   213  // SetupNetwork constructs blockchains with the given [blockchainSpecs] and adds them to the network manager.
   214  // Uses [execPath] as the AvalancheGo binary execution path for any started nodes.
   215  // Note: this assumes that the default network has already been constructed.
   216  func (n *NetworkManager) SetupNetwork(ctx context.Context, execPath string, blockchainSpecs []*rpcpb.BlockchainSpec) error {
   217  	cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
   218  	defer cancel()
   219  	if err := n.init(); err != nil {
   220  		return err
   221  	}
   222  	sresp, err := n.anrClient.CreateBlockchains(
   223  		ctx,
   224  		blockchainSpecs,
   225  	)
   226  	if err != nil {
   227  		return fmt.Errorf("failed to create blockchains: %w", err)
   228  	}
   229  
   230  	// TODO: network runner health should imply custom VM healthiness
   231  	// or provide a separate API for custom VM healthiness
   232  	// "start" is async, so wait some time for cluster health
   233  	log.Info("waiting for all VMs to report healthy", "VMID", evm.ID)
   234  	for {
   235  		v, err := n.anrClient.Health(ctx)
   236  		log.Info("Pinged CLI Health", "result", v, "err", err)
   237  		if err != nil {
   238  			time.Sleep(1 * time.Second)
   239  			continue
   240  		} else if ctx.Err() != nil {
   241  			return fmt.Errorf("failed to await healthy network: %w", ctx.Err())
   242  		}
   243  		break
   244  	}
   245  
   246  	status, err := n.anrClient.Status(cctx)
   247  	if err != nil {
   248  		return fmt.Errorf("failed to get ANR status: %w", err)
   249  	}
   250  	nodeInfos := status.GetClusterInfo().GetNodeInfos()
   251  
   252  	for _, chainSpec := range blockchainSpecs {
   253  		blockchainIDStr := sresp.ChainIds[0]
   254  		blockchainID, err := ids.FromString(blockchainIDStr)
   255  		if err != nil {
   256  			panic(err)
   257  		}
   258  		subnetIDStr := sresp.ClusterInfo.CustomChains[blockchainIDStr].SubnetId
   259  		subnetID, err := ids.FromString(subnetIDStr)
   260  		if err != nil {
   261  			panic(err)
   262  		}
   263  		subnet := &Subnet{
   264  			SubnetID:     subnetID,
   265  			BlockchainID: blockchainID,
   266  		}
   267  		for _, nodeName := range chainSpec.SubnetSpec.Participants {
   268  			subnet.ValidatorURIs = append(subnet.ValidatorURIs, nodeInfos[nodeName].Uri)
   269  		}
   270  		n.subnets = append(n.subnets, subnet)
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  // TeardownNetwork tears down the network constructed by the network manager and cleans up
   277  // everything associated with it.
   278  func (n *NetworkManager) TeardownNetwork() error {
   279  	if err := n.initClient(); err != nil {
   280  		return err
   281  	}
   282  	errs := wrappers.Errs{}
   283  	log.Info("Shutting down cluster")
   284  	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
   285  	_, err := n.anrClient.Stop(ctx)
   286  	cancel()
   287  	errs.Add(err)
   288  	errs.Add(n.anrClient.Close())
   289  	if n.serverCtxCancel != nil {
   290  		n.serverCtxCancel()
   291  	}
   292  	return errs.Err
   293  }
   294  
   295  // CloseClient closes the connection between the ANR client and server without terminating the
   296  // running network.
   297  func (n *NetworkManager) CloseClient() error {
   298  	if n.anrClient == nil {
   299  		return nil
   300  	}
   301  	err := n.anrClient.Close()
   302  	n.anrClient = nil
   303  	return err
   304  }
   305  
   306  // GetSubnets returns the IDs of the currently running subnets
   307  func (n *NetworkManager) GetSubnets() []ids.ID {
   308  	subnetIDs := make([]ids.ID, 0, len(n.subnets))
   309  	for _, subnet := range n.subnets {
   310  		subnetIDs = append(subnetIDs, subnet.SubnetID)
   311  	}
   312  	return subnetIDs
   313  }
   314  
   315  // GetSubnet retrieves the subnet details for the requested subnetID
   316  func (n *NetworkManager) GetSubnet(subnetID ids.ID) (*Subnet, bool) {
   317  	for _, subnet := range n.subnets {
   318  		if subnet.SubnetID == subnetID {
   319  			return subnet, true
   320  		}
   321  	}
   322  	return nil, false
   323  }
   324  
   325  func RegisterFiveNodeSubnetRun() func() *Subnet {
   326  	var (
   327  		config   = NewDefaultANRConfig()
   328  		manager  = NewNetworkManager(config)
   329  		numNodes = 5
   330  	)
   331  
   332  	_ = ginkgo.BeforeSuite(func() {
   333  		// Name 10 new validators (which should have BLS key registered)
   334  		subnetA := make([]string, 0)
   335  		for i := 1; i <= numNodes; i++ {
   336  			subnetA = append(subnetA, fmt.Sprintf("node%d-bls", i))
   337  		}
   338  
   339  		ctx := context.Background()
   340  		var err error
   341  		_, err = manager.StartDefaultNetwork(ctx)
   342  		gomega.Expect(err).Should(gomega.BeNil())
   343  		err = manager.SetupNetwork(
   344  			ctx,
   345  			config.AvalancheGoExecPath,
   346  			[]*rpcpb.BlockchainSpec{
   347  				{
   348  					VmName:      evm.IDStr,
   349  					Genesis:     "./tests/load/genesis/genesis.json",
   350  					ChainConfig: "",
   351  					SubnetSpec: &rpcpb.SubnetSpec{
   352  						Participants: subnetA,
   353  					},
   354  				},
   355  			},
   356  		)
   357  		gomega.Expect(err).Should(gomega.BeNil())
   358  	})
   359  
   360  	var _ = ginkgo.AfterSuite(func() {
   361  		gomega.Expect(manager).ShouldNot(gomega.BeNil())
   362  		gomega.Expect(manager.TeardownNetwork()).Should(gomega.BeNil())
   363  		// TODO: bootstrap an additional node to ensure that we can bootstrap the test data correctly
   364  	})
   365  
   366  	return func() *Subnet {
   367  		subnetIDs := manager.GetSubnets()
   368  		gomega.Expect(len(subnetIDs)).Should(gomega.Equal(1))
   369  		subnetID := subnetIDs[0]
   370  		subnetDetails, ok := manager.GetSubnet(subnetID)
   371  		gomega.Expect(ok).Should(gomega.BeTrue())
   372  		return subnetDetails
   373  	}
   374  }