github.com/MetalBlockchain/metalgo@v1.11.9/tests/fixture/tmpnet/subnet.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/json"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"time"
    14  
    15  	"github.com/MetalBlockchain/metalgo/ids"
    16  	"github.com/MetalBlockchain/metalgo/utils/constants"
    17  	"github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1"
    18  	"github.com/MetalBlockchain/metalgo/utils/perms"
    19  	"github.com/MetalBlockchain/metalgo/utils/set"
    20  	"github.com/MetalBlockchain/metalgo/utils/units"
    21  	"github.com/MetalBlockchain/metalgo/vms/platformvm"
    22  	"github.com/MetalBlockchain/metalgo/vms/platformvm/txs"
    23  	"github.com/MetalBlockchain/metalgo/vms/secp256k1fx"
    24  	"github.com/MetalBlockchain/metalgo/wallet/subnet/primary"
    25  	"github.com/MetalBlockchain/metalgo/wallet/subnet/primary/common"
    26  )
    27  
    28  const defaultSubnetDirName = "subnets"
    29  
    30  type Chain struct {
    31  	// Set statically
    32  	VMID    ids.ID
    33  	Config  string
    34  	Genesis []byte
    35  
    36  	// Set at runtime
    37  	ChainID      ids.ID
    38  	PreFundedKey *secp256k1.PrivateKey
    39  }
    40  
    41  // Write the chain configuration to the specified directory.
    42  func (c *Chain) WriteConfig(chainDir string) error {
    43  	// TODO(marun) Ensure removal of an existing file if no configuration should be provided
    44  	if len(c.Config) == 0 {
    45  		return nil
    46  	}
    47  
    48  	chainConfigDir := filepath.Join(chainDir, c.ChainID.String())
    49  	if err := os.MkdirAll(chainConfigDir, perms.ReadWriteExecute); err != nil {
    50  		return fmt.Errorf("failed to create chain config dir: %w", err)
    51  	}
    52  
    53  	path := filepath.Join(chainConfigDir, defaultConfigFilename)
    54  	if err := os.WriteFile(path, []byte(c.Config), perms.ReadWrite); err != nil {
    55  		return fmt.Errorf("failed to write chain config: %w", err)
    56  	}
    57  
    58  	return nil
    59  }
    60  
    61  type Subnet struct {
    62  	// A unique string that can be used to refer to the subnet across different temporary
    63  	// networks (since the SubnetID will be different every time the subnet is created)
    64  	Name string
    65  
    66  	Config FlagsMap
    67  
    68  	// The ID of the transaction that created the subnet
    69  	SubnetID ids.ID
    70  
    71  	// The private key that owns the subnet
    72  	OwningKey *secp256k1.PrivateKey
    73  
    74  	// IDs of the nodes responsible for validating the subnet
    75  	ValidatorIDs []ids.NodeID
    76  
    77  	Chains []*Chain
    78  }
    79  
    80  // Retrieves a wallet configured for use with the subnet
    81  func (s *Subnet) GetWallet(ctx context.Context, uri string) (primary.Wallet, error) {
    82  	keychain := secp256k1fx.NewKeychain(s.OwningKey)
    83  
    84  	// Only fetch the subnet transaction if a subnet ID is present. This won't be true when
    85  	// the wallet is first used to create the subnet.
    86  	txIDs := set.Set[ids.ID]{}
    87  	if s.SubnetID != ids.Empty {
    88  		txIDs.Add(s.SubnetID)
    89  	}
    90  
    91  	return primary.MakeWallet(ctx, &primary.WalletConfig{
    92  		URI:              uri,
    93  		AVAXKeychain:     keychain,
    94  		EthKeychain:      keychain,
    95  		PChainTxsToFetch: txIDs,
    96  	})
    97  }
    98  
    99  // Issues the subnet creation transaction and retains the result. The URI of a node is
   100  // required to issue the transaction.
   101  func (s *Subnet) Create(ctx context.Context, uri string) error {
   102  	wallet, err := s.GetWallet(ctx, uri)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	pWallet := wallet.P()
   107  
   108  	subnetTx, err := pWallet.IssueCreateSubnetTx(
   109  		&secp256k1fx.OutputOwners{
   110  			Threshold: 1,
   111  			Addrs: []ids.ShortID{
   112  				s.OwningKey.Address(),
   113  			},
   114  		},
   115  		common.WithContext(ctx),
   116  	)
   117  	if err != nil {
   118  		return fmt.Errorf("failed to create subnet %s: %w", s.Name, err)
   119  	}
   120  	s.SubnetID = subnetTx.ID()
   121  
   122  	return nil
   123  }
   124  
   125  func (s *Subnet) CreateChains(ctx context.Context, w io.Writer, uri string) error {
   126  	wallet, err := s.GetWallet(ctx, uri)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	pWallet := wallet.P()
   131  
   132  	if _, err := fmt.Fprintf(w, "Creating chains for subnet %q\n", s.Name); err != nil {
   133  		return err
   134  	}
   135  
   136  	for _, chain := range s.Chains {
   137  		createChainTx, err := pWallet.IssueCreateChainTx(
   138  			s.SubnetID,
   139  			chain.Genesis,
   140  			chain.VMID,
   141  			nil,
   142  			"",
   143  			common.WithContext(ctx),
   144  		)
   145  		if err != nil {
   146  			return fmt.Errorf("failed to create chain: %w", err)
   147  		}
   148  		chain.ChainID = createChainTx.ID()
   149  
   150  		if _, err := fmt.Fprintf(w, " created chain %q for VM %q on subnet %q\n", chain.ChainID, chain.VMID, s.Name); err != nil {
   151  			return err
   152  		}
   153  	}
   154  	return nil
   155  }
   156  
   157  // Add validators to the subnet
   158  func (s *Subnet) AddValidators(ctx context.Context, w io.Writer, apiURI string, nodes ...*Node) error {
   159  	wallet, err := s.GetWallet(ctx, apiURI)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	pWallet := wallet.P()
   164  
   165  	// Collect the end times for current validators to reuse for subnet validators
   166  	pvmClient := platformvm.NewClient(apiURI)
   167  	validators, err := pvmClient.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil)
   168  	if err != nil {
   169  		return err
   170  	}
   171  	endTimes := make(map[ids.NodeID]uint64)
   172  	for _, validator := range validators {
   173  		endTimes[validator.NodeID] = validator.EndTime
   174  	}
   175  
   176  	startTime := time.Now().Add(DefaultValidatorStartTimeDiff)
   177  	for _, node := range nodes {
   178  		endTime, ok := endTimes[node.NodeID]
   179  		if !ok {
   180  			return fmt.Errorf("failed to find end time for %s", node.NodeID)
   181  		}
   182  
   183  		_, err := pWallet.IssueAddSubnetValidatorTx(
   184  			&txs.SubnetValidator{
   185  				Validator: txs.Validator{
   186  					NodeID: node.NodeID,
   187  					Start:  uint64(startTime.Unix()),
   188  					End:    endTime,
   189  					Wght:   units.Schmeckle,
   190  				},
   191  				Subnet: s.SubnetID,
   192  			},
   193  			common.WithContext(ctx),
   194  		)
   195  		if err != nil {
   196  			return err
   197  		}
   198  
   199  		if _, err := fmt.Fprintf(w, " added %s as validator for subnet `%s`\n", node.NodeID, s.Name); err != nil {
   200  			return err
   201  		}
   202  	}
   203  
   204  	return nil
   205  }
   206  
   207  // Write the subnet configuration to disk
   208  func (s *Subnet) Write(subnetDir string, chainDir string) error {
   209  	if err := os.MkdirAll(subnetDir, perms.ReadWriteExecute); err != nil {
   210  		return fmt.Errorf("failed to create subnet dir: %w", err)
   211  	}
   212  	tmpnetConfigPath := filepath.Join(subnetDir, s.Name+".json")
   213  
   214  	// Since subnets are expected to be serialized for the first time
   215  	// without their chains having been created (i.e. chains will have
   216  	// empty IDs), use the absence of chain IDs as a prompt for a
   217  	// subnet name uniqueness check.
   218  	if len(s.Chains) > 0 && s.Chains[0].ChainID == ids.Empty {
   219  		_, err := os.Stat(tmpnetConfigPath)
   220  		if err != nil && !os.IsNotExist(err) {
   221  			return err
   222  		}
   223  		if err == nil {
   224  			return fmt.Errorf("a subnet with name %s already exists", s.Name)
   225  		}
   226  	}
   227  
   228  	// Write subnet configuration for tmpnet
   229  	bytes, err := DefaultJSONMarshal(s)
   230  	if err != nil {
   231  		return fmt.Errorf("failed to marshal tmpnet subnet %s: %w", s.Name, err)
   232  	}
   233  	if err := os.WriteFile(tmpnetConfigPath, bytes, perms.ReadWrite); err != nil {
   234  		return fmt.Errorf("failed to write tmpnet subnet config %s: %w", s.Name, err)
   235  	}
   236  
   237  	// The subnet and chain configurations for avalanchego can only be written once
   238  	// they have been created since the id of the creating transaction must be
   239  	// included in the path.
   240  	if s.SubnetID == ids.Empty {
   241  		return nil
   242  	}
   243  
   244  	// TODO(marun) Ensure removal of an existing file if no configuration should be provided
   245  	if len(s.Config) > 0 {
   246  		// Write subnet configuration for avalanchego
   247  		bytes, err = DefaultJSONMarshal(s.Config)
   248  		if err != nil {
   249  			return fmt.Errorf("failed to marshal avalanchego subnet config %s: %w", s.Name, err)
   250  		}
   251  
   252  		avgoConfigDir := filepath.Join(subnetDir, s.SubnetID.String())
   253  		if err := os.MkdirAll(avgoConfigDir, perms.ReadWriteExecute); err != nil {
   254  			return fmt.Errorf("failed to create avalanchego subnet config dir: %w", err)
   255  		}
   256  
   257  		avgoConfigPath := filepath.Join(avgoConfigDir, defaultConfigFilename)
   258  		if err := os.WriteFile(avgoConfigPath, bytes, perms.ReadWrite); err != nil {
   259  			return fmt.Errorf("failed to write avalanchego subnet config %s: %w", s.Name, err)
   260  		}
   261  	}
   262  
   263  	for _, chain := range s.Chains {
   264  		if err := chain.WriteConfig(chainDir); err != nil {
   265  			return err
   266  		}
   267  	}
   268  
   269  	return nil
   270  }
   271  
   272  // HasChainConfig indicates whether at least one of the subnet's
   273  // chains have explicit configuration. This can be used to determine
   274  // whether validator restart is required after chain creation to
   275  // ensure that chains are configured correctly.
   276  func (s *Subnet) HasChainConfig() bool {
   277  	for _, chain := range s.Chains {
   278  		if len(chain.Config) > 0 {
   279  			return true
   280  		}
   281  	}
   282  	return false
   283  }
   284  
   285  func waitForActiveValidators(
   286  	ctx context.Context,
   287  	w io.Writer,
   288  	pChainClient platformvm.Client,
   289  	subnet *Subnet,
   290  ) error {
   291  	ticker := time.NewTicker(DefaultPollingInterval)
   292  	defer ticker.Stop()
   293  
   294  	if _, err := fmt.Fprintf(w, "Waiting for validators of subnet %q to become active\n", subnet.Name); err != nil {
   295  		return err
   296  	}
   297  
   298  	if _, err := fmt.Fprint(w, " "); err != nil {
   299  		return err
   300  	}
   301  
   302  	for {
   303  		if _, err := fmt.Fprint(w, "."); err != nil {
   304  			return err
   305  		}
   306  		validators, err := pChainClient.GetCurrentValidators(ctx, subnet.SubnetID, nil)
   307  		if err != nil {
   308  			return err
   309  		}
   310  		validatorSet := set.NewSet[ids.NodeID](len(validators))
   311  		for _, validator := range validators {
   312  			validatorSet.Add(validator.NodeID)
   313  		}
   314  		allActive := true
   315  		for _, validatorID := range subnet.ValidatorIDs {
   316  			if !validatorSet.Contains(validatorID) {
   317  				allActive = false
   318  			}
   319  		}
   320  		if allActive {
   321  			if _, err := fmt.Fprintf(w, "\n saw the expected active validators of subnet %q\n", subnet.Name); err != nil {
   322  				return err
   323  			}
   324  			return nil
   325  		}
   326  
   327  		select {
   328  		case <-ctx.Done():
   329  			return fmt.Errorf("failed to see the expected active validators of subnet %q before timeout", subnet.Name)
   330  		case <-ticker.C:
   331  		}
   332  	}
   333  }
   334  
   335  // Reads subnets from [network dir]/subnets/[subnet name].json
   336  func readSubnets(subnetDir string) ([]*Subnet, error) {
   337  	if _, err := os.Stat(subnetDir); os.IsNotExist(err) {
   338  		return nil, nil
   339  	} else if err != nil {
   340  		return nil, err
   341  	}
   342  
   343  	entries, err := os.ReadDir(subnetDir)
   344  	if err != nil {
   345  		return nil, fmt.Errorf("failed to read subnet dir: %w", err)
   346  	}
   347  
   348  	subnets := []*Subnet{}
   349  	for _, entry := range entries {
   350  		if entry.IsDir() {
   351  			// Looking only for files
   352  			continue
   353  		}
   354  		if filepath.Ext(entry.Name()) != ".json" {
   355  			// Subnet files should have a .json extension
   356  			continue
   357  		}
   358  
   359  		subnetPath := filepath.Join(subnetDir, entry.Name())
   360  		bytes, err := os.ReadFile(subnetPath)
   361  		if err != nil {
   362  			return nil, fmt.Errorf("failed to read subnet file %s: %w", subnetPath, err)
   363  		}
   364  		subnet := &Subnet{}
   365  		if err := json.Unmarshal(bytes, subnet); err != nil {
   366  			return nil, fmt.Errorf("failed to unmarshal subnet from %s: %w", subnetPath, err)
   367  		}
   368  		subnets = append(subnets, subnet)
   369  	}
   370  
   371  	return subnets, nil
   372  }