From ebb1ae2599c0a9273018229ac968477c1b37def3 Mon Sep 17 00:00:00 2001 From: Giuliano Mega Date: Fri, 22 May 2026 19:20:53 -0300 Subject: [PATCH] feat: network presets (#1437) --- .../requests/node_lifecycle_request.nim | 7 ++ storage/conf.nim | 44 ++++++-- storage/presets.nim | 106 ++++++++++++++++++ storage/storage.nim | 11 +- tests/storage/testpresets.nim | 76 +++++++++++++ 5 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 storage/presets.nim create mode 100644 tests/storage/testpresets.nim diff --git a/library/storage_thread_requests/requests/node_lifecycle_request.nim b/library/storage_thread_requests/requests/node_lifecycle_request.nim index 6638cb03..105fc285 100644 --- a/library/storage_thread_requests/requests/node_lifecycle_request.nim +++ b/library/storage_thread_requests/requests/node_lifecycle_request.nim @@ -71,6 +71,13 @@ proc readValue*(r: var JsonReader, val: var Duration) = raise newException(SerializationError, "Cannot parse the duration: " & input) val = dur +proc readValue(r: var JsonReader, val: var NetworkPreset) = + let name = r.readValue(string) + let res = NetworkPresets.find(name) + if res.isNone: + raise newException(SerializationError, "Invalid network preset: " & name) + val = res.get() + type NodeLifecycleRequest* = object operation: NodeLifecycleMsgType configJson: cstring diff --git a/storage/conf.nim b/storage/conf.nim index d24fd3ea..0ae00e3e 100644 --- a/storage/conf.nim +++ b/storage/conf.nim @@ -42,11 +42,13 @@ import ./stores import ./units import ./utils import ./nat +import ./presets import ./utils/natutils from ./blockexchange/engine/downloadmanager import DefaultBlockRetries -export units, net, storagetypes, logutils, completeCmdArg, parseCmdArg, NatConfig +export + units, net, storagetypes, logutils, presets, completeCmdArg, parseCmdArg, NatConfig export DefaultQuotaBytes, DefaultBlockTtl, DefaultBlockInterval, DefaultNumBlocksPerInterval, @@ -179,11 +181,18 @@ type bootstrapNodes* {. desc: "Specifies one or more bootstrap nodes to use when " & - "connecting to the network", + "connecting to the network. When specified, overrides " & + "the network preset option.", abbr: "b", name: "bootstrap-node" .}: seq[SignedPeerRecord] + network* {. + desc: "The network to connect to. Options are: \n" & NetworkPresetsDescription, + name: "network", + defaultValue: DefaultNetworkPreset + .}: NetworkPreset + maxPeers* {. desc: "The maximum number of peers to connect to", defaultValue: 160, @@ -347,16 +356,6 @@ proc parseCmdArg*(T: type ThreadCount, input: string): T = quit QuitFailure return val.get() -proc parse*(T: type SignedPeerRecord, p: string): Result[SignedPeerRecord, string] = - var res: SignedPeerRecord - try: - if not res.fromURI(p): - return err("The uri is not a valid SignedPeerRecord: " & p) - return ok(res) - except LPError, Base64Error: - let e = getCurrentException() - return err(e.msg) - proc parseCmdArg*(T: type SignedPeerRecord, uri: string): T = let res = SignedPeerRecord.parse(uri) if res.isErr: @@ -417,6 +416,13 @@ proc parseCmdArg*(T: type Duration, val: string): T = quit QuitFailure dur +proc parseCmdArg*(T: type NetworkPreset, p: string): NetworkPreset = + let res = NetworkPresets.find(p) + if res.isNone: + fatal "Invalid network preset.", input = p + quit QuitFailure + return res.get() + proc readValue*(r: var TomlReader, val: var SignedPeerRecord) = without uri =? r.readValue(string).catch, err: error "invalid SignedPeerRecord configuration value", error = err.msg @@ -480,6 +486,17 @@ proc readValue*( except CatchableError as err: raise newException(SerializationError, err.msg) +proc readValue*( + r: var TomlReader, val: var NetworkPreset +) {.raises: [SerializationError, IOError].} = + let + str = r.readValue(string) + preset = NetworkPresets.find(str) + if preset.isNone: + raise newException(SerializationError, "Invalid network preset: " & str) + + val = preset.get() + # no idea why confutils needs this: proc completeCmdArg*(T: type NBytes, val: string): seq[string] = discard @@ -490,6 +507,9 @@ proc completeCmdArg*(T: type Duration, val: string): seq[string] = proc completeCmdArg*(T: type ThreadCount, val: string): seq[string] = discard +proc completeCmdArg*(T: type NetworkPreset, val: string): seq[string] = + NetworkPresets.findByPrefix(val) + # silly chronicles, colors is a compile-time property proc stripAnsi*(v: string): string = var diff --git a/storage/presets.nim b/storage/presets.nim new file mode 100644 index 00000000..822c3fd8 --- /dev/null +++ b/storage/presets.nim @@ -0,0 +1,106 @@ +# Presets are hard-coded configuration bundles that get compiled into code. They can refer +# to different things, but the canonical example are sets of bootstrap nodes that define +# logically different networks; e.g., "logos.dev" and "logos.test" refer to the Logos +# devnet and latest testnet, respectively. +import std/options +import std/strutils + +import pkg/chronicles +import pkg/codexdht/discv5/protocol +import pkg/libp2p/[errors, routing_record] +import pkg/results +import pkg/stew/base64 + +# A NetworkPreset is a set of bootstrap nodes (represented +# by their signed peer records) along with some description metadata. +type NetworkPreset* = object + name*: string + description*: string + unparsedRecords: seq[string] + +proc init*( + T: type NetworkPreset, name: string, description: string, records: seq[string] +): T = + result.name = name + result.description = description + + # We have to delay parsing of records to runtime because + # of https://github.com/nim-lang/Nim/issues/23468 + result.unparsedRecords = records + +func `$`*(preset: NetworkPreset): string = + "[" & preset.name & "]: " & preset.description + +func `$`*[N](presets: array[N, NetworkPreset]): string = + result = "" + for preset in presets: + result &= $preset & "; " + +proc parse*(T: type SignedPeerRecord, p: string): Result[SignedPeerRecord, string] = + var res: SignedPeerRecord + try: + if not res.fromURI(p): + return err("The uri is not a valid SignedPeerRecord: " & p) + return ok(res) + except LPError, Base64Error: + let e = getCurrentException() + return err(e.msg) + +proc `bootstrapNodes`*(self: NetworkPreset): seq[SignedPeerRecord] = + for record in self.unparsedRecords: + # Having an invalid SPR in a hardcoded config is a bug, a+ + # it should crash the node. + result.add(parse(SignedPeerRecord, record).tryGet()) + +const NetworkPresets* = [ + NetworkPreset.init( + "logos.dev", + "Logos devnet", + @[ + "spr:CiUIAhIhAwfZDeTtWNlSgRbZlZfvxLI5Bpy0lFEYN7gImS3oHNaSEgIDARpJCicAJQgCEiEDB9kN5O1Y2VKBFtmVl-" & + "_EsjkGnLSUURg3uAiZLegc1pIQ__O20AYaCwoJBBiQTsiRAiOCGgsKCQQYkE7IkQIjgipHMEUCIQCIZx-HlVsLXJLhD6SEV" & + "x6Zt_1aG9IqMq-Luvz8No_J0wIgc8I9PRtheG4s5tzHjkEJMLcq3Jf09IT_FGkzPcJm8h4", + "spr:CiUIAhIhA8d4LjRirtXO1M-JEmbhVA0CQeA7hHNR9BA7DvFsPKTEEgIDARpJCicAJQgCEiEDx3guNGKu1c7Uz4kSZu" & + "FUDQJB4DuEc1H0EDsO8Ww8pMQQhPW20AYaCwoJBCIq5juRAiOCGgsKCQQiKuY7kQIjgipGMEQCIHV_8nJ0iedWjlAxUhBm" & + "dAbDPLu5g2RmcnmJBD8cbD98AiAp1w9nAJgLlPIr41aMcdkds_eSoh8ImOVKvq6Idx-Ugg", + "spr:CiUIAhIhA_MocWwn1_t__FEONMqYluUjc9ZVkcvYRLo6C0GzTkbfEgIDARpJCicAJQgCEiED8yhxbCfX-3_8UQ40yp" & + "iW5SNz1lWRy9hEujoLQbNORt8QlfO20AYaCwoJBC_u5W-RAiOCGgsKCQQv7uVvkQIjgipGMEQCIHMpQO31gg4FoKYtDyTT" & + "QS8xFz1KEmfqH385EeMUNbhPAiBblCkmOfQBmXj6eryaSiXWsftgohE-SPbKwsASZ1Zs3Q", + ], + ), + NetworkPreset.init( + "codex.dev", + "Codex legacy devnet (deprecated)", + @[ + "spr:CiUIAhIhA-VlcoiRm02KyIzrcTP-ljFpzTljfBRRKTIvhMIwqBqWEgIDARpJCicAJQgCEiED5WVyiJGbTYrIjOtxM_6" & + "WMWnNOWN8FFEpMi-EwjCoGpYQs8n8wQYaCwoJBHTKubmRAnU6GgsKCQR0yrm5kQJ1OipHMEUCIQDwUNsfReB4ty7JFS" & + "5WVQ6n1fcko89qVAOfQEHixa03rgIgan2-uFNDT-r4s9TOkLe9YBkCbsRWYCHGGVJ25rLj0QE", + "spr:CiUIAhIhApIj9p6zJDRbw2NoCo-tj98Y760YbppRiEpGIE1yGaMzEgIDARpJCicAJQgCEiECkiP2nrMkNFvDY2gKj62P" & + "3xjvrRhumlGISkYgTXIZozMQvcz8wQYaCwoJBAWhF3WRAnVEGgsKCQQFoRd1kQJ1RCpGMEQCIFZB84O_nzPNuViqEGRL" & + "1vJTjHBJ-i5ZDgFL5XZxm4HAAiB8rbLHkUdFfWdiOmlencYVn0noSMRHzn4lJYoShuVzlw", + "spr:CiUIAhIhApqRgeWRPSXocTS9RFkQmwTZRG-Cdt7UR2N7POoz606ZEgIDARpJCicAJQgCEiECmpGB5ZE9JehxNL1EWRCb" & + "BNlEb4J23tRHY3s86jPrTpkQj8_8wQYaCwoJBAXfEfiRAnVOGgsKCQQF3xH4kQJ1TipGMEQCIGWJMsF57N1iIEQgTH7I" & + "rVOgEgv0J2P2v3jvQr5Cjy-RAiAy4aiZ8QtyDvCfl_K_w6SyZ9csFGkRNTpirq_M_QNgKw", + ], + ), +] + +proc `default`*(presets: openArray[NetworkPreset]): NetworkPreset = + presets[0] + +# Precomputes those as as consts so we can use them in nim-confutils CLI +# help strings. +const + NetworkPresetsDescription* = $NetworkPresets + DefaultNetworkPreset* = NetworkPresets.default + +proc find*(presets: openArray[NetworkPreset], p: string): Option[NetworkPreset] = + for preset in presets: + if preset.name == p: + return some(preset) + return none(NetworkPreset) + +proc findByPrefix*(presets: openArray[NetworkPreset], val: string): seq[string] = + for p in presets: + if p.name.startsWith(val): + result.add p.name diff --git a/storage/storage.nim b/storage/storage.nim index f332f530..8599a03d 100644 --- a/storage/storage.nim +++ b/storage/storage.nim @@ -216,6 +216,15 @@ proc new*( error "Failed to initialize discovery datastore", path = providersPath, err = discoveryStoreRes.error.msg + let bootstrapNodes = + if config.bootstrapNodes.len > 0: + info "Overriding network preset using custom bootstrap nodes", + nodes = config.bootstrapNodes + config.bootstrapNodes + else: + info "Bootstrapping node using a predefined network", network = $config.network + config.network.bootstrapNodes + let discoveryStore = Datastore(discoveryStoreRes.expect("Should create discovery datastore!")) @@ -224,7 +233,7 @@ proc new*( switch.peerInfo.privateKey, announceAddrs = @[listenMultiAddr], bindPort = config.discoveryPort, - bootstrapNodes = config.bootstrapNodes, + bootstrapNodes = bootstrapNodes, store = discoveryStore, ) diff --git a/tests/storage/testpresets.nim b/tests/storage/testpresets.nim new file mode 100644 index 00000000..ff4dc985 --- /dev/null +++ b/tests/storage/testpresets.nim @@ -0,0 +1,76 @@ +import std/options +import std/sequtils + +import pkg/codexdht/discv5/protocol +import pkg/toml_serialization +import pkg/unittest2 + +import pkg/storage/presets +import pkg/storage/conf + +const SPRs = [ + "spr:CiUIAhIhA-VlcoiRm02KyIzrcTP-ljFpzTljfBRRKTIvhMIwqBqWEgIDARpJCicAJQgCEiED5WVyiJGbTYrIjOtxM_6" & + "WMWnNOWN8FFEpMi-EwjCoGpYQs8n8wQYaCwoJBHTKubmRAnU6GgsKCQR0yrm5kQJ1OipHMEUCIQDwUNsfReB4ty7JFS" & + "5WVQ6n1fcko89qVAOfQEHixa03rgIgan2-uFNDT-r4s9TOkLe9YBkCbsRWYCHGGVJ25rLj0QE", + "spr:CiUIAhIhApIj9p6zJDRbw2NoCo-tj98Y760YbppRiEpGIE1yGaMzEgIDARpJCicAJQgCEiECkiP2nrMkNFvDY2gKj62P" & + "3xjvrRhumlGISkYgTXIZozMQvcz8wQYaCwoJBAWhF3WRAnVEGgsKCQQFoRd1kQJ1RCpGMEQCIFZB84O_nzPNuViqEGRL" & + "1vJTjHBJ-i5ZDgFL5XZxm4HAAiB8rbLHkUdFfWdiOmlencYVn0noSMRHzn4lJYoShuVzlw", +] + +# Construct presets as const to make sure that everything we do +# on `init` runs properly in VM (e.g. parsing SPRs in VM is a +# no-go because of: https://github.com/nim-lang/Nim/issues/23468) +const Presets = [ + NetworkPreset.init("preset1", "a preset", SPRs.toSeq), + NetworkPreset.init("preset2", "empty preset", @[]), +] + +type TestConfig = object + network: NetworkPreset + +suite "Network presets": + test "should construct presets correctly": + check Presets[0].name == "preset1" + check Presets[0].description == "a preset" + + check Presets[0].bootstrapNodes.len == 2 + check Presets[0].bootstrapNodes[0].toURI() == SPRs[0] + check Presets[0].bootstrapNodes[1].toURI() == SPRs[1] + + test "should find existing presets by name": + let + preset1 = Presets.find("preset1").get() + preset2 = Presets.find("preset2").get() + + check preset1.name == "preset1" + check preset2.name == "preset2" + + test "should return error when preset is not found": + let result = Presets.find("nonexistent") + check result.isNone + + test "should return presets matching prefix": + let result = Presets.findByPrefix("preset") + check result.len == 2 + check result[0] == "preset1" + check result[1] == "preset2" + + let result2 = Presets.findByPrefix("preset1") + check result2.len == 1 + check result2[0] == "preset1" + + test "should deserialize valid preset from TOML": + # Sadly, we have no option but reading from the global presets array + # here, unless we really want to complicate things. + let toml = """ + network = "logos.dev" + """ + let config = Toml.decode(toml, TestConfig) + check config.network.name == "logos.dev" + + test "should raise SerializationError for invalid preset": + let toml = """ + network = "nonexistent" + """ + expect SerializationError: + discard Toml.decode(toml, TestConfig)