func mapToSlice(m map[*TestNode]struct{}) []*TestNode {
	tns := make([]*TestNode, 0, len(m))
	for tn := range m {
		tns = append(tns, tn)
	}
	return tns
}
mapToSlice converts a map of TestNodes into a slice
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
}