From 5d82ec42ebda51a632453ef124e79c8e4b621e0a Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Thu, 27 Jun 2024 20:09:15 -0400 Subject: [PATCH] netdeploy: support hybrid p2p scenarios --- netdeploy/remote/deployedNetwork.go | 10 ++ netdeploy/remote/nodeConfig.go | 2 + netdeploy/remote/nodecfg/nodeConfigurator.go | 11 +- netdeploy/remote/nodecfg/nodeDir.go | 36 ++++- .../recipes/scenario1s-p2p/Makefile | 10 +- .../recipes/scenario1s-p2p/README.md | 9 +- .../scenario1s-p2p/copy-node-configs.py | 132 +++++++++++++++--- 7 files changed, 180 insertions(+), 30 deletions(-) diff --git a/netdeploy/remote/deployedNetwork.go b/netdeploy/remote/deployedNetwork.go index 25de422026..a58d8a15fb 100644 --- a/netdeploy/remote/deployedNetwork.go +++ b/netdeploy/remote/deployedNetwork.go @@ -1004,6 +1004,16 @@ func createHostSpec(host HostConfig, template cloudHost) (hostSpec cloudHostSpec ports[port] = true portList = append(portList, strconv.Itoa(port)) } + if node.P2PNetAddress != "" { + port, err = extractPublicPort(node.P2PNetAddress) + if err != nil { + return + } + if !ports[port] { + ports[port] = true + portList = append(portList, strconv.Itoa(port)) + } + } } // See if the APIEndpoint is open to the public, and if so add it diff --git a/netdeploy/remote/nodeConfig.go b/netdeploy/remote/nodeConfig.go index 4880d76eb9..bd4b63dac8 100644 --- a/netdeploy/remote/nodeConfig.go +++ b/netdeploy/remote/nodeConfig.go @@ -35,6 +35,8 @@ type NodeConfig struct { DeadlockOverride int `json:",omitempty"` // -1 = Disable deadlock detection, 0 = Use Default for build, 1 = Enable ConfigJSONOverride string `json:",omitempty"` // Raw json to merge into config.json after other modifications are complete P2PBootstrap bool // True if this node should be a p2p bootstrap node and registered in DNS + P2PNetAddress string `json:",omitempty"` + PublicAddress bool // NodeNameMatchRegex is tested against Name in generated configs and if matched the rest of the configs in this record are applied as a template NodeNameMatchRegex string `json:",omitempty"` diff --git a/netdeploy/remote/nodecfg/nodeConfigurator.go b/netdeploy/remote/nodecfg/nodeConfigurator.go index 842570bfc8..90c3f012ac 100644 --- a/netdeploy/remote/nodecfg/nodeConfigurator.go +++ b/netdeploy/remote/nodecfg/nodeConfigurator.go @@ -93,6 +93,10 @@ func (nc *nodeConfigurator) apply(rootConfigDir, rootNodeDir string) (err error) nc.genesisFile = filepath.Join(rootConfigDir, "genesisdata", config.GenesisJSONFile) nc.genesisData, err = bookkeeping.LoadGenesisFromFile(nc.genesisFile) + if err != nil { + return fmt.Errorf("error loading genesis from '%s': %v", nc.genesisFile, err) + + } nodeDirs, err := nc.prepareNodeDirs(nc.config.Nodes, rootConfigDir, rootNodeDir) if err != nil { return fmt.Errorf("error preparing node directories: %v", err) @@ -198,6 +202,11 @@ func (nc *nodeConfigurator) prepareNodeDirs(configs []remote.NodeConfig, rootCon return } +// getHostName creates a DNS name for a host +func (nc *nodeConfigurator) getNetworkHostName() string { + return nc.config.Name + "." + string(nc.genesisData.Network) + ".algodev.network" +} + func (nc *nodeConfigurator) registerDNSRecords() (err error) { cfZoneID, cfToken, err := getClouldflareCredentials() if err != nil { @@ -215,7 +224,7 @@ func (nc *nodeConfigurator) registerDNSRecords() (err error) { // If we need to register anything, first register a DNS entry // to map our network DNS name to our public name (or IP) provided to nodecfg // Network HostName = eg r1.testnet.algodev.network - networkHostName := nc.config.Name + "." + string(nc.genesisData.Network) + ".algodev.network" + networkHostName := nc.getNetworkHostName() isIP := net.ParseIP(nc.dnsName) != nil var recordType string if isIP { diff --git a/netdeploy/remote/nodecfg/nodeDir.go b/netdeploy/remote/nodecfg/nodeDir.go index bdfc037438..8d8787dd11 100644 --- a/netdeploy/remote/nodecfg/nodeDir.go +++ b/netdeploy/remote/nodecfg/nodeDir.go @@ -104,6 +104,11 @@ func (nd *nodeDir) configure() (err error) { return } + if err = nd.configurePublicAddress(nd.PublicAddress); err != nil { + fmt.Fprintf(os.Stdout, "Error during configurePublicAddress: %s\n", err) + return + } + if err = nd.configureP2PDNSBootstrap(nd.P2PBootstrap); err != nil { fmt.Fprintf(os.Stdout, "Error during configureP2PDNSBootstrap: %s\n", err) return @@ -160,10 +165,35 @@ func (nd *nodeDir) configureNetAddress() (err error) { nd.configurator.addRelaySrv(bootstrapRecord.PrimarySRVBootstrap, nd.NetAddress) } } + if nd.P2PNetAddress != "" { + fmt.Fprintf(os.Stdout, " - Assigning P2PNetAddress: %s\n", nd.P2PNetAddress) + nd.config.P2PNetAddress = nd.P2PNetAddress + } err = nd.saveConfig() return } +func (nd *nodeDir) configurePublicAddress(publicAddress bool) error { + if !publicAddress { + return nil + } + + if err := nd.ensureConfig(); err != nil { + return err + } + + if !nd.IsRelay() { + return errors.New("publicAddress is only valid for relay nodes") + } + + if nd.NetAddress[0] == ':' { + networkHostName := nd.configurator.getNetworkHostName() + nd.NetAddress + nd.config.PublicAddress = networkHostName + } + + return nil +} + func (nd *nodeDir) configureP2PDNSBootstrap(p2pBootstrap bool) error { if !p2pBootstrap { return nil @@ -179,7 +209,7 @@ func (nd *nodeDir) configureP2PDNSBootstrap(p2pBootstrap bool) error { if !nd.config.EnableP2P && !nd.config.EnableP2PHybridMode { return errors.New("p2p bootstrap requires EnableP2P or EnableP2PHybridMode to be set") } - if nd.NetAddress == "" && nd.config.P2PNetAddress == "" { + if nd.NetAddress == "" && nd.P2PNetAddress == "" { return errors.New("p2p bootstrap requires NetAddress or P2PNetAddress to be set") } if !nd.config.EnableGossipService { @@ -187,8 +217,8 @@ func (nd *nodeDir) configureP2PDNSBootstrap(p2pBootstrap bool) error { } netAddress := nd.NetAddress - if nd.config.P2PNetAddress != "" { - netAddress = nd.config.P2PNetAddress + if nd.P2PNetAddress != "" { + netAddress = nd.P2PNetAddress } key, err := p2p.GetPrivKey(config.Local{P2PPersistPeerID: true}, nd.dataDir) diff --git a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile index f4ec4b3c1f..7222fd3882 100644 --- a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile +++ b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile @@ -3,15 +3,17 @@ PARAMS=-w 20 -R 8 -N 20 -n 20 --npn-algod-nodes 10 --node-template node.json --r .PHONY: clean all +HYBRID ?= no + all: net.json genesis.json topology.json -node.json nonPartNode.json relay.json: - python3 copy-node-configs.py +node.json nonPartNode.json relay.json: copy-node-configs.py + python3 copy-node-configs.py --hybrid=${HYBRID} -net.json: node.json nonPartNode.json relay.json ${GOPATH}/bin/netgoal Makefile +net.json: node.json nonPartNode.json relay.json Makefile netgoal generate -t net -r /tmp/wat -o net.json ${PARAMS} -genesis.json: ${GOPATH}/bin/netgoal Makefile +genesis.json: Makefile netgoal generate -t genesis -r /tmp/wat -o genesis.l.json ${PARAMS} jq '.LastPartKeyRound=5000|.NetworkName="s1s-p2p"|.ConsensusProtocol="future"' < genesis.l.json > genesis.json rm genesis.l.json diff --git a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md index 1cad95bc2d..04e8b986c7 100644 --- a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md +++ b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md @@ -7,10 +7,17 @@ This is a copy of scenario1s with the following changes in nodes configuration: ## Build ```sh -export GOPATH=~/go make ``` +If want to configure a hybrid net, set the `HYBRID` mode parameter to: + - `p2p` meaning all nodes are p2pnet and 50% of them are hybrid + - `ws` meaning all nodes are wsnet and 50% of them are hybrid + +```sh +make -D HYBRID=p2p +``` + ## Run Run as usual cluster test scenario with algonet. diff --git a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py index 6ffbc01d8d..be0e6a1527 100644 --- a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py +++ b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py @@ -5,14 +5,116 @@ 3. Set DNSSecurityFlags: 0 to all configs """ +import argparse +import copy import json import os CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) SCENARIO1S_DIR = os.path.join(CURRENT_DIR, "..", "scenario1s") +def make_p2p_net(*args): + """convert config to a pure p2p network""" + for config in args: + override_json = json.loads(config.get("ConfigJSONOverride", "{}")) + override_json["EnableP2P"] = True + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + config["ConfigJSONOverride"] = json.dumps(override_json) + + net_address = config.get("NetAddress") + if net_address: + config["P2PBootstrap"] = True + altconfigs = config.get("AltConfigs", []) + if altconfigs: + for i, altconfig in enumerate(altconfigs): + override_json = json.loads(altconfig.get("ConfigJSONOverride", "{}")) + override_json["EnableP2P"] = True + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + altconfigs[i]["ConfigJSONOverride"] = json.dumps(override_json) + config["AltConfigs"] = altconfigs + + +def make_hybrid_p2p_net(*args): + """convert config to a hybrid p2p network: + - half of relays become hybrid and receive public address + - half of non-relay nodes become hybrid + - AltConfigs are used for hybrid nodes with FractionApply=0.5 + - Only one AltConfigs is supported and its FractionApply is forced to 0.5 + """ + for config in args: + override_json = json.loads(config.get("ConfigJSONOverride", "{}")) + override_json["EnableP2P"] = True + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + config["ConfigJSONOverride"] = json.dumps(override_json) + + net_address = config.get("NetAddress") + altconfigs = config.get("AltConfigs") + altconfig = None + if altconfigs: + altconfig = altconfigs[0] + else: + altconfig = copy.deepcopy(config) + + override_json = json.loads(altconfig.get("ConfigJSONOverride", "{}")) + override_json["EnableP2PHybridMode"] = True + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + altconfig["ConfigJSONOverride"] = json.dumps(override_json) + if net_address: # relay, set public address + altconfig["P2PBootstrap"] = True + altconfig["P2PNetAddress"] = "{{NetworkPort1}}" + altconfig["PublicAddress"] = True + altconfig['FractionApply'] = 0.5 + + altconfigs = [altconfig] + config["AltConfigs"] = altconfigs + + +def make_hybrid_ws_net(*args): + """convert config to a hybrid ws network: + - half of relays become hybrid and receive public address + - half of non-relay nodes become hybrid + - AltConfigs are used for hybrid nodes with FractionApply=0.5 + - Only one AltConfigs is supported and its FractionApply is forced to 0.5 + """ + for config in args: + override_json = json.loads(config.get("ConfigJSONOverride", "{}")) + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + config["ConfigJSONOverride"] = json.dumps(override_json) + + net_address = config.get("NetAddress") + altconfigs = config.get("AltConfigs") + altconfig = None + if altconfigs: + altconfig = altconfigs[0] + else: + altconfig = copy.deepcopy(config) + + override_json = json.loads(altconfig.get("ConfigJSONOverride", "{}")) + override_json["EnableP2PHybridMode"] = True + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + altconfig["ConfigJSONOverride"] = json.dumps(override_json) + if net_address: # relay, set public address + altconfig["P2PBootstrap"] = True + altconfig["P2PNetAddress"] = "{{NetworkPort1}}" + altconfig["PublicAddress"] = True + altconfig['FractionApply'] = 0.5 + + altconfigs = [altconfig] + config["AltConfigs"] = altconfigs + + def main(): """main""" + ap = argparse.ArgumentParser() + ap.add_argument('--hybrid', type=str, help='Hybrid mode: p2p, ws') + args = ap.parse_args() + + hybrid_mode = args.hybrid + if hybrid_mode not in ("p2p", "ws"): + hybrid_mode = None + + print('Hybrid mode:', hybrid_mode) + with open(os.path.join(SCENARIO1S_DIR, "node.json"), "r") as f: node = json.load(f) with open(os.path.join(SCENARIO1S_DIR, "relay.json"), "r") as f: @@ -20,27 +122,15 @@ def main(): with open(os.path.join(SCENARIO1S_DIR, "nonPartNode.json"), "r") as f: non_part_node = json.load(f) - # make all relays P2PBootstrap'able - relay["P2PBootstrap"] = True - - # enable P2P for all configs - for config in (node, relay, non_part_node): - override = config.get("ConfigJSONOverride") - if override: - override_json = json.loads(override) - override_json["EnableP2P"] = True - override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC - config["ConfigJSONOverride"] = json.dumps(override_json) - altconfigs = config.get("AltConfigs", []) - if altconfigs: - for i, altconfig in enumerate(altconfigs): - override = altconfig.get("ConfigJSONOverride") - if override: - override_json = json.loads(override) - override_json["EnableP2P"] = True - override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC - altconfigs[i]["ConfigJSONOverride"] = json.dumps(override_json) - config["AltConfigs"] = altconfigs + # in p2p-only mode all relays are P2PBootstrap-able + if not hybrid_mode: + make_p2p_net(node, relay, non_part_node) + elif hybrid_mode == 'p2p': + make_hybrid_p2p_net(node, relay, non_part_node) + elif hybrid_mode == 'ws': + make_hybrid_ws_net(node, relay, non_part_node) + else: + raise ValueError(f"Invalid hybrid mode: { hybrid_mode }") with open("node.json", "w") as f: json.dump(node, f, indent=4)