From 53de98f9fc092c1c8217fc6f523d3633472beec1 Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Tue, 19 Sep 2023 21:49:03 +0200 Subject: [PATCH] e2e: Add warp test with xsvm --- scripts/tests.e2e.sh | 3 + tests/e2e/e2e.go | 18 +- tests/e2e/e2e_test.go | 3 +- tests/e2e/p/warp.go | 143 ++++++++++ tests/fixture/testnet/config.go | 11 +- tests/fixture/testnet/local/network.go | 91 ++++-- tests/fixture/testnet/subnets.go | 369 +++++++++++++++++++++++++ 7 files changed, 608 insertions(+), 30 deletions(-) create mode 100644 tests/e2e/p/warp.go create mode 100644 tests/fixture/testnet/subnets.go diff --git a/scripts/tests.e2e.sh b/scripts/tests.e2e.sh index fd78ec45093f..53306549e413 100755 --- a/scripts/tests.e2e.sh +++ b/scripts/tests.e2e.sh @@ -25,6 +25,9 @@ go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.1.4 ACK_GINKGO_RC=true ginkgo build ./tests/e2e ./tests/e2e/e2e.test --help +# Enable subnet testing by building xsvm +./scripts/build_xsvm.sh + ################################# E2E_USE_PERSISTENT_NETWORK="${E2E_USE_PERSISTENT_NETWORK:-}" TESTNETCTL_NETWORK_DIR="${TESTNETCTL_NETWORK_DIR:-}" diff --git a/tests/e2e/e2e.go b/tests/e2e/e2e.go index 5345a2df7b29..8a8beb316252 100644 --- a/tests/e2e/e2e.go +++ b/tests/e2e/e2e.go @@ -192,14 +192,7 @@ func AddEphemeralNode(network testnet.Network, flags testnet.FlagsMap) testnet.N node, err := network.AddEphemeralNode(ginkgo.GinkgoWriter, flags) require.NoError(err) - // Ensure node is stopped on teardown. It's configuration is not removed to enable - // collection in CI to aid in troubleshooting failures. - ginkgo.DeferCleanup(func() { - tests.Outf("shutting down ephemeral node %q\n", node.GetID()) - ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) - defer cancel() - require.NoError(node.Stop(ctx)) - }) + RegisterNodeforCleanup(node) return node } @@ -254,3 +247,12 @@ func WithSuggestedGasPrice(ethClient ethclient.Client) common.Option { baseFee := SuggestGasPrice(ethClient) return common.WithBaseFee(baseFee) } + +func RegisterNodeforCleanup(node testnet.Node) { + ginkgo.DeferCleanup(func() { + tests.Outf("shutting down ephemeral node %q\n", node.GetID()) + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + require.NoError(ginkgo.GinkgoT(), node.Stop(ctx)) + }) +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 0e5a880ce5ae..a5f9ff675c94 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -77,11 +77,10 @@ var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { // Load or create a test network var network *local.LocalNetwork if len(persistentNetworkDir) > 0 { - tests.Outf("{{yellow}}Using a pre-existing network configured at %s{{/}}\n", persistentNetworkDir) - var err error network, err = local.ReadNetwork(persistentNetworkDir) require.NoError(err) + tests.Outf("{{yellow}}Using a pre-existing network configured at %s{{/}}\n", network.Dir) } else { tests.Outf("{{magenta}}Starting network with %q{{/}}\n", avalancheGoExecPath) diff --git a/tests/e2e/p/warp.go b/tests/e2e/p/warp.go new file mode 100644 index 000000000000..193470309ebd --- /dev/null +++ b/tests/e2e/p/warp.go @@ -0,0 +1,143 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "errors" + "fmt" + "math" + "os" + "path/filepath" + + ginkgo "github.com/onsi/ginkgo/v2" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/tests/e2e" + "github.com/ava-labs/avalanchego/tests/fixture/testnet" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/export" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/importtx" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/transfer" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" +) + +var _ = e2e.DescribePChain("[Warp]", func() { + require := require.New(ginkgo.GinkgoT()) + + ginkgo.It("should support transfers between subnets", func() { + // TODO(marun) make the plugin path configurable + pluginDir := filepath.Join(os.Getenv("GOPATH"), "src/github.com/ava-labs/avalanchego/build/plugins") + if fileInfo, err := os.Stat(pluginDir); errors.Is(err, os.ErrNotExist) || !fileInfo.IsDir() { + ginkgo.Skip(fmt.Sprintf("invalid plugin dir %s", pluginDir)) + } + + ginkgo.By("creating wallet with a funded key to create subnets with") + nodeURI := e2e.Env.GetRandomNodeURI() + keychain := e2e.Env.NewKeychain(1) + privateKey := keychain.Keys[0] + baseWallet := e2e.Env.NewWallet(keychain, nodeURI) + pWallet := baseWallet.P() + + ginkgo.By("Defining the configuration for an xsvm subnet") + genesisBytes, err := genesis.Codec.Marshal(genesis.Version, &genesis.Genesis{ + Timestamp: 0, + Allocations: []genesis.Allocation{ + { + Address: privateKey.Address(), + Balance: math.MaxUint64, + }, + }, + }) + require.NoError(err) + subnetSpec := testnet.SubnetSpec{ + Blockchains: []testnet.BlockchainSpec{ + { + VMName: "xsvm", + Genesis: genesisBytes, + }, + }, + Nodes: []testnet.NodeSpec{ + { + Flags: testnet.FlagsMap{ + config.PluginDirKey: pluginDir, + }, + Count: testnet.DefaultNodeCount, + }, + }, + } + + // TODO(marun) Simplify this call for e2e + ginkgo.By("creating 2 xsvm subnets") + network := e2e.Env.GetNetwork() + subnets, err := testnet.CreateSubnets( + ginkgo.GinkgoWriter, + e2e.DefaultTimeout, + pWallet, + privateKey.Address(), + network, + e2e.RegisterNodeforCleanup, + subnetSpec, + subnetSpec, + ) + require.NoError(err) + + sourceSubnet := subnets[0] + sourceChainID := sourceSubnet.BlockchainIDs[0] + destinationSubnet := subnets[1] + destinationChainID := destinationSubnet.BlockchainIDs[0] + + ginkgo.By(fmt.Sprintf("exporting from blockchain %s on subnet %s", sourceChainID, sourceSubnet.ID)) + exportTxStatus, err := export.Export( + e2e.DefaultContext(), + &export.Config{ + URI: sourceSubnet.Nodes[0].GetProcessContext().URI, + SourceChainID: sourceChainID, + DestinationChainID: destinationChainID, + Amount: units.Schmeckle, + To: privateKey.Address(), + PrivateKey: privateKey, + }, + ) + require.NoError(err) + + ginkgo.By(fmt.Sprintf("issuing transactions on chain %s on subnet %s to activate snowman++ consensus", + destinationChainID, destinationSubnet.ID)) + for i := 0; i < 3; i++ { + _, err = transfer.Transfer( + e2e.DefaultContext(), + &transfer.Config{ + URI: destinationSubnet.Nodes[0].GetProcessContext().URI, + ChainID: destinationChainID, + AssetID: destinationChainID, + Amount: units.Schmeckle, + To: privateKey.Address(), + PrivateKey: privateKey, + }, + ) + require.NoError(err) + } + + ginkgo.By(fmt.Sprintf("importing to blockchain %s on subnet %s", sourceChainID, sourceSubnet.ID)) + sourceURIs := make([]string, len(sourceSubnet.Nodes)) + for i, node := range sourceSubnet.Nodes { + sourceURIs[i] = node.GetProcessContext().URI + } + _, err = importtx.Import( + e2e.DefaultContext(), + &importtx.Config{ + URI: destinationSubnet.Nodes[0].GetProcessContext().URI, + SourceURIs: sourceURIs, + SourceChainID: sourceChainID.String(), + DestinationChainID: destinationChainID.String(), + TxID: exportTxStatus.TxID, + PrivateKey: privateKey, + }, + ) + require.NoError(err) + + // TODO(marun) Verify the balances on both chains + }) +}) diff --git a/tests/fixture/testnet/config.go b/tests/fixture/testnet/config.go index 74c4e09692f6..a264b260f8a5 100644 --- a/tests/fixture/testnet/config.go +++ b/tests/fixture/testnet/config.go @@ -98,6 +98,15 @@ func (f FlagsMap) Write(path string, description string) error { return nil } +// Return a deep copy of the flags map. +func (f FlagsMap) Copy() FlagsMap { + newMap := make(FlagsMap, len(f)) + for k, v := range f { + newMap[k] = v + } + return newMap +} + // Utility function simplifying construction of a FlagsMap from a file. func ReadFlagsMap(path string, description string) (*FlagsMap, error) { bytes, err := os.ReadFile(path) @@ -120,7 +129,7 @@ func DefaultJSONMarshal(v interface{}) ([]byte, error) { // common to all nodes in a given network. type NetworkConfig struct { Genesis *genesis.UnparsedConfig - CChainConfig FlagsMap + ChainConfigs map[string]FlagsMap DefaultFlags FlagsMap FundedKeys []*secp256k1.PrivateKey } diff --git a/tests/fixture/testnet/local/network.go b/tests/fixture/testnet/local/network.go index 45a420e694c7..c0bd99dc7263 100644 --- a/tests/fixture/testnet/local/network.go +++ b/tests/fixture/testnet/local/network.go @@ -31,6 +31,8 @@ const ( networkHealthCheckInterval = 200 * time.Millisecond defaultEphemeralDirName = "ephemeral" + + defaultChainConfigFilename = "config.json" ) var ( @@ -110,6 +112,9 @@ func (ln *LocalNetwork) GetNodes() []testnet.Node { func (ln *LocalNetwork) AddEphemeralNode(w io.Writer, flags testnet.FlagsMap) (testnet.Node, error) { if flags == nil { flags = testnet.FlagsMap{} + } else { + // Avoid modifying the input flags map + flags = flags.Copy() } return ln.AddLocalNode(w, &LocalNode{ NodeConfig: testnet.NodeConfig{ @@ -202,7 +207,19 @@ func StartNetwork( // Read a network from the provided directory. func ReadNetwork(dir string) (*LocalNetwork, error) { - network := &LocalNetwork{Dir: dir} + // Ensure a real and absolute network dir so that node + // configuration that embeds the network path will continue to + // work regardless of symlink and working directory changes. + absDir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + realDir, err := filepath.EvalSymlinks(absDir) + if err != nil { + return nil, err + } + + network := &LocalNetwork{Dir: realDir} if err := network.ReadAll(); err != nil { return nil, fmt.Errorf("failed to read local network: %w", err) } @@ -269,8 +286,11 @@ func (ln *LocalNetwork) PopulateLocalNetworkConfig(networkID uint32, nodeCount i return err } - if ln.CChainConfig == nil { - ln.CChainConfig = LocalCChainConfig() + if _, ok := ln.ChainConfigs["C"]; !ok { + if ln.ChainConfigs == nil { + ln.ChainConfigs = map[string]testnet.FlagsMap{} + } + ln.ChainConfigs["C"] = LocalCChainConfig() } // Default flags need to be set in advance of node config @@ -474,26 +494,59 @@ func (ln *LocalNetwork) GetChainConfigDir() string { return filepath.Join(ln.Dir, "chains") } -func (ln *LocalNetwork) GetCChainConfigPath() string { - return filepath.Join(ln.GetChainConfigDir(), "C", "config.json") -} - -func (ln *LocalNetwork) ReadCChainConfig() error { - chainConfig, err := testnet.ReadFlagsMap(ln.GetCChainConfigPath(), "C-Chain config") +func (ln *LocalNetwork) ReadChainConfigs() error { + baseChainConfigDir := ln.GetChainConfigDir() + entries, err := os.ReadDir(baseChainConfigDir) if err != nil { - return err + return fmt.Errorf("failed to read chain config dir: %w", err) + } + + // Clear the map of data that may end up stale (e.g. if a given + // chain is in the map but no longer exists on disk) + ln.ChainConfigs = map[string]testnet.FlagsMap{} + + for _, entry := range entries { + if !entry.IsDir() { + // Chain config files are expected to be nested under a + // directory with the name of the chain alias. + continue + } + chainAlias := entry.Name() + configPath := filepath.Join(baseChainConfigDir, chainAlias, defaultChainConfigFilename) + if _, err := os.Stat(configPath); os.IsNotExist(err) { + // No config file present + continue + } + chainConfig, err := testnet.ReadFlagsMap(configPath, fmt.Sprintf("%s chain config", chainAlias)) + if err != nil { + return err + } + ln.ChainConfigs[chainAlias] = *chainConfig } - ln.CChainConfig = *chainConfig + return nil } -func (ln *LocalNetwork) WriteCChainConfig() error { - path := ln.GetCChainConfigPath() - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, perms.ReadWriteExecute); err != nil { - return fmt.Errorf("failed to create C-Chain config dir: %w", err) +func (ln *LocalNetwork) WriteChainConfigs() error { + baseChainConfigDir := ln.GetChainConfigDir() + + for chainAlias, chainConfig := range ln.ChainConfigs { + // Create the directory + chainConfigDir := filepath.Join(baseChainConfigDir, chainAlias) + if err := os.MkdirAll(chainConfigDir, perms.ReadWriteExecute); err != nil { + return fmt.Errorf("failed to create %s chain config dir: %w", chainAlias, err) + } + + // Write the file + path := filepath.Join(chainConfigDir, defaultChainConfigFilename) + if err := chainConfig.Write(path, fmt.Sprintf("%s chain config", chainAlias)); err != nil { + return err + } } - return ln.CChainConfig.Write(path, "C-Chain config") + + // TODO(marun) Ensure the removal of chain aliases that aren't present in the map + + return nil } // Used to marshal/unmarshal persistent local network defaults. @@ -571,7 +624,7 @@ func (ln *LocalNetwork) WriteAll() error { if err := ln.WriteGenesis(); err != nil { return err } - if err := ln.WriteCChainConfig(); err != nil { + if err := ln.WriteChainConfigs(); err != nil { return err } if err := ln.WriteDefaults(); err != nil { @@ -588,7 +641,7 @@ func (ln *LocalNetwork) ReadConfig() error { if err := ln.ReadGenesis(); err != nil { return err } - if err := ln.ReadCChainConfig(); err != nil { + if err := ln.ReadChainConfigs(); err != nil { return err } return ln.ReadDefaults() diff --git a/tests/fixture/testnet/subnets.go b/tests/fixture/testnet/subnets.go new file mode 100644 index 000000000000..507009b3a639 --- /dev/null +++ b/tests/fixture/testnet/subnets.go @@ -0,0 +1,369 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package testnet + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "time" + + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/chain/p" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" +) + +const ( + // TODO(marun) Need to reconcile these constants with those in e2e.go + + DefaultTimeout = 2 * time.Minute + + // Interval appropriate for network operations that should be + // retried periodically but not too often. + DefaultPollingInterval = 500 * time.Millisecond + + // Start time must be a minimum of 15s ahead of the current time + // or validator addition will fail. + DefaultValidatorStartTimeDiff = 20 * time.Second +) + +// Specifies the configuration for a new subnet. +type SubnetSpec struct { + SubnetConfig string + Blockchains []BlockchainSpec + Nodes []NodeSpec +} + +// Specifies the configuration for a new blockchain. +type BlockchainSpec struct { + VMName string + ChainConfig string + Genesis []byte +} + +// Specifies the configuration for one or more nodes. +type NodeSpec struct { + Flags FlagsMap + Count int +} + +// Collects the result of subnet creation +type CreatedSubnet struct { + ID ids.ID + BlockchainIDs []ids.ID + Nodes []Node +} + +func GetVMID(vmName string) (ids.ID, error) { + if len(vmName) > 32 { + return ids.Empty, fmt.Errorf("VM name must be <= 32 bytes, found %d", len(vmName)) + } + b := make([]byte, 32) + copy(b, []byte(vmName)) + return ids.ToID(b) +} + +type NodeCleanupFunc func(Node) + +func CreateSubnets( + w io.Writer, + txTimeout time.Duration, + pWallet p.Wallet, + owningAddress ids.ShortID, + network Network, + nodeCleanupFunc NodeCleanupFunc, + subnetSpecs ...SubnetSpec, +) ( + []CreatedSubnet, + error, +) { + createdSubnets := make([]CreatedSubnet, len(subnetSpecs)) + for i, subnetSpec := range subnetSpecs { + createdSubnet, err := CreateSubnet( + w, + context.Background(), + txTimeout, + pWallet, + owningAddress, + network, + nodeCleanupFunc, + subnetSpec, + ) + if err != nil { + return nil, fmt.Errorf("failed to create subnet %d: %w", i, err) + } + createdSubnets[i] = *createdSubnet + } + + allNodes := []Node{} + for _, createdSubnet := range createdSubnets { + allNodes = append(allNodes, createdSubnet.Nodes...) + } + + if _, err := fmt.Fprintf(w, "waiting for new nodes to report healthy\n"); err != nil { + return nil, err + } + // Wait to check health until after nodes have been started and added as validators to + // minimize the duration required for both nodes to report healthy. + for _, node := range allNodes { + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + err := WaitForHealthy(ctx, node) + if err != nil { + return nil, err + } + if _, err := fmt.Fprintf(w, " %s is healthy @ %s\n", node.GetID(), node.GetProcessContext().URI); err != nil { + return nil, err + } + } + + // Wait for new nodes to become active validators + + if _, err := fmt.Fprintf(w, "waiting for new nodes to become active validators of the primary network\n"); err != nil { + return nil, err + } + nodeIDs := make([]ids.NodeID, len(allNodes)) + for i := range allNodes { + nodeIDs[i] = allNodes[i].GetID() + } + nodeURI := allNodes[0].GetProcessContext().URI + if err := waitForActiveValidators(w, nodeURI, constants.PrimaryNetworkID, "the primary network", nodeIDs); err != nil { + return nil, err + } + + for _, createdSubnet := range createdSubnets { + nodeIDs := make([]ids.NodeID, len(createdSubnet.Nodes)) + for i, node := range createdSubnet.Nodes { + nodeIDs[i] = node.GetID() + } + subnetDescription := fmt.Sprintf("subnet %s", createdSubnet.ID) + if _, err := fmt.Fprintf(w, "waiting for new nodes to become active validators of %s\n", subnetDescription); err != nil { + return nil, err + } + if err := waitForActiveValidators(w, nodeURI, createdSubnet.ID, subnetDescription, nodeIDs); err != nil { + return nil, err + } + } + + return createdSubnets, nil +} + +func CreateSubnet( + w io.Writer, + rootContext context.Context, + txTimeout time.Duration, + pWallet p.Wallet, + owningAddress ids.ShortID, + network Network, + nodeCleanupFunc NodeCleanupFunc, + spec SubnetSpec, +) ( + *CreatedSubnet, + error, +) { + owner := &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + owningAddress, + }, + } + + if _, err := fmt.Fprintf(w, "creating a new subnet\n"); err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(rootContext, txTimeout) + defer cancel() + subnetTx, err := pWallet.IssueCreateSubnetTx( + owner, + common.WithContext(ctx), + ) + if err != nil { + return nil, fmt.Errorf("failed to create subnet: %w", err) + } + subnetID := subnetTx.ID() + + blockchainIDs := make([]ids.ID, len(spec.Blockchains)) + for i, blockchainSpec := range spec.Blockchains { + if _, err := fmt.Fprintf(w, "creating blockchain on subnet %s\n", subnetID); err != nil { + return nil, err + } + vmID, err := GetVMID(blockchainSpec.VMName) + if err != nil { + return nil, fmt.Errorf("failed to determine VM ID for blockchain: %w", err) + } + ctx, cancel := context.WithTimeout(rootContext, txTimeout) + defer cancel() + createChainTx, err := pWallet.IssueCreateChainTx( + subnetID, + blockchainSpec.Genesis, + vmID, + nil, + blockchainSpec.VMName, + common.WithContext(ctx), + ) + if err != nil { + return nil, fmt.Errorf("failed to create blockchain: %w", err) + } + blockchainIDs[i] = createChainTx.ID() + } + + if _, err := fmt.Fprintf(w, "creating nodes for subnet %s\n", subnetID); err != nil { + return nil, err + } + allNodes := []Node{} + for _, nodeSpec := range spec.Nodes { + // Copy before modifying + flags := nodeSpec.Flags.Copy() + flags[config.TrackSubnetsKey] = subnetID.String() + for i := 0; i < nodeSpec.Count; i++ { + node, err := network.AddEphemeralNode(w, flags) + if err != nil { + return nil, err + } + nodeCleanupFunc(node) + allNodes = append(allNodes, node) + } + } + + // Add new nodes as validators of the primary network + delegationPercent := 0.10 // 10% + delegationFee := uint32(reward.PercentDenominator * delegationPercent) + + for _, node := range allNodes { + nodeID := node.GetID() + + if _, err := fmt.Fprintf(w, "deriving proof of possession for %s\n", nodeID); err != nil { + return nil, err + } + signingKey, err := node.GetConfig().Flags.GetStringVal(config.StakingSignerKeyContentKey) + if err != nil { + return nil, err + } + signingKeyBytes, err := base64.StdEncoding.DecodeString(signingKey) + if err != nil { + return nil, err + } + secretKey, err := bls.SecretKeyFromBytes(signingKeyBytes) + if err != nil { + return nil, err + } + proofOfPossession := signer.NewProofOfPossession(secretKey) + + // The end time will be reused as the end time for subnet validation + now := time.Now() + endTime := uint64(now.Add(genesis.LocalParams.MaxStakeDuration).Unix()) + + if _, err := fmt.Fprintf(w, "adding %s as a validator of the primary network\n", nodeID); err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(rootContext, txTimeout) + defer cancel() + _, err = pWallet.IssueAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(now.Add(DefaultValidatorStartTimeDiff).Unix()), + End: endTime, + Wght: genesis.LocalParams.MinValidatorStake, + }, + Subnet: ids.Empty, + }, + proofOfPossession, + pWallet.AVAXAssetID(), + owner, // validation owner + owner, // delegation owner + delegationFee, + common.WithContext(ctx), + ) + if err != nil { + return nil, err + } + + if _, err := fmt.Fprintf(w, "adding %s as a validator of subnet %s\n", nodeID, subnetID); err != nil { + return nil, err + } + ctx, cancel = context.WithTimeout(rootContext, txTimeout) + defer cancel() + _, err = pWallet.IssueAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(time.Now().Add(DefaultValidatorStartTimeDiff).Unix()), + End: endTime, + Wght: units.Schmeckle, + }, + Subnet: subnetID, + }, + common.WithContext(ctx), + ) + if err != nil { + return nil, err + } + } + + return &CreatedSubnet{ + ID: subnetID, + BlockchainIDs: blockchainIDs, + Nodes: allNodes, + }, nil +} + +func waitForActiveValidators(w io.Writer, uri string, subnetID ids.ID, subnetDescription string, nodeIDs []ids.NodeID) error { + pChainClient := platformvm.NewClient(uri) + + ticker := time.NewTicker(DefaultPollingInterval) + defer ticker.Stop() + + if _, err := fmt.Fprintf(w, " "); err != nil { + return err + } + + rootContext, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + for { + if _, err := fmt.Fprintf(w, "."); err != nil { + return err + } + ctx, cancel := context.WithTimeout(rootContext, DefaultTimeout) + defer cancel() + validators, err := pChainClient.GetCurrentValidators(rootContext, subnetID, nil) + if err != nil { + return err + } + validatorSet := set.NewSet[ids.NodeID](len(validators)) + for _, validator := range validators { + validatorSet.Add(validator.NodeID) + } + allActive := true + for _, nodeID := range nodeIDs { + if !validatorSet.Contains(nodeID) { + allActive = false + } + } + if allActive { + if _, err := fmt.Fprintf(w, "\n saw the expected active validators of %s\n", subnetDescription); err != nil { + return err + } + return nil + } + + select { + case <-ctx.Done(): + return fmt.Errorf("failed to see the expected active validators of %s before timeout", subnetDescription) + case <-ticker.C: + } + } +}