logos-delivery/waku/factory/node_factory.nim
Prem Chaitanya Prathi d8bbef0c5b
feat(mix): bump libp2p stack to v2.0.0 + adopt stateless RLN spam protection
Combines five dep-and-build changes that all flow from the libp2p v2.0.0
upgrade and the move to the extracted libp2p_mix / mix-rln plugin stack:

waku.nimble:
  * libp2p: ff8d51857 -> c43199378 (release/v2.0.0 tip; sha-pinned until
    vacp2p cuts a v2.0.0 tag).
  * Drop the bare `zlib < 0.2` cap — no longer needed by the upgraded
    libp2p.
  * websock: bare ">= 0.4.0" — replaces the d4cd68b URL+SHA workaround
    that pinned through a libp2p commit-specific websock SHA.
  * nim-json-rpc: switch to chaitanyaprem/nim-json-rpc#f05fad25 — relaxes
    websock cap to allow >=0.4.0. TODO: revert to status-im/nim-json-rpc
    once status-im/nim-json-rpc#277 merges and a tag is cut.
  * lsquic: bare ">= 0.4.1" (drops URL form).
  * Add mix-rln-spam-protection-plugin pin (23b278b4) and nim-libp2p-mix
    pin (50c4ab4f — PR #14 HEAD); the plugin pins the same libp2p_mix
    SHA so the diamond dep collapses to a single source.

waku/factory/waku.nim:
  * Explicit HPService.setup(switch) / AutonatService.setup(switch)
    calls. libp2p v2.0.0's Service lifecycle refactor (libp2p#2462)
    removed switch.start's auto-setup loop, so any caller that assigns
    directly to switch.services (we do) is responsible for calling
    setup() themselves. Without it, AutonatService.addressMapper stays
    nil and peerInfo.expandAddrs SIGSEGVs during start(). Wrapped in
    try/except for ServiceSetupError so a setup failure surfaces as a
    logged error rather than a crash.

Build / scripts:
  * scripts/build_rln_mix.sh removed and Makefile simplified — librln
    is now a single shared archive built from zerokit's `stateless`
    features (no separate librln_mix archive).
  * simulations/mixnet/build_setup.sh + setup_credentials.nim updated
    to use librln_v2.0.2.a directly and run RLN keystore setup before
    nodes start.

Validated:
  * Cold local-cache nimble setup --localdeps -y.
  * wakunode2 and chat2mix link cleanly.
  * Mixnet roundtrip sim: [PASS] bob received message from alice.
  * RLN proof generation + verification on every in-path mix node:
    5 gen_called == 5 verified, 0 SPAM_PROOF_* errors.
2026-06-04 16:54:44 +05:30

509 lines
18 KiB
Nim

import
std/[options, sequtils],
chronicles,
chronos,
libp2p/peerid,
libp2p/protocols/pubsub/gossipsub,
libp2p/protocols/connectivity/relay/relay,
libp2p/nameresolving/dnsresolver,
libp2p/crypto/crypto,
libp2p/crypto/curve25519,
libp2p/crypto/rng as libp2p_rng,
bearssl/rand
import
./internal_config,
./networks_config,
./waku_conf,
./builder,
./validator_signed,
../waku_enr/sharding,
../waku_node,
../waku_core,
../waku_core/codecs,
../waku_rln_relay,
../discovery/waku_dnsdisc,
../waku_archive/retention_policy as policy,
../waku_archive/retention_policy/builder as policy_builder,
../waku_archive/driver as driver,
../waku_archive/driver/builder as driver_builder,
../waku_store,
../waku_store/common as store_common,
../waku_filter_v2,
../waku_peer_exchange,
../discovery/waku_kademlia,
../node/peer_manager,
../node/peer_manager/peer_store/waku_peer_storage,
../node/peer_manager/peer_store/migrations as peer_store_sqlite_migrations,
../waku_lightpush_legacy/common,
../common/rate_limit/setting
## Peer persistence
const PeerPersistenceDbUrl = "peers.db"
proc setupPeerStorage(): Result[Option[WakuPeerStorage], string] =
let db = ?SqliteDatabase.new(PeerPersistenceDbUrl)
?peer_store_sqlite_migrations.migrate(db)
let res = WakuPeerStorage.new(db).valueOr:
return err("failed to init peer store" & error)
return ok(some(res))
## Init waku node instance
proc initNode(
conf: WakuConf,
netConfig: NetConfig,
rng: ref HmacDrbgContext,
record: enr.Record,
peerStore: Option[WakuPeerStorage],
relay: Relay,
dynamicBootstrapNodes: openArray[RemotePeerInfo] = @[],
): Result[WakuNode, string] =
## Setup a basic Waku v2 node based on a supplied configuration
## file. Optionally include persistent peer storage.
## No protocols are mounted yet.
let pStorage =
if peerStore.isNone():
nil
else:
peerStore.get()
let (secureKey, secureCert) =
if conf.webSocketConf.isSome() and conf.webSocketConf.get().secureConf.isSome():
let wssConf = conf.webSocketConf.get().secureConf.get()
(some(wssConf.keyPath), some(wssConf.certPath))
else:
(none(string), none(string))
let nameResolver =
DnsResolver.new(conf.dnsAddrsNameServers.mapIt(initTAddress(it, Port(53))))
# Build waku node instance
var builder = WakuNodeBuilder.init()
builder.withRng(rng)
builder.withNodeKey(conf.nodeKey)
builder.withRecord(record)
builder.withNetworkConfiguration(netConfig)
builder.withPeerStorage(pStorage, capacity = conf.peerStoreCapacity)
builder.withSwitchConfiguration(
maxConnections = some(conf.maxConnections.int),
secureKey = secureKey,
secureCert = secureCert,
nameResolver = nameResolver,
sendSignedPeerRecord = conf.relayPeerExchange,
# We send our own signed peer record when peer exchange enabled
agentString = some(conf.agentString),
)
builder.withColocationLimit(conf.colocationLimit)
if conf.maxRelayPeers.isSome():
let
maxRelayPeers = conf.maxRelayPeers.get()
maxConnections = conf.maxConnections
# Calculate the ratio as percentages
relayRatio = (maxRelayPeers.float / maxConnections.float) * 100
serviceRatio = 100 - relayRatio
builder.withPeerManagerConfig(
maxConnections = conf.maxConnections,
relayServiceRatio = $relayRatio & ":" & $serviceRatio,
shardAware = conf.relayShardedPeerManagement,
)
error "maxRelayPeers is deprecated. It is recommended to use relayServiceRatio instead. If relayServiceRatio is not set, it will be automatically calculated based on maxConnections and maxRelayPeers."
else:
builder.withPeerManagerConfig(
maxConnections = conf.maxConnections,
relayServiceRatio = conf.relayServiceRatio,
shardAware = conf.relayShardedPeerManagement,
)
builder.withRateLimit(conf.rateLimit)
builder.withCircuitRelay(relay)
let node = ?builder.build().mapErr(
proc(err: string): string =
"failed to create waku node instance: " & err
)
ok(node)
## Mount protocols
proc getAutoshards*(
node: WakuNode, contentTopics: seq[string]
): Result[seq[RelayShard], string] =
if node.wakuAutoSharding.isNone():
return err("Static sharding used, cannot get shards from content topics")
var autoShards: seq[RelayShard]
for contentTopic in contentTopics:
let shard = node.wakuAutoSharding.get().getShard(contentTopic).valueOr:
return err("Could not parse content topic: " & error)
autoShards.add(shard)
return ok(autoshards)
proc setupProtocols(
node: WakuNode, conf: WakuConf
): Future[Result[void, string]] {.async.} =
## Setup configured protocols on an existing Waku v2 node.
## Optionally include persistent message storage.
## No protocols are started yet.
var allShards = conf.subscribeShards
node.mountMetadata(conf.clusterId, allShards).isOkOr:
return err("failed to mount waku metadata protocol: " & error)
var onFatalErrorAction = proc(msg: string) {.gcsafe, closure.} =
## Action to be taken when an internal error occurs during the node run.
## e.g. the connection with the database is lost and not recovered.
error "Unrecoverable error occurred", error = msg
quit(QuitFailure)
#mount mix
if conf.mixConf.isSome():
let mixConf = conf.mixConf.get()
(await node.mountMix(conf.clusterId, mixConf.mixKey, mixConf.mixnodes)).isOkOr:
return err("failed to mount waku mix protocol: " & $error)
# Setup extended kademlia discovery
if conf.kademliaDiscoveryConf.isSome():
let mixPubKey =
if conf.mixConf.isSome():
some(conf.mixConf.get().mixPubKey)
else:
none(Curve25519Key)
node.wakuKademlia = WakuKademlia.new(
node.switch,
ExtendedServiceDiscoveryParams(
bootstrapNodes: conf.kademliaDiscoveryConf.get().bootstrapNodes,
mixPubKey: mixPubKey,
advertiseMix: conf.mixConf.isSome(),
),
node.peerManager,
rng = libp2p_rng.newBearSslRng(node.rng),
getMixNodePoolSize = proc(): int {.gcsafe, raises: [].} =
if node.wakuMix.isNil():
0
else:
node.getMixNodePoolSize(),
isNodeStarted = proc(): bool {.gcsafe, raises: [].} =
node.started,
).valueOr:
return err("failed to setup kademlia discovery: " & error)
if conf.storeServiceConf.isSome():
let storeServiceConf = conf.storeServiceConf.get()
let archiveDriver = (
await driver.ArchiveDriver.new(
storeServiceConf.dbUrl, storeServiceConf.dbVacuum, storeServiceConf.dbMigration,
storeServiceConf.maxNumDbConnections, onFatalErrorAction,
)
).valueOr:
return err("failed to setup archive driver: " & error)
let retPolicies = policy.RetentionPolicy.new(storeServiceConf.retentionPolicies).valueOr:
return err("failed to create retention policy: " & error)
node.mountArchive(archiveDriver, retPolicies).isOkOr:
return err("failed to mount waku archive protocol: " & error)
# Store setup
try:
await mountStore(node, node.rateLimitSettings.getSetting(STOREV3))
except CatchableError:
return err("failed to mount waku store protocol: " & getCurrentExceptionMsg())
if storeServiceConf.storeSyncConf.isSome():
let confStoreSync = storeServiceConf.storeSyncConf.get()
(
await node.mountStoreSync(
conf.clusterId, conf.subscribeShards, conf.contentTopics,
confStoreSync.rangeSec, confStoreSync.intervalSec,
confStoreSync.relayJitterSec,
)
).isOkOr:
return err("failed to mount waku store sync protocol: " & $error)
if conf.remoteStoreNode.isSome():
let storeNode = parsePeerInfo(conf.remoteStoreNode.get()).valueOr:
return err("failed to set node waku store-sync peer: " & error)
node.peerManager.addServicePeer(storeNode, WakuReconciliationCodec)
node.peerManager.addServicePeer(storeNode, WakuTransferCodec)
mountStoreClient(node)
if conf.remoteStoreNode.isSome():
let storeNode = parsePeerInfo(conf.remoteStoreNode.get()).valueOr:
return err("failed to set node waku store peer: " & error)
node.peerManager.addServicePeer(storeNode, WakuStoreCodec)
if conf.storeServiceConf.isSome and conf.storeServiceConf.get().resume:
node.setupStoreResume()
if conf.shardingConf.kind == AutoSharding:
node.mountAutoSharding(conf.clusterId, conf.shardingConf.numShardsInCluster).isOkOr:
return err("failed to mount waku auto sharding: " & error)
else:
warn("Auto sharding is disabled")
# Mount relay on all nodes
var peerExchangeHandler = none(RoutingRecordsHandler)
if conf.relayPeerExchange:
proc handlePeerExchange(
peer: PeerId, topic: string, peers: seq[RoutingRecordsPair]
) {.gcsafe.} =
## Handle peers received via gossipsub peer exchange
# TODO: Only consider peers on pubsub topics we subscribe to
let exchangedPeers = peers.filterIt(it.record.isSome())
# only peers with populated records
.mapIt(toRemotePeerInfo(it.record.get()))
info "adding exchanged peers",
src = peer, topic = topic, numPeers = exchangedPeers.len
for peer in exchangedPeers:
# Peers added are filtered by the peer manager
node.peerManager.addPeer(peer, PeerOrigin.PeerExchange)
peerExchangeHandler = some(handlePeerExchange)
# TODO: when using autosharding, the user should not be expected to pass any shards, but only content topics
# Hence, this joint logic should be removed in favour of an either logic:
# use passed shards (static) or deduce shards from content topics (auto)
let autoShards =
if node.wakuAutoSharding.isSome():
node.getAutoshards(conf.contentTopics).valueOr:
return err("Could not get autoshards: " & error)
else:
@[]
info "Shards created from content topics",
contentTopics = conf.contentTopics, shards = autoShards
let confShards = conf.subscribeShards.mapIt(
RelayShard(clusterId: conf.clusterId, shardId: uint16(it))
)
let shards = confShards & autoShards
if conf.relay:
info "Setting max message size", num_bytes = conf.maxMessageSizeBytes
(
await mountRelay(
node, peerExchangeHandler = peerExchangeHandler, int(conf.maxMessageSizeBytes)
)
).isOkOr:
return err("failed to mount waku relay protocol: " & $error)
# Add validation keys to protected topics
var subscribedProtectedShards: seq[ProtectedShard]
for shardKey in conf.protectedShards:
if shardKey.shard notin conf.subscribeShards:
warn "protected shard not in subscribed shards, skipping adding validator",
protectedShard = shardKey.shard, subscribedShards = shards
continue
subscribedProtectedShards.add(shardKey)
notice "routing only signed traffic",
protectedShard = shardKey.shard, publicKey = shardKey.key
node.wakuRelay.addSignedShardsValidator(subscribedProtectedShards, conf.clusterId)
if conf.rendezvous:
await node.mountRendezvous(conf.clusterId, shards)
await node.mountRendezvousClient(conf.clusterId)
# Keepalive mounted on all nodes
try:
await mountLibp2pPing(node)
except CatchableError:
return err("failed to mount libp2p ping protocol: " & getCurrentExceptionMsg())
if conf.rlnRelayConf.isSome():
let rlnRelayConf = conf.rlnRelayConf.get()
let rlnConf = WakuRlnConfig(
dynamic: rlnRelayConf.dynamic,
credIndex: rlnRelayConf.credIndex,
ethContractAddress: rlnRelayConf.ethContractAddress,
chainId: rlnRelayConf.chainId,
ethClientUrls: rlnRelayConf.ethClientUrls,
creds: rlnRelayConf.creds,
userMessageLimit: rlnRelayConf.userMessageLimit,
epochSizeSec: rlnRelayConf.epochSizeSec,
onFatalErrorAction: onFatalErrorAction,
)
try:
await node.mountRlnRelay(rlnConf)
except CatchableError:
return err("failed to mount waku RLN relay protocol: " & getCurrentExceptionMsg())
# NOTE Must be mounted after relay
if conf.lightPush:
try:
(await mountLightPush(node, node.rateLimitSettings.getSetting(LIGHTPUSH))).isOkOr:
return err("failed to mount waku lightpush protocol: " & $error)
(await mountLegacyLightPush(node, node.rateLimitSettings.getSetting(LIGHTPUSH))).isOkOr:
return err("failed to mount waku legacy lightpush protocol: " & $error)
except CatchableError:
return err("failed to mount waku lightpush protocol: " & getCurrentExceptionMsg())
mountLightPushClient(node)
mountLegacyLightPushClient(node)
if conf.remoteLightPushNode.isSome():
let lightPushNode = parsePeerInfo(conf.remoteLightPushNode.get()).valueOr:
return err("failed to set node waku lightpush peer: " & error)
node.peerManager.addServicePeer(lightPushNode, WakuLightPushCodec)
node.peerManager.addServicePeer(lightPushNode, WakuLegacyLightPushCodec)
# Filter setup. NOTE Must be mounted after relay
if conf.filterServiceConf.isSome():
let confFilter = conf.filterServiceConf.get()
try:
await mountFilter(
node,
subscriptionTimeout = chronos.seconds(confFilter.subscriptionTimeout),
maxFilterPeers = confFilter.maxPeersToServe,
maxFilterCriteriaPerPeer = confFilter.maxCriteria,
rateLimitSetting = node.rateLimitSettings.getSetting(FILTER),
)
except CatchableError:
return err("failed to mount waku filter protocol: " & getCurrentExceptionMsg())
await node.mountFilterClient()
if conf.remoteFilterNode.isSome():
let filterNode = parsePeerInfo(conf.remoteFilterNode.get()).valueOr:
return err("failed to set node waku filter peer: " & error)
try:
node.peerManager.addServicePeer(filterNode, WakuFilterSubscribeCodec)
except CatchableError:
return
err("failed to mount waku filter client protocol: " & getCurrentExceptionMsg())
# waku peer exchange setup
if conf.peerExchangeService:
try:
await mountPeerExchange(
node, some(conf.clusterId), node.rateLimitSettings.getSetting(PEEREXCHG)
)
except CatchableError:
return
err("failed to mount waku peer-exchange protocol: " & getCurrentExceptionMsg())
if conf.remotePeerExchangeNode.isSome():
let peerExchangeNode = parsePeerInfo(conf.remotePeerExchangeNode.get()).valueOr:
return err("failed to set node waku peer-exchange peer: " & error)
node.peerManager.addServicePeer(peerExchangeNode, WakuPeerExchangeCodec)
if conf.peerExchangeDiscovery:
await node.mountPeerExchangeClient()
return ok()
## Start node
proc startNode*(
node: WakuNode, conf: WakuConf, dynamicBootstrapNodes: seq[RemotePeerInfo] = @[]
): Future[Result[void, string]] {.async: (raises: []).} =
## Start a configured node and all mounted protocols.
## Connect to static nodes and start
## keep-alive, if configured.
info "Running nwaku node", version = git_version
try:
await node.start()
except CatchableError:
return err("failed to start waku node: " & getCurrentExceptionMsg())
# Connect to configured static nodes
if conf.staticNodes.len > 0:
try:
await connectToNodes(node, conf.staticNodes, "static")
except CatchableError:
return err("failed to connect to static nodes: " & getCurrentExceptionMsg())
if dynamicBootstrapNodes.len > 0:
info "Connecting to dynamic bootstrap peers"
try:
await connectToNodes(node, dynamicBootstrapNodes, "dynamic bootstrap")
except CatchableError:
return
err("failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg())
# retrieve px peers and add the to the peer store
if conf.remotePeerExchangeNode.isSome():
var desiredOutDegree = DefaultPXNumPeersReq
if not node.wakuRelay.isNil() and node.wakuRelay.parameters.d.uint64() > 0:
desiredOutDegree = node.wakuRelay.parameters.d.uint64()
(await node.fetchPeerExchangePeers(desiredOutDegree)).isOkOr:
error "error while fetching peers from peer exchange", error = error
# TODO: behavior described by comment is undesired. PX as client should be used in tandem with discv5.
#
# Use px to periodically get peers if discv5 is disabled, as discv5 nodes have their own
# periodic loop to find peers and px returned peers actually come from discv5
if conf.peerExchangeDiscovery and not conf.discv5Conf.isSome():
node.startPeerExchangeLoop()
# Maintain relay connections
if conf.relay:
node.peerManager.start()
if not node.wakuKademlia.isNil():
let minMixPeers = if conf.mixConf.isSome(): 4 else: 0
(await node.wakuKademlia.start(minMixPeers = minMixPeers)).isOkOr:
return err("failed to start kademlia discovery: " & error)
return ok()
proc setupNode*(
wakuConf: WakuConf, rng: ref HmacDrbgContext = HmacDrbgContext.new(), relay: Relay
): Future[Result[WakuNode, string]] {.async.} =
let netConfig = (
await networkConfiguration(
wakuConf.clusterId, wakuConf.endpointConf, wakuConf.discv5Conf,
wakuConf.webSocketConf, wakuConf.wakuFlags, wakuConf.dnsAddrsNameServers,
wakuConf.portsShift, clientId,
)
).valueOr:
error "failed to create internal config", error = error
return err("failed to create internal config: " & error)
let record = enrConfiguration(wakuConf, netConfig).valueOr:
error "failed to create record", error = error
return err("failed to create record: " & error)
if isClusterMismatched(record, wakuConf.clusterId):
error "cluster id mismatch configured shards"
return err("cluster id mismatch configured shards")
info "Setting up storage"
## Peer persistence
var peerStore: Option[WakuPeerStorage]
if wakuConf.peerPersistence:
peerStore = setupPeerStorage().valueOr:
error "Setting up storage failed", error = "failed to setup peer store " & error
return err("Setting up storage failed: " & error)
info "Initializing node"
let node = initNode(wakuConf, netConfig, rng, record, peerStore, relay).valueOr:
error "Initializing node failed", error = error
return err("Initializing node failed: " & error)
info "Mounting protocols"
try:
(await node.setupProtocols(wakuConf)).isOkOr:
error "Mounting protocols failed", error = error
return err("Mounting protocols failed: " & error)
except CatchableError:
return err("Exception setting up protocols: " & getCurrentExceptionMsg())
return ok(node)