Merge 37ee2d53eeebda9310b6f424fbc59fa548702c8e into 64a0ed7d967454d9c3b345023719e6ca5d73f129

This commit is contained in:
Fabiana Cecin 2026-06-03 11:36:39 +02:00 committed by GitHub
commit ad2a52b7f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1910 additions and 1543 deletions

View File

@ -96,7 +96,7 @@ when isMainModule:
wakuNodeConf.shards = @[conf.shard]
wakuNodeConf.contentTopics = conf.contentTopics
wakuNodeConf.clusterId = conf.clusterId
wakuNodeConf.clusterId = some(conf.clusterId)
## TODO: Depending on the tester needs we might extend here with shards, clusterId, etc...
wakuNodeConf.metricsServer = true
@ -123,7 +123,7 @@ when isMainModule:
error "Waku initialization failed", error = error
quit(QuitFailure)
(waitFor startWaku(addr waku)).isOkOr:
(waitFor waku.start()).isOkOr:
error "Starting waku failed", error = error
quit(QuitFailure)

View File

@ -55,7 +55,7 @@ when isMainModule:
error "Waku initialization failed", error = error
quit(QuitFailure)
(waitFor startWaku(addr waku)).isOkOr:
(waitFor waku.start()).isOkOr:
error "Starting waku failed", error = error
quit(QuitFailure)

View File

@ -1,7 +1,7 @@
## Reliable Channel event types emitted to API consumers.
##
## Lifecycle events for individual segments (sent / propagated / errored)
## are the same as the network-level ones the DeliveryService already
## are the same as the network-level ones the MessagingClient already
## emits — `requestId` is shared across layers — so we just re-export
## `waku/events/message_events` and avoid declaring duplicates.
##

View File

@ -20,8 +20,7 @@ import bearssl/rand
import stew/byteutils
import libp2p/crypto/crypto as libp2p_crypto
import waku/api/api
import waku/factory/waku as waku_factory
import waku/api/types
import waku/node/delivery_service/send_service
import waku/waku_core/topics
@ -32,7 +31,7 @@ import ./rate_limit_manager/rate_limit_manager
import ./encryption/encryption
export
api, waku_factory, events, segmentation, scalable_data_sync, rate_limit_manager,
types, send_service, events, segmentation, scalable_data_sync, rate_limit_manager,
encryption
const LipWireReliableChannelVersion* = "RELIABLE-CHANNEL-API/1"
@ -47,9 +46,10 @@ type
SendHandler* = proc(envelope: MessageEnvelope): Future[Result[RequestId, string]] {.
async: (raises: [CatchableError]), gcsafe
.}
## Egress dispatch boundary. Defaults to `waku.send`; tests inject a
## fake that records calls and returns canned `RequestId`s so the
## send state machine can be exercised end-to-end without a network.
## Egress dispatch boundary. Typically wraps `MessagingClient.send`;
## tests inject a fake that records calls and returns canned
## `RequestId`s so the send state machine can be exercised end-to-end
## without a network.
MessagePersistence {.pure.} = enum
Persistent
@ -264,20 +264,20 @@ proc onReadyToSend(
meta: LipWireReliableChannelVersion.toBytes(),
)
## `waku.send` is not annotated `(raises: [])`, but this listener is.
## `sendHandler` is not annotated `(raises: [])`, but this listener is.
## Convert any raise to a Result error so the state machine handles
## both failure modes (Result.err and exception) through one path.
let sendRes =
try:
await self.sendHandler(envelope)
except CatchableError as e:
Result[RequestId, string].err("waku send raised: " & e.msg)
Result[RequestId, string].err("messaging send raised: " & e.msg)
let messagingReqId = sendRes.valueOr:
MessageErrorEvent.emit(
self.brokerCtx,
MessageErrorEvent(
requestId: channelReqId, messageHash: "", error: "waku send failed: " & error
requestId: channelReqId, messageHash: "", error: "messaging send failed: " & error
),
)
self.markSegmentFailed(channelReqId)
@ -374,7 +374,7 @@ proc onMessageReceived(
proc new*(
T: type ReliableChannel,
waku: Waku,
sendHandler: SendHandler,
channelId: ChannelId,
contentTopic: ContentTopic,
senderId: SdsParticipantID,
@ -382,7 +382,6 @@ proc new*(
sdsConfig: SdsConfig,
rateConfig: RateLimitConfig,
brokerCtx: BrokerContext = globalBrokerContext(),
sendHandler: SendHandler = nil,
): T =
## Pipeline handlers (segmentation/SDS/rate-limit) are constructed
## inside the channel rather than handed in by the caller — they are
@ -391,19 +390,11 @@ proc new*(
## `Decrypt` request brokers, so the channel keeps no per-instance
## encryption state either.
##
## `sendHandler` defaults to `waku.send`; tests pass a fake to drive
## the send state machine without touching the network.
let resolvedSendHandler =
if sendHandler.isNil():
proc(
envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
return await waku.send(envelope)
else:
sendHandler
## `sendHandler` is the egress dispatch. The owning `ReliableChannelManager`
## typically constructs it as a closure over `MessagingClient.send`. Tests
## pass a fake to drive the send state machine without touching the network.
let chn = T(
sendHandler: resolvedSendHandler,
sendHandler: sendHandler,
channelId: channelId,
contentTopic: contentTopic,
senderId: senderId,

View File

@ -10,11 +10,10 @@ import results
import chronos
import stew/byteutils
import waku/api/api
import waku/api/api_conf
import brokers/broker_context
import waku/events/message_events as waku_message_events
import waku/factory/waku as waku_factory
import waku/node/delivery_service/delivery_service
import waku/messaging_client
import waku/waku_core/topics
import ./reliable_channel
@ -24,40 +23,43 @@ export reliable_channel
type ReliableChannelManager* = ref object
channels: Table[ChannelId, ReliableChannel]
waku: Waku
## Owned by the manager. The channel layer reaches the messaging
## API through `waku.send(envelope)`; constructing DeliveryTasks
## directly would breach the layer boundary.
messagingClient: MessagingClient
## Borrowed from the owning `Waku`.
sendHandler: SendHandler
## Default egress dispatch for channels created through this manager.
## Constructed at mount time as a closure over `MessagingClient.send`
## so the channel layer itself stays callable-only.
brokerCtx: BrokerContext
proc new*(
T: type ReliableChannelManager,
conf: WakuNodeConf,
messagingClient: MessagingClient,
sendHandler: SendHandler,
brokerCtx: BrokerContext = globalBrokerContext(),
): Future[Result[T, string]] {.async.} =
## TODO !! The proper ownership chain is:
## ReliableChannelManager -> DeliveryService (MessagingClient) -> Waku (Kernel/Protocols) -> WakuNode,
## and this will be implemented in the future. For now, `createNode`
## is called here to get a Waku instance, and the WakuNode is immediately discarded.
## This is a temporary workaround to get the API
let waku = ?(await createNode(conf))
let manager = T(
channels: initTable[ChannelId, ReliableChannel](), waku: waku, brokerCtx: brokerCtx
): Result[T, string] =
if messagingClient.isNil():
return err("messaging client is required")
if sendHandler.isNil():
return err("sendHandler is required")
return ok(
T(
channels: initTable[ChannelId, ReliableChannel](),
messagingClient: messagingClient,
sendHandler: sendHandler,
brokerCtx: brokerCtx,
)
)
return ok(manager)
proc start*(self: ReliableChannelManager): Result[void, string] =
## Bring the owned DeliveryService up. Separated from `new` so callers
## can register encryption providers / create channels before traffic
## starts flowing.
self.waku.deliveryService.startDeliveryService()
## Placeholder: per-channel listeners are installed in `ReliableChannel.new`,
## so the manager has nothing to start at this layer. Kept for symmetry
## with the `Waku` mount/start lifecycle and as a hook for future state.
discard
ok()
proc stop*(self: ReliableChannelManager) {.async.} =
if not self.waku.isNil():
await self.waku.deliveryService.stopDeliveryService()
## Placeholder mirror of `start`.
discard
proc createReliableChannel*(
self: ReliableChannelManager,
@ -66,17 +68,17 @@ proc createReliableChannel*(
senderId: SdsParticipantID,
sendHandler: SendHandler = nil,
): Result[ChannelId, string] =
## Spec entry point. The `DeliveryService` and `rng` the channel needs
## are sourced from the owning `ReliableChannelManager` rather than
## passed per call. Encryption is wired up through the `Encrypt`/
## `Decrypt` request brokers — the application installs its own
## providers (or `setNoopEncryption()`) before traffic flows.
## Spec entry point. The `sendHandler` and `rng` the channel needs are
## sourced from the owning `ReliableChannelManager` rather than passed
## per call. Encryption is wired up through the `Encrypt`/`Decrypt`
## request brokers — the application installs its own providers
## (or `setNoopEncryption()`) before traffic flows.
##
## Segmentation, SDS and rate-limit configs will eventually be read
## from the node's `NodeConfig`. Defaults for now.
##
## `sendHandler` is left `nil` in production so the channel uses the
## owned `waku.send`; tests pass a fake to bypass the network.
## `sendHandler` defaults to the manager's default (constructed at mount
## from `MessagingClient.send`); tests pass a fake to bypass the network.
if self.channels.hasKey(channelId):
return err("channel already exists: " & channelId)
@ -95,8 +97,14 @@ proc createReliableChannel*(
epochPeriodSec: DefaultEpochPeriodSec, messagesPerEpoch: DefaultMessagesPerEpoch
)
let effectiveSendHandler =
if sendHandler.isNil():
self.sendHandler
else:
sendHandler
let chn = ReliableChannel.new(
waku = self.waku,
sendHandler = effectiveSendHandler,
channelId = channelId,
contentTopic = contentTopic,
senderId = senderId,
@ -104,7 +112,6 @@ proc createReliableChannel*(
sdsConfig = sdsConfig,
rateConfig = rateConfig,
brokerCtx = self.brokerCtx,
sendHandler = sendHandler,
)
self.channels[channelId] = chn
@ -137,5 +144,5 @@ proc send*(
## `ReliableChannel` installs its own `MessageReceivedEvent` listener
## in `ReliableChannel.new`, filters by spec marker and `contentTopic`,
## and routes to its private `onMessageReceived`. This keeps the lower
## layer (MessagingAPI/Waku) unaware of the existence of ReliableChannel
## layer (MessagingClient/Waku) unaware of the existence of ReliableChannel
## and keeps the manager out of per-channel event dispatch.

View File

@ -82,8 +82,12 @@ when isMainModule:
echo("Waku node created successfully!")
node.mountMessagingClient().isOkOr:
echo "Failed to mount messaging: ", error
quit(QuitFailure)
# Start the node
(waitFor startWaku(addr node)).isOkOr:
(waitFor node.start()).isOkOr:
echo "Failed to start node: ", error
quit(QuitFailure)

View File

@ -30,25 +30,25 @@ proc setup*(): Waku =
# Override configuration
conf.maxMessageSize = twnNetworkConf.maxMessageSize
conf.clusterId = twnNetworkConf.clusterId
conf.clusterId = some(twnNetworkConf.clusterId)
conf.rlnRelayEthContractAddress = twnNetworkConf.rlnRelayEthContractAddress
conf.rlnRelayDynamic = twnNetworkConf.rlnRelayDynamic
conf.discv5Discovery = twnNetworkConf.discv5Discovery
conf.rlnRelayDynamic = some(twnNetworkConf.rlnRelayDynamic)
conf.discv5Discovery = some(twnNetworkConf.discv5Discovery)
conf.discv5BootstrapNodes =
conf.discv5BootstrapNodes & twnNetworkConf.discv5BootstrapNodes
conf.rlnEpochSizeSec = twnNetworkConf.rlnEpochSizeSec
conf.rlnRelayUserMessageLimit = twnNetworkConf.rlnRelayUserMessageLimit
conf.rlnEpochSizeSec = some(twnNetworkConf.rlnEpochSizeSec)
conf.rlnRelayUserMessageLimit = some(twnNetworkConf.rlnRelayUserMessageLimit)
# Only set rlnRelay to true if relay is configured
if conf.relay:
conf.rlnRelay = twnNetworkConf.rlnRelay
conf.rlnRelay = some(twnNetworkConf.rlnRelay)
info "Starting node"
var waku = (waitFor Waku.new(conf)).valueOr:
error "Waku initialization failed", error = error
quit(QuitFailure)
(waitFor startWaku(addr waku)).isOkOr:
(waitFor waku.start()).isOkOr:
error "Starting waku failed", error = error
quit(QuitFailure)

View File

@ -1,11 +1,11 @@
import std/[json, strutils, tables]
import chronos, chronicles, results, confutils, confutils/std/net, ffi
import std/json
import chronos, chronicles, results, ffi
import
waku/factory/waku,
waku/node/waku_node,
waku/api/[api, types],
waku/events/[message_events, health_events],
tools/confutils/cli_args,
tools/confutils/conf_from_json,
../declare_lib,
../json_event
@ -15,59 +15,11 @@ proc `%`*(id: RequestId): JsonNode =
registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]):
proc(configJson: cstring): Future[Result[string, string]] {.async.} =
## Parse the JSON configuration using fieldPairs approach (WakuNodeConf)
var conf = defaultWakuNodeConf().valueOr:
return err("Failed creating default conf: " & error)
let conf = parseConfJson($configJson).valueOr:
error "Failed to assemble WakuNodeConf from JSON",
error = error, configJson = $configJson
return err(error)
var jsonNode: JsonNode
try:
jsonNode = parseJson($configJson)
except Exception:
let exceptionMsg = getCurrentExceptionMsg()
error "Failed to parse config JSON",
error = exceptionMsg, configJson = $configJson
return err(
"Failed to parse config JSON: " & exceptionMsg & " configJson string: " &
$configJson
)
var jsonFields: Table[string, (string, JsonNode)]
for key, value in jsonNode:
let lowerKey = key.toLowerAscii()
if jsonFields.hasKey(lowerKey):
error "Duplicate configuration option found when normalized to lowercase",
key = key
return err(
"Duplicate configuration option found when normalized to lowercase: '" & key &
"'"
)
jsonFields[lowerKey] = (key, value)
for confField, confValue in fieldPairs(conf):
let lowerField = confField.toLowerAscii()
if jsonFields.hasKey(lowerField):
let (jsonKey, jsonValue) = jsonFields[lowerField]
let formattedString = ($jsonValue).strip(chars = {'\"'})
try:
confValue = parseCmdArg(typeof(confValue), formattedString)
except Exception:
return err(
"Failed to parse field '" & confField & "' from JSON key '" & jsonKey & "': " &
getCurrentExceptionMsg() & ". Value: " & formattedString
)
jsonFields.del(lowerField)
if jsonFields.len > 0:
var unknownKeys = newSeq[string]()
for _, (jsonKey, _) in pairs(jsonFields):
unknownKeys.add(jsonKey)
error "Unrecognized configuration option(s) found", option = unknownKeys
return err("Unrecognized configuration option(s) found: " & $unknownKeys)
# Create the node
ctx.myLib[] = (await api.createNode(conf)).valueOr:
let errMsg = $error
chronicles.error "CreateNodeRequest failed", err = errMsg
@ -96,7 +48,7 @@ proc logosdelivery_create_node(
): pointer {.dynlib, exportc, cdecl.} =
initializeLibrary()
if isNil(callback):
if callback.isNil():
echo "error: missing callback in logosdelivery_create_node"
return nil
@ -172,7 +124,17 @@ proc logosdelivery_start_node(
chronicles.error "ConnectionStatusChange.listen failed", err = $error
return err("ConnectionStatusChange.listen failed: " & $error)
(await startWaku(addr ctx.myLib[])).isOkOr:
ctx.myLib[].mountMessagingClient().isOkOr:
let errMsg = $error
chronicles.error "mountMessagingClient failed", error = errMsg
return err("failed to mount messaging: " & errMsg)
ctx.myLib[].mountReliableChannelManager().isOkOr:
let errMsg = $error
chronicles.error "mountReliableChannelManager failed", err = errMsg
return err("failed to mount reliable channel manager: " & errMsg)
(await ctx.myLib[].start()).isOkOr:
let errMsg = $error
chronicles.error "START_NODE failed", err = errMsg
return err("failed to start: " & errMsg)

View File

@ -71,7 +71,7 @@ registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]):
proc waku_start(
ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
) {.ffi.} =
(await startWaku(ctx[].myLib)).isOkOr:
(await ctx.myLib[].start()).isOkOr:
error "START_NODE failed", error = error
return err("failed to start: " & $error)
return ok("")

View File

@ -30,7 +30,7 @@ let
# while others use the repo root. Pass both so the compiler finds either layout.
pathArgs =
builtins.concatStringsSep " "
(builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" ])
(builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" "--path:${p}/sds" ])
(builtins.attrValues otherDeps));
libExt =

View File

@ -97,13 +97,15 @@ suite "LM API health checking":
conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16
conf.clusterId = some(3'u16)
conf.numShardsInNetwork = 1
conf.rest = false
client = (await createNode(conf)).valueOr:
raiseAssert error
(await startWaku(addr client)).isOkOr:
client.mountMessagingClient().isOkOr:
raiseAssert error
(await client.start()).isOkOr:
raiseAssert error
asyncTeardown:
@ -274,14 +276,16 @@ suite "LM API health checking":
edgeConf.listenAddress = parseIpAddress("0.0.0.0")
edgeConf.tcpPort = Port(0)
edgeConf.discv5UdpPort = Port(0)
edgeConf.clusterId = 3'u16
edgeConf.clusterId = some(3'u16)
edgeConf.maxMessageSize = "150 KiB"
edgeConf.rest = false
edgeWaku = (await createNode(edgeConf)).valueOr:
raiseAssert "Failed to create edge node: " & error
(await startWaku(addr edgeWaku)).isOkOr:
edgeWaku.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount edge messaging: " & error
(await edgeWaku.start()).isOkOr:
raiseAssert "Failed to start edge waku: " & error
let relayReq = await RequestProtocolHealth.request(

View File

@ -6,6 +6,7 @@ import libp2p/[peerid, peerinfo, crypto/crypto]
import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync]
import ../waku_archive/archive_utils
import waku/messaging_client
import
waku,
@ -16,7 +17,6 @@ import
waku_relay/protocol,
waku_archive,
waku_archive/common as archive_common,
node/delivery_service/delivery_service,
node/delivery_service/recv_service,
]
import waku/factory/waku_conf
@ -67,9 +67,9 @@ proc createApiNodeConf(numShards: uint16 = 1): WakuNodeConf =
conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16
conf.clusterId = some(3'u16)
conf.numShardsInNetwork = numShards
conf.reliabilityEnabled = true
conf.reliabilityEnabled = some(true)
conf.rest = false
result = conf
@ -147,7 +147,8 @@ suite "Messaging API, Receive Service (store recovery)":
subscriber = (await createNode(createApiNodeConf(numShards))).expect(
"Failed to create subscriber"
)
(await startWaku(addr subscriber)).expect("Failed to start subscriber")
subscriber.mountMessagingClient().expect("Failed to mount messaging")
(await subscriber.start()).expect("Failed to start subscriber")
# publish after the subscriber exists but before it connects to the
# store; the message reaches the archive but the subscriber doesn't
@ -185,7 +186,7 @@ suite "Messaging API, Receive Service (store recovery)":
await eventManager.teardown()
# trigger store check, should recover and deliver via MessageReceivedEvent
await subscriber.deliveryService.recvService.checkStore()
await subscriber.messagingClient.recvService.checkStore()
let received = await eventManager.waitForEvents(TestTimeout)
check received

View File

@ -126,9 +126,9 @@ proc createApiNodeConf(mode: cli_args.WakuMode = cli_args.WakuMode.Core): WakuNo
conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16
conf.clusterId = some(3'u16)
conf.numShardsInNetwork = 1
conf.reliabilityEnabled = true
conf.reliabilityEnabled = some(true)
conf.rest = false
result = conf
@ -241,7 +241,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error
(await startWaku(addr node)).isOkOr:
node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error
# node is not connected !
@ -263,7 +265,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error
(await startWaku(addr node)).isOkOr:
node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(
@ -297,7 +301,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error
(await startWaku(addr node)).isOkOr:
node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[relayNode1PeerInfo])
@ -327,7 +333,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error
(await startWaku(addr node)).isOkOr:
node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[lightpushNodePeerInfo])
@ -357,7 +365,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error
(await startWaku(addr node)).isOkOr:
node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[lightpushNodePeerInfo, storeNodePeerInfo])
@ -411,7 +421,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf(cli_args.WakuMode.Edge))).valueOr:
raiseAssert error
(await startWaku(addr node)).isOkOr:
node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[fakeLightpushNodePeerInfo])

View File

@ -5,6 +5,7 @@ import chronos, testutils/unittests, stew/byteutils
import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto]
import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync]
import waku/messaging_client
import
waku,
@ -14,13 +15,14 @@ import
events/message_events,
waku_relay/protocol,
node/kernel_api/filter,
node/delivery_service/subscription_manager,
node/subscription_manager,
]
import waku/factory/waku_conf
import tools/confutils/cli_args
const TestTimeout = chronos.seconds(10)
const NegativeTestTimeout = chronos.seconds(2)
const EdgeWaitTimeout = chronos.seconds(60)
type ReceiveEventListenerManager = ref object
brokerCtx: BrokerContext
@ -75,9 +77,9 @@ proc createApiNodeConf(
conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16
conf.clusterId = some(3'u16)
conf.numShardsInNetwork = numShards
conf.reliabilityEnabled = true
conf.reliabilityEnabled = some(true)
conf.rest = false
result = conf
@ -85,7 +87,8 @@ proc setupSubscriberNode(conf: WakuNodeConf): Future[Waku] {.async.} =
var node: Waku
lockNewGlobalBrokerContext:
node = (await createNode(conf)).expect("Failed to create subscriber node")
(await startWaku(addr node)).expect("Failed to start subscriber node")
node.mountMessagingClient().expect("Failed to mount messaging")
(await node.start()).expect("Failed to start subscriber node")
return node
proc setupNetwork(
@ -161,20 +164,39 @@ proc getRelayShard(node: WakuNode, contentTopic: ContentTopic): PubsubTopic =
return PubsubTopic($shardObj)
proc waitForMesh(node: WakuNode, shard: PubsubTopic) {.async.} =
for _ in 0 ..< 50:
let deadline = Moment.now() + EdgeWaitTimeout
while Moment.now() < deadline:
if node.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0:
return
await sleepAsync(100.milliseconds)
raise newException(ValueError, "GossipSub Mesh failed to stabilize on " & shard)
proc waitForEdgeSubs(w: Waku, shard: PubsubTopic) {.async.} =
let sm = w.deliveryService.subscriptionManager
for _ in 0 ..< 50:
if sm.edgeFilterPeerCount(shard) > 0:
let deadline = Moment.now() + EdgeWaitTimeout
while Moment.now() < deadline:
if w.node.subscriptionManager.edgeFilterPeerCount(shard) > 0:
return
await sleepAsync(100.milliseconds)
raise newException(ValueError, "Edge filter subscription failed on " & shard)
proc edgePeersReached(w: Waku, shard: PubsubTopic, n: int): Future[bool] {.async.} =
let deadline = Moment.now() + EdgeWaitTimeout
while Moment.now() < deadline:
if w.node.subscriptionManager.edgeFilterPeerCount(shard) >= n:
return true
await sleepAsync(100.milliseconds)
return false
proc edgePeersDroppedBelow(
w: Waku, shard: PubsubTopic, n: int
): Future[bool] {.async.} =
let deadline = Moment.now() + EdgeWaitTimeout
while Moment.now() < deadline:
if w.node.subscriptionManager.edgeFilterPeerCount(shard) < n:
return true
await sleepAsync(100.milliseconds)
return false
proc publishToMesh(
net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte]
): Future[Result[int, string]] {.async.} =
@ -621,7 +643,8 @@ suite "Messaging API, SubscriptionManager":
var subscriber: Waku
lockNewGlobalBrokerContext:
subscriber = (await createNode(conf)).expect("Failed to create edge subscriber")
(await startWaku(addr subscriber)).expect("Failed to start edge subscriber")
subscriber.mountMessagingClient().expect("Failed to mount messaging")
(await subscriber.start()).expect("Failed to start edge subscriber")
# Connect edge subscriber to both filter servers so selectPeers finds both
await subscriber.node.connectToNodes(@[publisherPeerInfo, meshBuddyPeerInfo])
@ -632,12 +655,7 @@ suite "Messaging API, SubscriptionManager":
(await subscriber.subscribe(testTopic)).expect("Failed to subscribe")
# Wait for dialing both filter servers (HealthyThreshold = 2)
for _ in 0 ..< 100:
if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2:
break
await sleepAsync(100.milliseconds)
check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2
check await edgePeersReached(subscriber, shard, 2)
# Verify message delivery with both servers alive
await waitForMesh(publisher, shard)
@ -659,12 +677,8 @@ suite "Messaging API, SubscriptionManager":
await subscriber.node.disconnectNode(meshBuddyPeerInfo)
# Wait for the dead peer to be pruned
for _ in 0 ..< 50:
if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) < 2:
break
await sleepAsync(100.milliseconds)
check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 1
check await edgePeersDroppedBelow(subscriber, shard, 2)
check subscriber.node.subscriptionManager.edgeFilterPeerCount(shard) >= 1
# Verify messages still arrive through the surviving filter server (publisher)
eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1)
@ -758,7 +772,8 @@ suite "Messaging API, SubscriptionManager":
var subscriber: Waku
lockNewGlobalBrokerContext:
subscriber = (await createNode(conf)).expect("Failed to create edge subscriber")
(await startWaku(addr subscriber)).expect("Failed to start edge subscriber")
subscriber.mountMessagingClient().expect("Failed to mount messaging")
(await subscriber.start()).expect("Failed to start edge subscriber")
await subscriber.node.connectToNodes(
@[publisherPeerInfo, meshBuddyPeerInfo, sparePeerInfo]
@ -770,23 +785,13 @@ suite "Messaging API, SubscriptionManager":
(await subscriber.subscribe(testTopic)).expect("Failed to subscribe")
# Wait for 2 confirmed peers (HealthyThreshold). The 3rd is available but not dialed.
for _ in 0 ..< 100:
if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2:
break
await sleepAsync(100.milliseconds)
require subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) ==
2
check await edgePeersReached(subscriber, shard, 2)
require subscriber.node.subscriptionManager.edgeFilterPeerCount(shard) == 2
await subscriber.node.disconnectNode(meshBuddyPeerInfo)
# Wait for the sub loop to detect the loss and dial a replacement
for _ in 0 ..< 100:
if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2:
break
await sleepAsync(100.milliseconds)
check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2
check await edgePeersReached(subscriber, shard, 2)
await waitForMesh(publisher, shard)

View File

@ -37,7 +37,7 @@ suite "WakuNodeConf - mode-driven toWakuConf":
var conf = defaultWakuNodeConf().valueOr:
raiseAssert error
conf.mode = Core
conf.clusterId = 1
conf.clusterId = some(1'u16)
## When
let wakuConfRes = conf.toWakuConf()
@ -58,7 +58,7 @@ suite "WakuNodeConf - mode-driven toWakuConf":
var conf = defaultWakuNodeConf().valueOr:
raiseAssert error
conf.mode = Edge
conf.clusterId = 1
conf.clusterId = some(1'u16)
## When
let wakuConfRes = conf.toWakuConf()
@ -81,7 +81,7 @@ suite "WakuNodeConf - mode-driven toWakuConf":
conf.mode = cli_args.WakuMode.noMode
conf.relay = true
conf.lightpush = false
conf.clusterId = 5
conf.clusterId = some(5'u16)
## When
let wakuConfRes = conf.toWakuConf()
@ -122,7 +122,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
let conf = confRes.get()
check:
conf.mode == cli_args.WakuMode.noMode
conf.clusterId == 0
conf.clusterId.isNone()
conf.logLevel == logging.LogLevel.INFO
test "JSON with mode and clusterId":
@ -134,7 +134,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
let conf = confRes.get()
check:
conf.mode == Core
conf.clusterId == 42
conf.clusterId == some(42'u16)
test "JSON with Edge mode":
## Given / When
@ -165,7 +165,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
require confRes.isOk()
let conf = confRes.get()
check:
conf.clusterId == 99
conf.clusterId == some(99'u16)
conf.numShardsInNetwork == 16
test "JSON with unknown fields is silently ignored":
@ -177,7 +177,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
require confRes.isOk()
let conf = confRes.get()
check:
conf.clusterId == 5
conf.clusterId == some(5'u16)
test "Invalid JSON syntax returns error":
## Given / When

View File

@ -1,6 +1,6 @@
{.used.}
import std/[net]
import std/[net, options]
import chronos, testutils/unittests, stew/byteutils
import brokers/broker_context
@ -24,9 +24,9 @@ proc createApiNodeConf(): WakuNodeConf =
conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16
conf.clusterId = some(3'u16)
conf.numShardsInNetwork = 1
conf.reliabilityEnabled = true
conf.reliabilityEnabled = some(true)
conf.rest = false
return conf
@ -35,7 +35,7 @@ suite "Reliable Channel - ingress":
## Unit test for the receive side of the API: instead of standing
## up two libp2p nodes and a relay mesh, we drive the manager
## directly by emitting a `MessageReceivedEvent` (the exact event
## the DeliveryService emits when a `WakuMessage` arrives off the
## the MessagingClient emits when a `WakuMessage` arrives off the
## wire). The manager must:
## - drop traffic missing the Reliable Channel spec marker
## - dispatch the matching channel's `onMessageReceived`
@ -45,13 +45,15 @@ suite "Reliable Channel - ingress":
contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "hello reliable channel".toBytes()
var waku: Waku
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect(
"Failed to create manager"
)
waku = (await createNode(createApiNodeConf())).expect("createNode")
waku.mountMessagingClient().expect("mountMessagingClient")
waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
## Noop encryption providers so the Encrypt/Decrypt brokers have
## something to dispatch to; without this the channel falls back to
@ -95,7 +97,7 @@ suite "Reliable Channel - ingress":
if arrived:
check received.read() == appPayload
await manager.stop()
(await waku.stop()).expect("stop")
asyncTest "manager drops unmarked WakuMessage":
## Mirror of the above: same content topic, but `meta` is empty
@ -105,13 +107,15 @@ suite "Reliable Channel - ingress":
contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "foreign payload".toBytes()
var waku: Waku
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect(
"Failed to create manager"
)
waku = (await createNode(createApiNodeConf())).expect("createNode")
waku.mountMessagingClient().expect("mountMessagingClient")
waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
setNoopEncryption()
@ -146,7 +150,7 @@ suite "Reliable Channel - ingress":
await sleepAsync(100.milliseconds)
check not fired
await manager.stop()
(await waku.stop()).expect("stop")
suite "Reliable Channel - send state machine":
asyncTest "MessageSentEvent finalises the channelReqId as Sent":
@ -162,13 +166,15 @@ suite "Reliable Channel - send state machine":
contentTopic = ContentTopic("/reliable-channel/test/sm-success")
fakeMsgReqId = RequestId("fake-msg-req-1")
var waku: Waku
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect(
"Failed to create manager"
)
waku = (await createNode(createApiNodeConf())).expect("createNode")
waku.mountMessagingClient().expect("mountMessagingClient")
waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
setNoopEncryption()
@ -213,7 +219,7 @@ suite "Reliable Channel - send state machine":
if finalised:
check sentFut.read() == channelReqId
await manager.stop()
(await waku.stop()).expect("stop")
asyncTest "two independent channelReqIds are finalised independently":
## Two `send()` calls -> two independent `channelReqId`s, each with
@ -227,13 +233,15 @@ suite "Reliable Channel - send state machine":
channelId = ChannelId("sm-multi-channel")
contentTopic = ContentTopic("/reliable-channel/test/sm-multi")
var waku: Waku
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect(
"Failed to create manager"
)
waku = (await createNode(createApiNodeConf())).expect("createNode")
waku.mountMessagingClient().expect("mountMessagingClient")
waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
setNoopEncryption()
@ -303,7 +311,7 @@ suite "Reliable Channel - send state machine":
if erroredArrived:
check erroredFut.read() == channelReqId2
await manager.stop()
(await waku.stop()).expect("stop")
asyncTest "TODO: channelReqId not pruned until ALL its segments are final":
## Placeholder for the multi-sibling prune rule. Today's

View File

@ -206,8 +206,9 @@ suite "Waku Conf - build with cluster conf":
assert conf.rlnRelayConf.isSome
let rlnRelayConf = conf.rlnRelayConf.get()
check rlnRelayConf.ethContractAddress.string ==
networkConf.rlnRelayEthContractAddress
# actually match the explicit contractAddress, which is the value set on the builder above
# this proves that an explicit builder call wins over the same field set via the preset
check rlnRelayConf.ethContractAddress.string == contractAddress
check rlnRelayConf.dynamic == networkConf.rlnRelayDynamic
check rlnRelayConf.chainId == networkConf.rlnRelayChainId
check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec
@ -247,10 +248,6 @@ suite "Waku Conf - build with cluster conf":
let networkConf = NetworkConf.LogosDevConf()
var builder = WakuConfBuilder.init()
builder.withNetworkConf(networkConf)
# Note: builder.withNumShardsInCluster() is not called when the
# value that comes from the CLI path is 0 (which means it was
# either set to 0 or was left unset).
builder.withShardingConf(StaticSharding)
## When
let conf = builder.build().expect("build should succeed")

View File

@ -15,8 +15,7 @@ import
node/health_monitor/protocol_health,
node/health_monitor/topic_health,
node/health_monitor/node_health_monitor,
node/delivery_service/delivery_service,
node/delivery_service/subscription_manager,
messaging_client,
node/kernel_api/relay,
node/kernel_api/store,
node/kernel_api/lightpush,
@ -27,6 +26,7 @@ import
]
import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils
import waku/node/subscription_manager
const MockDLow = 4 # Mocked GossipSub DLow value
@ -229,8 +229,8 @@ suite "Health Monitor - events":
await nodeA.start()
let ds =
DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService")
ds.startDeliveryService().expect("Failed to start DeliveryService")
MessagingClient.new(false, nodeA).expect("Failed to create MessagingClient")
ds.start().expect("Failed to start MessagingClient")
let monitorA = NodeHealthMonitor.new(nodeA)
@ -317,7 +317,7 @@ suite "Health Monitor - events":
lastStatus == ConnectionStatus.Disconnected
await monitorA.stopHealthMonitor()
await ds.stopDeliveryService()
await ds.stop()
await nodeA.stop()
asyncTest "Edge health driven by confirmed filter subscriptions":
@ -333,9 +333,9 @@ suite "Health Monitor - events":
await nodeA.start()
let ds =
DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService")
ds.startDeliveryService().expect("Failed to start DeliveryService")
let subMgr = ds.subscriptionManager
MessagingClient.new(false, nodeA).expect("Failed to create MessagingClient")
ds.start().expect("Failed to start MessagingClient")
let subMgr = nodeA.subscriptionManager
var nodeB: WakuNode
lockNewGlobalBrokerContext:
@ -416,7 +416,7 @@ suite "Health Monitor - events":
await EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis)
check shardHealthOk == true
check subMgr.edgeFilterSubStates.len > 0
check nodeA.subscriptionManager.edgeFilterSubStates.len > 0
healthSignal.clear()
deadline = Moment.now() + TestConnectivityTimeLimit
@ -428,7 +428,7 @@ suite "Health Monitor - events":
check lastStatus == ConnectionStatus.PartiallyConnected
await ds.stopDeliveryService()
await ds.stop()
await monitorA.stopHealthMonitor()
await nodeB.stop()
await nodeA.stop()

View File

@ -9,7 +9,8 @@ import
libp2p/peerId,
libp2p/crypto/crypto,
eth/keys,
eth/p2p/discoveryv5/enr
eth/p2p/discoveryv5/enr,
brokers/broker_context
import
waku/[
@ -184,114 +185,115 @@ suite "Waku Peer Exchange":
suite "Waku Peer Exchange with discv5":
asyncTest "Node successfully exchanges px peers with real discv5":
## Given (copied from test_waku_discv5.nim)
let
# todo: px flag
flags = CapabilitiesBitfield.init(
lightpush = false, filter = false, store = false, relay = true
)
bindIp = parseIpAddress("0.0.0.0")
extIp = parseIpAddress("127.0.0.1")
lockNewGlobalBrokerContext:
## Given (copied from test_waku_discv5.nim)
let
# todo: px flag
flags = CapabilitiesBitfield.init(
lightpush = false, filter = false, store = false, relay = true
)
bindIp = parseIpAddress("0.0.0.0")
extIp = parseIpAddress("127.0.0.1")
nodeKey1 = generateSecp256k1Key()
nodeTcpPort1 = Port(64010)
nodeUdpPort1 = Port(9000)
node1 = newTestWakuNode(
nodeKey1,
bindIp,
nodeTcpPort1,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort1),
nodeKey1 = generateSecp256k1Key()
nodeTcpPort1 = Port(64010)
nodeUdpPort1 = Port(9000)
node1 = newTestWakuNode(
nodeKey1,
bindIp,
nodeTcpPort1,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort1),
)
nodeKey2 = generateSecp256k1Key()
nodeTcpPort2 = Port(64012)
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode(
nodeKey2,
bindIp,
nodeTcpPort2,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort2),
)
nodeKey3 = generateSecp256k1Key()
nodeTcpPort3 = Port(64014)
nodeUdpPort3 = Port(9004)
node3 = newTestWakuNode(
nodeKey3,
bindIp,
nodeTcpPort3,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3),
)
# discv5
let conf1 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
)
nodeKey2 = generateSecp256k1Key()
nodeTcpPort2 = Port(64012)
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode(
nodeKey2,
bindIp,
nodeTcpPort2,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort2),
let disc1 =
WakuDiscoveryV5.new(node1.rng, conf1, some(node1.enr), some(node1.peerManager))
let conf2 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort2,
privateKey: keys.PrivateKey(nodeKey2.skkey),
bootstrapRecords: @[disc1.protocol.getRecord()],
autoupdateRecord: true,
)
nodeKey3 = generateSecp256k1Key()
nodeTcpPort3 = Port(64014)
nodeUdpPort3 = Port(9004)
node3 = newTestWakuNode(
nodeKey3,
bindIp,
nodeTcpPort3,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3),
let disc2 =
WakuDiscoveryV5.new(node2.rng, conf2, some(node2.enr), some(node2.peerManager))
await allFutures(node1.start(), node2.start(), node3.start())
let resultDisc1StartRes = await disc1.start()
assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error
let resultDisc2StartRes = await disc2.start()
assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
## When
var attempts = 10
while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and
attempts > 0:
await sleepAsync(1.seconds)
attempts -= 1
# node2 can be connected, so will be returned by peer exchange
require (
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo())
)
# discv5
let conf1 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
)
# Mount peer exchange
await node1.mountPeerExchange()
await node3.mountPeerExchange()
await node3.mountPeerExchangeClient()
let disc1 =
WakuDiscoveryV5.new(node1.rng, conf1, some(node1.enr), some(node1.peerManager))
let dialResponse =
await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
let conf2 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort2,
privateKey: keys.PrivateKey(nodeKey2.skkey),
bootstrapRecords: @[disc1.protocol.getRecord()],
autoupdateRecord: true,
)
check dialResponse.isOk
let disc2 =
WakuDiscoveryV5.new(node2.rng, conf2, some(node2.enr), some(node2.peerManager))
let
requestPeers = 1
currentPeers = node3.peerManager.switch.peerStore.peers.len
let res = await node3.fetchPeerExchangePeers(1)
check res.tryGet() == 1
await allFutures(node1.start(), node2.start(), node3.start())
let resultDisc1StartRes = await disc1.start()
assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error
let resultDisc2StartRes = await disc2.start()
assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
# Then node3 has received 1 peer from node1
check:
node3.peerManager.switch.peerStore.peers.len == currentPeers + requestPeers
## When
var attempts = 10
while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and
attempts > 0:
await sleepAsync(1.seconds)
attempts -= 1
# node2 can be connected, so will be returned by peer exchange
require (
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo())
)
# Mount peer exchange
await node1.mountPeerExchange()
await node3.mountPeerExchange()
await node3.mountPeerExchangeClient()
let dialResponse =
await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
check dialResponse.isOk
let
requestPeers = 1
currentPeers = node3.peerManager.switch.peerStore.peers.len
let res = await node3.fetchPeerExchangePeers(1)
check res.tryGet() == 1
# Then node3 has received 1 peer from node1
check:
node3.peerManager.switch.peerStore.peers.len == currentPeers + requestPeers
await allFutures(
[node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
)
await allFutures(
[node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
)

View File

@ -1,24 +1,28 @@
{.used.}
import chronos, testutils/unittests, std/options
import std/[net, options]
import chronos, testutils/unittests
import waku
import tools/confutils/cli_args
import waku/factory/networks_config
import waku/factory/conf_builder/conf_builder
suite "Waku API - Create node":
asyncTest "Create node with minimal configuration":
## Given
var nodeConf = defaultWakuNodeConf().valueOr:
raiseAssert error
raiseAssert "defaultWakuNodeConf failed: " & error
nodeConf.mode = Core
nodeConf.clusterId = 3'u16
nodeConf.clusterId = some(3'u16)
nodeConf.rest = false
# This is the actual minimal config but as the node auto-start, it is not suitable for tests
## When
let node = (await createNode(nodeConf)).valueOr:
raiseAssert error
raiseAssert "createNode (minimal config) failed: " & error
## Then
check:
@ -29,9 +33,9 @@ suite "Waku API - Create node":
asyncTest "Create node with full configuration":
## Given
var nodeConf = defaultWakuNodeConf().valueOr:
raiseAssert error
raiseAssert "defaultWakuNodeConf failed: " & error
nodeConf.mode = Core
nodeConf.clusterId = 99'u16
nodeConf.clusterId = some(99'u16)
nodeConf.rest = false
nodeConf.numShardsInNetwork = 16
nodeConf.maxMessageSize = "1024 KiB"
@ -44,7 +48,7 @@ suite "Waku API - Create node":
## When
let node = (await createNode(nodeConf)).valueOr:
raiseAssert error
raiseAssert "createNode (full config) failed: " & error
## Then
check:
@ -61,9 +65,9 @@ suite "Waku API - Create node":
asyncTest "Create node with mixed entry nodes (enrtree, multiaddr)":
## Given
var nodeConf = defaultWakuNodeConf().valueOr:
raiseAssert error
raiseAssert "defaultWakuNodeConf failed: " & error
nodeConf.mode = Core
nodeConf.clusterId = 42'u16
nodeConf.clusterId = some(42'u16)
nodeConf.rest = false
nodeConf.entryNodes = @[
"enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im",
@ -72,7 +76,7 @@ suite "Waku API - Create node":
## When
let node = (await createNode(nodeConf)).valueOr:
raiseAssert error
raiseAssert "createNode (mixed entry nodes) failed: " & error
## Then
check:

View File

@ -431,7 +431,7 @@ suite "Waku Discovery v5":
let waku0 = (await Waku.new(conf)).valueOr:
raiseAssert error
(waitFor startWaku(addr waku0)).isOkOr:
(waitFor waku0.start()).isOkOr:
raiseAssert error
confBuilder.withNodeKey(crypto.PrivateKey.random(Secp256k1, myRng[])[])
@ -445,7 +445,7 @@ suite "Waku Discovery v5":
let waku1 = (await Waku.new(conf1)).valueOr:
raiseAssert error
(waitFor startWaku(addr waku1)).isOkOr:
(waitFor waku1.start()).isOkOr:
raiseAssert error
await waku1.node.mountPeerExchange()
@ -461,7 +461,7 @@ suite "Waku Discovery v5":
let waku2 = (await Waku.new(conf2)).valueOr:
raiseAssert error
(waitFor startWaku(addr waku2)).isOkOr:
(waitFor waku2.start()).isOkOr:
raiseAssert error
# leave some time for discv5 to act

View File

@ -5,7 +5,8 @@ import
testutils/unittests,
chronos,
libp2p/[switch, peerId, crypto/crypto],
eth/[keys, p2p/discoveryv5/enr]
eth/[keys, p2p/discoveryv5/enr],
brokers/broker_context
import
waku/[
@ -31,110 +32,113 @@ suite "Waku Peer Exchange":
suite "request":
asyncTest "Retrieve and provide peer exchange peers from discv5":
## Given (copied from test_waku_discv5.nim)
let
# todo: px flag
flags = CapabilitiesBitfield.init(
lightpush = false, filter = false, store = false, relay = true
)
bindIp = parseIpAddress("0.0.0.0")
extIp = parseIpAddress("127.0.0.1")
lockNewGlobalBrokerContext:
## Given (copied from test_waku_discv5.nim)
let
# todo: px flag
flags = CapabilitiesBitfield.init(
lightpush = false, filter = false, store = false, relay = true
)
bindIp = parseIpAddress("0.0.0.0")
extIp = parseIpAddress("127.0.0.1")
nodeKey1 = generateSecp256k1Key()
nodeTcpPort1 = Port(64010)
nodeUdpPort1 = Port(9000)
node1 = newTestWakuNode(
nodeKey1,
bindIp,
nodeTcpPort1,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort1),
nodeKey1 = generateSecp256k1Key()
nodeTcpPort1 = Port(64010)
nodeUdpPort1 = Port(9000)
node1 = newTestWakuNode(
nodeKey1,
bindIp,
nodeTcpPort1,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort1),
)
nodeKey2 = generateSecp256k1Key()
nodeTcpPort2 = Port(64012)
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode(
nodeKey2,
bindIp,
nodeTcpPort2,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort2),
)
nodeKey3 = generateSecp256k1Key()
nodeTcpPort3 = Port(64014)
nodeUdpPort3 = Port(9004)
node3 = newTestWakuNode(
nodeKey3,
bindIp,
nodeTcpPort3,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3),
)
# discv5
let conf1 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
)
nodeKey2 = generateSecp256k1Key()
nodeTcpPort2 = Port(64012)
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode(
nodeKey2,
bindIp,
nodeTcpPort2,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort2),
let disc1 = WakuDiscoveryV5.new(
node1.rng, conf1, some(node1.enr), some(node1.peerManager)
)
nodeKey3 = generateSecp256k1Key()
nodeTcpPort3 = Port(64014)
nodeUdpPort3 = Port(9004)
node3 = newTestWakuNode(
nodeKey3,
bindIp,
nodeTcpPort3,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3),
let conf2 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort2,
privateKey: keys.PrivateKey(nodeKey2.skkey),
bootstrapRecords: @[disc1.protocol.getRecord()],
autoupdateRecord: true,
)
# discv5
let conf1 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
)
let disc2 = WakuDiscoveryV5.new(
node2.rng, conf2, some(node2.enr), some(node2.peerManager)
)
let disc1 =
WakuDiscoveryV5.new(node1.rng, conf1, some(node1.enr), some(node1.peerManager))
await allFutures(node1.start(), node2.start(), node3.start())
let resultDisc1StartRes = await disc1.start()
assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error
let resultDisc2StartRes = await disc2.start()
assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
let conf2 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort2,
privateKey: keys.PrivateKey(nodeKey2.skkey),
bootstrapRecords: @[disc1.protocol.getRecord()],
autoupdateRecord: true,
)
## When
var attempts = 10
while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and
attempts > 0:
await sleepAsync(1.seconds)
attempts -= 1
let disc2 =
WakuDiscoveryV5.new(node2.rng, conf2, some(node2.enr), some(node2.peerManager))
# node2 can be connected, so will be returned by peer exchange
require (
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo())
)
await allFutures(node1.start(), node2.start(), node3.start())
let resultDisc1StartRes = await disc1.start()
assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error
let resultDisc2StartRes = await disc2.start()
assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
# Mount peer exchange
await node1.mountPeerExchange()
await node3.mountPeerExchange()
## When
var attempts = 10
while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and
attempts > 0:
await sleepAsync(1.seconds)
attempts -= 1
let dialResponse =
await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
let response = dialResponse.get()
# node2 can be connected, so will be returned by peer exchange
require (
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo())
)
## Then
check:
response.get().peerInfos.len == 1
response.get().peerInfos[0].enr == disc2.protocol.localNode.record.raw
# Mount peer exchange
await node1.mountPeerExchange()
await node3.mountPeerExchange()
let dialResponse =
await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
let response = dialResponse.get()
## Then
check:
response.get().peerInfos.len == 1
response.get().peerInfos[0].enr == disc2.protocol.localNode.record.raw
await allFutures(
[node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
)
await allFutures(
[node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
)
asyncTest "Request returns some discovered peers":
let

View File

@ -46,7 +46,7 @@ suite "Wakunode2 - Waku initialization":
var waku = (waitFor Waku.new(conf)).valueOr:
raiseAssert error
(waitFor startWaku(addr waku)).isOkOr:
(waitFor waku.start()).isOkOr:
raiseAssert error
## Then
@ -71,7 +71,7 @@ suite "Wakunode2 - Waku initialization":
var waku = (waitFor Waku.new(conf)).valueOr:
raiseAssert error
(waitFor startWaku(addr waku)).isOkOr:
(waitFor waku.start()).isOkOr:
raiseAssert error
## Then
@ -128,7 +128,7 @@ suite "Wakunode2 - Waku initialization":
(waitFor waku.stop()).isOkOr:
raiseAssert error
(waitFor startWaku(addr waku)).isOkOr:
(waitFor waku.start()).isOkOr:
raiseAssert error
let portsJson = waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts)

View File

@ -117,20 +117,23 @@ type WakuNodeConf* = object
name: "rln-relay-eth-private-key"
.}: string
# TODO: Remove "Default is" when it's already visible on the CLI
# Option-typed; desc states the default since the CLI can't auto-show it for none().
rlnRelayUserMessageLimit* {.
desc:
"Set a user message limit for the rln membership registration. Must be a positive integer. Default is 1.",
defaultValue: 1,
"Set a user message limit for the rln membership registration. Must be a positive integer. Default is " &
$DefaultRlnRelayUserMessageLimit & ".",
defaultValue: none(uint64),
name: "rln-relay-user-message-limit"
.}: uint64
.}: Option[uint64]
# Option-typed; desc states the default since the CLI can't auto-show it for none().
rlnEpochSizeSec* {.
desc:
"Epoch size in seconds used to rate limit RLN memberships. Default is 1 second.",
defaultValue: 1,
"Epoch size in seconds used to rate limit RLN memberships. Default is " &
$DefaultRlnRelayEpochSizeSec & " second.",
defaultValue: none(uint64),
name: "rln-relay-epoch-sec"
.}: uint64
.}: Option[uint64]
maxMessageSize* {.
desc:
@ -170,12 +173,15 @@ type WakuNodeConf* = object
name: "preset"
.}: string
# Option-typed; desc states the default since the CLI can't auto-show it for none().
clusterId* {.
desc:
"Cluster id that the node is running in. Node in a different cluster id is disconnected.",
defaultValue: 0,
desc: static(
"Cluster id that the node is running in. Node in a different cluster id is disconnected. Default is " &
$DefaultClusterId & "."
),
defaultValue: none(uint16),
name: "cluster-id"
.}: uint16
.}: Option[uint16]
agentString* {.
defaultValue: "logos-delivery-" & cli_args.git_version,
@ -291,11 +297,14 @@ hence would have reachability issues.""",
name: "relay-shard-manager"
.}: bool
# Option-typed; desc states the default since the CLI can't auto-show it for none().
rlnRelay* {.
desc: "Enable spam protection through rln-relay: true|false.",
defaultValue: false,
desc:
"Enable spam protection through rln-relay: true|false. Default is " &
$DefaultRlnRelayEnabled & ".",
defaultValue: none(bool),
name: "rln-relay"
.}: bool
.}: Option[bool]
rlnRelayCredIndex* {.
desc: "the index of the onchain commitment to use",
@ -304,9 +313,9 @@ hence would have reachability issues.""",
rlnRelayDynamic* {.
desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false.",
defaultValue: false,
defaultValue: none(bool),
name: "rln-relay-dynamic"
.}: bool
.}: Option[bool]
entryNodes* {.
desc:
@ -466,13 +475,14 @@ hence would have reachability issues.""",
.}: string
## Reliability config
# Option-typed; desc states the default since the CLI can't auto-show it for none().
reliabilityEnabled* {.
desc:
"""Adds an extra effort in the delivery/reception of messages by leveraging store-v3 requests.
with the drawback of consuming some more bandwidth.""",
defaultValue: true,
"""Adds an extra effort in the delivery/reception of messages by leveraging store-v3 requests, with the drawback of consuming some more bandwidth. Default is """ &
$DefaultP2pReliability & ".",
defaultValue: none(bool),
name: "reliability"
.}: bool
.}: Option[bool]
## REST HTTP config
rest* {.
@ -557,8 +567,11 @@ with the drawback of consuming some more bandwidth.""",
.}: string
## Discovery v5 config
# Option-typed; desc states the default since the CLI can't auto-show it for none().
discv5Discovery* {.
desc: "Enable discovering nodes via Node Discovery v5.",
desc:
"Enable discovering nodes via Node Discovery v5. Default is " &
$DefaultDiscv5Enabled & ".",
defaultValue: none(bool),
name: "discv5-discovery"
.}: Option[bool]
@ -627,8 +640,12 @@ with the drawback of consuming some more bandwidth.""",
.}: bool
#Mix config
mix* {.desc: "Enable mix protocol: true|false", defaultValue: false, name: "mix".}:
bool
# Option-typed; desc states the default since the CLI can't auto-show it for none().
mix* {.
desc: "Enable mix protocol: true|false. Default is " & $DefaultMix & ".",
defaultValue: none(bool),
name: "mix"
.}: Option[bool]
mixkey* {.
desc:
@ -643,12 +660,14 @@ with the drawback of consuming some more bandwidth.""",
.}: seq[MixNodePubInfo]
# Kademlia Discovery config
# Option-typed; desc states the default since the CLI can't auto-show it for none().
enableKadDiscovery* {.
desc:
"Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network.",
defaultValue: false,
"Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network. Default is " &
$DefaultKadEnabled & ".",
defaultValue: none(bool),
name: "enable-kad-discovery"
.}: bool
.}: Option[bool]
kadBootstrapNodes* {.
desc:
@ -919,7 +938,7 @@ proc toKeystoreGeneratorConf*(n: WakuNodeConf): RlnKeystoreGeneratorConf =
chainId: UInt256.fromBytesBE(n.rlnRelayChainId.toBytesBE()),
ethClientUrls: n.ethClientUrls.mapIt(string(it)),
ethContractAddress: n.rlnRelayEthContractAddress,
userMessageLimit: n.rlnRelayUserMessageLimit,
userMessageLimit: n.rlnRelayUserMessageLimit.get(DefaultRlnRelayUserMessageLimit),
ethPrivateKey: n.rlnRelayEthPrivateKey,
credPath: n.rlnRelayCredPath,
credPassword: n.rlnRelayCredPassword,
@ -955,7 +974,7 @@ proc toNetworkConf(
proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
var b = WakuConfBuilder.init()
let networkConf = toNetworkConf(n.preset, some(n.clusterId)).valueOr:
let networkConf = toNetworkConf(n.preset, n.clusterId).valueOr:
return err("Error determining cluster from preset: " & $error)
if networkConf.isSome():
@ -964,7 +983,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
b.withLogLevel(n.logLevel)
b.withLogFormat(n.logFormat)
b.rlnRelayConf.withEnabled(n.rlnRelay)
if n.rlnRelay.isSome():
b.rlnRelayConf.withEnabled(n.rlnRelay.get())
if n.rlnRelayCredPath != "":
b.rlnRelayConf.withCredPath(n.rlnRelayCredPath)
if n.rlnRelayCredPassword != "":
@ -976,18 +996,22 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
if n.rlnRelayChainId != 0:
b.rlnRelayConf.withChainId(n.rlnRelayChainId)
b.rlnRelayConf.withUserMessageLimit(n.rlnRelayUserMessageLimit)
b.rlnRelayConf.withEpochSizeSec(n.rlnEpochSizeSec)
if n.rlnRelayUserMessageLimit.isSome():
b.rlnRelayConf.withUserMessageLimit(n.rlnRelayUserMessageLimit.get())
if n.rlnEpochSizeSec.isSome():
b.rlnRelayConf.withEpochSizeSec(n.rlnEpochSizeSec.get())
if n.rlnRelayCredIndex.isSome():
b.rlnRelayConf.withCredIndex(n.rlnRelayCredIndex.get())
b.rlnRelayConf.withDynamic(n.rlnRelayDynamic)
if n.rlnRelayDynamic.isSome():
b.rlnRelayConf.withDynamic(n.rlnRelayDynamic.get())
if n.maxMessageSize != "":
b.withMaxMessageSize(n.maxMessageSize)
b.withProtectedShards(n.protectedShards)
b.withClusterId(n.clusterId)
if n.clusterId.isSome():
b.withClusterId(n.clusterId.get())
b.withAgentString(n.agentString)
@ -1041,7 +1065,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
if n.numShardsInNetwork != 0:
b.withNumShardsInCluster(n.numShardsInNetwork)
b.withShardingConf(AutoSharding)
else:
elif networkConf.isNone():
b.withShardingConf(StaticSharding)
# It is not possible to pass an empty sequence on the CLI
@ -1074,9 +1098,10 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
b.storeServiceConf.storeSyncConf.withRangeSec(n.storeSyncRange)
b.storeServiceConf.storeSyncConf.withRelayJitterSec(n.storeSyncRelayJitter)
b.mixConf.withEnabled(n.mix)
if n.mix.isSome():
b.mixConf.withEnabled(n.mix.get())
b.withMix(n.mix.get())
b.mixConf.withMixNodes(n.mixnodes)
b.withMix(n.mix)
if n.mixkey.isSome():
b.mixConf.withMixKey(n.mixkey.get())
@ -1086,7 +1111,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
b.filterServiceConf.withMaxCriteria(n.filterMaxCriteria)
b.withLightPush(n.lightpush)
b.withP2pReliability(n.reliabilityEnabled)
if n.reliabilityEnabled.isSome():
b.withP2pReliability(n.reliabilityEnabled.get())
b.restServerConf.withEnabled(n.rest)
b.restServerConf.withListenAddress(n.restAddress)
@ -1129,7 +1155,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
b.withLocalStoragePath(n.localStoragePath)
b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery)
if n.enableKadDiscovery.isSome():
b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery.get())
b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes)
# Mode-driven configuration overrides

View File

@ -0,0 +1,121 @@
import std/[json, strutils, tables]
import confutils, confutils/std/net, results
import ./cli_args
proc collectJsonFields*(
jsonNode: JsonNode
): Result[Table[string, (string, JsonNode)], string] =
## Walk the top-level JSON object and key it by lowercased names.
if jsonNode.kind != JObject:
return err("config JSON must be a JSON object, got " & $jsonNode.kind)
var jsonFields: Table[string, (string, JsonNode)]
for key, value in jsonNode:
let lowerKey = key.toLowerAscii()
if jsonFields.hasKey(lowerKey):
let firstKey = jsonFields[lowerKey][0]
return err(
"Duplicate configuration option (case-insensitive): '" & firstKey & "' and '" &
key & "'"
)
jsonFields[lowerKey] = (key, value)
return ok(jsonFields)
proc unknownKeysError(
jsonFields: Table[string, (string, JsonNode)], prefix: string
): string =
## Format leftover JSON keys as an error message.
var keys = newSeq[string]()
for _, (jsonKey, _) in pairs(jsonFields):
keys.add(jsonKey)
return prefix & ": " & $keys
proc jsonScalarToString(node: JsonNode): Result[string, string] =
## Convert a scalar JSON value to its string form.
case node.kind
of JString:
return ok(node.getStr())
of JInt:
return ok($node.getInt())
of JFloat:
return ok($node.getFloat())
of JBool:
return ok($node.getBool())
of JNull:
return ok("")
else:
return err("expected scalar JSON value, got " & $node.kind)
proc applyJsonFieldsToConf(
conf: var WakuNodeConf,
jsonFields: var Table[string, (string, JsonNode)],
parseErrPrefix: string,
unknownErrPrefix: string,
): Result[void, string] =
## Walk `conf`'s fields and write each one matched (case-insensitive) by
## `jsonFields`. seq fields take a JArray (full replace); scalar fields
## take any scalar JSON kind. Errors on leftover unknown keys.
for confField, confValue in fieldPairs(conf):
let lowerField = confField.toLowerAscii()
if jsonFields.hasKey(lowerField):
let (jsonKey, jsonValue) = jsonFields[lowerField]
when confValue is seq:
if jsonValue.kind != JArray:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey &
"' must be a JSON array"
)
var newSeq: typeof(confValue) = @[]
for item in jsonValue:
let formattedItem = jsonScalarToString(item).valueOr:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
error
)
try:
type ElemType = typeof(confValue[0])
newSeq.add(parseCmdArg(ElemType, formattedItem))
except CatchableError as e:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
e.msg & ". Value: " & formattedItem
)
confValue = newSeq
else:
let formattedString = jsonScalarToString(jsonValue).valueOr:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
error
)
try:
confValue = parseCmdArg(typeof(confValue), formattedString)
except CatchableError as e:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
e.msg & ". Value: " & formattedString
)
jsonFields.del(lowerField)
if jsonFields.len > 0:
return err(unknownKeysError(jsonFields, unknownErrPrefix))
return ok()
proc assembleFullConf*(
jsonFields: Table[string, (string, JsonNode)]
): Result[WakuNodeConf, string] =
## Build a WakuNodeConf from a flat JSON object whose keys are WakuNodeConf field names.
var conf = ?defaultWakuNodeConf()
var fields = jsonFields
?applyJsonFieldsToConf(
conf, fields, "Failed to parse field", "Unrecognized configuration option(s) found"
)
return ok(conf)
proc parseConfJson*(jsonStr: string): Result[WakuNodeConf, string] =
## Parse a flat JSON config whose keys are WakuNodeConf field names.
var jsonNode: JsonNode
try:
jsonNode = parseJson(jsonStr)
except CatchableError as e:
return err("Failed to parse config JSON: " & e.msg)
let jsonFields = ?collectJsonFields(jsonNode)
return assembleFullConf(jsonFields)

View File

@ -1,10 +1,12 @@
import chronicles, chronos, results
import std/[net, options]
import chronicles, chronos, libp2p/peerid, results
import waku/factory/waku
import waku/messaging_client
import waku/[requests/health_requests, waku_core, waku_node]
import waku/node/delivery_service/send_service
import waku/node/delivery_service/subscription_manager
import libp2p/peerid
import waku/node/subscription_manager
import ../../tools/confutils/cli_args
import ./[api_conf, types]
@ -38,39 +40,15 @@ proc subscribe*(
): Future[Result[void, string]] {.async.} =
?checkApiAvailability(w)
return w.deliveryService.subscriptionManager.subscribe(contentTopic)
return w.node.subscriptionManager.subscribe(contentTopic)
proc unsubscribe*(w: Waku, contentTopic: ContentTopic): Result[void, string] =
?checkApiAvailability(w)
return w.deliveryService.subscriptionManager.unsubscribe(contentTopic)
return w.node.subscriptionManager.unsubscribe(contentTopic)
proc send*(
w: Waku, envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async.} =
?checkApiAvailability(w)
let isSubbed = w.deliveryService.subscriptionManager
.isSubscribed(envelope.contentTopic)
.valueOr(false)
if not isSubbed:
info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic
w.deliveryService.subscriptionManager.subscribe(envelope.contentTopic).isOkOr:
warn "Failed to auto-subscribe", error = error
return err("Failed to auto-subscribe before sending: " & error)
let requestId = RequestId.new(w.rng)
let deliveryTask = DeliveryTask.new(requestId, envelope, w.brokerCtx).valueOr:
return err("API send: Failed to create delivery task: " & error)
info "API send: scheduling delivery task",
requestId = $requestId,
pubsubTopic = deliveryTask.pubsubTopic,
contentTopic = deliveryTask.msg.contentTopic,
msgHash = deliveryTask.msgHash.to0xHex(),
myPeerId = w.node.peerId()
asyncSpawn w.deliveryService.sendService.send(deliveryTask)
return ok(requestId)
return await w.messagingClient.send(envelope)

View File

@ -4,7 +4,13 @@ import ../waku_conf
logScope:
topics = "waku conf builder discv5"
const DefaultDiscv5UdpPort*: Port = Port(9000)
const
DefaultDiscv5Enabled*: bool = false
DefaultDiscv5BitsPerHop*: int = 1
DefaultDiscv5BucketIpLimit*: uint = 2
DefaultDiscv5EnrAutoUpdate*: bool = true
DefaultDiscv5TableIpLimit*: uint = 10
DefaultDiscv5UdpPort*: Port = Port(9000)
###########################
## Discv5 Config Builder ##
@ -48,17 +54,17 @@ proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string])
b.bootstrapNodes = concat(b.bootstrapNodes, bootstrapNodes)
proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultDiscv5Enabled):
return ok(none(Discv5Conf))
return ok(
some(
Discv5Conf(
bootstrapNodes: b.bootstrapNodes,
bitsPerHop: b.bitsPerHop.get(1),
bucketIpLimit: b.bucketIpLimit.get(2),
enrAutoUpdate: b.enrAutoUpdate.get(true),
tableIpLimit: b.tableIpLimit.get(10),
bitsPerHop: b.bitsPerHop.get(DefaultDiscv5BitsPerHop),
bucketIpLimit: b.bucketIpLimit.get(DefaultDiscv5BucketIpLimit),
enrAutoUpdate: b.enrAutoUpdate.get(DefaultDiscv5EnrAutoUpdate),
tableIpLimit: b.tableIpLimit.get(DefaultDiscv5TableIpLimit),
udpPort: b.udpPort.get(DefaultDiscv5UdpPort),
)
)

View File

@ -4,6 +4,12 @@ import ../waku_conf
logScope:
topics = "waku conf builder filter service"
const
DefaultFilterEnabled*: bool = false
DefaultFilterMaxPeersToServe*: uint32 = 500
DefaultFilterSubscriptionTimeout*: uint16 = 300
DefaultFilterMaxCriteria*: uint32 = 1000
###################################
## Filter Service Config Builder ##
###################################
@ -37,15 +43,15 @@ proc withMaxCriteria*(b: var FilterServiceConfBuilder, maxCriteria: uint32) =
b.maxCriteria = some(maxCriteria)
proc build*(b: FilterServiceConfBuilder): Result[Option[FilterServiceConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultFilterEnabled):
return ok(none(FilterServiceConf))
return ok(
some(
FilterServiceConf(
maxPeersToServe: b.maxPeersToServe.get(500),
subscriptionTimeout: b.subscriptionTimeout.get(300),
maxCriteria: b.maxCriteria.get(1000),
maxPeersToServe: b.maxPeersToServe.get(DefaultFilterMaxPeersToServe),
subscriptionTimeout: b.subscriptionTimeout.get(DefaultFilterSubscriptionTimeout),
maxCriteria: b.maxCriteria.get(DefaultFilterMaxCriteria),
)
)
)

View File

@ -5,18 +5,20 @@ import waku/factory/waku_conf
logScope:
topics = "waku conf builder kademlia discovery"
const DefaultKadEnabled*: bool = false
#######################################
## Kademlia Discovery Config Builder ##
#######################################
type KademliaDiscoveryConfBuilder* = object
enabled*: bool
enabled*: Option[bool]
bootstrapNodes*: seq[string]
proc init*(T: type KademliaDiscoveryConfBuilder): KademliaDiscoveryConfBuilder =
KademliaDiscoveryConfBuilder()
proc withEnabled*(b: var KademliaDiscoveryConfBuilder, enabled: bool) =
b.enabled = enabled
b.enabled = some(enabled)
proc withBootstrapNodes*(
b: var KademliaDiscoveryConfBuilder, bootstrapNodes: seq[string]
@ -27,7 +29,7 @@ proc build*(
b: KademliaDiscoveryConfBuilder
): Result[Option[KademliaDiscoveryConf], string] =
# Kademlia is enabled if explicitly enabled OR if bootstrap nodes are provided
let enabled = b.enabled or b.bootstrapNodes.len > 0
let enabled = b.enabled.get(DefaultKadEnabled) or b.bootstrapNodes.len > 0
if not enabled:
return ok(none(KademliaDiscoveryConf))

View File

@ -4,7 +4,11 @@ import ../waku_conf
logScope:
topics = "waku conf builder metrics server"
const DefaultMetricsHttpPort*: Port = Port(8008)
const
DefaultMetricsEnabled*: bool = false
DefaultMetricsHttpAddress*: IpAddress = static parseIpAddress("127.0.0.1")
DefaultMetricsHttpPort*: Port = Port(8008)
DefaultMetricsLogging*: bool = false
###################################
## Metrics Server Config Builder ##
@ -35,15 +39,15 @@ proc withLogging*(b: var MetricsServerConfBuilder, logging: bool) =
b.logging = some(logging)
proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultMetricsEnabled):
return ok(none(MetricsServerConf))
return ok(
some(
MetricsServerConf(
httpAddress: b.httpAddress.get(static parseIpAddress("127.0.0.1")),
httpAddress: b.httpAddress.get(DefaultMetricsHttpAddress),
httpPort: b.httpPort.get(DefaultMetricsHttpPort),
logging: b.logging.get(false),
logging: b.logging.get(DefaultMetricsLogging),
)
)
)

View File

@ -5,6 +5,8 @@ import ../waku_conf, waku/waku_mix
logScope:
topics = "waku conf builder mix"
const DefaultMixEnabled*: bool = false
##################################
## Mix Config Builder ##
##################################
@ -26,7 +28,7 @@ proc withMixNodes*(b: var MixConfBuilder, mixNodes: seq[MixNodePubInfo]) =
b.mixNodes = mixNodes
proc build*(b: MixConfBuilder): Result[Option[MixConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultMixEnabled):
return ok(none[MixConf]())
else:
if b.mixKey.isSome():

View File

@ -4,7 +4,10 @@ import ../waku_conf
logScope:
topics = "waku conf builder rest server"
const DefaultRestPort*: Port = Port(8645)
const
DefaultRestEnabled*: bool = false
DefaultRestPort*: Port = Port(8645)
DefaultRestAdmin*: bool = false
################################
## REST Server Config Builder ##
@ -43,7 +46,7 @@ proc withRelayCacheCapacity*(b: var RestServerConfBuilder, relayCacheCapacity: u
b.relayCacheCapacity = some(relayCacheCapacity)
proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultRestEnabled):
return ok(none(RestServerConf))
if b.listenAddress.isNone():
@ -57,7 +60,7 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] =
allowOrigin: b.allowOrigin,
listenAddress: b.listenAddress.get(),
port: b.port.get(DefaultRestPort),
admin: b.admin.get(false),
admin: b.admin.get(DefaultRestAdmin),
relayCacheCapacity: b.relayCacheCapacity.get(),
)
)

View File

@ -4,6 +4,11 @@ import ../waku_conf
logScope:
topics = "waku conf builder rln relay"
const
DefaultRlnRelayEnabled*: bool = false
DefaultRlnRelayEpochSizeSec*: uint64 = 1
DefaultRlnRelayUserMessageLimit*: uint64 = 1
##############################
## RLN Relay Config Builder ##
##############################
@ -56,7 +61,7 @@ proc withUserMessageLimit*(b: var RlnRelayConfBuilder, userMessageLimit: uint64)
b.userMessageLimit = some(userMessageLimit)
proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultRlnRelayEnabled):
return ok(none(RlnRelayConf))
if b.chainId.isNone():
@ -78,11 +83,6 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
return err("rlnRelay.ethClientUrls is not specified")
if b.ethContractAddress.get("") == "":
return err("rlnRelay.ethContractAddress is not specified")
if b.epochSizeSec.isNone():
return err("rlnRelay.epochSizeSec is not specified")
if b.userMessageLimit.isNone():
return err("rlnRelay.userMessageLimit is not specified")
return ok(
some(
RlnRelayConf(
@ -92,8 +92,8 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
dynamic: b.dynamic.get(),
ethClientUrls: b.ethClientUrls.get(),
ethContractAddress: b.ethContractAddress.get(),
epochSizeSec: b.epochSizeSec.get(),
userMessageLimit: b.userMessageLimit.get(),
epochSizeSec: b.epochSizeSec.get(DefaultRlnRelayEpochSizeSec),
userMessageLimit: b.userMessageLimit.get(DefaultRlnRelayUserMessageLimit),
)
)
)

View File

@ -5,6 +5,14 @@ import ../waku_conf, ./store_sync_conf_builder
logScope:
topics = "waku conf builder store service"
const
DefaultStoreEnabled*: bool = false
DefaultStoreDbMigration*: bool = true
DefaultStoreDbVacuum*: bool = false
DefaultStoreMaxNumDbConnections*: int = 50
DefaultStoreResume*: bool = false
DefaultStoreRetentionPolicy*: string = "time:" & $2.days.seconds
##################################
## Store Service Config Builder ##
##################################
@ -77,7 +85,7 @@ proc validateRetentionPolicies(policies: seq[string]): Result[void, string] =
return ok()
proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultStoreEnabled):
return ok(none(StoreServiceConf))
if b.dbUrl.get("") == "":
@ -88,7 +96,7 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string
let retentionPolicies =
if b.retentionPolicies.len == 0:
@["time:" & $2.days.seconds]
@[DefaultStoreRetentionPolicy]
else:
validateRetentionPolicies(b.retentionPolicies).isOkOr:
return err("invalid retention policies: " & error)
@ -97,12 +105,12 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string
return ok(
some(
StoreServiceConf(
dbMigration: b.dbMigration.get(true),
dbMigration: b.dbMigration.get(DefaultStoreDbMigration),
dbURl: b.dbUrl.get(),
dbVacuum: b.dbVacuum.get(false),
maxNumDbConnections: b.maxNumDbConnections.get(50),
dbVacuum: b.dbVacuum.get(DefaultStoreDbVacuum),
maxNumDbConnections: b.maxNumDbConnections.get(DefaultStoreMaxNumDbConnections),
retentionPolicies: retentionPolicies,
resume: b.resume.get(false),
resume: b.resume.get(DefaultStoreResume),
storeSyncConf: storeSyncConf,
)
)

View File

@ -4,6 +4,8 @@ import ../waku_conf
logScope:
topics = "waku conf builder store sync"
const DefaultStoreSyncEnabled*: bool = false
##################################
## Store Sync Config Builder ##
##################################
@ -30,7 +32,7 @@ proc withRelayJitterSec*(b: var StoreSyncConfBuilder, relayJitterSec: uint32) =
b.relayJitterSec = some(relayJitterSec)
proc build*(b: StoreSyncConfBuilder): Result[Option[StoreSyncConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultStoreSyncEnabled):
return ok(none(StoreSyncConf))
if b.rangeSec.isNone():

View File

@ -13,6 +13,9 @@ import
factory/networks_config,
common/logging,
common/utils/parse_size_units,
node/peer_manager,
waku_core/message/default_values,
waku_core/topics/pubsub_topic,
waku_enr/capabilities,
persistency/persistency,
],
@ -35,9 +38,35 @@ import
logScope:
topics = "waku conf builder"
# Picks up the same -d:git_version=... build flag that cli_args.nim defines.
const git_version {.strdefine.} = "(unknown)"
const
DefaultMaxConnections* = 150
DefaultRelay*: bool = false
DefaultLightPush*: bool = false
DefaultPeerExchange*: bool = false
DefaultStoreSyncMount*: bool = false
DefaultRendezvous*: bool = false
DefaultMix*: bool = false
DefaultRelayPeerExchange*: bool = false
DefaultLogLevel*: logging.LogLevel = logging.LogLevel.INFO
DefaultLogFormat*: logging.LogFormat = logging.LogFormat.TEXT
DefaultNatStrategy*: string = "none"
DefaultP2pTcpPort*: Port = Port(60000)
DefaultP2pListenAddress*: IpAddress = static parseIpAddress("0.0.0.0")
DefaultPortsShift*: uint16 = 0
DefaultExtMultiAddrsOnly*: bool = false
DefaultDnsAddrsNameServers*: seq[IpAddress] =
@[static parseIpAddress("1.1.1.1"), static parseIpAddress("1.0.0.1")]
DefaultPeerPersistence*: bool = false
DefaultAgentString*: string = "logos-delivery " & git_version
DefaultRelayShardedPeerManagement*: bool = false
DefaultRelayServiceRatio*: string = "50:50"
DefaultCircuitRelayClient*: bool = false
DefaultP2pReliability*: bool = true
DefaultNumShardsInCluster*: uint16 = 1
DefaultShardingConfKind*: ShardingConfKind = AutoSharding
type MaxMessageSizeKind* = enum
mmskNone
@ -309,117 +338,124 @@ proc buildShardingConf(
bNumShardsInCluster: Option[uint16],
bSubscribeShards: Option[seq[uint16]],
): (ShardingConf, seq[uint16]) =
case bShardingConfKind.get(AutoSharding)
case bShardingConfKind.get(DefaultShardingConfKind)
of StaticSharding:
(ShardingConf(kind: StaticSharding), bSubscribeShards.get(@[]))
of AutoSharding:
let numShardsInCluster = bNumShardsInCluster.get(1)
let numShardsInCluster = bNumShardsInCluster.get(DefaultNumShardsInCluster)
let shardingConf =
ShardingConf(kind: AutoSharding, numShardsInCluster: numShardsInCluster)
let upperShard = uint16(numShardsInCluster - 1)
(shardingConf, bSubscribeShards.get(toSeq(0.uint16 .. upperShard)))
template checkSetPresetValueToField[T](
field: var Option[T], presetVal: T, msg: static string
) =
## Set the field to the preset's value, unless the field is already set
## (explicit wins). Warn iff the field's existing value differs from the
## preset's. No-op if they agree.
if field.isSome():
if field.get() != presetVal:
warn msg, used = field.get(), discarded = presetVal
else:
field = some(presetVal)
proc checkAddPresetValueToField[T](field: var seq[T], presetVals: seq[T]) =
## Append the preset's list values to the field's existing list. Lists
## concat rather than override; both the user's and the preset's entries
## end up in the final list.
field = field & presetVals
proc applyNetworkConf(builder: var WakuConfBuilder) =
# Apply network conf, overrides most values passed individually
# If you want to tweak values, don't use networkConf
# TODO: networkconf should be one field of the conf builder so that this function becomes unnecessary
## NetworkConf = network presets.
## Cascade the chosen preset's values onto builder fields the user hasn't set.
## User-set fields stay; preset fills the gaps and warns on conflict (explicit wins).
## List fields concat (preset's nodes appended to user's).
if builder.networkConf.isNone():
return
return # If there is no preset given, then nothing to do.
let networkConf = builder.networkConf.get()
if builder.clusterId.isSome():
warn "Cluster id was provided alongside a network conf",
used = networkConf.clusterId, discarded = builder.clusterId.get()
builder.clusterId = some(networkConf.clusterId)
checkSetPresetValueToField(
builder.clusterId, networkConf.clusterId,
"Cluster id was provided alongside a network conf",
)
# Apply relay parameters
if builder.relay.get(false) and networkConf.rlnRelay:
if builder.rlnRelayConf.enabled.isSome():
warn "RLN Relay was provided alongside a network conf",
used = networkConf.rlnRelay, discarded = builder.rlnRelayConf.enabled
builder.rlnRelayConf.withEnabled(true)
if builder.rlnRelayConf.ethContractAddress.get("") != "":
warn "RLN Relay ETH Contract Address was provided alongside a network conf",
used = networkConf.rlnRelayEthContractAddress.string,
discarded = builder.rlnRelayConf.ethContractAddress.get().string
builder.rlnRelayConf.withEthContractAddress(networkConf.rlnRelayEthContractAddress)
if builder.rlnRelayConf.chainId.isSome():
warn "RLN Relay Chain Id was provided alongside a network conf",
used = networkConf.rlnRelayChainId, discarded = builder.rlnRelayConf.chainId
builder.rlnRelayConf.withChainId(networkConf.rlnRelayChainId)
if builder.rlnRelayConf.dynamic.isSome():
warn "RLN Relay Dynamic was provided alongside a network conf",
used = networkConf.rlnRelayDynamic, discarded = builder.rlnRelayConf.dynamic
builder.rlnRelayConf.withDynamic(networkConf.rlnRelayDynamic)
if builder.rlnRelayConf.epochSizeSec.isSome():
warn "RLN Epoch Size in Seconds was provided alongside a network conf",
used = networkConf.rlnEpochSizeSec,
discarded = builder.rlnRelayConf.epochSizeSec
builder.rlnRelayConf.withEpochSizeSec(networkConf.rlnEpochSizeSec)
if builder.rlnRelayConf.userMessageLimit.isSome():
warn "RLN Relay User Message Limit was provided alongside a network conf",
used = networkConf.rlnRelayUserMessageLimit,
discarded = builder.rlnRelayConf.userMessageLimit
if builder.rlnRelayConf.userMessageLimit.get(0) == 0:
## only override with the "preset" value if there was not explicit set value
builder.rlnRelayConf.withUserMessageLimit(networkConf.rlnRelayUserMessageLimit)
if builder.relay.get(DefaultRelay) and networkConf.rlnRelay:
checkSetPresetValueToField(
builder.rlnRelayConf.enabled,
networkConf.rlnRelay, # true
"RLN Relay was provided alongside a network conf",
)
checkSetPresetValueToField(
builder.rlnRelayConf.ethContractAddress, networkConf.rlnRelayEthContractAddress,
"RLN Relay ETH Contract Address was provided alongside a network conf",
)
checkSetPresetValueToField(
builder.rlnRelayConf.chainId, networkConf.rlnRelayChainId,
"RLN Relay Chain Id was provided alongside a network conf",
)
checkSetPresetValueToField(
builder.rlnRelayConf.dynamic, networkConf.rlnRelayDynamic,
"RLN Relay Dynamic was provided alongside a network conf",
)
checkSetPresetValueToField(
builder.rlnRelayConf.epochSizeSec, networkConf.rlnEpochSizeSec,
"RLN Epoch Size in Seconds was provided alongside a network conf",
)
checkSetPresetValueToField(
builder.rlnRelayConf.userMessageLimit, networkConf.rlnRelayUserMessageLimit,
"RLN Relay User Message Limit was provided alongside a network conf",
)
# End Apply relay parameters
case builder.maxMessageSize.kind
of mmskNone:
discard
builder.withMaxMessageSize(parseCorrectMsgSize(networkConf.maxMessageSize))
of mmskStr, mmskInt:
warn "Max Message Size was provided alongside a network conf",
used = networkConf.maxMessageSize, discarded = $builder.maxMessageSize
builder.withMaxMessageSize(parseCorrectMsgSize(networkConf.maxMessageSize))
if builder.shardingConf.isSome():
warn "Sharding Conf was provided alongside a network conf",
used = networkConf.shardingConf.kind, discarded = builder.shardingConf
used = $builder.maxMessageSize, discarded = networkConf.maxMessageSize
checkSetPresetValueToField(
builder.shardingConf, networkConf.shardingConf.kind,
"Sharding Conf was provided alongside a network conf",
)
case networkConf.shardingConf.kind
of StaticSharding:
builder.shardingConf = some(StaticSharding)
of AutoSharding:
builder.shardingConf = some(AutoSharding)
if builder.numShardsInCluster.isSome():
warn "Num Shards In Cluster overrides network conf preset",
used = builder.numShardsInCluster.get(),
ignored = networkConf.shardingConf.numShardsInCluster
else:
builder.numShardsInCluster = some(networkConf.shardingConf.numShardsInCluster)
checkSetPresetValueToField(
builder.numShardsInCluster, networkConf.shardingConf.numShardsInCluster,
"Num Shards In Cluster overrides network conf preset",
)
of StaticSharding:
discard
if networkConf.discv5Discovery:
if builder.discv5Conf.enabled.isNone:
builder.discv5Conf.withEnabled(networkConf.discv5Discovery)
checkSetPresetValueToField(
builder.discv5Conf.enabled, networkConf.discv5Discovery,
"Discv5 Discovery was provided alongside a network conf",
)
checkAddPresetValueToField(
builder.discv5Conf.bootstrapNodes, networkConf.discv5BootstrapNodes
)
if builder.discv5Conf.bootstrapNodes.len == 0 and
networkConf.discv5BootstrapNodes.len > 0:
warn "Discv5 Bootstrap nodes were provided alongside a network conf",
used = networkConf.discv5BootstrapNodes,
discarded = builder.discv5Conf.bootstrapNodes
builder.discv5Conf.withBootstrapNodes(networkConf.discv5BootstrapNodes)
checkSetPresetValueToField(
builder.kademliaDiscoveryConf.enabled, networkConf.enableKadDiscovery,
"Kademlia Discovery was provided alongside a network conf",
)
checkAddPresetValueToField(
builder.kademliaDiscoveryConf.bootstrapNodes, networkConf.kadBootstrapNodes
)
if networkConf.enableKadDiscovery:
if not builder.kademliaDiscoveryConf.enabled:
builder.kademliaDiscoveryConf.withEnabled(networkConf.enableKadDiscovery)
if builder.kademliaDiscoveryConf.bootstrapNodes.len == 0 and
networkConf.kadBootstrapNodes.len > 0:
builder.kademliaDiscoveryConf.withBootstrapNodes(networkConf.kadBootstrapNodes)
if networkConf.mix:
if builder.mix.isNone:
builder.mix = some(networkConf.mix)
if builder.p2pReliability.isNone:
builder.withP2pReliability(networkConf.p2pReliability)
checkSetPresetValueToField(
builder.mix, networkConf.mix, "Mix was provided alongside a network conf"
)
checkSetPresetValueToField(
builder.p2pReliability, networkConf.p2pReliability,
"P2P Reliability was provided alongside a network conf",
)
# Process entry nodes from network config - classify and distribute
if networkConf.entryNodes.len > 0:
@ -457,44 +493,44 @@ proc build*(
builder.relay.get()
else:
warn "whether to mount relay is not specified, defaulting to not mounting"
false
DefaultRelay
let lightPush =
if builder.lightPush.isSome():
builder.lightPush.get()
else:
warn "whether to mount lightPush is not specified, defaulting to not mounting"
false
DefaultLightPush
let peerExchange =
if builder.peerExchange.isSome():
builder.peerExchange.get()
else:
warn "whether to mount peerExchange is not specified, defaulting to not mounting"
false
DefaultPeerExchange
let storeSync =
if builder.storeSync.isSome():
builder.storeSync.get()
else:
warn "whether to mount storeSync is not specified, defaulting to not mounting"
false
DefaultStoreSyncMount
let rendezvous =
if builder.rendezvous.isSome():
builder.rendezvous.get()
else:
warn "whether to mount rendezvous is not specified, defaulting to not mounting"
false
DefaultRendezvous
let mix =
if builder.mix.isSome():
builder.mix.get()
else:
warn "whether to mount mix is not specified, defaulting to not mounting"
false
DefaultMix
let relayPeerExchange = builder.relayPeerExchange.get(false)
let relayPeerExchange = builder.relayPeerExchange.get(DefaultRelayPeerExchange)
let nodeKey = ?nodeKey(builder, rng)
@ -503,7 +539,7 @@ proc build*(
# TODO: ClusterId should never be defaulted, instead, presets
# should be defined and used
warn("Cluster Id was not specified, defaulting to 0")
0.uint16
DefaultClusterId
else:
builder.clusterId.get().uint16
@ -522,8 +558,9 @@ proc build*(
of mmskStr:
?parseMsgSize(builder.maxMessageSize.str)
else:
warn "Max Message Size not specified, defaulting to 150KiB"
parseCorrectMsgSize("150KiB")
warn "Max Message Size not specified, defaulting to DefaultMaxWakuMessageSize",
default = DefaultMaxWakuMessageSizeStr
DefaultMaxWakuMessageSize
let contentTopics = builder.contentTopics.get(@[])
@ -568,21 +605,21 @@ proc build*(
builder.logLevel.get()
else:
warn "Log Level not specified, defaulting to INFO"
logging.LogLevel.INFO
DefaultLogLevel
let logFormat =
if builder.logFormat.isSome():
builder.logFormat.get()
else:
warn "Log Format not specified, defaulting to TEXT"
logging.LogFormat.TEXT
DefaultLogFormat
let natStrategy =
if builder.natStrategy.isSome():
builder.natStrategy.get()
else:
warn "Nat Strategy is not specified, defaulting to none"
"none"
DefaultNatStrategy
let p2pTcpPort = builder.p2pTcpPort.get(DefaultP2pTcpPort)
@ -591,14 +628,14 @@ proc build*(
builder.p2pListenAddress.get()
else:
warn "P2P listening address not specified, listening on 0.0.0.0"
(static parseIpAddress("0.0.0.0"))
DefaultP2pListenAddress
let portsShift =
if builder.portsShift.isSome():
builder.portsShift.get()
else:
warn "Ports Shift is not specified, defaulting to 0"
0.uint16
DefaultPortsShift
let dns4DomainName =
if builder.dns4DomainName.isSome():
@ -621,21 +658,21 @@ proc build*(
builder.extMultiAddrsOnly.get()
else:
warn "Whether to only announce external multiaddresses is not specified, defaulting to false"
false
DefaultExtMultiAddrsOnly
let dnsAddrsNameServers =
if builder.dnsAddrsNameServers.len != 0:
builder.dnsAddrsNameServers
else:
warn "DNS name servers IPs not provided, defaulting to Cloudflare's."
@[static parseIpAddress("1.1.1.1"), static parseIpAddress("1.0.0.1")]
DefaultDnsAddrsNameServers
let peerPersistence =
if builder.peerPersistence.isSome():
builder.peerPersistence.get()
else:
warn "Peer persistence not specified, defaulting to false"
false
DefaultPeerPersistence
let maxConnections =
if builder.maxConnections.isSome():
@ -649,15 +686,13 @@ proc build*(
warn "max-connections less than DefaultMaxConnections; we suggest using DefaultMaxConnections or more for better connectivity",
provided = maxConnections, recommended = DefaultMaxConnections
# TODO: Do the git version thing here
let agentString = builder.agentString.get("logos-delivery")
let agentString = builder.agentString.get(DefaultAgentString)
# TODO: use `DefaultColocationLimit`. the user of this value should
# probably be defining a config object
let colocationLimit = builder.colocationLimit.get(5)
let colocationLimit = builder.colocationLimit.get(DefaultColocationLimit)
# TODO: is there a strategy for experimental features? delete vs promote
let relayShardedPeerManagement = builder.relayShardedPeerManagement.get(false)
let relayShardedPeerManagement =
builder.relayShardedPeerManagement.get(DefaultRelayShardedPeerManagement)
let wakuFlags = CapabilitiesBitfield.init(
lightpush = lightPush and relay,
@ -718,12 +753,12 @@ proc build*(
agentString: agentString,
colocationLimit: colocationLimit,
maxRelayPeers: builder.maxRelayPeers,
relayServiceRatio: builder.relayServiceRatio.get("50:50"),
relayServiceRatio: builder.relayServiceRatio.get(DefaultRelayServiceRatio),
rateLimit: rateLimit,
circuitRelayClient: builder.circuitRelayClient.get(false),
circuitRelayClient: builder.circuitRelayClient.get(DefaultCircuitRelayClient),
staticNodes: builder.staticNodes,
relayShardedPeerManagement: relayShardedPeerManagement,
p2pReliability: builder.p2pReliability.get(false),
p2pReliability: builder.p2pReliability.get(DefaultP2pReliability),
wakuFlags: wakuFlags,
localStoragePath: builder.localStoragePath.get(DefaultStoragePath),
)

View File

@ -4,7 +4,10 @@ import waku/factory/waku_conf
logScope:
topics = "waku conf builder websocket"
const DefaultWebSocketPort*: Port = Port(8000)
const
DefaultWebSocketEnabled*: bool = false
DefaultWebSocketSecureEnabled*: bool = false
DefaultWebSocketPort*: Port = Port(8000)
##############################
## WebSocket Config Builder ##
@ -40,10 +43,10 @@ proc withCertPath*(b: var WebSocketConfBuilder, certPath: string) =
b.certPath = some(certPath)
proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] =
if not b.enabled.get(false):
if not b.enabled.get(DefaultWebSocketEnabled):
return ok(none(WebSocketConf))
if not b.secureEnabled.get(false):
if not b.secureEnabled.get(DefaultWebSocketSecureEnabled):
return ok(
some(
WebSocketConf(

View File

@ -1,6 +1,7 @@
{.push raises: [].}
import chronicles, results, stint
import waku/waku_core/message/default_values
logScope:
topics = "waku networks conf"
@ -17,7 +18,7 @@ type
of StaticSharding:
discard
type NetworkConf* = object
type NetworkConf* = object ## A network "preset" (--preset=twn, --preset=logos.dev).
maxMessageSize*: string # TODO: static convert to a uint64
clusterId*: uint16
rlnRelay*: bool
@ -41,7 +42,7 @@ type NetworkConf* = object
proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf =
const RelayChainId = 59141'u256
return NetworkConf(
maxMessageSize: "150KiB",
maxMessageSize: DefaultMaxWakuMessageSizeStr,
clusterId: 1,
rlnRelay: true,
rlnRelayEthContractAddress: "0xB9cd878C90E49F797B4431fBF4fb333108CB90e6",
@ -68,7 +69,7 @@ proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf =
proc LogosDevConf*(T: type NetworkConf): NetworkConf =
const ZeroChainId = 0'u256
return NetworkConf(
maxMessageSize: "150KiB",
maxMessageSize: DefaultMaxWakuMessageSizeStr,
clusterId: 2,
rlnRelay: false,
rlnRelayEthContractAddress: "",

View File

@ -30,12 +30,12 @@ import
waku_enr/sharding,
waku_enr/multiaddr,
api/types,
messaging_client,
common/logging,
node/peer_manager,
node/health_monitor,
node/waku_metrics,
node/delivery_service/delivery_service,
node/delivery_service/subscription_manager,
node/subscription_manager,
rest_api/message_cache,
rest_api/endpoint/server,
rest_api/endpoint/builder as rest_server_builder,
@ -48,6 +48,7 @@ import
factory/app_callbacks,
persistency/persistency,
],
channels/reliable_channel_manager,
./waku_conf,
./waku_state_info
@ -73,7 +74,9 @@ type Waku* = ref object
healthMonitor*: NodeHealthMonitor
deliveryService*: DeliveryService
messagingClient*: MessagingClient
reliableChannelManager*: ReliableChannelManager
restServer*: WakuRestServerRef
metricsServer*: MetricsHttpServerRef
@ -215,10 +218,6 @@ proc new*(
error "Failed setting up app callbacks", error = error
return err("Failed setting up app callbacks: " & $error)
## Delivery Monitor
let deliveryService = DeliveryService.new(wakuConf.p2pReliability, node).valueOr:
return err("could not create delivery service: " & $error)
var waku = Waku(
stateInfo: WakuStateInfo.init(node),
conf: wakuConf,
@ -226,7 +225,6 @@ proc new*(
key: wakuConf.nodeKey,
node: node,
healthMonitor: healthMonitor,
deliveryService: deliveryService,
appCallbacks: appCallbacks,
restServer: restServer,
brokerCtx: brokerCtx,
@ -254,9 +252,9 @@ proc getPorts(
return ok((tcpPort: tcpPort, websocketPort: websocketPort))
proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.async.} =
let conf = waku[].conf
let (tcpPort, websocketPort) = getPorts(waku[].node.switch.peerInfo.listenAddrs).valueOr:
proc getRunningNetConfig(waku: Waku): Future[Result[NetConfig, string]] {.async.} =
let conf = waku.conf
let (tcpPort, websocketPort) = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr:
return err("Could not retrieve ports: " & error)
if tcpPort.isSome():
@ -276,67 +274,67 @@ proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.as
return ok(netConf)
proc updateEnr(waku: ptr Waku): Future[Result[void, string]] {.async.} =
proc updateEnr(waku: Waku): Future[Result[void, string]] {.async.} =
let netConf: NetConfig = (await getRunningNetConfig(waku)).valueOr:
return err("error calling updateNetConfig: " & $error)
let record = enrConfiguration(waku[].conf, netConf).valueOr:
let record = enrConfiguration(waku.conf, netConf).valueOr:
return err("ENR setup failed: " & error)
if isClusterMismatched(record, waku[].conf.clusterId):
if isClusterMismatched(record, waku.conf.clusterId):
return err("cluster-id mismatch configured shards")
waku[].node.enr = record
waku.node.enr = record
# If TCP/WS was configured with port 0, node.announcedAddresses was built
# pre-bind with a port value of 0. In any case, the resync is harmless.
waku[].node.announcedAddresses = netConf.announcedAddresses
waku.node.announcedAddresses = netConf.announcedAddresses
return ok()
proc updateAddressInENR(waku: ptr Waku): Result[void, string] =
let addresses: seq[MultiAddress] = waku[].node.announcedAddresses
proc updateAddressInENR(waku: Waku): Result[void, string] =
let addresses: seq[MultiAddress] = waku.node.announcedAddresses
let encodedAddrs = multiaddr.encodeMultiaddrs(addresses)
## First update the enr info contained in WakuNode
let keyBytes = waku[].key.getRawBytes().valueOr:
let keyBytes = waku.key.getRawBytes().valueOr:
return err("failed to retrieve raw bytes from waku key: " & $error)
let parsedPk = keys.PrivateKey.fromHex(keyBytes.toHex()).valueOr:
return err("failed to parse the private key: " & $error)
let enrFields = @[toFieldPair(MultiaddrEnrField, encodedAddrs)]
waku[].node.enr.update(parsedPk, extraFields = enrFields).isOkOr:
waku.node.enr.update(parsedPk, extraFields = enrFields).isOkOr:
return err("failed to update multiaddress in ENR updateAddressInENR: " & $error)
info "Waku node ENR updated successfully with new multiaddress",
enr = waku[].node.enr.toUri(), record = $(waku[].node.enr)
enr = waku.node.enr.toUri(), record = $(waku.node.enr)
## Now update the ENR infor in discv5
if not waku[].wakuDiscv5.isNil():
waku[].wakuDiscv5.protocol.localNode.record = waku[].node.enr
let enr = waku[].wakuDiscv5.protocol.localNode.record
if not waku.wakuDiscv5.isNil():
waku.wakuDiscv5.protocol.localNode.record = waku.node.enr
let enr = waku.wakuDiscv5.protocol.localNode.record
info "Waku discv5 ENR updated successfully with new multiaddress",
enr = enr.toUri(), record = $(enr)
return ok()
proc updateWaku(waku: ptr Waku): Future[Result[void, string]] {.async.} =
proc updateWaku(waku: Waku): Future[Result[void, string]] {.async.} =
(await updateEnr(waku)).isOkOr:
return err("error calling updateEnr: " & $error)
?updateAnnouncedAddrWithPrimaryIpAddr(waku[].node)
?updateAnnouncedAddrWithPrimaryIpAddr(waku.node)
?updateAddressInENR(waku)
return ok()
proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} =
proc startDnsDiscoveryRetryLoop(waku: Waku): Future[void] {.async.} =
while true:
await sleepAsync(30.seconds)
if waku.conf.dnsDiscoveryConf.isSome():
let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get()
waku[].dynamicBootstrapNodes = (
waku.dynamicBootstrapNodes = (
await waku_dnsdisc.retrieveDynamicBootstrapNodes(
dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers
)
@ -344,35 +342,61 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} =
error "Retrieving dynamic bootstrap nodes failed", error = error
continue
if not waku[].wakuDiscv5.isNil():
let dynamicBootstrapEnrs = waku[].dynamicBootstrapNodes
.filterIt(it.hasUdpPort())
.mapIt(it.enr.get().toUri())
if not waku.wakuDiscv5.isNil():
let dynamicBootstrapEnrs =
waku.dynamicBootstrapNodes.filterIt(it.hasUdpPort()).mapIt(it.enr.get().toUri())
var discv5BootstrapEnrs: seq[enr.Record]
# parse enrURIs from the configuration and add the resulting ENRs to the discv5BootstrapEnrs seq
for enrUri in dynamicBootstrapEnrs:
addBootstrapNode(enrUri, discv5BootstrapEnrs)
waku[].wakuDiscv5.updateBootstrapRecords(
waku[].wakuDiscv5.protocol.bootstrapRecords & discv5BootstrapEnrs
waku.wakuDiscv5.updateBootstrapRecords(
waku.wakuDiscv5.protocol.bootstrapRecords & discv5BootstrapEnrs
)
info "Connecting to dynamic bootstrap peers"
try:
await connectToNodes(
waku[].node, waku[].dynamicBootstrapNodes, "dynamic bootstrap"
)
await connectToNodes(waku.node, waku.dynamicBootstrapNodes, "dynamic bootstrap")
except CatchableError:
error "failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg()
return
proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: []).} =
if waku[].node.started:
warn "startWaku: waku node already started"
proc mountMessagingClient*(waku: Waku): Result[void, string] =
if not waku.messagingClient.isNil():
return err("messaging client already mounted")
if waku.node.started:
return err("cannot mount messaging client on a started node")
waku.messagingClient = MessagingClient.new(waku.conf.p2pReliability, waku.node).valueOr:
return err("could not create messaging client: " & $error)
return ok()
proc mountReliableChannelManager*(waku: Waku): Result[void, string] =
if not waku.reliableChannelManager.isNil():
return err("reliable channel manager already mounted")
if waku.messagingClient.isNil():
return err("reliable channel manager requires a mounted messaging client")
if waku.node.started:
return err("cannot mount reliable channel manager on a started node")
let messagingClient = waku.messagingClient
let defaultSendHandler: SendHandler = proc(
envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
return await messagingClient.send(envelope)
waku.reliableChannelManager = ReliableChannelManager.new(
messagingClient, defaultSendHandler, waku.brokerCtx
).valueOr:
return err("could not create reliable channel manager: " & $error)
return ok()
proc start*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} =
if waku.node.started:
warn "start: waku node already started"
return ok()
info "Retrieve dynamic bootstrap nodes"
let conf = waku[].conf
let conf = waku.conf
if conf.dnsDiscoveryConf.isSome():
let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get()
@ -390,9 +414,9 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
error "Retrieving dynamic bootstrap nodes failed",
error = dynamicBootstrapNodesRes.error
# Start Dns Discovery retry loop
waku[].dnsRetryLoopHandle = waku.startDnsDiscoveryRetryLoop()
waku.dnsRetryLoopHandle = waku.startDnsDiscoveryRetryLoop()
else:
waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get()
waku.dynamicBootstrapNodes = dynamicBootstrapNodesRes.get()
## Initialize persistency singleton instance - we don't need the instance itself here,
## but this ensures it's initialized before any store job starts.
@ -405,12 +429,12 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr:
return err("failed to read bound ports from switch: " & $error)
waku[].node.ports.tcp = bound.tcpPort.get(Port(0)).uint16
waku[].node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16
waku.node.ports.tcp = bound.tcpPort.get(Port(0)).uint16
waku.node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16
## Discv5
if conf.discv5Conf.isSome():
waku[].wakuDiscV5 = (
waku.wakuDiscV5 = (
await waku_discv5.setupAndStartDiscv5(
waku.node.enr,
waku.node.peerManager,
@ -425,23 +449,21 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
).valueOr:
return err("failed to start waku discovery v5: " & error)
waku[].node.ports.discv5Udp = waku[].wakuDiscV5.udpPort.uint16
waku[].conf.discv5Conf.get().udpPort = waku[].wakuDiscV5.udpPort
waku.node.ports.discv5Udp = waku.wakuDiscV5.udpPort.uint16
waku.conf.discv5Conf.get().udpPort = waku.wakuDiscV5.udpPort
## Update waku data that is set dynamically on node start
try:
(await updateWaku(waku)).isOkOr:
return err("Error in startWaku: " & $error)
return err("Error in start: " & $error)
except CatchableError:
return err("Caught exception in startWaku: " & getCurrentExceptionMsg())
return err("Caught exception in start: " & getCurrentExceptionMsg())
## Reliability
if not waku[].deliveryService.isNil():
waku[].deliveryService.startDeliveryService().isOkOr:
return err("failed to start delivery service: " & $error)
waku.node.subscriptionManager.subscribeAllAutoshards().isOkOr:
return err("failed to auto-subscribe autosharding shards: " & $error)
## Health Monitor
waku[].healthMonitor.startHealthMonitor().isOkOr:
waku.healthMonitor.startHealthMonitor().isOkOr:
return err("failed to start health monitor: " & $error)
## Setup RequestConnectionStatus provider
@ -450,7 +472,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
globalBrokerContext(),
proc(): Result[RequestConnectionStatus, string] =
try:
let healthReport = waku[].healthMonitor.getSyncNodeHealthReport()
let healthReport = waku.healthMonitor.getSyncNodeHealthReport()
return
ok(RequestConnectionStatus(connectionStatus: healthReport.connectionStatus))
except CatchableError:
@ -467,7 +489,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
): Future[Result[RequestProtocolHealth, string]] {.async.} =
try:
let protocolHealthStatus =
await waku[].healthMonitor.getProtocolHealthInfo(protocol)
await waku.healthMonitor.getProtocolHealthInfo(protocol)
return ok(RequestProtocolHealth(healthStatus: protocolHealthStatus))
except CatchableError:
return err("Failed to get protocol health: " & getCurrentExceptionMsg()),
@ -480,7 +502,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
globalBrokerContext(),
proc(): Future[Result[RequestHealthReport, string]] {.async.} =
try:
let report = await waku[].healthMonitor.getNodeHealthReport()
let report = await waku.healthMonitor.getNodeHealthReport()
return ok(RequestHealthReport(healthReport: report))
except CatchableError:
return err("Failed to get health report: " & getCurrentExceptionMsg()),
@ -489,9 +511,9 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
if conf.restServerConf.isSome():
rest_server_builder.startRestServerProtocolSupport(
waku[].restServer,
waku[].node,
waku[].wakuDiscv5,
waku.restServer,
waku.node,
waku.wakuDiscv5,
conf.restServerConf.get(),
conf.relay,
conf.lightPush,
@ -509,21 +531,27 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
)
).valueOr:
return err("Starting monitoring and external interfaces failed: " & error)
waku[].metricsServer = server
waku[].node.ports.metrics = port.uint16
waku[].conf.metricsServerConf.get().httpPort = port
waku.metricsServer = server
waku.node.ports.metrics = port.uint16
waku.conf.metricsServerConf.get().httpPort = port
except CatchableError:
return err(
"Caught exception starting monitoring and external interfaces failed: " &
getCurrentExceptionMsg()
)
waku[].healthMonitor.setOverallHealth(HealthStatus.READY)
waku.healthMonitor.setOverallHealth(HealthStatus.READY)
if not waku.messagingClient.isNil():
waku.messagingClient.start().isOkOr:
return err("failed to start messaging client: " & $error)
if not waku.reliableChannelManager.isNil():
waku.reliableChannelManager.start().isOkOr:
return err("failed to start reliable channel manager: " & $error)
return ok()
proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} =
## Waku shutdown
if not waku.node.started:
warn "stop: attempting to stop node that isn't running"
@ -538,9 +566,11 @@ proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} =
if not waku.wakuDiscv5.isNil():
await waku.wakuDiscv5.stop()
if not waku.deliveryService.isNil():
await waku.deliveryService.stopDeliveryService()
waku.deliveryService = nil
if not waku.reliableChannelManager.isNil():
await waku.reliableChannelManager.stop()
if not waku.messagingClient.isNil():
await waku.messagingClient.stop()
if not waku.node.isNil():
await waku.node.stop()

63
waku/messaging_client.nim Normal file
View File

@ -0,0 +1,63 @@
import results, chronos
import chronicles
import
./api/types,
./node/[
waku_node,
subscription_manager,
delivery_service/recv_service,
delivery_service/send_service,
delivery_service/send_service/delivery_task,
]
type MessagingClient* = ref object
node: WakuNode
sendService*: SendService
recvService*: RecvService
started: bool
proc new*(
T: type MessagingClient, useP2PReliability: bool, node: WakuNode
): Result[T, string] =
let sendService = ?SendService.new(useP2PReliability, node)
let recvService = RecvService.new(node)
ok(T(node: node, sendService: sendService, recvService: recvService))
proc start*(self: MessagingClient): Result[void, string] =
if self.started:
return ok()
self.recvService.startRecvService()
self.sendService.startSendService()
self.started = true
ok()
proc stop*(self: MessagingClient) {.async.} =
if not self.started:
return
await self.sendService.stopSendService()
await self.recvService.stopRecvService()
self.started = false
proc send*(
self: MessagingClient, envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async.} =
## High-level messaging API send. Auto-subscribes to the content topic
## (so the local node sees its own gossipsub broadcast), builds a
## `DeliveryTask`, and hands it to the send service. Returns the request
## id the caller can correlate with `MessageSentEvent` / `MessageErrorEvent`.
let isSubbed =
self.node.subscriptionManager.isSubscribed(envelope.contentTopic).valueOr(false)
if not isSubbed:
info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic
self.node.subscriptionManager.subscribe(envelope.contentTopic).isOkOr:
warn "Failed to auto-subscribe", error = error
return err("Failed to auto-subscribe before sending: " & error)
let requestId = RequestId.new(self.node.rng)
let deliveryTask = DeliveryTask.new(requestId, envelope, self.node.brokerCtx).valueOr:
return err("MessagingClient.send: Failed to create delivery task: " & error)
asyncSpawn self.sendService.send(deliveryTask)
return ok(requestId)

View File

@ -1,44 +0,0 @@
## This module helps to ensure the correct transmission and reception of messages
import results
import chronos, chronicles
import
./recv_service,
./send_service,
./subscription_manager,
waku/[
waku_core, waku_node, waku_store/client, waku_relay/protocol, waku_lightpush/client
]
type DeliveryService* = ref object
sendService*: SendService
recvService*: RecvService
subscriptionManager*: SubscriptionManager
proc new*(
T: type DeliveryService, useP2PReliability: bool, w: WakuNode
): Result[T, string] =
## storeClient is needed to give store visitility to DeliveryService
## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendService to re-publish
let subscriptionManager = SubscriptionManager.new(w)
let sendService = ?SendService.new(useP2PReliability, w, subscriptionManager)
let recvService = RecvService.new(w, subscriptionManager)
return ok(
DeliveryService(
sendService: sendService,
recvService: recvService,
subscriptionManager: subscriptionManager,
)
)
proc startDeliveryService*(self: DeliveryService): Result[void, string] =
?self.subscriptionManager.startSubscriptionManager()
self.recvService.startRecvService()
self.sendService.startSendService()
return ok()
proc stopDeliveryService*(self: DeliveryService) {.async.} =
await self.sendService.stopSendService()
await self.recvService.stopRecvService()
await self.subscriptionManager.stopSubscriptionManager()

View File

@ -4,17 +4,17 @@
import std/[tables, sequtils, options, sets]
import chronos, chronicles, libp2p/utility
import ../[subscription_manager]
import brokers/broker_context
import
waku/[
waku_core,
waku_core/topics,
waku_store/client,
waku_store/common,
waku_filter_v2/client,
waku_core/topics,
events/message_events,
waku_node,
node/subscription_manager,
]
const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries
@ -38,7 +38,6 @@ type RecvService* = ref object of RootObj
brokerCtx: BrokerContext
node: WakuNode
seenMsgListener: MessageSeenEventListener
subscriptionManager: SubscriptionManager
recentReceivedMsgs: seq[RecvMessage]
@ -77,7 +76,9 @@ proc processIncomingMessage(
## or if the message is a duplicate (recently-seen). Otherwise, save it as
## recently-seen, emit a MessageReceivedEvent, and return true.
if not self.subscriptionManager.isSubscribed(pubsubTopic, message.contentTopic):
if not self.node.subscriptionManager.isContentSubscribed(
pubsubTopic, message.contentTopic
):
trace "skipping message as I am not subscribed",
shard = pubsubTopic, contentTopic = message.contentTopic
return false
@ -101,7 +102,7 @@ proc checkStore*(self: RecvService) {.async.} =
self.endTimeToCheck = getNowInNanosecondTime()
## query store and deliver new recovered messages per subscribed topic
for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics:
for pubsubTopic, contentTopics in self.node.subscriptionManager.subscribedContentTopics:
let storeResp: StoreQueryResponse = (
await self.node.wakuStoreClient.queryToAny(
StoreQueryRequest(
@ -146,7 +147,7 @@ proc msgChecker(self: RecvService) {.async.} =
await sleepAsync(StoreCheckPeriod)
await self.checkStore()
proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T =
proc new*(T: typedesc[RecvService], node: WakuNode): T =
## The storeClient will help to acquire any possible missed messages
let now = getNowInNanosecondTime()
@ -154,7 +155,6 @@ proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T =
node: node,
startTimeToCheck: now,
brokerCtx: node.brokerCtx,
subscriptionManager: s,
recentReceivedMsgs: @[],
)

View File

@ -6,10 +6,10 @@ import chronos, chronicles, libp2p/utility
import brokers/broker_context
import
./[send_processor, relay_processor, lightpush_processor, delivery_task],
../[subscription_manager],
waku/[
waku_core,
node/waku_node,
node/subscription_manager,
node/peer_manager,
waku_store/client,
waku_store/common,
@ -58,7 +58,6 @@ type SendService* = ref object of RootObj
node: WakuNode
checkStoreForMessages: bool
subscriptionManager: SubscriptionManager
proc setupSendProcessorChain(
peerManager: PeerManager,
@ -96,10 +95,7 @@ proc setupSendProcessorChain(
return ok(processors[0])
proc new*(
T: typedesc[SendService],
preferP2PReliability: bool,
w: WakuNode,
s: SubscriptionManager,
T: typedesc[SendService], preferP2PReliability: bool, w: WakuNode
): Result[T, string] =
if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil():
return err(
@ -120,7 +116,6 @@ proc new*(
sendProcessor: sendProcessorChain,
node: w,
checkStoreForMessages: checkStoreForMessages,
subscriptionManager: s,
)
return ok(sendService)
@ -263,7 +258,7 @@ proc send*(self: SendService, task: DeliveryTask) {.async.} =
info "SendService.send: processing delivery task",
requestId = task.requestId, msgHash = task.msgHash.to0xHex()
self.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr:
self.node.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr:
error "SendService.send: failed to subscribe to content topic",
contentTopic = task.msg.contentTopic, error = error

View File

@ -1,596 +0,0 @@
import std/[sequtils, sets, tables, options, strutils], chronos, chronicles, results
import libp2p/[peerid, peerinfo]
import brokers/broker_context
import
waku/[
waku_core,
waku_core/topics,
waku_core/topics/sharding,
waku_node,
waku_relay,
waku_filter_v2/common as filter_common,
waku_filter_v2/client as filter_client,
waku_filter_v2/protocol as filter_protocol,
events/health_events,
events/peer_events,
requests/health_requests,
node/peer_manager,
node/health_monitor/topic_health,
node/health_monitor/connection_status,
]
# ---------------------------------------------------------------------------
# Logos Messaging API SubscriptionManager
#
# Maps all topic subscription intent and centralizes all consistency
# maintenance of the pubsub and content topic subscription model across
# the various network drivers that handle topics (Edge/Filter and Core/Relay).
# ---------------------------------------------------------------------------
type EdgeFilterSubState* = object
peers: seq[RemotePeerInfo]
## Filter service peers with confirmed subscriptions on this shard.
pending: seq[Future[void]] ## In-flight dial futures for peers not yet confirmed.
pendingPeers: HashSet[PeerId] ## PeerIds of peers currently being dialed.
currentHealth: TopicHealth
## Cached health derived from peers.len; updated on every peer set change.
func toTopicHealth*(peersCount: int): TopicHealth =
if peersCount >= HealthyThreshold:
TopicHealth.SUFFICIENTLY_HEALTHY
elif peersCount > 0:
TopicHealth.MINIMALLY_HEALTHY
else:
TopicHealth.UNHEALTHY
type SubscriptionManager* = ref object of RootObj
node: WakuNode
contentTopicSubs: Table[PubsubTopic, HashSet[ContentTopic]]
## Map of Shard to ContentTopic needed because e.g. WakuRelay is PubsubTopic only.
## A present key with an empty HashSet value means pubsubtopic already subscribed
## (via subscribePubsubTopics()) but there's no specific content topic interest yet.
edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState]
## Per-shard filter subscription state for edge mode.
edgeFilterWakeup: AsyncEvent
## Signalled when the edge filter sub loop should re-reconcile.
edgeFilterSubLoopFut: Future[void]
edgeFilterHealthLoopFut: Future[void]
peerEventListener: WakuPeerEventListener
## Listener for peer connect/disconnect events (edge filter wakeup).
iterator subscribedTopics*(
self: SubscriptionManager
): (PubsubTopic, HashSet[ContentTopic]) =
## Iterate over all subscribed content topics, batched per shard.
## This is guaranteed to return a non-empty `topics` (content topics) list on iteration.
for pubsub, topics in self.contentTopicSubs.pairs:
# We are iterating over subscribed content topics; if we are subscribed to
# a shard but have no subscription (interest) for any content topic in that
# shard, then avoid triggering an iteration that doesn't advance the intent
# to iterate over content topic subscriptions.
if topics.len == 0:
continue
yield (pubsub, topics)
proc edgeFilterPeerCount*(sm: SubscriptionManager, shard: PubsubTopic): int =
sm.edgeFilterSubStates.withValue(shard, state):
return state.peers.len
return 0
proc new*(T: typedesc[SubscriptionManager], node: WakuNode): T =
SubscriptionManager(
node: node, contentTopicSubs: initTable[PubsubTopic, HashSet[ContentTopic]]()
)
proc addContentTopicInterest(
self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic
): Result[void, string] =
var changed = false
if not self.contentTopicSubs.hasKey(shard):
self.contentTopicSubs[shard] = initHashSet[ContentTopic]()
changed = true
self.contentTopicSubs.withValue(shard, cTopics):
if not cTopics[].contains(topic):
cTopics[].incl(topic)
changed = true
if changed and not isNil(self.edgeFilterWakeup):
self.edgeFilterWakeup.fire()
return ok()
proc removeContentTopicInterest(
self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic
): Result[void, string] =
var changed = false
self.contentTopicSubs.withValue(shard, cTopics):
if cTopics[].contains(topic):
cTopics[].excl(topic)
changed = true
if cTopics[].len == 0 and isNil(self.node.wakuRelay):
self.contentTopicSubs.del(shard) # We're done with cTopics here
if changed and not isNil(self.edgeFilterWakeup):
self.edgeFilterWakeup.fire()
return ok()
proc subscribePubsubTopics(
self: SubscriptionManager, shards: seq[PubsubTopic]
): Result[void, string] =
if isNil(self.node.wakuRelay):
return err("subscribePubsubTopics requires a Relay")
var errors: seq[string]
for shard in shards:
if not self.contentTopicSubs.hasKey(shard):
self.node.subscribe((kind: PubsubSub, topic: shard), nil).isOkOr:
errors.add("shard " & shard & ": " & error)
continue
self.contentTopicSubs[shard] = initHashSet[ContentTopic]()
if errors.len > 0:
return err("subscribeShard errors: " & errors.join("; "))
return ok()
proc getShardForContentTopic(
self: SubscriptionManager, topic: ContentTopic
): Result[PubsubTopic, string] =
if self.node.wakuAutoSharding.isSome():
let shardObj = ?self.node.wakuAutoSharding.get().getShard(topic)
return ok($shardObj)
return err("SubscriptionManager requires AutoSharding")
proc isSubscribed*(
self: SubscriptionManager, topic: ContentTopic
): Result[bool, string] =
let shard = ?self.getShardForContentTopic(topic)
return ok(
self.contentTopicSubs.hasKey(shard) and self.contentTopicSubs[shard].contains(topic)
)
proc isSubscribed*(
self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic
): bool {.raises: [].} =
self.contentTopicSubs.withValue(shard, cTopics):
return cTopics[].contains(contentTopic)
return false
proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] =
if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient):
return err("SubscriptionManager requires either Relay or Filter Client.")
let shard = ?self.getShardForContentTopic(topic)
if not isNil(self.node.wakuRelay) and not self.contentTopicSubs.hasKey(shard):
?self.subscribePubsubTopics(@[shard])
?self.addContentTopicInterest(shard, topic)
return ok()
proc unsubscribe*(
self: SubscriptionManager, topic: ContentTopic
): Result[void, string] =
if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient):
return err("SubscriptionManager requires either Relay or Filter Client.")
let shard = ?self.getShardForContentTopic(topic)
if self.isSubscribed(shard, topic):
?self.removeContentTopicInterest(shard, topic)
return ok()
# ---------------------------------------------------------------------------
# Edge Filter driver for the Logos Messaging API
#
# The SubscriptionManager absorbs natively the responsibility of using the
# Edge Filter protocol to effect subscriptions and message receipt for edge.
# ---------------------------------------------------------------------------
const EdgeFilterSubscribeTimeout = chronos.seconds(15)
## Timeout for a single filter subscribe/unsubscribe RPC to a service peer.
const EdgeFilterPingTimeout = chronos.seconds(5)
## Timeout for a filter ping health check.
const EdgeFilterLoopInterval = chronos.seconds(30)
## Interval for the edge filter health ping loop.
const EdgeFilterSubLoopDebounce = chronos.seconds(1)
## Debounce delay to coalesce rapid-fire wakeups into a single reconciliation pass.
type EdgeDialTask = object
peer: RemotePeerInfo
shard: PubsubTopic
topics: seq[ContentTopic]
proc updateShardHealth(
self: SubscriptionManager, shard: PubsubTopic, state: var EdgeFilterSubState
) =
## Recompute and emit health for a shard after its peer set changed.
let newHealth = toTopicHealth(state.peers.len)
if newHealth != state.currentHealth:
state.currentHealth = newHealth
EventShardTopicHealthChange.emit(self.node.brokerCtx, shard, newHealth)
proc removePeer(self: SubscriptionManager, shard: PubsubTopic, peerId: PeerId) =
## Remove a peer from edgeFilterSubStates for the given shard,
## update health, and wake the sub loop to dial a replacement.
## Best-effort unsubscribe so the service peer stops pushing to us.
self.edgeFilterSubStates.withValue(shard, state):
var peer: RemotePeerInfo
var found = false
for p in state.peers:
if p.peerId == peerId:
peer = p
found = true
break
if not found:
return
state.peers.keepItIf(it.peerId != peerId)
self.updateShardHealth(shard, state[])
self.edgeFilterWakeup.fire()
if not self.node.wakuFilterClient.isNil():
self.contentTopicSubs.withValue(shard, topics):
let ct = toSeq(topics[])
if ct.len > 0:
proc doUnsubscribe() {.async.} =
discard await self.node.wakuFilterClient.unsubscribe(peer, shard, ct)
asyncSpawn doUnsubscribe()
type SendChunkedFilterRpcKind = enum
FilterSubscribe
FilterUnsubscribe
proc sendChunkedFilterRpc(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
topics: seq[ContentTopic],
kind: SendChunkedFilterRpcKind,
): Future[bool] {.async.} =
## Send a chunked filter subscribe or unsubscribe RPC. Returns true on
## success. On failure the peer is removed and false is returned.
try:
var i = 0
while i < topics.len:
let chunk =
topics[i ..< min(i + filter_protocol.MaxContentTopicsPerRequest, topics.len)]
let fut =
case kind
of FilterSubscribe:
self.node.wakuFilterClient.subscribe(peer, shard, chunk)
of FilterUnsubscribe:
self.node.wakuFilterClient.unsubscribe(peer, shard, chunk)
if not (await fut.withTimeout(EdgeFilterSubscribeTimeout)) or fut.read().isErr():
trace "sendChunkedFilterRpc: chunk failed",
op = kind, shard = shard, peer = peer.peerId
self.removePeer(shard, peer.peerId)
return false
i += filter_protocol.MaxContentTopicsPerRequest
except CatchableError as exc:
debug "sendChunkedFilterRpc: failed",
op = kind, shard = shard, peer = peer.peerId, err = exc.msg
self.removePeer(shard, peer.peerId)
return false
return true
proc syncFilterDeltas(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
added: seq[ContentTopic],
removed: seq[ContentTopic],
) {.async.} =
## Push content topic changes (adds/removes) to an already-tracked peer.
if added.len > 0:
if not await self.sendChunkedFilterRpc(peer, shard, added, FilterSubscribe):
return
if removed.len > 0:
discard await self.sendChunkedFilterRpc(peer, shard, removed, FilterUnsubscribe)
proc dialFilterPeer(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
contentTopics: seq[ContentTopic],
) {.async.} =
## Subscribe a new peer to all content topics on a shard and start tracking it.
self.edgeFilterSubStates.withValue(shard, state):
state.pendingPeers.incl(peer.peerId)
try:
if not await self.sendChunkedFilterRpc(peer, shard, contentTopics, FilterSubscribe):
return
self.edgeFilterSubStates.withValue(shard, state):
if state.peers.anyIt(it.peerId == peer.peerId):
trace "dialFilterPeer: peer already tracked, skipping duplicate",
shard = shard, peer = peer.peerId
return
state.peers.add(peer)
self.updateShardHealth(shard, state[])
trace "dialFilterPeer: successfully subscribed to all chunks",
shard = shard, peer = peer.peerId, totalPeers = state.peers.len
do:
trace "dialFilterPeer: shard removed while subscribing, discarding result",
shard = shard, peer = peer.peerId
finally:
self.edgeFilterSubStates.withValue(shard, state):
state.pendingPeers.excl(peer.peerId)
proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} =
## Periodically pings all connected filter service peers to verify they are
## still alive at the application layer. Peers that fail the ping are removed.
while true:
await sleepAsync(EdgeFilterLoopInterval)
if self.node.wakuFilterClient.isNil():
warn "filter client is nil within edge filter health loop"
continue
var connected = initTable[PeerId, RemotePeerInfo]()
for state in self.edgeFilterSubStates.values:
for peer in state.peers:
if self.node.peerManager.switch.peerStore.isConnected(peer.peerId):
connected[peer.peerId] = peer
var alive = initHashSet[PeerId]()
if connected.len > 0:
var pingTasks: seq[(PeerId, Future[FilterSubscribeResult])]
for peer in connected.values:
pingTasks.add(
(peer.peerId, self.node.wakuFilterClient.ping(peer, EdgeFilterPingTimeout))
)
# extract future tasks from (PeerId, Future) tuples and await them
await allFutures(pingTasks.mapIt(it[1]))
for (peerId, task) in pingTasks:
if task.read().isOk():
alive.incl(peerId)
var changed = false
for shard, state in self.edgeFilterSubStates.mpairs:
let oldLen = state.peers.len
state.peers.keepItIf(it.peerId notin connected or alive.contains(it.peerId))
if state.peers.len < oldLen:
changed = true
self.updateShardHealth(shard, state)
trace "Edge Filter health degraded by Ping failure",
shard = shard, new = state.currentHealth
if changed:
self.edgeFilterWakeup.fire()
proc selectFilterCandidates(
self: SubscriptionManager, shard: PubsubTopic, exclude: HashSet[PeerId], needed: int
): seq[RemotePeerInfo] =
## Select filter service peer candidates for a shard.
# Start with every filter server peer that can serve the shard
var allCandidates = self.node.peerManager.selectPeers(
filter_common.WakuFilterSubscribeCodec, some(shard)
)
# Remove all already used in this shard or being dialed for it
allCandidates.keepItIf(it.peerId notin exclude)
# Collect peer IDs already tracked on other shards
var trackedOnOther = initHashSet[PeerId]()
for otherShard, otherState in self.edgeFilterSubStates.pairs:
if otherShard != shard:
for peer in otherState.peers:
trackedOnOther.incl(peer.peerId)
# Prefer peers we already have a connection to first, preserving shuffle
var candidates =
allCandidates.filterIt(it.peerId in trackedOnOther) &
allCandidates.filterIt(it.peerId notin trackedOnOther)
# We need to return 'needed' peers only
if candidates.len > needed:
candidates.setLen(needed)
return candidates
proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} =
## Reconciles filter subscriptions with the desired state from SubscriptionManager.
var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]()
while true:
await self.edgeFilterWakeup.wait()
await sleepAsync(EdgeFilterSubLoopDebounce)
self.edgeFilterWakeup.clear()
trace "edgeFilterSubLoop: woke up"
if isNil(self.node.wakuFilterClient):
trace "edgeFilterSubLoop: wakuFilterClient is nil, skipping"
continue
let desired = self.contentTopicSubs
trace "edgeFilterSubLoop: desired state", numShards = desired.len
let allShards = toHashSet(toSeq(desired.keys)) + toHashSet(toSeq(lastSynced.keys))
# Step 1: read state across all shards at once and
# create a list of peer dial tasks and shard tracking to delete.
var dialTasks: seq[EdgeDialTask]
var shardsToDelete: seq[PubsubTopic]
for shard in allShards:
let currTopics = desired.getOrDefault(shard)
let prevTopics = lastSynced.getOrDefault(shard)
if shard notin self.edgeFilterSubStates:
self.edgeFilterSubStates[shard] =
EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY)
let addedTopics = toSeq(currTopics - prevTopics)
let removedTopics = toSeq(prevTopics - currTopics)
self.edgeFilterSubStates.withValue(shard, state):
state.peers.keepItIf(
self.node.peerManager.switch.peerStore.isConnected(it.peerId)
)
state.pending.keepItIf(not it.finished)
if addedTopics.len > 0 or removedTopics.len > 0:
for peer in state.peers:
asyncSpawn self.syncFilterDeltas(peer, shard, addedTopics, removedTopics)
if currTopics.len == 0:
shardsToDelete.add(shard)
else:
self.updateShardHealth(shard, state[])
let needed = max(0, HealthyThreshold - state.peers.len - state.pending.len)
if needed > 0:
let tracked = state.peers.mapIt(it.peerId).toHashSet() + state.pendingPeers
let candidates = self.selectFilterCandidates(shard, tracked, needed)
let toDial = min(needed, candidates.len)
trace "edgeFilterSubLoop: shard reconciliation",
shard = shard,
num_peers = state.peers.len,
num_pending = state.pending.len,
num_needed = needed,
num_available = candidates.len,
toDial = toDial
for i in 0 ..< toDial:
dialTasks.add(
EdgeDialTask(
peer: candidates[i], shard: shard, topics: toSeq(currTopics)
)
)
# Step 2: execute deferred shard tracking deletion and dial tasks.
for shard in shardsToDelete:
self.edgeFilterSubStates.withValue(shard, state):
for fut in state.pending:
if not fut.finished:
await fut.cancelAndWait()
self.edgeFilterSubStates.del(shard)
for task in dialTasks:
let fut = self.dialFilterPeer(task.peer, task.shard, task.topics)
self.edgeFilterSubStates.withValue(task.shard, state):
state.pending.add(fut)
lastSynced = desired
proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] =
## Start the edge filter orchestration loops.
## Caller must ensure this is only called in edge mode (relay nil, filter client present).
self.edgeFilterWakeup = newAsyncEvent()
self.peerEventListener = WakuPeerEvent.listen(
self.node.brokerCtx,
proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} =
if evt.kind == WakuPeerEventKind.EventDisconnected or
evt.kind == WakuPeerEventKind.EventMetadataUpdated:
self.edgeFilterWakeup.fire()
,
).valueOr:
return err("Failed to listen to peer events for edge filter: " & error)
self.edgeFilterSubLoopFut = self.edgeFilterSubLoop()
self.edgeFilterHealthLoopFut = self.edgeFilterHealthLoop()
return ok()
proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} =
## Stop the edge filter orchestration loops and clean up pending futures.
if not isNil(self.edgeFilterSubLoopFut):
await self.edgeFilterSubLoopFut.cancelAndWait()
self.edgeFilterSubLoopFut = nil
if not isNil(self.edgeFilterHealthLoopFut):
await self.edgeFilterHealthLoopFut.cancelAndWait()
self.edgeFilterHealthLoopFut = nil
for shard, state in self.edgeFilterSubStates:
for fut in state.pending:
if not fut.finished:
await fut.cancelAndWait()
await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener)
# ---------------------------------------------------------------------------
# SubscriptionManager Lifecycle (calls Edge behavior above)
#
# startSubscriptionManager and stopSubscriptionManager orchestrate both the
# core (relay) and edge (filter) paths, and register/clear broker providers.
# ---------------------------------------------------------------------------
proc startSubscriptionManager*(self: SubscriptionManager): Result[void, string] =
# Register edge filter broker providers. The shard/content health providers
# in WakuNode query these via the broker as a fallback when relay health is
# not available. If edge mode is not active, these providers simply return
# NOT_SUBSCRIBED / strength 0, which is harmless.
RequestEdgeShardHealth.setProvider(
self.node.brokerCtx,
proc(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] =
self.edgeFilterSubStates.withValue(shard, state):
return ok(RequestEdgeShardHealth(health: state.currentHealth))
return ok(RequestEdgeShardHealth(health: TopicHealth.NOT_SUBSCRIBED)),
).isOkOr:
error "Can't set provider for RequestEdgeShardHealth", error = error
RequestEdgeFilterPeerCount.setProvider(
self.node.brokerCtx,
proc(): Result[RequestEdgeFilterPeerCount, string] =
var minPeers = high(int)
for state in self.edgeFilterSubStates.values:
minPeers = min(minPeers, state.peers.len)
if minPeers == high(int):
minPeers = 0
return ok(RequestEdgeFilterPeerCount(peerCount: minPeers)),
).isOkOr:
error "Can't set provider for RequestEdgeFilterPeerCount", error = error
if self.node.wakuRelay.isNil():
return self.startEdgeFilterLoops()
# Core mode: auto-subscribe relay to all shards in autosharding.
if self.node.wakuAutoSharding.isSome():
let autoSharding = self.node.wakuAutoSharding.get()
let clusterId = autoSharding.clusterId
let numShards = autoSharding.shardCountGenZero
if numShards > 0:
var clusterPubsubTopics = newSeqOfCap[PubsubTopic](numShards)
for i in 0 ..< numShards:
let shardObj = RelayShard(clusterId: clusterId, shardId: uint16(i))
clusterPubsubTopics.add(PubsubTopic($shardObj))
self.subscribePubsubTopics(clusterPubsubTopics).isOkOr:
error "Failed to auto-subscribe Relay to cluster shards: ", error = error
else:
info "SubscriptionManager has no AutoSharding configured; skipping auto-subscribe."
return ok()
proc stopSubscriptionManager*(self: SubscriptionManager) {.async: (raises: []).} =
if self.node.wakuRelay.isNil():
await self.stopEdgeFilterLoops()
RequestEdgeShardHealth.clearProvider(self.node.brokerCtx)
RequestEdgeFilterPeerCount.clearProvider(self.node.brokerCtx)

View File

@ -14,6 +14,7 @@ import
events/health_events,
events/peer_events,
node/waku_node,
node/node_telemetry,
node/peer_manager,
node/kernel_api,
node/health_monitor/online_monitor,

View File

@ -21,6 +21,7 @@ import
import
../waku_node,
../node_telemetry,
../../waku_core,
../../waku_core/topics/sharding,
../../waku_filter_v2,

View File

@ -19,6 +19,7 @@ import
import
../waku_node,
../node_telemetry,
../../waku_peer_exchange,
../../waku_core,
../peer_manager,

View File

@ -29,90 +29,18 @@ import
waku_store_sync,
waku_rln_relay,
node/waku_node,
node/subscription_manager,
node/peer_manager,
events/message_events,
]
export waku_relay.WakuRelayHandler
declarePublicHistogram waku_histogram_message_size,
"message size histogram in kB",
buckets = [
0.0, 1.0, 3.0, 5.0, 15.0, 50.0, 75.0, 100.0, 125.0, 150.0, 500.0, 700.0, 1000.0, Inf
]
logScope:
topics = "waku node relay api"
## Waku relay
proc registerRelayHandler(
node: WakuNode, topic: PubsubTopic, appHandler: WakuRelayHandler = nil
): bool =
## Registers the only handler for the given topic.
## Notice that this handler internally calls other handlers, such as filter,
## archive, etc, plus the handler provided by the application.
## Returns `true` if a mesh subscription was created or `false` if the relay
## was already subscribed to the topic.
let alreadySubscribed = node.wakuRelay.isSubscribed(topic)
if not appHandler.isNil():
if not alreadySubscribed or not node.legacyAppHandlers.hasKey(topic):
node.legacyAppHandlers[topic] = appHandler
else:
debug "Legacy appHandler already exists for active PubsubTopic, ignoring new handler",
topic = topic
if alreadySubscribed:
return false
proc traceHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
let msgSizeKB = msg.payload.len / 1000
waku_node_messages.inc(labelValues = ["relay"])
waku_histogram_message_size.observe(msgSizeKB)
proc filterHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuFilter.isNil():
return
await node.wakuFilter.handleMessage(topic, msg)
proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuArchive.isNil():
return
await node.wakuArchive.handleMessage(topic, msg)
proc syncHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuStoreReconciliation.isNil():
return
node.wakuStoreReconciliation.messageIngress(topic, msg)
proc internalHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
MessageSeenEvent.emit(node.brokerCtx, topic, msg)
let uniqueTopicHandler = proc(
topic: PubsubTopic, msg: WakuMessage
): Future[void] {.async, gcsafe.} =
await traceHandler(topic, msg)
await filterHandler(topic, msg)
await archiveHandler(topic, msg)
await syncHandler(topic, msg)
await internalHandler(topic, msg)
# Call the legacy (kernel API) app handler if it exists.
# Normally, hasKey is false and the MessageSeenEvent bus (new API) is used instead.
# But we need to support legacy behavior (kernel API use), hence this.
# NOTE: We can delete `legacyAppHandlers` if instead we refactor WakuRelay to support multiple
# PubsubTopic handlers, since that's actually supported by libp2p PubSub (bigger refactor...)
if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil():
await node.legacyAppHandlers[topic](topic, msg)
node.wakuRelay.subscribe(topic, uniqueTopicHandler)
proc getTopicOfSubscriptionEvent(
node: WakuNode, subscription: SubscriptionEvent
): Result[(PubsubTopic, Option[ContentTopic]), string] =
@ -143,21 +71,15 @@ proc subscribe*(
error "Invalid API call to `subscribe`. WakuRelay not mounted."
return err("Invalid API call to `subscribe`. WakuRelay not mounted.")
let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr:
let (pubsubTopic, _) = getTopicOfSubscriptionEvent(node, subscription).valueOr:
error "Failed to decode subscription event", error = error
return err("Failed to decode subscription event: " & error)
if node.registerRelayHandler(pubsubTopic, handler):
info "subscribe", pubsubTopic, contentTopicOp
node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic))
else:
if isNil(handler):
warn "No-effect API call to subscribe. Already subscribed to topic", pubsubTopic
else:
info "subscribe (was already subscribed in the mesh; appHandler set)",
pubsubTopic = pubsubTopic
return ok()
# strict version
#if contentTopicOp.isSome():
# return
# node.subscriptionManager.subscribe(pubsubTopic, contentTopicOp.get(), handler)
return node.subscriptionManager.subscribeShard(pubsubTopic, handler)
proc unsubscribe*(
node: WakuNode, subscription: SubscriptionEvent
@ -170,26 +92,14 @@ proc unsubscribe*(
error "Invalid API call to `unsubscribe`. WakuRelay not mounted."
return err("Invalid API call to `unsubscribe`. WakuRelay not mounted.")
let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr:
let (pubsubTopic, _) = getTopicOfSubscriptionEvent(node, subscription).valueOr:
error "Failed to decode unsubscribe event", error = error
return err("Failed to decode unsubscribe event: " & error)
let hadHandler = node.legacyAppHandlers.hasKey(pubsubTopic)
if hadHandler:
node.legacyAppHandlers.del(pubsubTopic)
if node.wakuRelay.isSubscribed(pubsubTopic):
info "unsubscribe", pubsubTopic, contentTopicOp
node.wakuRelay.unsubscribe(pubsubTopic)
node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: pubsubTopic))
else:
if not hadHandler:
warn "No-effect API call to `unsubscribe`. Was not subscribed", pubsubTopic
else:
info "unsubscribe (was not subscribed in the mesh; appHandler removed)",
pubsubTopic = pubsubTopic
return ok()
# strict version
#if contentTopicOp.isSome():
# return node.subscriptionManager.unsubscribe(pubsubTopic, contentTopicOp.get())
return node.subscriptionManager.unsubscribeAll(pubsubTopic)
proc isSubscribed*(
node: WakuNode, subscription: SubscriptionEvent

View File

@ -0,0 +1,27 @@
{.push raises: [].}
import metrics
declarePublicGauge waku_version,
"Waku version info (in git describe format)", ["version"]
declarePublicCounter waku_node_errors, "number of wakunode errors", ["type"]
declarePublicGauge waku_lightpush_peers, "number of lightpush peers"
declarePublicGauge waku_filter_peers, "number of filter peers"
declarePublicGauge waku_store_peers, "number of store peers"
declarePublicGauge waku_px_peers,
"number of peers (in the node's peerManager) supporting the peer exchange protocol"
declarePublicCounter waku_node_messages, "number of messages received", ["type"]
declarePublicHistogram waku_histogram_message_size,
"message size histogram in kB",
buckets = [
0.0, 1.0, 3.0, 5.0, 15.0, 50.0, 75.0, 100.0, 125.0, 150.0, 500.0, 700.0, 1000.0, Inf
]
{.pop.}

116
waku/node/node_types.nim Normal file
View File

@ -0,0 +1,116 @@
{.push raises: [].}
import
std/[options, tables, sets],
chronos,
results,
eth/keys,
bearssl/rand,
eth/p2p/discoveryv5/enr,
libp2p/crypto/crypto,
libp2p/[multiaddress, multicodec],
libp2p/protocols/ping,
libp2p/protocols/mix/mix_protocol,
brokers/broker_context
import
waku/[
waku_core,
waku_relay,
waku_archive,
waku_store/protocol as store,
waku_store/client as store_client,
waku_store/resume,
waku_store_sync,
waku_filter_v2,
waku_filter_v2/client as filter_client,
waku_metadata,
waku_rendezvous/protocol,
waku_rendezvous/client as rendezvous_client,
waku_lightpush_legacy/client as legacy_lightpush_client,
waku_lightpush_legacy as legacy_lightpush_protocol,
waku_lightpush/client as lightpush_client,
waku_lightpush as lightpush_protocol,
waku_peer_exchange,
waku_rln_relay,
waku_mix,
common/rate_limit/setting,
discovery/waku_kademlia,
net/bound_ports,
events/peer_events,
],
./peer_manager,
./health_monitor/topic_health
# key and crypto modules different
type
# TODO: Move to application instance (e.g., `WakuNode2`)
WakuInfo* = object # NOTE One for simplicity, can extend later as needed
listenAddresses*: seq[string]
enrUri*: string #multiaddrStrings*: seq[string]
mixPubKey*: Option[string]
# NOTE based on Eth2Node in NBC eth2_network.nim
WakuNode* = ref object
peerManager*: PeerManager
switch*: Switch
wakuRelay*: WakuRelay
wakuArchive*: waku_archive.WakuArchive
wakuStore*: store.WakuStore
wakuStoreClient*: store_client.WakuStoreClient
wakuStoreResume*: StoreResume
wakuStoreReconciliation*: SyncReconciliation
wakuStoreTransfer*: SyncTransfer
wakuFilter*: waku_filter_v2.WakuFilter
wakuFilterClient*: filter_client.WakuFilterClient
wakuRlnRelay*: WakuRLNRelay
wakuLegacyLightPush*: WakuLegacyLightPush
wakuLegacyLightpushClient*: WakuLegacyLightPushClient
wakuLightPush*: WakuLightPush
wakuLightpushClient*: WakuLightPushClient
wakuPeerExchange*: WakuPeerExchange
wakuPeerExchangeClient*: WakuPeerExchangeClient
wakuMetadata*: WakuMetadata
wakuAutoSharding*: Option[Sharding]
enr*: enr.Record
libp2pPing*: Ping
rng*: ref rand.HmacDrbgContext
brokerCtx*: BrokerContext
wakuRendezvous*: WakuRendezVous
wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient
announcedAddresses*: seq[MultiAddress]
extMultiAddrsOnly*: bool # When true, skip automatic IP address replacement
started*: bool # Indicates that node has started listening
topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent]
rateLimitSettings*: ProtocolRateLimitSettings
legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler]
## Kernel API Relay appHandlers (if any)
subscriptionManager*: SubscriptionManager
wakuMix*: WakuMix
kademliaDiscoveryLoop*: Future[void]
wakuKademlia*: WakuKademlia
ports*: BoundPorts
ShardSubscription* = object
contentTopics*: HashSet[ContentTopic]
directShardSub*: bool
## shard subscribed directly (PubsubSub), independent of content-topic interest
EdgeFilterSubState* = object
peers*: seq[RemotePeerInfo]
pending*: seq[Future[void]]
pendingPeers*: HashSet[PeerId]
currentHealth*: TopicHealth
SubscriptionManager* = ref object of RootObj
node*: WakuNode
shards*: Table[PubsubTopic, ShardSubscription]
edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState]
edgeFilterWakeup*: AsyncEvent
edgeFilterSubLoopFut*: Future[void]
edgeFilterConnectionLoopFut*: Future[void]
peerEventListener*: WakuPeerEventListener
ownsEdgeShardHealthProvider*: bool
ownsEdgeFilterPeerCountProvider*: bool
{.pop.}

View File

@ -0,0 +1,708 @@
import std/[sequtils, sets, tables, options], chronos, chronicles, metrics, results
import libp2p/[peerid, peerinfo]
import brokers/broker_context
import
waku/[
waku_core,
waku_core/topics/sharding,
node/node_types,
node/node_telemetry,
waku_relay,
waku_archive,
waku_store_sync,
waku_filter_v2/common as filter_common,
waku_filter_v2/client as filter_client,
waku_filter_v2/protocol as filter_protocol,
events/health_events,
events/message_events,
events/peer_events,
requests/health_requests,
node/peer_manager,
node/health_monitor/topic_health,
node/health_monitor/connection_status,
]
{.push raises: [].}
proc registerRelayHandler(
node: WakuNode, shard: PubsubTopic, appHandler: WakuRelayHandler = nil
): bool =
## Returns true iff we did a new (and only) subscription for this shard in GossipSub.
let alreadySubscribed = node.wakuRelay.isSubscribed(shard)
if not appHandler.isNil():
if not alreadySubscribed or not node.legacyAppHandlers.hasKey(shard):
node.legacyAppHandlers[shard] = appHandler
else:
debug "Legacy appHandler already exists for active PubsubTopic, ignoring new handler",
topic = shard
if alreadySubscribed:
return false
proc traceHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
let msgSizeKB = msg.payload.len / 1000
waku_node_messages.inc(labelValues = ["relay"])
waku_histogram_message_size.observe(msgSizeKB)
proc filterHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuFilter.isNil():
return
await node.wakuFilter.handleMessage(topic, msg)
proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuArchive.isNil():
return
await node.wakuArchive.handleMessage(topic, msg)
proc syncHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuStoreReconciliation.isNil():
return
node.wakuStoreReconciliation.messageIngress(topic, msg)
proc internalHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
MessageSeenEvent.emit(node.brokerCtx, topic, msg)
let uniqueTopicHandler = proc(
topic: PubsubTopic, msg: WakuMessage
): Future[void] {.async, gcsafe.} =
await traceHandler(topic, msg)
await filterHandler(topic, msg)
await archiveHandler(topic, msg)
await syncHandler(topic, msg)
await internalHandler(topic, msg)
if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil():
await node.legacyAppHandlers[topic](topic, msg)
node.wakuRelay.subscribe(shard, uniqueTopicHandler)
return true
proc unregisterRelayHandler(node: WakuNode, shard: PubsubTopic): bool =
## Returns true iff we had a subscription for this shard in GossipSub and it was removed.
if node.legacyAppHandlers.hasKey(shard):
node.legacyAppHandlers.del(shard)
if node.wakuRelay.isSubscribed(shard):
node.wakuRelay.unsubscribe(shard)
return true
return false
proc doRelaySubscribe(
node: WakuNode, shard: PubsubTopic, appHandler: WakuRelayHandler = nil
): bool =
## Subscribes the node to a shard.
## Returns true if we actually subscribed (transitioned from unsubscribed to subscribed).
## Emit the shard subscription event if we actually subscribed.
let installed = node.registerRelayHandler(shard, appHandler)
if installed:
node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: shard))
return installed
proc doRelayUnsubscribe(node: WakuNode, shard: PubsubTopic): bool =
## Unsubscribes the node from a shard.
## Returns true if we actually unsubscribed (transitioned from subscribed to unsubscribed).
## Emit the shard unsubscription event if we actually unsubscribed.
let unsubscribed = node.unregisterRelayHandler(shard)
if unsubscribed:
node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: shard))
return unsubscribed
proc new*(T: type SubscriptionManager, node: WakuNode): T =
T(
node: node,
shards: initTable[PubsubTopic, ShardSubscription](),
edgeFilterSubStates: initTable[PubsubTopic, EdgeFilterSubState](),
edgeFilterWakeup: newAsyncEvent(),
)
func wanted(entry: ShardSubscription): bool =
## True if the shard has content-topic interest or a direct subscription.
return entry.contentTopics.len > 0 or entry.directShardSub
proc isContentSubscribed*(
self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic
): bool =
self.shards.withValue(shard, sub):
return contentTopic in sub.contentTopics
return false
iterator subscribedContentTopics*(
self: SubscriptionManager
): (PubsubTopic, HashSet[ContentTopic]) =
## Yields each shard with its non-empty content-topic set.
for shard, sub in self.shards.pairs:
if sub.contentTopics.len > 0:
yield (shard, sub.contentTopics)
func toTopicHealth*(peersCount: int): TopicHealth =
if peersCount >= HealthyThreshold:
return TopicHealth.SUFFICIENTLY_HEALTHY
elif peersCount > 0:
return TopicHealth.MINIMALLY_HEALTHY
else:
return TopicHealth.UNHEALTHY
proc edgeFilterPeerCount*(self: SubscriptionManager, shard: PubsubTopic): int =
self.edgeFilterSubStates.withValue(shard, state):
return state.peers.len
return 0
proc getShardForContentTopic(
self: SubscriptionManager, topic: ContentTopic
): Result[PubsubTopic, string] =
if self.node.wakuAutoSharding.isSome():
let shardObj = ?self.node.wakuAutoSharding.get().getShard(topic)
return ok($shardObj)
return err("autosharding is not configured; pass an explicit shard")
proc subscribeShard*(
self: SubscriptionManager, shard: PubsubTopic, handler: WakuRelayHandler = nil
): Result[void, string] =
## Subscribes to the shard directly and joins the relay mesh.
var added = false
self.shards.withValue(shard, entry):
if not entry.directShardSub:
entry.directShardSub = true
added = true
do:
self.shards[shard] = ShardSubscription(
contentTopics: initHashSet[ContentTopic](), directShardSub: true
)
added = true
if added:
self.edgeFilterWakeup.fire()
if not isNil(self.node.wakuRelay):
discard self.node.doRelaySubscribe(shard, handler)
return ok()
proc unsubscribeShard*(
self: SubscriptionManager, shard: PubsubTopic
): Result[void, string] =
## Drops the direct shard subscription; unsubscribes the mesh if no content topic wants it.
var removed = false
var shardEmpty = false
self.shards.withValue(shard, entry):
if entry.directShardSub:
entry.directShardSub = false
removed = true
shardEmpty = not entry[].wanted()
if removed:
self.edgeFilterWakeup.fire()
if shardEmpty:
self.shards.del(shard)
if not isNil(self.node.wakuRelay):
discard self.node.doRelayUnsubscribe(shard)
return ok()
proc subscribe*(
self: SubscriptionManager,
shard: PubsubTopic,
contentTopic: ContentTopic,
handler: WakuRelayHandler = nil,
): Result[void, string] =
## Adds content-topic interest on the shard and joins the relay mesh.
var added = false
self.shards.withValue(shard, entry):
if contentTopic notin entry.contentTopics:
entry.contentTopics.incl(contentTopic)
added = true
do:
var entry = ShardSubscription(contentTopics: initHashSet[ContentTopic]())
entry.contentTopics.incl(contentTopic)
self.shards[shard] = entry
added = true
if added:
self.edgeFilterWakeup.fire()
if not isNil(self.node.wakuRelay):
discard self.node.doRelaySubscribe(shard, handler)
return ok()
proc unsubscribe*(
self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic
): Result[void, string] =
## Drops content-topic interest on the shard; unsubscribes the mesh if nothing else wants it.
var removed = false
var shardEmpty = false
self.shards.withValue(shard, entry):
if contentTopic in entry.contentTopics:
entry.contentTopics.excl(contentTopic)
removed = true
shardEmpty = not entry[].wanted()
if removed:
self.edgeFilterWakeup.fire()
if shardEmpty:
self.shards.del(shard)
if not isNil(self.node.wakuRelay):
discard self.node.doRelayUnsubscribe(shard)
return ok()
proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] =
## Subscribes to a content topic, resolving its shard via autosharding.
let shard = ?self.getShardForContentTopic(topic)
return self.subscribe(shard, topic)
proc unsubscribe*(
self: SubscriptionManager, topic: ContentTopic
): Result[void, string] =
## Unsubscribes from a content topic, resolving its shard via autosharding.
let shard = ?self.getShardForContentTopic(topic)
return self.unsubscribe(shard, topic)
proc unsubscribeAll*(
self: SubscriptionManager, shard: PubsubTopic
): Result[void, string] =
## Drops every content topic on the shard, then the direct subscription.
var snapshot: seq[ContentTopic]
self.shards.withValue(shard, sub):
snapshot = toSeq(sub.contentTopics)
for contentTopic in snapshot:
?self.unsubscribe(shard, contentTopic)
return self.unsubscribeShard(shard)
proc isSubscribed*(
self: SubscriptionManager, topic: ContentTopic
): Result[bool, string] =
let shard = ?self.getShardForContentTopic(topic)
return ok(self.isContentSubscribed(shard, topic))
proc subscribeAllAutoshards*(self: SubscriptionManager): Result[void, string] =
## Subscribes the relay to every shard in the configured autosharding cluster.
if self.node.wakuRelay.isNil() or self.node.wakuAutoSharding.isNone():
return ok()
let autoSharding = self.node.wakuAutoSharding.get()
let numShards = autoSharding.shardCountGenZero
if numShards == 0:
return ok()
for i in 0'u32 ..< numShards:
let shardObj = RelayShard(clusterId: autoSharding.clusterId, shardId: uint16(i))
self.subscribeShard(PubsubTopic($shardObj)).isOkOr:
error "failed to auto-subscribe relay to cluster shard",
shard = $shardObj, error = error
ok()
{.pop.}
const EdgeFilterSubscribeTimeout = chronos.seconds(15)
## Timeout for a single filter subscribe/unsubscribe RPC to a service peer.
const EdgeFilterPingTimeout = chronos.seconds(5)
## Timeout for a filter ping health check.
const EdgeFilterLoopInterval = chronos.seconds(30)
## Interval for the edge filter health ping loop.
const EdgeFilterSubLoopDebounce = chronos.seconds(1)
## Debounce delay to coalesce rapid-fire wakeups into a single reconciliation pass.
type EdgeDialTask = object
peer: RemotePeerInfo
shard: PubsubTopic
topics: seq[ContentTopic]
proc updateShardHealth(
self: SubscriptionManager, shard: PubsubTopic, state: var EdgeFilterSubState
) =
## Recompute and emit health for a shard after its peer set changed.
let newHealth = toTopicHealth(state.peers.len)
if newHealth != state.currentHealth:
state.currentHealth = newHealth
EventShardTopicHealthChange.emit(self.node.brokerCtx, shard, newHealth)
proc removePeer(self: SubscriptionManager, shard: PubsubTopic, peerId: PeerId) =
## Remove a peer from edgeFilterSubStates for the given shard,
## update health, and wake the sub loop to dial a replacement.
## Best-effort unsubscribe so the service peer stops pushing to us.
self.edgeFilterSubStates.withValue(shard, state):
var idx = -1
for i, p in state.peers:
if p.peerId == peerId:
idx = i
break
if idx < 0:
return
let peer = state.peers[idx]
state.peers.del(idx)
self.updateShardHealth(shard, state[])
self.edgeFilterWakeup.fire()
if not self.node.wakuFilterClient.isNil():
self.shards.withValue(shard, sub):
let ct = toSeq(sub.contentTopics)
if ct.len > 0:
proc doUnsubscribe() {.async.} =
discard await self.node.wakuFilterClient.unsubscribe(peer, shard, ct)
asyncSpawn doUnsubscribe()
type SendChunkedFilterRpcKind = enum
FilterSubscribe
FilterUnsubscribe
proc sendChunkedFilterRpc(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
topics: seq[ContentTopic],
kind: SendChunkedFilterRpcKind,
): Future[bool] {.async.} =
## Send a chunked filter subscribe or unsubscribe RPC. Returns true on
## success. On failure the peer is removed and false is returned.
try:
var i = 0
while i < topics.len:
let chunk =
topics[i ..< min(i + filter_protocol.MaxContentTopicsPerRequest, topics.len)]
let fut =
case kind
of FilterSubscribe:
self.node.wakuFilterClient.subscribe(peer, shard, chunk)
of FilterUnsubscribe:
self.node.wakuFilterClient.unsubscribe(peer, shard, chunk)
if not (await fut.withTimeout(EdgeFilterSubscribeTimeout)) or fut.read().isErr():
trace "sendChunkedFilterRpc: chunk failed",
op = kind, shard = shard, peer = peer.peerId
self.removePeer(shard, peer.peerId)
return false
i += filter_protocol.MaxContentTopicsPerRequest
except CatchableError as exc:
debug "sendChunkedFilterRpc: failed",
op = kind, shard = shard, peer = peer.peerId, err = exc.msg
self.removePeer(shard, peer.peerId)
return false
return true
proc syncFilterDeltas(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
added: seq[ContentTopic],
removed: seq[ContentTopic],
) {.async.} =
## Push content topic changes (adds/removes) to an already-tracked peer.
if added.len > 0:
if not await self.sendChunkedFilterRpc(peer, shard, added, FilterSubscribe):
return
if removed.len > 0:
discard await self.sendChunkedFilterRpc(peer, shard, removed, FilterUnsubscribe)
proc dialFilterPeer(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
contentTopics: seq[ContentTopic],
) {.async.} =
## Subscribe a new peer to all content topics on a shard and start tracking it.
self.edgeFilterSubStates.withValue(shard, state):
state.pendingPeers.incl(peer.peerId)
try:
if not await self.sendChunkedFilterRpc(peer, shard, contentTopics, FilterSubscribe):
return
self.edgeFilterSubStates.withValue(shard, state):
if state.peers.anyIt(it.peerId == peer.peerId):
trace "dialFilterPeer: peer already tracked, skipping duplicate",
shard = shard, peer = peer.peerId
return
state.peers.add(peer)
self.updateShardHealth(shard, state[])
trace "dialFilterPeer: successfully subscribed to all chunks",
shard = shard, peer = peer.peerId, totalPeers = state.peers.len
do:
trace "dialFilterPeer: shard removed while subscribing, discarding result",
shard = shard, peer = peer.peerId
finally:
self.edgeFilterSubStates.withValue(shard, state):
state.pendingPeers.excl(peer.peerId)
proc edgeFilterConnectionLoop(self: SubscriptionManager) {.async.} =
## Periodically pings all tracked filter service peers to verify they are
## still alive at the application layer. Peers that fail the ping are removed.
while true:
await sleepAsync(EdgeFilterLoopInterval)
if self.node.wakuFilterClient.isNil():
warn "filter client is nil within edge filter connection loop"
continue
var connected = initTable[PeerId, RemotePeerInfo]()
for state in self.edgeFilterSubStates.values:
for peer in state.peers:
if self.node.peerManager.switch.peerStore.isConnected(peer.peerId):
connected[peer.peerId] = peer
var alive = initHashSet[PeerId]()
if connected.len > 0:
var pingTasks: seq[(PeerId, Future[FilterSubscribeResult])]
for peer in connected.values:
pingTasks.add(
(peer.peerId, self.node.wakuFilterClient.ping(peer, EdgeFilterPingTimeout))
)
await allFutures(pingTasks.mapIt(it[1]))
for (peerId, task) in pingTasks:
if task.read().isOk():
alive.incl(peerId)
var changed = false
for shard, state in self.edgeFilterSubStates.mpairs:
let oldLen = state.peers.len
state.peers.keepItIf(it.peerId notin connected or alive.contains(it.peerId))
if state.peers.len < oldLen:
changed = true
self.updateShardHealth(shard, state)
trace "Edge Filter health degraded by Ping failure",
shard = shard, new = state.currentHealth
if changed:
self.edgeFilterWakeup.fire()
proc selectFilterCandidates(
self: SubscriptionManager, shard: PubsubTopic, exclude: HashSet[PeerId], needed: int
): seq[RemotePeerInfo] =
## Select filter service peer candidates for a shard.
# Start with every filter server peer that can serve the shard
var allCandidates = self.node.peerManager.selectPeers(
filter_common.WakuFilterSubscribeCodec, some(shard)
)
# Remove all already used in this shard or being dialed for it
allCandidates.keepItIf(it.peerId notin exclude)
# Collect peer IDs already tracked on other shards
var trackedOnOther = initHashSet[PeerId]()
for otherShard, otherState in self.edgeFilterSubStates.pairs:
if otherShard != shard:
for peer in otherState.peers:
trackedOnOther.incl(peer.peerId)
# Prefer peers we already have a connection to first, preserving shuffle
var candidates =
allCandidates.filterIt(it.peerId in trackedOnOther) &
allCandidates.filterIt(it.peerId notin trackedOnOther)
# We need to return 'needed' peers only
if candidates.len > needed:
candidates.setLen(needed)
return candidates
proc edgeFilterSubLoop(self: SubscriptionManager) {.async.} =
## Reconciles filter subscriptions with the desired state from SubscriptionManager.
var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]()
while true:
await self.edgeFilterWakeup.wait()
await sleepAsync(EdgeFilterSubLoopDebounce)
self.edgeFilterWakeup.clear()
trace "edgeFilterSubLoop: woke up"
if isNil(self.node.wakuFilterClient):
trace "edgeFilterSubLoop: wakuFilterClient is nil, skipping"
continue
var newSynced = initTable[PubsubTopic, HashSet[ContentTopic]]()
var allShards: HashSet[PubsubTopic]
for shard, sub in self.shards.pairs:
if sub.contentTopics.len > 0:
newSynced[shard] = sub.contentTopics
allShards.incl(shard)
for shard in lastSynced.keys:
allShards.incl(shard)
trace "edgeFilterSubLoop: desired state", numShards = newSynced.len
# Step 1: read state across all shards at once and
# create a list of peer dial tasks and shard tracking to delete.
var dialTasks: seq[EdgeDialTask]
var shardsToDelete: seq[PubsubTopic]
for shard in allShards:
# Compute added/removed deltas via direct iteration; no HashSet copies.
var addedTopics: seq[ContentTopic]
var removedTopics: seq[ContentTopic]
newSynced.withValue(shard, curr):
lastSynced.withValue(shard, prev):
for t in curr[]:
if t notin prev[]:
addedTopics.add(t)
for t in prev[]:
if t notin curr[]:
removedTopics.add(t)
do:
for t in curr[]:
addedTopics.add(t)
do:
lastSynced.withValue(shard, prev):
for t in prev[]:
removedTopics.add(t)
discard self.edgeFilterSubStates.mgetOrPut(
shard, EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY)
)
self.edgeFilterSubStates.withValue(shard, state):
state.peers.keepItIf(
self.node.peerManager.switch.peerStore.isConnected(it.peerId)
)
state.pending.keepItIf(not it.finished)
if addedTopics.len > 0 or removedTopics.len > 0:
for peer in state.peers:
asyncSpawn self.syncFilterDeltas(peer, shard, addedTopics, removedTopics)
if shard notin newSynced:
shardsToDelete.add(shard)
else:
self.updateShardHealth(shard, state[])
let needed = max(0, HealthyThreshold - state.peers.len - state.pending.len)
if needed > 0:
var tracked: HashSet[PeerId]
for p in state.peers:
tracked.incl(p.peerId)
for p in state.pendingPeers:
tracked.incl(p)
let candidates = self.selectFilterCandidates(shard, tracked, needed)
let toDial = min(needed, candidates.len)
trace "edgeFilterSubLoop: shard reconciliation",
shard = shard,
num_peers = state.peers.len,
num_pending = state.pending.len,
num_needed = needed,
num_available = candidates.len,
toDial = toDial
var dialTopics: seq[ContentTopic]
newSynced.withValue(shard, curr):
dialTopics = toSeq(curr[])
for i in 0 ..< toDial:
dialTasks.add(
EdgeDialTask(peer: candidates[i], shard: shard, topics: dialTopics)
)
# Step 2: execute deferred shard tracking deletion and dial tasks.
for shard in shardsToDelete:
self.edgeFilterSubStates.withValue(shard, state):
for fut in state.pending:
if not fut.finished:
await fut.cancelAndWait()
self.edgeFilterSubStates.del(shard)
for task in dialTasks:
let fut = self.dialFilterPeer(task.peer, task.shard, task.topics)
self.edgeFilterSubStates.withValue(task.shard, state):
state.pending.add(fut)
lastSynced = newSynced
proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] =
## Start the edge filter orchestration loops.
## Caller must ensure this is only called in edge mode (relay nil, filter client present).
self.peerEventListener = WakuPeerEvent.listen(
self.node.brokerCtx,
proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} =
if evt.kind == WakuPeerEventKind.EventDisconnected:
# We know a peer is gone, so if it was a service filter peer for this
# edge node, remove it from the list of service filter peers for each
# shard it served and re-evaluate shard health for the affected shards.
for shard, state in self.edgeFilterSubStates.mpairs:
let oldLen = state.peers.len
state.peers.keepItIf(it.peerId != evt.peerId)
if state.peers.len < oldLen:
self.updateShardHealth(shard, state)
self.edgeFilterWakeup.fire()
elif evt.kind == WakuPeerEventKind.EventMetadataUpdated:
self.edgeFilterWakeup.fire(),
).valueOr:
return err("Failed to listen to peer events for edge filter: " & error)
self.edgeFilterSubLoopFut = self.edgeFilterSubLoop()
self.edgeFilterConnectionLoopFut = self.edgeFilterConnectionLoop()
return ok()
proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} =
## Stop the edge filter orchestration loops and clean up pending futures.
if not isNil(self.edgeFilterSubLoopFut):
await self.edgeFilterSubLoopFut.cancelAndWait()
self.edgeFilterSubLoopFut = nil
if not isNil(self.edgeFilterConnectionLoopFut):
await self.edgeFilterConnectionLoopFut.cancelAndWait()
self.edgeFilterConnectionLoopFut = nil
for shard, state in self.edgeFilterSubStates:
for fut in state.pending:
if not fut.finished:
await fut.cancelAndWait()
await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener)
proc start*(self: SubscriptionManager): Result[void, string] =
let edgeShardHealthRes = RequestEdgeShardHealth.setProvider(
self.node.brokerCtx,
proc(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] =
self.edgeFilterSubStates.withValue(shard, state):
return ok(RequestEdgeShardHealth(health: state.currentHealth))
return ok(RequestEdgeShardHealth(health: TopicHealth.NOT_SUBSCRIBED)),
)
self.ownsEdgeShardHealthProvider = edgeShardHealthRes.isOk()
if edgeShardHealthRes.isErr():
error "Can't set provider for RequestEdgeShardHealth",
error = edgeShardHealthRes.error
let edgeFilterPeerCountRes = RequestEdgeFilterPeerCount.setProvider(
self.node.brokerCtx,
proc(): Result[RequestEdgeFilterPeerCount, string] =
var minPeers = high(int)
for state in self.edgeFilterSubStates.values:
minPeers = min(minPeers, state.peers.len)
if minPeers == high(int):
minPeers = 0
return ok(RequestEdgeFilterPeerCount(peerCount: minPeers)),
)
self.ownsEdgeFilterPeerCountProvider = edgeFilterPeerCountRes.isOk()
if edgeFilterPeerCountRes.isErr():
error "Can't set provider for RequestEdgeFilterPeerCount",
error = edgeFilterPeerCountRes.error
# Start Edge workers only when we are in Edge mode (relay not mounted)
# AND the filter client is mounted (otherwise the loops have nothing
# to talk to and just spam "filter client is nil" warnings).
if self.node.wakuRelay.isNil() and not self.node.wakuFilterClient.isNil():
return self.startEdgeFilterLoops()
return ok()
proc stop*(self: SubscriptionManager) {.async: (raises: []).} =
# Stop Edge workers if we started them in `start` (Edge mode + filter client).
if self.node.wakuRelay.isNil() and not self.node.wakuFilterClient.isNil():
await self.stopEdgeFilterLoops()
# Only clear providers we actually registered: another SubscriptionManager
# sharing this brokerCtx may have won the race, and clearing its provider
# would leave the broker silently provider-less.
if self.ownsEdgeShardHealthProvider:
RequestEdgeShardHealth.clearProvider(self.node.brokerCtx)
self.ownsEdgeShardHealthProvider = false
if self.ownsEdgeFilterPeerCountProvider:
RequestEdgeFilterPeerCount.clearProvider(self.node.brokerCtx)
self.ownsEdgeFilterPeerCountProvider = false

View File

@ -4,6 +4,7 @@ import chronicles, chronos, metrics, metrics/chronos_httpserver
import
waku/[net/auto_port, waku_rln_relay/protocol_metrics as rln_metrics, utils/collector],
./peer_manager,
./node_telemetry,
./waku_node
const LogInterval = 10.minutes

View File

@ -60,23 +60,14 @@ import
requests/health_requests,
events/health_events,
events/message_events,
events/peer_events,
],
waku/discovery/waku_kademlia,
waku/net/[bound_ports, net_config],
./peer_manager,
./health_monitor/health_status,
./health_monitor/topic_health
declarePublicCounter waku_node_messages, "number of messages received", ["type"]
declarePublicGauge waku_version,
"Waku version info (in git describe format)", ["version"]
declarePublicCounter waku_node_errors, "number of wakunode errors", ["type"]
declarePublicGauge waku_lightpush_peers, "number of lightpush peers"
declarePublicGauge waku_filter_peers, "number of filter peers"
declarePublicGauge waku_store_peers, "number of store peers"
declarePublicGauge waku_px_peers,
"number of peers (in the node's peerManager) supporting the peer exchange protocol"
./health_monitor/topic_health,
./node_telemetry
logScope:
topics = "waku node"
@ -94,53 +85,10 @@ const clientId* = "Nimbus Waku v2 node"
const WakuNodeVersionString* = "version / git commit hash: " & git_version
# key and crypto modules different
type
# TODO: Move to application instance (e.g., `WakuNode2`)
WakuInfo* = object # NOTE One for simplicity, can extend later as needed
listenAddresses*: seq[string]
enrUri*: string #multiaddrStrings*: seq[string]
mixPubKey*: Option[string]
import ./node_types
export node_types
# NOTE based on Eth2Node in NBC eth2_network.nim
WakuNode* = ref object
peerManager*: PeerManager
switch*: Switch
wakuRelay*: WakuRelay
wakuArchive*: waku_archive.WakuArchive
wakuStore*: store.WakuStore
wakuStoreClient*: store_client.WakuStoreClient
wakuStoreResume*: StoreResume
wakuStoreReconciliation*: SyncReconciliation
wakuStoreTransfer*: SyncTransfer
wakuFilter*: waku_filter_v2.WakuFilter
wakuFilterClient*: filter_client.WakuFilterClient
wakuRlnRelay*: WakuRLNRelay
wakuLegacyLightPush*: WakuLegacyLightPush
wakuLegacyLightpushClient*: WakuLegacyLightPushClient
wakuLightPush*: WakuLightPush
wakuLightpushClient*: WakuLightPushClient
wakuPeerExchange*: WakuPeerExchange
wakuPeerExchangeClient*: WakuPeerExchangeClient
wakuMetadata*: WakuMetadata
wakuAutoSharding*: Option[Sharding]
enr*: enr.Record
libp2pPing*: Ping
rng*: ref rand.HmacDrbgContext
brokerCtx*: BrokerContext
wakuRendezvous*: WakuRendezVous
wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient
announcedAddresses*: seq[MultiAddress]
extMultiAddrsOnly*: bool # When true, skip automatic IP address replacement
started*: bool # Indicates that node has started listening
topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent]
rateLimitSettings*: ProtocolRateLimitSettings
legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler]
## Kernel API Relay appHandlers (if any)
wakuMix*: WakuMix
kademliaDiscoveryLoop*: Future[void]
wakuKademlia*: WakuKademlia
ports*: BoundPorts
import ./subscription_manager
proc deduceRelayShard(
node: WakuNode,
@ -230,6 +178,8 @@ proc new*(
peerManager.setShardGetter(node.getShardsGetter(@[]))
node.subscriptionManager = SubscriptionManager.new(node)
return node
proc peerInfo*(node: WakuNode): PeerInfo =
@ -600,6 +550,9 @@ proc start*(node: WakuNode) {.async.} =
node.startProvidersAndListeners()
node.subscriptionManager.start().isOkOr:
error "failed to start subscription manager", error = error
if not zeroPortPresent:
updateAnnouncedAddrWithPrimaryIpAddr(node).isOkOr:
error "failed update announced addr", error = $error
@ -611,6 +564,8 @@ proc start*(node: WakuNode) {.async.} =
proc stop*(node: WakuNode) {.async.} =
## By stopping the switch we are stopping all the underlying mounted protocols
await node.subscriptionManager.stop()
node.stopProvidersAndListeners()
## NOTE: This will dispatch gossipsub stop to the WakuRelay.stop method override

View File

@ -38,14 +38,14 @@ RequestBroker:
proc signature(protocol: WakuProtocol): Future[Result[RequestProtocolHealth, string]]
# Get edge filter health for a single shard (set by DeliveryService when edge mode is active)
# Get edge filter health for a single shard (set when edge mode is active)
RequestBroker(sync):
type RequestEdgeShardHealth* = object
health*: TopicHealth
proc signature(shard: PubsubTopic): Result[RequestEdgeShardHealth, string]
# Get edge filter confirmed peer count (set by DeliveryService when edge mode is active)
# Get edge filter confirmed peer count (set when edge mode is active)
RequestBroker(sync):
type RequestEdgeFilterPeerCount* = object
peerCount*: int