feat: network presets (#1437)

This commit is contained in:
Giuliano Mega 2026-05-22 19:20:53 -03:00 committed by GitHub
parent 1218a7edbe
commit ebb1ae2599
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 231 additions and 13 deletions

View File

@ -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

View File

@ -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

106
storage/presets.nim Normal file
View File

@ -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

View File

@ -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,
)

View File

@ -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)