diff --git a/cmd/beekeeper/cmd/cluster.go b/cmd/beekeeper/cmd/cluster.go index 47e31f235..ae4cf0341 100644 --- a/cmd/beekeeper/cmd/cluster.go +++ b/cmd/beekeeper/cmd/cluster.go @@ -2,264 +2,314 @@ package cmd import ( "context" + "errors" "fmt" "github.com/ethersphere/beekeeper/pkg/config" + "github.com/ethersphere/beekeeper/pkg/logging" "github.com/ethersphere/beekeeper/pkg/orchestration" orchestrationK8S "github.com/ethersphere/beekeeper/pkg/orchestration/k8s" - "golang.org/x/sync/errgroup" + "github.com/ethersphere/node-funder/pkg/funder" ) +const bootnodeMode string = "bootnode" + +type nodeResult struct { + ethAddress string + err error +} + func (c *command) deleteCluster(ctx context.Context, clusterName string, cfg *config.Config, deleteStorage bool) (err error) { clusterConfig, ok := cfg.Clusters[clusterName] if !ok { return fmt.Errorf("cluster %s not defined", clusterName) } - clusterOptions := clusterConfig.Export() - clusterOptions.K8SClient = c.k8sClient - clusterOptions.SwapClient = c.swapClient - - cluster := orchestrationK8S.NewCluster(clusterConfig.GetName(), clusterOptions, c.log) + cluster := configureCluster(clusterConfig, c) // delete node groups - for ng, v := range clusterConfig.GetNodeGroups() { - c.log.Infof("deleting %s node group", ng) + for ngName, v := range clusterConfig.GetNodeGroups() { + c.log.Infof("deleting %s node group", ngName) ngConfig, ok := cfg.NodeGroups[v.Config] if !ok { return fmt.Errorf("node group profile %s not defined", v.Config) } - if v.Mode == "bootnode" { // TODO: implement standalone mode + if v.Mode == bootnodeMode { // TODO: implement standalone mode // register node group - cluster.AddNodeGroup(ng, ngConfig.Export()) + cluster.AddNodeGroup(ngName, ngConfig.Export()) // delete nodes from the node group - g, err := cluster.NodeGroup(ng) + g, err := cluster.NodeGroup(ngName) if err != nil { - return err + return fmt.Errorf("get node group: %w", err) } for i := 0; i < len(v.Nodes); i++ { - nName := fmt.Sprintf("%s-%d", ng, i) + nName := fmt.Sprintf("%s-%d", ngName, i) if len(v.Nodes[i].Name) > 0 { nName = v.Nodes[i].Name } if err := g.DeleteNode(ctx, nName); err != nil { - return fmt.Errorf("deleting node %s from the node group %s", nName, ng) + return fmt.Errorf("deleting node %s from the node group %s", nName, ngName) } if deleteStorage && *ngConfig.PersistenceEnabled { pvcName := fmt.Sprintf("data-%s-0", nName) - if err := c.k8sClient.PVC.Delete(ctx, pvcName, clusterOptions.Namespace); err != nil { + if err := c.k8sClient.PVC.Delete(ctx, pvcName, clusterConfig.GetNamespace()); err != nil { return fmt.Errorf("deleting pvc %s: %w", pvcName, err) } } } } else { // register node group - cluster.AddNodeGroup(ng, ngConfig.Export()) + cluster.AddNodeGroup(ngName, ngConfig.Export()) // delete nodes from the node group - g, err := cluster.NodeGroup(ng) + ng, err := cluster.NodeGroup(ngName) if err != nil { return err } if len(v.Nodes) > 0 { for i := 0; i < len(v.Nodes); i++ { - nName := fmt.Sprintf("%s-%d", ng, i) + nName := fmt.Sprintf("%s-%d", ngName, i) if len(v.Nodes[i].Name) > 0 { nName = v.Nodes[i].Name } - if err := g.DeleteNode(ctx, nName); err != nil { - return fmt.Errorf("deleting node %s from the node group %s", nName, ng) + if err := ng.DeleteNode(ctx, nName); err != nil { + return fmt.Errorf("deleting node %s from the node group %s", nName, ngName) } if deleteStorage && *ngConfig.PersistenceEnabled { pvcName := fmt.Sprintf("data-%s-0", nName) - if err := c.k8sClient.PVC.Delete(ctx, pvcName, clusterOptions.Namespace); err != nil { + if err := c.k8sClient.PVC.Delete(ctx, pvcName, clusterConfig.GetNamespace()); err != nil { return fmt.Errorf("deleting pvc %s: %w", pvcName, err) } } } } else { for i := 0; i < v.Count; i++ { - nName := fmt.Sprintf("%s-%d", ng, i) - if err := g.DeleteNode(ctx, nName); err != nil { - return fmt.Errorf("deleting node %s from the node group %s", nName, ng) + nName := fmt.Sprintf("%s-%d", ngName, i) + if err := ng.DeleteNode(ctx, nName); err != nil { + return fmt.Errorf("deleting node %s from the node group %s", nName, ngName) } if deleteStorage && *ngConfig.PersistenceEnabled { pvcName := fmt.Sprintf("data-%s-0", nName) - if err := c.k8sClient.PVC.Delete(ctx, pvcName, clusterOptions.Namespace); err != nil { + if err := c.k8sClient.PVC.Delete(ctx, pvcName, clusterConfig.GetNamespace()); err != nil { return fmt.Errorf("deleting pvc %s: %w", pvcName, err) } } } } - } } return } -func (c *command) setupCluster(ctx context.Context, clusterName string, cfg *config.Config, start bool) (cluster orchestration.Cluster, err error) { +func (c *command) setupCluster(ctx context.Context, clusterName string, cfg *config.Config, startCluster bool) (cluster orchestration.Cluster, err error) { + const ( + optionNameChainNodeEndpoint = "geth-url" + optionNameWalletKey = "wallet-key" + ) + clusterConfig, ok := cfg.Clusters[clusterName] if !ok { return nil, fmt.Errorf("cluster %s not defined", clusterName) } - clusterOptions := clusterConfig.Export() - clusterOptions.K8SClient = c.k8sClient - clusterOptions.SwapClient = c.swapClient - - cluster = orchestrationK8S.NewCluster(clusterConfig.GetName(), clusterOptions, c.log) - bootnodes := "" - - errGroup := new(errgroup.Group) + var chainNodeEndpoint string + if chainNodeEndpoint = c.globalConfig.GetString(optionNameChainNodeEndpoint); chainNodeEndpoint == "" { + return nil, errors.New("chain node endpoint (geth-url) not provided") + } - for ng, v := range clusterConfig.GetNodeGroups() { - ngConfig, ok := cfg.NodeGroups[v.Config] - if !ok { - return nil, fmt.Errorf("node group profile %s not defined", v.Config) - } + var walletKey string + if walletKey = c.globalConfig.GetString(optionNameWalletKey); walletKey == "" { + return nil, errors.New("wallet key not provided") + } - if v.Mode == "bootnode" { // TODO: implement standalone mode - beeConfig, ok := cfg.BeeConfigs[v.BeeConfig] - if !ok { - return nil, fmt.Errorf("bee profile %s not defined", v.BeeConfig) - } + var fundOpts orchestration.FundingOptions + if startCluster { + fundOpts = ensureFundingDefaults(clusterConfig.Funding.Export(), c.log) + } - // add node group to the cluster - cluster.AddNodeGroup(ng, ngConfig.Export()) + cluster = configureCluster(clusterConfig, c) - // start nodes in the node group - g, err := cluster.NodeGroup(ng) - if err != nil { - return nil, err - } + nodeResultChan := make(chan nodeResult) + defer close(nodeResultChan) - for i, node := range v.Nodes { - // set node name - nName := fmt.Sprintf("%s-%d", ng, i) - if len(node.Name) > 0 { - nName = node.Name - } + // setup bootnode node group + fundAddresses, bootnodes, err := setupNodes(ctx, clusterConfig, cfg, true, cluster, startCluster, "", nodeResultChan) + if err != nil { + return nil, fmt.Errorf("setup node group bootnode: %w", err) + } - // set bootnodes - bConfig := beeConfig.Export() - bConfig.Bootnodes = fmt.Sprintf(node.Bootnodes, clusterConfig.GetNamespace()) // TODO: improve bootnode management, support more than 2 bootnodes - bootnodes += bConfig.Bootnodes + " " + // fund bootnode node group if cluster is started + if startCluster { + err = fund(ctx, fundAddresses, chainNodeEndpoint, walletKey, fundOpts) + if err != nil { + return nil, fmt.Errorf("funding node group bootnode: %w", err) + } + c.log.Infof("bootnode node group funded") + } - // set NodeOptions - nOptions := orchestration.NodeOptions{ - Config: &bConfig, - } - if len(node.Clef.Key) > 0 { - nOptions.ClefKey = node.Clef.Key - } - if len(node.Clef.Password) > 0 { - nOptions.ClefPassword = node.Clef.Password - } - if len(node.LibP2PKey) > 0 { - nOptions.LibP2PKey = node.LibP2PKey - } - if len(node.SwarmKey) > 0 { - nOptions.SwarmKey = orchestration.EncryptedKey(node.SwarmKey) - } + // setup other node groups + fundAddresses, _, err = setupNodes(ctx, clusterConfig, cfg, false, cluster, startCluster, bootnodes, nodeResultChan) + if err != nil { + return nil, fmt.Errorf("setup other node groups: %w", err) + } - errGroup.Go(func() error { - if start { - return g.SetupNode(ctx, nName, nOptions, clusterConfig.Funding.Export()) - } else { - return g.AddNode(ctx, nName, nOptions) - } - }) - } + // fund other node groups if cluster is started + if startCluster { + err = fund(ctx, fundAddresses, chainNodeEndpoint, walletKey, fundOpts) + if err != nil { + return nil, fmt.Errorf("fund other node groups: %w", err) } + c.log.Infof("node groups funded") } + c.log.Infof("cluster %s setup completed", clusterName) - if err := errGroup.Wait(); err != nil { - return nil, fmt.Errorf("starting node group bootnode: %w", err) + return cluster, nil +} + +func ensureFundingDefaults(fundOpts orchestration.FundingOptions, log logging.Logger) orchestration.FundingOptions { + if fundOpts.Eth == 0 { + fundOpts.Eth = 0.1 // default eth value + log.Warningf("funding options, eth, is not provided, using default value %f", fundOpts.Eth) + } + if fundOpts.Bzz == 0 { + fundOpts.Bzz = 100 // default bzz value + log.Warningf("funding options, bzz, is not provided, using default value %f", fundOpts.Bzz) } + log.Infof("fund options, eth: %f, bzz: %f", fundOpts.Eth, fundOpts.Bzz) + return fundOpts +} + +func configureCluster(clusterConfig config.Cluster, c *command) orchestration.Cluster { + clusterOpts := clusterConfig.Export() + clusterOpts.K8SClient = c.k8sClient + clusterOpts.SwapClient = c.swapClient + return orchestrationK8S.NewCluster(clusterConfig.GetName(), clusterOpts, c.log) +} + +func setupNodes(ctx context.Context, clusterConfig config.Cluster, cfg *config.Config, bootnode bool, cluster orchestration.Cluster, startCluster bool, bootnodesIn string, nodeResultCh chan nodeResult) (fundAddresses []string, bootnodesOut string, err error) { + var nodeCount uint32 + for ngName, v := range clusterConfig.GetNodeGroups() { + + if (v.Mode != bootnodeMode && bootnode) || (v.Mode == bootnodeMode && !bootnode) { + continue + } - for ng, v := range clusterConfig.GetNodeGroups() { ngConfig, ok := cfg.NodeGroups[v.Config] if !ok { - return nil, fmt.Errorf("node group profile %s not defined", v.Config) + return nil, "", fmt.Errorf("node group profile %s not defined", v.Config) } + ngOptions := ngConfig.Export() - if v.Mode != "bootnode" { // TODO: support standalone nodes - // set bootnodes - beeConfig, ok := cfg.BeeConfigs[v.BeeConfig] - if !ok { - return nil, fmt.Errorf("bee profile %s not defined", v.BeeConfig) - } + beeConfig, ok := cfg.BeeConfigs[v.BeeConfig] + if !ok { + return nil, "", fmt.Errorf("bee profile %s not defined", v.BeeConfig) + } + bConfig := beeConfig.Export() - bConfig := beeConfig.Export() - bConfig.Bootnodes = bootnodes - // add node group to the cluster - ngOptions := ngConfig.Export() + if !bootnode { + bConfig.Bootnodes = bootnodesIn ngOptions.BeeConfig = &bConfig - cluster.AddNodeGroup(ng, ngOptions) + } - // start nodes in the node group - g, err := cluster.NodeGroup(ng) - if err != nil { - return nil, err + cluster.AddNodeGroup(ngName, ngOptions) + + // start nodes in the node group + ng, err := cluster.NodeGroup(ngName) + if err != nil { + return nil, "", fmt.Errorf("get node group: %w", err) + } + + for i, node := range v.Nodes { + // set node name + nodeName := fmt.Sprintf("%s-%d", ngName, i) + if len(node.Name) > 0 { + nodeName = node.Name } - for i, node := range v.Nodes { - // set node name - nName := fmt.Sprintf("%s-%d", ng, i) - if len(node.Name) > 0 { - nName = node.Name - } - // set NodeOptions - nOptions := orchestration.NodeOptions{} - if len(node.Clef.Key) > 0 { - nOptions.ClefKey = node.Clef.Key - } - if len(node.Clef.Password) > 0 { - nOptions.ClefPassword = node.Clef.Password - } - if len(node.LibP2PKey) > 0 { - nOptions.LibP2PKey = node.LibP2PKey - } - if len(node.SwarmKey) > 0 { - nOptions.SwarmKey = orchestration.EncryptedKey(node.SwarmKey) - } + var nodeOpts orchestration.NodeOptions - errGroup.Go(func() error { - if start { - return g.SetupNode(ctx, nName, nOptions, clusterConfig.Funding.Export()) - } else { - return g.AddNode(ctx, nName, nOptions) - } - }) + if bootnode { + // set bootnodes + bConfig.Bootnodes = fmt.Sprintf(node.Bootnodes, clusterConfig.GetNamespace()) // TODO: improve bootnode management, support more than 2 bootnodes + bootnodesOut += bootnodesIn + bConfig.Bootnodes + " " + nodeOpts = setupNodeOptions(node, &bConfig) + } else { + nodeOpts = setupNodeOptions(node, nil) } - if len(v.Nodes) == 0 { - for i := 0; i < v.Count; i++ { - // set node name - nName := fmt.Sprintf("%s-%d", ng, i) - - errGroup.Go(func() error { - if start { - return g.SetupNode(ctx, nName, orchestration.NodeOptions{}, clusterConfig.Funding.Export()) - } else { - return g.AddNode(ctx, nName, orchestration.NodeOptions{}) - } - }) - } + nodeCount++ + go setupOrAddNode(ctx, startCluster, ng, nodeName, nodeOpts, nodeResultCh) + } + + if len(v.Nodes) == 0 && !bootnode { + for i := 0; i < v.Count; i++ { + // set node name + nodeName := fmt.Sprintf("%s-%d", ngName, i) + nodeCount++ + go setupOrAddNode(ctx, startCluster, ng, nodeName, orchestration.NodeOptions{}, nodeResultCh) } } } - if err := errGroup.Wait(); err != nil { - return nil, fmt.Errorf("starting node groups: %w", err) + // wait for nodes to be setup and get their eth addresses + // or wait for nodes to be added and check for errors + for i := uint32(0); i < nodeCount; i++ { + nodeResult := <-nodeResultCh + if nodeResult.err != nil { + return nil, "", fmt.Errorf("setup or add node result: %w", nodeResult.err) + } + if nodeResult.ethAddress != "" { + fundAddresses = append(fundAddresses, nodeResult.ethAddress) + } } - return + return fundAddresses, bootnodesOut, nil +} + +func setupOrAddNode(ctx context.Context, startCluster bool, ng orchestration.NodeGroup, nName string, nodeOpts orchestration.NodeOptions, ch chan<- nodeResult) { + if startCluster { + ethAddress, err := ng.SetupNode(ctx, nName, nodeOpts) + ch <- nodeResult{ethAddress: ethAddress, err: err} + } else { + err := ng.AddNode(ctx, nName, nodeOpts) + ch <- nodeResult{err: err} + } +} + +func setupNodeOptions(node config.ClusterNode, bConfig *orchestration.Config) orchestration.NodeOptions { + nOptions := orchestration.NodeOptions{ + Config: bConfig, + } + if len(node.Clef.Key) > 0 { + nOptions.ClefKey = node.Clef.Key + } + if len(node.Clef.Password) > 0 { + nOptions.ClefPassword = node.Clef.Password + } + if len(node.LibP2PKey) > 0 { + nOptions.LibP2PKey = node.LibP2PKey + } + if len(node.SwarmKey) > 0 { + nOptions.SwarmKey = orchestration.EncryptedKey(node.SwarmKey) + } + return nOptions +} + +func fund(ctx context.Context, fundAddresses []string, chainNodeEndpoint string, walletKey string, fundOpts orchestration.FundingOptions) error { + return funder.Fund(ctx, funder.Config{ + Addresses: fundAddresses, + ChainNodeEndpoint: chainNodeEndpoint, + WalletKey: walletKey, + MinAmounts: funder.MinAmounts{ + NativeCoin: fundOpts.Eth, + SwarmToken: fundOpts.Bzz, + }, + }, nil, nil) } diff --git a/pkg/beekeeper/beekeeper.go b/pkg/beekeeper/beekeeper.go index 976b89a03..cb54354f0 100644 --- a/pkg/beekeeper/beekeeper.go +++ b/pkg/beekeeper/beekeeper.go @@ -167,7 +167,7 @@ func updateNodeGroup(ctx context.Context, ng orchestration.NodeGroup, a Actions, // add nodes for _, n := range toAdd { - if err := ng.SetupNode(ctx, n, orchestration.NodeOptions{}, orchestration.FundingOptions{}); err != nil { + if _, err := ng.SetupNode(ctx, n, orchestration.NodeOptions{}); err != nil { return fmt.Errorf("add start node %s: %w", n, err) } c, err := ng.NodeClient(n) @@ -272,7 +272,7 @@ func updateNodeGroupConcurrently(ctx context.Context, ng orchestration.NodeGroup <-updateSemaphore }() - if err := ng.SetupNode(ctx, n, orchestration.NodeOptions{}, orchestration.FundingOptions{}); err != nil { + if _, err := ng.SetupNode(ctx, n, orchestration.NodeOptions{}); err != nil { return fmt.Errorf("add start node %s: %w", n, err) } c, err := ng.NodeClient(n) diff --git a/pkg/orchestration/k8s/nodegroup.go b/pkg/orchestration/k8s/nodegroup.go index 51f1bef82..7c1e1a1b7 100644 --- a/pkg/orchestration/k8s/nodegroup.go +++ b/pkg/orchestration/k8s/nodegroup.go @@ -371,85 +371,31 @@ func (g *NodeGroup) DeleteNode(ctx context.Context, name string) (err error) { return } -// Fund adds funds to the node -func (g *NodeGroup) Fund(ctx context.Context, name string, o orchestration.NodeOptions, f orchestration.FundingOptions) (err error) { +// GetEthAddress returns ethereum address of the node +func (ng *NodeGroup) GetEthAddress(ctx context.Context, name string, o orchestration.NodeOptions) (string, error) { var a bee.Addresses - if f.Eth > 0 || f.Bzz > 0 || f.GBzz > 0 { - a.Ethereum, _ = o.SwarmKey.GetEthAddress() - if a.Ethereum == "" { - retries := 5 - for { - c, err := g.NodeClient(name) - if err != nil { - return err - } - a, err = c.Addresses(ctx) - if err != nil { - retries-- - if retries == 0 { - return fmt.Errorf("get %s address: %w", name, err) - } - time.Sleep(nodeRetryTimeout) - continue - } - break - } - } - g.logger.Infof("fund eth address: %s", a.Ethereum) - } - - if f.Eth > 0 { + a.Ethereum, _ = o.SwarmKey.GetEthAddress() + if a.Ethereum == "" { retries := 5 for { - tx, err := g.cluster.swap.SendETH(ctx, a.Ethereum, f.Eth) + c, err := ng.NodeClient(name) if err != nil { - retries-- - if retries == 0 { - return fmt.Errorf("send eth: %w", err) - } - time.Sleep(nodeRetryTimeout) - continue + return "", fmt.Errorf("get %s node client: %w", name, err) } - g.logger.Infof("%s funded with %.2f ETH, transaction: %s", name, f.Eth, tx) - break - } - } - - if f.Bzz > 0 { - retries := 5 - for { - tx, err := g.cluster.swap.SendBZZ(ctx, a.Ethereum, f.Bzz) + a, err = c.Addresses(ctx) if err != nil { retries-- if retries == 0 { - return fmt.Errorf("send eth: %w", err) + return "", fmt.Errorf("get %s address: %w", name, err) } time.Sleep(nodeRetryTimeout) continue } - g.logger.Infof("%s funded with %.2f BZZ, transaction: %s", name, f.Bzz, tx) break } } - - if f.GBzz > 0 { - retries := 5 - for { - tx, err := g.cluster.swap.SendGBZZ(ctx, a.Ethereum, f.GBzz) - if err != nil { - retries-- - if retries == 0 { - return fmt.Errorf("send eth: %w", err) - } - time.Sleep(nodeRetryTimeout) - continue - } - g.logger.Infof("%s funded with %.2f gBZZ, transaction: %s", name, f.GBzz, tx) - break - } - } - - return + ng.logger.Infof("fund eth address: %s", a.Ethereum) + return a.Ethereum, nil } // GroupReplicationFactor returns the total number of nodes in the node group that contain given chunk @@ -798,27 +744,28 @@ func (g *NodeGroup) RunningNodes(ctx context.Context) (running []string, err err } // SetupNode creates new node in the node group, starts it in the k8s cluster and funds it -func (g *NodeGroup) SetupNode(ctx context.Context, name string, o orchestration.NodeOptions, f orchestration.FundingOptions) (err error) { +func (g *NodeGroup) SetupNode(ctx context.Context, name string, o orchestration.NodeOptions) (ethAddress string, err error) { g.logger.Infof("starting setup node: %s", name) if err := g.AddNode(ctx, name, o); err != nil { - return fmt.Errorf("add node %s: %w", name, err) + return "", fmt.Errorf("add node %s: %w", name, err) } if err := g.PregenerateSwarmKey(ctx, name); err != nil { - return fmt.Errorf("pregenerate Swarm key for node %s: %w", name, err) + return "", fmt.Errorf("pregenerate Swarm key for node %s: %w", name, err) } if err := g.CreateNode(ctx, name); err != nil { - return fmt.Errorf("create node %s in k8s: %w", name, err) + return "", fmt.Errorf("create node %s in k8s: %w", name, err) } if err := g.StartNode(ctx, name); err != nil { - return fmt.Errorf("start node %s in k8s: %w", name, err) + return "", fmt.Errorf("start node %s in k8s: %w", name, err) } - if err := g.Fund(ctx, name, o, f); err != nil { - return fmt.Errorf("fund node %s: %w", name, err) + ethAddress, err = g.GetEthAddress(ctx, name, o) + if err != nil { + return "", fmt.Errorf("get eth address for funding: %w", err) } return diff --git a/pkg/orchestration/nodegroup.go b/pkg/orchestration/nodegroup.go index 2d96ea2a8..4dd4f2e8b 100644 --- a/pkg/orchestration/nodegroup.go +++ b/pkg/orchestration/nodegroup.go @@ -8,27 +8,27 @@ import ( ) type NodeGroup interface { + Accounting(ctx context.Context) (infos NodeGroupAccounting, err error) AddNode(ctx context.Context, name string, o NodeOptions) (err error) Addresses(ctx context.Context) (addrs NodeGroupAddresses, err error) - Accounting(ctx context.Context) (infos NodeGroupAccounting, err error) Balances(ctx context.Context) (balances NodeGroupBalances, err error) CreateNode(ctx context.Context, name string) (err error) DeleteNode(ctx context.Context, name string) (err error) - Fund(ctx context.Context, name string, o NodeOptions, f FundingOptions) (err error) + GetEthAddress(ctx context.Context, name string, o NodeOptions) (ethAddress string, err error) GroupReplicationFactor(ctx context.Context, a swarm.Address) (grf int, err error) Name() string + Node(name string) (Node, error) + NodeClient(name string) (*bee.Client, error) + NodeReady(ctx context.Context, name string) (ok bool, err error) Nodes() map[string]Node NodesClients(ctx context.Context) (map[string]*bee.Client, error) NodesClientsAll(ctx context.Context) map[string]*bee.Client NodesSorted() (l []string) - Node(name string) (Node, error) - NodeClient(name string) (*bee.Client, error) Overlays(ctx context.Context) (overlays NodeGroupOverlays, err error) Peers(ctx context.Context) (peers NodeGroupPeers, err error) - NodeReady(ctx context.Context, name string) (ok bool, err error) RunningNodes(ctx context.Context) (running []string, err error) - SetupNode(ctx context.Context, name string, o NodeOptions, f FundingOptions) (err error) Settlements(ctx context.Context) (settlements NodeGroupSettlements, err error) + SetupNode(ctx context.Context, name string, o NodeOptions) (ethAddress string, err error) Size() int StartNode(ctx context.Context, name string) (err error) StopNode(ctx context.Context, name string) (err error)