func (tg *TestGroup) AddNodes(nps ...node.NodeParams) ([]*TestNode, error) {
	newNodes := make(map[*TestNode]struct{})
	newHosts := make(map[*TestNode]struct{})
	newRenters := make(map[*TestNode]struct{})
	newMiners := make(map[*TestNode]struct{})
	for _, np := range nps {
		if np.Dir == "" {
			np.Dir = randomDir()
		}
		node, err := NewCleanNode(np)
		if err != nil {
			return mapToSlice(newNodes), build.ExtendErr("failed to create host", err)
		}
		tg.nodes[node] = struct{}{}
		newNodes[node] = struct{}{}
		if np.Host != nil || np.CreateHost {
			tg.hosts[node] = struct{}{}
			newHosts[node] = struct{}{}
		}
		if np.Renter != nil || np.CreateRenter {
			tg.renters[node] = struct{}{}
			newRenters[node] = struct{}{}
		}
		if np.Miner != nil || np.CreateMiner {
			tg.miners[node] = struct{}{}
			newMiners[node] = struct{}{}
		}
	}
	return mapToSlice(newNodes), tg.setupNodes(newHosts, newNodes, newRenters)
}
AddNodes creates a node and adds it to the group.
package siatest
import (
	"math"
	"reflect"
	"strconv"
	"sync"
	"time"
	"github.com/NebulousLabs/Sia/build"
	"github.com/NebulousLabs/Sia/modules"
	"github.com/NebulousLabs/Sia/node"
	"github.com/NebulousLabs/Sia/node/api/client"
	"github.com/NebulousLabs/Sia/types"
	"github.com/NebulousLabs/errors"
	"github.com/NebulousLabs/fastrand"
)
type (
	GroupParams struct {
		Hosts   int  
		Renters int  
		Miners  int  
	}
	TestGroup struct {
		nodes   map[*TestNode]struct{}
		hosts   map[*TestNode]struct{}
		renters map[*TestNode]struct{}
		miners  map[*TestNode]struct{}
		dir string
	}
)
var (
	DefaultAllowance = modules.Allowance{
		Funds:       types.SiacoinPrecision.Mul64(1e3),
		Hosts:       5,
		Period:      50,
		RenewWindow: 24,
	}
)
func NewGroup(nodeParams ...node.NodeParams) (*TestGroup, error) {
	tg := &TestGroup{
		nodes:   make(map[*TestNode]struct{}),
		hosts:   make(map[*TestNode]struct{}),
		renters: make(map[*TestNode]struct{}),
		miners:  make(map[*TestNode]struct{}),
	}
	nodes := make([]*TestNode, 0, len(nodeParams))
	for _, np := range nodeParams {
		node, err := NewCleanNode(np)
		if err != nil {
			return nil, errors.AddContext(err, "failed to create clean node")
		}
		tg.nodes[node] = struct{}{}
		nodes = append(nodes, node)
		if np.Host != nil || np.CreateHost {
			tg.hosts[node] = struct{}{}
		}
		if np.Renter != nil || np.CreateRenter {
			tg.renters[node] = struct{}{}
		}
		if np.Miner != nil || np.CreateMiner {
			tg.miners[node] = struct{}{}
		}
	}
	if len(tg.miners) == 0 {
		return nil, errors.New("cannot fund group without miners")
	}
	miner := tg.Miners()[0]
	for i := types.BlockHeight(0); i <= types.MaturityDelay+types.TaxHardforkHeight; i++ {
		if err := miner.MineBlock(); err != nil {
			return nil, errors.AddContext(err, "failed to mine block for funding")
		}
	}
	return tg, tg.setupNodes(tg.hosts, tg.nodes, tg.renters)
}
func NewGroupFromTemplate(groupParams GroupParams) (*TestGroup, error) {
	var params []node.NodeParams
	for i := 0; i < groupParams.Hosts; i++ {
		params = append(params, node.Host(randomDir()))
	}
	for i := 0; i < groupParams.Renters; i++ {
		params = append(params, node.Renter(randomDir()))
	}
	for i := 0; i < groupParams.Miners; i++ {
		params = append(params, Miner(randomDir()))
	}
	return NewGroup(params...)
}
func addStorageFolderToHosts(hosts map[*TestNode]struct{}) error {
	errs := make([]error, len(hosts))
	wg := new(sync.WaitGroup)
	i := 0
	for host := range hosts {
		wg.Add(1)
		go func(i int, host *TestNode) {
			errs[i] = host.HostStorageFoldersAddPost(host.Dir, 1048576)
			wg.Done()
		}(i, host)
		i++
	}
	wg.Wait()
	return errors.Compose(errs...)
}
func announceHosts(hosts map[*TestNode]struct{}) error {
	for host := range hosts {
		if err := host.HostModifySettingPost(client.HostParamAcceptingContracts, true); err != nil {
			return errors.AddContext(err, "failed to set host to accepting contracts")
		}
		if err := host.HostAnnouncePost(); err != nil {
			return errors.AddContext(err, "failed to announce host")
		}
	}
	return nil
}
func fullyConnectNodes(nodes []*TestNode) error {
	for i, nodeA := range nodes {
		for _, nodeB := range nodes[i+1:] {
			err := build.Retry(100, 100*time.Millisecond, func() error {
				if err := nodeA.GatewayConnectPost(nodeB.GatewayAddress()); err != nil && err != client.ErrPeerExists {
					return errors.AddContext(err, "failed to connect to peer")
				}
				isPeer1, err1 := nodeA.hasPeer(nodeB)
				isPeer2, err2 := nodeB.hasPeer(nodeA)
				if err1 != nil || err2 != nil {
					return build.ExtendErr("couldn't determine if nodeA and nodeB are connected",
						errors.Compose(err1, err2))
				}
				if isPeer1 && isPeer2 {
					return nil
				}
				return errors.New("nodeA and nodeB are not peers of each other")
			})
			if err != nil {
				return err
			}
		}
	}
	return nil
}
func fundNodes(miner *TestNode, nodes map[*TestNode]struct{}) error {
	wg, err := miner.WalletGet()
	if err != nil {
		return errors.AddContext(err, "failed to get miner's balance")
	}
	txnsPerNode := uint64(25)
	scos := make([]types.SiacoinOutput, 0, uint64(len(nodes))*txnsPerNode)
	funding := wg.ConfirmedSiacoinBalance.Div64(uint64(len(nodes))).Div64(txnsPerNode + 1)
	for node := range nodes {
		wag, err := node.WalletAddressGet()
		if err != nil {
			return errors.AddContext(err, "failed to get wallet address")
		}
		for i := uint64(0); i < txnsPerNode; i++ {
			scos = append(scos, types.SiacoinOutput{
				Value:      funding,
				UnlockHash: wag.Address,
			})
		}
	}
	_, err = miner.WalletSiacoinsMultiPost(scos)
	if err != nil {
		return errors.AddContext(err, "failed to send funding txn")
	}
	if err := miner.MineBlock(); err != nil {
		return errors.AddContext(err, "failed to mine funding txn")
	}
	for node := range nodes {
		err := Retry(100, 100*time.Millisecond, func() error {
			wtg, err := node.WalletTransactionsGet(0, math.MaxInt32)
			if err != nil {
				return err
			}
			if len(wtg.ConfirmedTransactions) == 0 {
				return errors.New("confirmed transactions should be greater than 0")
			}
			return nil
		})
		if err != nil {
			return err
		}
	}
	return nil
}
func hostsInRenterDBCheck(miner *TestNode, renters map[*TestNode]struct{}, hosts map[*TestNode]struct{}) error {
	for renter := range renters {
		if renter.params.SkipHostDiscovery {
			continue
		}
		for host := range hosts {
			numRetries := 0
			err := Retry(600, 100*time.Millisecond, func() error {
				numRetries++
				if renter == host {
					return nil
				}
				err := errors.AddContext(renter.KnowsHost(host), "renter doesn't know host")
				if err != nil && numRetries%10 == 0 {
					return errors.Compose(err, miner.MineBlock())
				}
				if err != nil {
					return err
				}
				return nil
			})
			if err != nil {
				return build.ExtendErr("not all renters can see all hosts", err)
			}
		}
	}
	return nil
}
func mapToSlice(m map[*TestNode]struct{}) []*TestNode {
	tns := make([]*TestNode, 0, len(m))
	for tn := range m {
		tns = append(tns, tn)
	}
	return tns
}
func randomDir() string {
	dir, err := TestDir(strconv.Itoa(fastrand.Intn(math.MaxInt32)))
	if err != nil {
		panic(errors.AddContext(err, "failed to create testing directory"))
	}
	return dir
}
func setRenterAllowances(renters map[*TestNode]struct{}) error {
	for renter := range renters {
		if renter.params.SkipSetAllowance {
			continue
		}
		allowance := DefaultAllowance
		if !reflect.DeepEqual(renter.params.Allowance, modules.Allowance{}) {
			allowance = renter.params.Allowance
		}
		if err := renter.RenterPostAllowance(allowance); err != nil {
			return err
		}
	}
	return nil
}
func synchronizationCheck(nodes map[*TestNode]struct{}) error {
	var longestChainNode *TestNode
	var longestChain types.BlockHeight
	for n := range nodes {
		ncg, err := n.ConsensusGet()
		if err != nil {
			return err
		}
		if ncg.Height > longestChain {
			longestChain = ncg.Height
			longestChainNode = n
		}
	}
	lcg, err := longestChainNode.ConsensusGet()
	if err != nil {
		return err
	}
	for n := range nodes {
		err := Retry(600, 100*time.Millisecond, func() error {
			ncg, err := n.ConsensusGet()
			if err != nil {
				return err
			}
			if lcg.CurrentBlock == ncg.CurrentBlock {
				return nil
			}
			if lcg.Height != ncg.Height {
				return errors.New("blockHeight doesn't match")
			}
			if lcg.CurrentBlock != ncg.CurrentBlock {
				return errors.New("ids don't match")
			}
			return nil
		})
		if err != nil {
			return err
		}
	}
	return nil
}
func waitForContracts(miner *TestNode, renters map[*TestNode]struct{}, hosts map[*TestNode]struct{}) error {
	hostMap := make(map[string]struct{})
	for host := range hosts {
		pk, err := host.HostPublicKey()
		if err != nil {
			return build.ExtendErr("failed to build hostMap", err)
		}
		hostMap[string(pk.Key)] = struct{}{}
	}
	for renter := range renters {
		numRetries := 0
		rg, err := renter.RenterGet()
		if err != nil {
			return err
		}
		expectedContracts := rg.Settings.Allowance.Hosts
		if uint64(len(hosts)) < expectedContracts {
			expectedContracts = uint64(len(hosts))
		}
		err = Retry(1000, 100, func() error {
			numRetries++
			contracts := uint64(0)
			rc, err := renter.RenterContractsGet()
			if err != nil {
				return err
			}
			for _, c := range rc.ActiveContracts {
				if _, exists := hostMap[string(c.HostPublicKey.Key)]; exists {
					contracts++
				}
			}
			if contracts < expectedContracts {
				if numRetries%10 == 0 {
					if err := miner.MineBlock(); err != nil {
						return err
					}
				}
				return errors.New("renter hasn't formed enough contracts")
			}
			return nil
		})
		if err != nil {
			return err
		}
	}
	return miner.MineBlock()
}
func (tg *TestGroup) AddNodeN(np node.NodeParams, n int) ([]*TestNode, error) {
	nps := make([]node.NodeParams, n)
	for i := 0; i < n; i++ {
		nps[i] = np
	}
	return tg.AddNodes(nps...)
}