mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-08-09 21:33:25 +00:00
* Reshape per-layer API into api/ folders and thin the FFI over them
Each layer now separates its constructible core from its public surface:
- core module (waku.nim / messaging_client.nim /
reliable_channel_manager.nim): the type plus new/start/stop and the
private construction helpers.
- api/ folder: one module per differentiated set of operations
(waku: topics/relay/filter/lightpush/store/peer_manager/discovery/
debug/health) plus an events surface.
The waku api is reshaped to be the complete operation surface the C
bindings need, so the library no longer reaches into node internals:
relayPublish returns the message hash, relaySubscribe takes an optional
handler, filter/lightpush auto-select the service peer, connectedPeersInfo
returns structured data, pingPeer honours the timeout, plus
relayNumPeersInMesh / relayNumConnectedPeers / isOnline. library/ is now a
thin C-ABI shim: each {.ffi.} proc only marshals cstring/JSON/callbacks and
delegates to ctx.myLib[].waku.<op> (or messagingClient.<op>).
app_callbacks re-exports the modules defining its handler types, which the
included FFI files previously relied on by leakage.
Events move next to the surface that owns them, with each dependency kept
pointing the right way:
- waku/events/ relocated under waku/api/events/.
- channel events live in channels/api/events.nim.
- the four messaging-level message events move to messaging/api/events;
MessageSeenEvent stays in waku because it is emitted by waku core, so
moving it would make waku depend on the messaging layer.
- delivery_events renamed to filter_subscribe_events to match the
OnFilterSubscribe/Unsubscribe events it actually declares.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Add reliable-channel FFI ops + events (nim-ffi v0.1.3)
Expose the reliable-channel layer through the v0.1.3 FFI:
- channel_create / channel_send / channel_close call the
ReliableChannelManager api (createReliableChannel / send / closeChannel),
marshalling channel id + base64 payload + ephemeral by hand
- channel message received / sent / errored are surfaced by listening to the
channel-layer broker events in start_node and forwarding them through
callEventCallback (received payload base64-encoded), dropped in stop_node
Stays on nim-ffi v0.1.3 (no typed/CBOR rewrite).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Expose reliable-channel ops in the stable C header (#3851)
The library already ships as a single .so with a tiered header surface
(liblogosdelivery.h = stable Messaging/Reliable-Channels, liblogosdelivery_kernel.h
= advanced Kernel). Per that tiering, the reliable-channel ops belong on the
stable surface, so declare channel_create / channel_send / channel_close in
liblogosdelivery.h and document the channel lifecycle events delivered through
the event callback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Graft PR#3975 interface layer onto decomposed foundation (events deduped)
Add IKernel/IMessagingClient/IReliableChannelManager/ILogosDelivery interface
classes under logos_delivery/api/. The EventBroker types PR#3975 hoisted into
these files already exist in PR#3989's decomposed */api/events/ modules, so the
interface files re-export those modules instead of redefining the types
(avoids 8 duplicate EventBroker definitions). api/types.nim kept at the
foundation version (ChannelId stays in channels/types.nim, which the decomposed
modules import).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Wire impl classes to interfaces (inherit; relocate SendHandler)
- Waku : IKernel, MessagingClient : IMessagingClient,
ReliableChannelManager : IReliableChannelManager.
- The operation procs already live in PR#3989's decomposed */api/ modules and
stay as plain procs (nothing dispatches through the interface types, so no
method-ization is needed).
- SendHandler now lives in reliable_channel_manager_api.nim (its PR#3975 home);
removed the duplicate from reliable_channel.nim, which re-exports the
interface module so channels/api/{channel_lifecycle,send} still see it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Wire LogosDelivery to ILogosDelivery orchestrator interface
LogosDelivery : ILogosDelivery; start/stop/isOnline become method overrides.
Peripheral PR#3975 edits (lightpush/store clients, self_req_handlers,
statistics) are import-reorg artifacts of deleting waku/utils/requests.nim,
which the decomposed structure keeps -- so they are intentionally not ported.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Dedup EventConnectionStatusChange (re-export from health_events)
9th duplicate EventBroker type: defined in both logos_delivery_api.nim and the
decomposed waku/api/events/health_events.nim. The interface file now re-exports
it. liblogosdelivery builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Move events back into interface-class source files (restore #3975 placement)
Reverses the earlier dedup-by-re-export: event TYPE definitions now live in the
interface classes, and the emptied decomposed event files are removed.
- MessageSeenEvent -> logos_delivery/api/kernel_api.nim
- Message{Sent,Error,Propagated,Received}Event -> api/messaging_client_api.nim
- ChannelMessage{Received,Sent,Error}Event -> api/reliable_channel_manager_api.nim
- EventConnectionStatusChange -> api/logos_delivery_api.nim
Deleted (became empty after the move):
- logos_delivery/waku/api/events/message_events.nim
- logos_delivery/messaging/api/events.nim
- logos_delivery/channels/api/events.nim
health_events.nim keeps its two remaining events (content/shard topic health).
Rewiring: each layer re-exports its interface module (waku->kernel_api,
messaging_client->messaging_client_api, reliable_channel->reliable_channel_manager_api,
which also re-exports messaging_client_api). Deep emitters/listeners
(subscription_manager, waku_node, waku_node/relay, node_health_monitor,
recv_service, send_service) import the owning interface module directly.
kernel_api stays below node level (types/topics/message/store-common) so the
node->kernel_api imports are acyclic. liblogosdelivery builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* nph formatting
---------
Co-authored-by: Ivan FB <ivansete@status.im>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1172 lines
42 KiB
Nim
1172 lines
42 KiB
Nim
{.used.}
|
|
|
|
import std/[net, options, os]
|
|
from std/times import epochTime
|
|
import chronos, testutils/unittests, stew/byteutils
|
|
import brokers/broker_context
|
|
|
|
import ../testlib/[common, wakucore, wakunode, testasync]
|
|
|
|
import logos_delivery
|
|
import logos_delivery/waku/[waku_node, waku_core]
|
|
import logos_delivery/waku/factory/waku_conf
|
|
import logos_delivery/api/messaging_client_api as waku_message_events
|
|
import tools/confutils/cli_args
|
|
|
|
import logos_delivery/channels/reliable_channel_manager
|
|
import logos_delivery/channels/encryption/noop_encryption
|
|
import logos_delivery/waku/persistency/keys
|
|
import logos_delivery/waku/persistency/sds_persistency
|
|
|
|
## Full nim-sds API: ingress tests act as the remote peer producing real
|
|
## SDS envelopes; protocol-semantics tests decode wires and meta snapshots.
|
|
import sds
|
|
import snapshot_codec
|
|
|
|
const TestTimeout = chronos.seconds(15)
|
|
|
|
proc createApiNodeConf(): WakuNodeConf =
|
|
var conf = defaultWakuNodeConf().valueOr:
|
|
raiseAssert error
|
|
conf.mode = cli_args.WakuMode.Core
|
|
conf.listenAddress = parseIpAddress("0.0.0.0")
|
|
conf.tcpPort = Port(0)
|
|
conf.discv5UdpPort = Port(0)
|
|
conf.clusterId = some(3'u16)
|
|
conf.numShardsInNetwork = 1
|
|
conf.reliabilityEnabled = some(true)
|
|
conf.rest = false
|
|
return conf
|
|
|
|
suite "Reliable Channel - ingress":
|
|
asyncTest "manager dispatches marked WakuMessage to the right channel":
|
|
## 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 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`
|
|
## - emit `ChannelMessageReceivedEvent` with the payload
|
|
const
|
|
channelId = ChannelId("test-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/proto")
|
|
let appPayload = "hello reliable channel".toBytes()
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
## Noop encryption providers so the Encrypt/Decrypt brokers have
|
|
## something to dispatch to; without this the channel falls back to
|
|
## plaintext anyway, but installing them is the documented setup.
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
let received = newFuture[seq[byte]]("channel-message-received")
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if not received.finished() and evt.channelId == channelId:
|
|
received.complete(evt.payload)
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
## Build a `WakuMessage` as it would arrive off the wire: spec marker
|
|
## on `meta`, right content topic, payload wrapped in a real SDS
|
|
## envelope by a stand-in remote peer.
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
let sdsWire = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
appPayload, "ingress-test-msg-1", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrapOutgoingMessage")
|
|
|
|
let inboundMsg = WakuMessage(
|
|
payload: sdsWire,
|
|
contentTopic: contentTopic,
|
|
version: 0,
|
|
meta: LipWireReliableChannelVersion.toBytes(),
|
|
)
|
|
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(messageHash: "", message: inboundMsg),
|
|
)
|
|
|
|
let arrived = await received.withTimeout(TestTimeout)
|
|
check arrived
|
|
if arrived:
|
|
check received.read() == appPayload
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "manager drops unmarked WakuMessage":
|
|
## Mirror of the above: same content topic, but `meta` is empty
|
|
## (i.e. foreign traffic). The channel-level event must NOT fire.
|
|
const
|
|
channelId = ChannelId("test-channel-2")
|
|
contentTopic = ContentTopic("/reliable-channel/test/proto")
|
|
let appPayload = "foreign payload".toBytes()
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
var fired = false
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
fired = true
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
let inboundMsg = WakuMessage(
|
|
payload: appPayload,
|
|
contentTopic: contentTopic,
|
|
version: 0,
|
|
meta: @[], ## no Reliable Channel spec marker
|
|
)
|
|
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(messageHash: "", message: inboundMsg),
|
|
)
|
|
|
|
## Give the event broker a chance to fan out.
|
|
await sleepAsync(100.milliseconds)
|
|
check not fired
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
suite "Reliable Channel - send state machine":
|
|
asyncTest "MessageSentEvent finalises the channelReqId as Sent":
|
|
## Drives the real send pipeline (`send` -> segmentation -> SDS ->
|
|
## rate_limit -> encrypt -> dispatch) via a fake `SendHandler` that
|
|
## returns a canned `RequestId` instead of hitting the network.
|
|
## Emitting the delivery-layer `MessageSentEvent` must drive the
|
|
## channel-level state machine through `Confirmed` and produce a
|
|
## `ChannelMessageSentEvent` (channel-level terminal event) for the
|
|
## `channelReqId` returned by `send()`.
|
|
const
|
|
channelId = ChannelId("sm-success-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/sm-success")
|
|
fakeMsgReqId = RequestId("fake-msg-req-1")
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
var sendCalls = 0
|
|
let fakeSend: SendHandler = proc(
|
|
env: MessageEnvelope
|
|
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
|
|
sendCalls.inc
|
|
return ok(fakeMsgReqId)
|
|
|
|
discard manager
|
|
.createReliableChannel(
|
|
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
|
|
)
|
|
.expect("createReliableChannel")
|
|
|
|
let sentFut = newFuture[RequestId]("channel-sent")
|
|
discard ChannelMessageSentEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageSentEvent) {.async: (raises: []).} =
|
|
if not sentFut.finished() and evt.channelId == channelId:
|
|
sentFut.complete(evt.requestId)
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageSentEvent")
|
|
|
|
let channelReqId = (await manager.send(channelId, "hello".toBytes())).expect("send")
|
|
|
|
let dispatchDeadline = Moment.now() + 1.seconds
|
|
while Moment.now() < dispatchDeadline and sendCalls == 0:
|
|
await sleepAsync(5.milliseconds)
|
|
check sendCalls == 1
|
|
|
|
waku_message_events.MessageSentEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageSentEvent(requestId: fakeMsgReqId, messageHash: ""),
|
|
)
|
|
|
|
let finalised = await sentFut.withTimeout(1.seconds)
|
|
check finalised
|
|
if finalised:
|
|
check sentFut.read() == channelReqId
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "two independent channelReqIds are finalised independently":
|
|
## Two `send()` calls -> two independent `channelReqId`s, each with
|
|
## one segment under the current segmentation skeleton
|
|
## (`performSegmentation` always emits exactly one segment). The
|
|
## fake `SendHandler` returns distinct `messagingReqId`s; finalising
|
|
## the first emits `ChannelMessageSentEvent` for its `channelReqId`,
|
|
## finalising the second as a failure emits `ChannelMessageErrorEvent`
|
|
## for the other.
|
|
const
|
|
channelId = ChannelId("sm-multi-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/sm-multi")
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
var msgReqIds: seq[RequestId]
|
|
let fakeSend: SendHandler = proc(
|
|
env: MessageEnvelope
|
|
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
|
|
let id = RequestId("fake-msg-req-" & $(msgReqIds.len + 1))
|
|
msgReqIds.add(id)
|
|
return ok(id)
|
|
|
|
discard manager
|
|
.createReliableChannel(
|
|
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
|
|
)
|
|
.expect("createReliableChannel")
|
|
|
|
let sentFut = newFuture[RequestId]("channel-sent")
|
|
let erroredFut = newFuture[RequestId]("channel-errored")
|
|
discard ChannelMessageSentEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageSentEvent) {.async: (raises: []).} =
|
|
if not sentFut.finished() and evt.channelId == channelId:
|
|
sentFut.complete(evt.requestId)
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageSentEvent")
|
|
discard ChannelMessageErrorEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageErrorEvent) {.async: (raises: []).} =
|
|
if not erroredFut.finished() and evt.channelId == channelId:
|
|
erroredFut.complete(evt.requestId)
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageErrorEvent")
|
|
|
|
let channelReqId1 =
|
|
(await manager.send(channelId, "first".toBytes())).expect("send 1")
|
|
let channelReqId2 =
|
|
(await manager.send(channelId, "second".toBytes())).expect("send 2")
|
|
|
|
let dispatchDeadline = Moment.now() + 1.seconds
|
|
while Moment.now() < dispatchDeadline and msgReqIds.len < 2:
|
|
await sleepAsync(5.milliseconds)
|
|
check msgReqIds.len == 2
|
|
|
|
waku_message_events.MessageSentEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageSentEvent(requestId: msgReqIds[0], messageHash: ""),
|
|
)
|
|
let sentArrived = await sentFut.withTimeout(1.seconds)
|
|
check sentArrived
|
|
if sentArrived:
|
|
check sentFut.read() == channelReqId1
|
|
## The second `channelReqId` must NOT have finalised yet — its
|
|
## segment is still `InFlight`.
|
|
check not erroredFut.finished()
|
|
|
|
waku_message_events.MessageErrorEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageErrorEvent(
|
|
requestId: msgReqIds[1], messageHash: "", error: "synthetic"
|
|
),
|
|
)
|
|
let erroredArrived = await erroredFut.withTimeout(1.seconds)
|
|
check erroredArrived
|
|
if erroredArrived:
|
|
check erroredFut.read() == channelReqId2
|
|
|
|
(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
|
|
## `performSegmentation` (segmentation skeleton) always emits
|
|
## exactly one segment per `send()`, so multiple siblings under one
|
|
## `channelReqId` cannot be produced through the real pipeline.
|
|
## Implement once segmentation does real chunking: send a payload
|
|
## larger than `DefaultSegmentSizeBytes`, capture the N
|
|
## `messagingReqId`s from a fake `SendHandler`, finalise some, and
|
|
## assert prune only fires once every sibling is final.
|
|
skip()
|
|
|
|
asyncTest "sibling MessageSentEvent during sendHandler await does not corrupt state":
|
|
## Regression test for the prune-during-await race
|
|
## (PR #3914 review comment r3324891059). Locks in that a sibling
|
|
## `MessageSentEvent` firing while `onReadyToSend` is paused at an
|
|
## `await` does not lose the second `channelReqId`'s terminal
|
|
## event.
|
|
const
|
|
channelId = ChannelId("sm-race-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/sm-race")
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
var msgReqIds: seq[RequestId]
|
|
var sendsReturned = 0
|
|
let fakeSend: SendHandler = proc(
|
|
env: MessageEnvelope
|
|
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
|
|
## Call 2 fires the first segment's terminal event and then
|
|
## yields, so the listener task runs while the second segment
|
|
## is still mid-`await` in `onReadyToSend` — the exact race
|
|
## window the regression test targets.
|
|
let id = RequestId("race-msg-req-" & $(msgReqIds.len + 1))
|
|
msgReqIds.add(id)
|
|
if msgReqIds.len == 2:
|
|
waku_message_events.MessageSentEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageSentEvent(requestId: msgReqIds[0], messageHash: ""),
|
|
)
|
|
await sleepAsync(50.milliseconds)
|
|
sendsReturned.inc()
|
|
return ok(id)
|
|
|
|
discard manager
|
|
.createReliableChannel(
|
|
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
|
|
)
|
|
.expect("createReliableChannel")
|
|
|
|
var finalisedReqIds: seq[RequestId]
|
|
let bothFinalised = newFuture[void]("both-finalised")
|
|
discard ChannelMessageSentEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageSentEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
finalisedReqIds.add(evt.requestId)
|
|
if finalisedReqIds.len == 2 and not bothFinalised.finished():
|
|
bothFinalised.complete()
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageSentEvent")
|
|
|
|
let channelReqId1 =
|
|
(await manager.send(channelId, "first".toBytes())).expect("send 1")
|
|
|
|
## Drain the first segment fully before queueing the second, so
|
|
## the rate-limit FIFO between sibling sends isn't itself under
|
|
## test here.
|
|
let firstDispatched = Moment.now() + 1.seconds
|
|
while Moment.now() < firstDispatched and msgReqIds.len < 1:
|
|
await sleepAsync(5.milliseconds)
|
|
check msgReqIds.len == 1
|
|
|
|
let channelReqId2 =
|
|
(await manager.send(channelId, "second".toBytes())).expect("send 2")
|
|
|
|
## Wait until `fakeSend(m2)` has fully returned and yield once
|
|
## more so `onReadyToSend`'s post-await continuation gets a chance
|
|
## to register `id2` in `inflightMessagingIds` before we emit its
|
|
## terminal event.
|
|
let dispatchDeadline = Moment.now() + 1.seconds
|
|
while Moment.now() < dispatchDeadline and sendsReturned < 2:
|
|
await sleepAsync(5.milliseconds)
|
|
check sendsReturned == 2
|
|
await sleepAsync(50.milliseconds)
|
|
|
|
## Finalise the second segment from the outside. If the race
|
|
## corrupted state, `channelReqId2`'s entry would never reach
|
|
## `inflightMessagingIds` and this event would silently miss.
|
|
waku_message_events.MessageSentEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageSentEvent(requestId: msgReqIds[1], messageHash: ""),
|
|
)
|
|
|
|
let arrived = await bothFinalised.withTimeout(2.seconds)
|
|
check arrived
|
|
if arrived:
|
|
check finalisedReqIds.len == 2
|
|
check channelReqId1 in finalisedReqIds
|
|
check channelReqId2 in finalisedReqIds
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
suite "Reliable Channel - SDS persistence":
|
|
asyncTest "send persists SDS channel state through the persistency job":
|
|
## A send must flush `sds.meta` + `sds.log` through the shared "sds"
|
|
## job. Writes resolve on enqueue, so reads poll.
|
|
const
|
|
channelId = ChannelId("sds-persist-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/persist")
|
|
|
|
Persistency.reset()
|
|
let root = getTempDir() / ("reliable_channel_sds_" & $epochTime().int)
|
|
removeDir(root)
|
|
let persistency = Persistency.instance(root).expect("persistency init")
|
|
defer:
|
|
Persistency.reset()
|
|
removeDir(root)
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
lockNewGlobalBrokerContext:
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
let fakeSend: SendHandler = proc(
|
|
env: MessageEnvelope
|
|
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
|
|
return ok(RequestId("persist-msg-req-1"))
|
|
|
|
discard manager
|
|
.createReliableChannel(
|
|
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
|
|
)
|
|
.expect("createReliableChannel")
|
|
|
|
discard (await manager.send(channelId, "persist me".toBytes())).expect("send")
|
|
|
|
## Same handle the channel layer writes through (`openJob` is idempotent).
|
|
let job = persistency.openJob("sds").expect("openJob sds")
|
|
let chanKey = toKey(SdsChannelID(channelId))
|
|
|
|
proc pollMetaExists(): Future[bool] {.async.} =
|
|
let deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline:
|
|
let r = await job.exists(CatMeta, chanKey)
|
|
if r.isOk() and r.get():
|
|
return true
|
|
await sleepAsync(5.milliseconds)
|
|
return false
|
|
|
|
proc pollLogRow(): Future[bool] {.async.} =
|
|
## `sds.log` keys rows by (channelId, messageId) — scan the prefix.
|
|
let deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline:
|
|
let r = await job.scanPrefix(CatLog, chanKey)
|
|
if r.isOk() and r.get().len > 0:
|
|
return true
|
|
await sleepAsync(5.milliseconds)
|
|
return false
|
|
|
|
check await pollMetaExists()
|
|
check await pollLogRow()
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
## A marked WakuMessage carrying an SDS envelope, as it arrives off the wire.
|
|
proc sdsWakuMessage(contentTopic: ContentTopic, sdsWire: seq[byte]): WakuMessage =
|
|
WakuMessage(
|
|
payload: sdsWire,
|
|
contentTopic: contentTopic,
|
|
version: 0,
|
|
meta: LipWireReliableChannelVersion.toBytes(),
|
|
)
|
|
|
|
suite "Reliable Channel - SDS lifecycle":
|
|
asyncTest "out-of-order segments are parked and delivered in causal order":
|
|
## m2 depends on m1: m2 alone delivers nothing; m1 then delivers both,
|
|
## in causal order.
|
|
const
|
|
channelId = ChannelId("sds-causal-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/causal")
|
|
let payload1 = "first message".toBytes()
|
|
let payload2 = "second message".toBytes()
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
var deliveries: seq[seq[byte]]
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
deliveries.add(evt.payload)
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
let wire1 = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
payload1, "causal-m1", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap m1")
|
|
let wire2 = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
payload2, "causal-m2", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap m2")
|
|
|
|
## m2 first: missing dependency m1 -> parked, nothing delivered.
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "hash-m2", message: sdsWakuMessage(contentTopic, wire2)
|
|
),
|
|
)
|
|
await sleepAsync(100.milliseconds)
|
|
check deliveries.len == 0
|
|
|
|
## m1 arrives: m1 delivered, then the parked m2 released after it.
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "hash-m1", message: sdsWakuMessage(contentTopic, wire1)
|
|
),
|
|
)
|
|
let deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and deliveries.len < 2:
|
|
await sleepAsync(5.milliseconds)
|
|
check deliveries.len == 2
|
|
if deliveries.len == 2:
|
|
check deliveries[0] == payload1
|
|
check deliveries[1] == payload2
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "duplicate SDS envelope is delivered to the app only once":
|
|
const
|
|
channelId = ChannelId("sds-dup-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/dup")
|
|
let appPayload = "deliver once".toBytes()
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
var deliveryCount = 0
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
deliveryCount.inc()
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
let wire = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
appPayload, "dup-m1", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap")
|
|
|
|
## Same envelope twice (different hashes) — the second must be suppressed.
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "dup-hash-1", message: sdsWakuMessage(contentTopic, wire)
|
|
),
|
|
)
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "dup-hash-2", message: sdsWakuMessage(contentTopic, wire)
|
|
),
|
|
)
|
|
await sleepAsync(200.milliseconds)
|
|
check deliveryCount == 1
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "SDS envelope for a foreign channel is dropped":
|
|
## Same content topic, different SDS channel id — dropped before unwrap.
|
|
const
|
|
channelId = ChannelId("sds-foreign-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/foreign")
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
var fired = false
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
fired = true
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
let wire = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
"not for you".toBytes(), "foreign-m1", SdsChannelID("some-other-channel")
|
|
)
|
|
).expect("wrap")
|
|
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "foreign-hash", message: sdsWakuMessage(contentTopic, wire)
|
|
),
|
|
)
|
|
await sleepAsync(200.milliseconds)
|
|
check not fired
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "received history survives channel close and re-create":
|
|
## Receive m1, close, re-create, replay m1: the duplicate is only
|
|
## suppressed if the history was actually restored from SQLite.
|
|
const
|
|
channelId = ChannelId("sds-restore-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/restore")
|
|
let appPayload = "survive restart".toBytes()
|
|
|
|
Persistency.reset()
|
|
let root = getTempDir() / ("reliable_channel_sds_restore_" & $epochTime().int)
|
|
removeDir(root)
|
|
let persistency = Persistency.instance(root).expect("persistency init")
|
|
defer:
|
|
Persistency.reset()
|
|
removeDir(root)
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
var deliveryCount = 0
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
deliveryCount.inc()
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
let wire = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
appPayload, "restore-m1", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap")
|
|
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "restore-hash-1", message: sdsWakuMessage(contentTopic, wire)
|
|
),
|
|
)
|
|
var deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and deliveryCount < 1:
|
|
await sleepAsync(5.milliseconds)
|
|
check deliveryCount == 1
|
|
|
|
## Writes resolve on enqueue — wait until the row is applied before closing.
|
|
let job = persistency.openJob("sds").expect("openJob sds")
|
|
let chanKey = toKey(SdsChannelID(channelId))
|
|
deadline = Moment.now() + 2.seconds
|
|
var logVisible = false
|
|
while Moment.now() < deadline and not logVisible:
|
|
let r = await job.scanPrefix(CatLog, chanKey)
|
|
logVisible = r.isOk() and r.get().len > 0
|
|
if not logVisible:
|
|
await sleepAsync(5.milliseconds)
|
|
check logVisible
|
|
|
|
(await manager.closeChannel(channelId)).expect("closeChannel")
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("re-createReliableChannel")
|
|
|
|
## Replay the same envelope. Only a restored history suppresses it.
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "restore-hash-2", message: sdsWakuMessage(contentTopic, wire)
|
|
),
|
|
)
|
|
await sleepAsync(300.milliseconds)
|
|
check deliveryCount == 1
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
suite "Reliable Channel - SDS protocol semantics":
|
|
asyncTest "a reply references the received message and advances the lamport clock":
|
|
## After receiving m1, our outgoing wire must reference m1 in its causal
|
|
## history (that reference IS the ack) with a higher lamport.
|
|
const
|
|
channelId = ChannelId("sds-semantics-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/semantics")
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
var capturedWires: seq[seq[byte]]
|
|
let fakeSend: SendHandler = proc(
|
|
env: MessageEnvelope
|
|
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
|
|
## Noop encryption is identity, so the envelope payload IS the SDS wire.
|
|
capturedWires.add(env.payload)
|
|
return ok(RequestId("semantics-req-" & $capturedWires.len))
|
|
|
|
discard manager
|
|
.createReliableChannel(
|
|
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
|
|
)
|
|
.expect("createReliableChannel")
|
|
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
let wire1 = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
"from remote".toBytes(), "semantics-m1", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap m1")
|
|
let m1 = deserializeMessage(wire1).expect("deserialize m1")
|
|
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "semantics-hash-1", message: sdsWakuMessage(contentTopic, wire1)
|
|
),
|
|
)
|
|
await sleepAsync(100.milliseconds)
|
|
|
|
discard (await manager.send(channelId, "reply".toBytes())).expect("send")
|
|
var deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and capturedWires.len < 1:
|
|
await sleepAsync(5.milliseconds)
|
|
check capturedWires.len == 1
|
|
|
|
let reply = deserializeMessage(capturedWires[0]).expect("deserialize reply")
|
|
check SdsMessageID("semantics-m1") in reply.causalHistory.getMessageIds()
|
|
check reply.lamportTimestamp > m1.lamportTimestamp
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "an unacknowledged send is acked by a later remote message":
|
|
## Our send sits in the outgoing buffer (visible in the persisted meta)
|
|
## until any later remote message references it — then the buffer drains.
|
|
const
|
|
channelId = ChannelId("sds-ack-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/ack")
|
|
|
|
Persistency.reset()
|
|
let root = getTempDir() / ("reliable_channel_sds_ack_" & $epochTime().int)
|
|
removeDir(root)
|
|
let persistency = Persistency.instance(root).expect("persistency init")
|
|
defer:
|
|
Persistency.reset()
|
|
removeDir(root)
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
var capturedWires: seq[seq[byte]]
|
|
let fakeSend: SendHandler = proc(
|
|
env: MessageEnvelope
|
|
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
|
|
capturedWires.add(env.payload)
|
|
return ok(RequestId("ack-req-" & $capturedWires.len))
|
|
|
|
discard manager
|
|
.createReliableChannel(
|
|
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
|
|
)
|
|
.expect("createReliableChannel")
|
|
|
|
discard (await manager.send(channelId, "needs ack".toBytes())).expect("send")
|
|
var deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and capturedWires.len < 1:
|
|
await sleepAsync(5.milliseconds)
|
|
check capturedWires.len == 1
|
|
|
|
let job = persistency.openJob("sds").expect("openJob sds")
|
|
let chanKey = toKey(SdsChannelID(channelId))
|
|
|
|
proc outgoingBufferLen(): Future[int] {.async.} =
|
|
## Decode the persisted meta snapshot; -1 while not yet readable.
|
|
let r = await job.get(CatMeta, chanKey)
|
|
if r.isErr() or r.get().isNone():
|
|
return -1
|
|
let meta = ChannelMeta.decode(r.get().get()).valueOr:
|
|
return -1
|
|
return meta.outgoingBuffer.len
|
|
|
|
## After the send the message must sit unacknowledged in the buffer.
|
|
deadline = Moment.now() + 2.seconds
|
|
var bufLen = -1
|
|
while Moment.now() < deadline and bufLen != 1:
|
|
bufLen = await outgoingBufferLen()
|
|
if bufLen != 1:
|
|
await sleepAsync(5.milliseconds)
|
|
check bufLen == 1
|
|
|
|
## The remote received our wire; its next message references it.
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
discard
|
|
(await remotePeer.unwrapReceivedMessage(capturedWires[0])).expect("remote unwrap")
|
|
let ackCarrier = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
"any later message".toBytes(), "ack-carrier-1", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap ack carrier")
|
|
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "ack-hash-1", message: sdsWakuMessage(contentTopic, ackCarrier)
|
|
),
|
|
)
|
|
|
|
## Receiving it must drain the outgoing buffer (op-end meta flush).
|
|
deadline = Moment.now() + 2.seconds
|
|
bufLen = -1
|
|
while Moment.now() < deadline and bufLen != 0:
|
|
bufLen = await outgoingBufferLen()
|
|
if bufLen != 0:
|
|
await sleepAsync(5.milliseconds)
|
|
check bufLen == 0
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "three-deep dependency chain is released in causal order":
|
|
## m1 <- m2 <- m3 arriving as m3, m2, m1: all held until m1 lands,
|
|
## then released as m1, m2, m3.
|
|
const
|
|
channelId = ChannelId("sds-chain-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/chain")
|
|
let payloads =
|
|
@["chain first".toBytes(), "chain second".toBytes(), "chain third".toBytes()]
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
var deliveries: seq[seq[byte]]
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
deliveries.add(evt.payload)
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
var wires: seq[seq[byte]]
|
|
for i in 0 .. 2:
|
|
wires.add(
|
|
(
|
|
await remotePeer.wrapOutgoingMessage(
|
|
payloads[i], "chain-m" & $(i + 1), SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap chain-m" & $(i + 1))
|
|
)
|
|
|
|
## Deepest first: m3, then m2 — both must be parked.
|
|
for i in [2, 1]:
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "chain-hash-" & $(i + 1),
|
|
message: sdsWakuMessage(contentTopic, wires[i]),
|
|
),
|
|
)
|
|
await sleepAsync(150.milliseconds)
|
|
check deliveries.len == 0
|
|
|
|
## The root arrives: everything drains in causal order.
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "chain-hash-1", message: sdsWakuMessage(contentTopic, wires[0])
|
|
),
|
|
)
|
|
let deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and deliveries.len < 3:
|
|
await sleepAsync(5.milliseconds)
|
|
check deliveries.len == 3
|
|
if deliveries.len == 3:
|
|
check deliveries[0] == payloads[0]
|
|
check deliveries[1] == payloads[1]
|
|
check deliveries[2] == payloads[2]
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "sync envelope without app payload is consumed silently":
|
|
## Sync traffic has no app payload: no event, and normal traffic
|
|
## keeps flowing afterwards.
|
|
const
|
|
channelId = ChannelId("sds-sync-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/sync")
|
|
let appPayload = "real message".toBytes()
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
discard manager
|
|
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
|
|
.expect("createReliableChannel")
|
|
|
|
var deliveryCount = 0
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
deliveryCount.inc()
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
## Hand-built sync envelope: valid SDS message, empty content.
|
|
let syncMsg = SdsMessage.init(
|
|
messageId = SdsMessageID("sync-1"),
|
|
lamportTimestamp = 42,
|
|
causalHistory = @[],
|
|
channelId = SdsChannelID(channelId),
|
|
content = @[],
|
|
bloomFilter = @[],
|
|
)
|
|
let syncWire = serializeMessage(syncMsg).expect("serialize sync")
|
|
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "sync-hash-1", message: sdsWakuMessage(contentTopic, syncWire)
|
|
),
|
|
)
|
|
await sleepAsync(150.milliseconds)
|
|
check deliveryCount == 0
|
|
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
let wire = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
appPayload, "sync-m1", SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap")
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "sync-hash-2", message: sdsWakuMessage(contentTopic, wire)
|
|
),
|
|
)
|
|
let deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and deliveryCount < 1:
|
|
await sleepAsync(5.milliseconds)
|
|
check deliveryCount == 1
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "identical payloads get distinct message ids and both deliver":
|
|
## Identical content sent twice must get distinct message ids and
|
|
## reach the app twice — not collapse via the SDS duplicate check.
|
|
const
|
|
channelId = ChannelId("sds-unique-id-channel")
|
|
contentTopic = ContentTopic("/reliable-channel/test/unique-id")
|
|
let appPayload = "ok".toBytes()
|
|
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
var brokerCtx: BrokerContext
|
|
lockNewGlobalBrokerContext:
|
|
brokerCtx = globalBrokerContext()
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
setNoopEncryption()
|
|
|
|
var capturedWires: seq[seq[byte]]
|
|
let fakeSend: SendHandler = proc(
|
|
env: MessageEnvelope
|
|
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
|
|
capturedWires.add(env.payload)
|
|
return ok(RequestId("unique-req-" & $capturedWires.len))
|
|
|
|
discard manager
|
|
.createReliableChannel(
|
|
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
|
|
)
|
|
.expect("createReliableChannel")
|
|
|
|
var deliveries: seq[seq[byte]]
|
|
discard ChannelMessageReceivedEvent
|
|
.listen(
|
|
brokerCtx,
|
|
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
|
|
if evt.channelId == channelId:
|
|
deliveries.add(evt.payload)
|
|
,
|
|
)
|
|
.expect("listen ChannelMessageReceivedEvent")
|
|
|
|
## Send side: the same payload twice must produce two distinct ids.
|
|
discard (await manager.send(channelId, appPayload)).expect("send 1")
|
|
discard (await manager.send(channelId, appPayload)).expect("send 2")
|
|
var deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and capturedWires.len < 2:
|
|
await sleepAsync(5.milliseconds)
|
|
check capturedWires.len == 2
|
|
let id1 = deserializeMessage(capturedWires[0]).expect("wire 1").messageId
|
|
let id2 = deserializeMessage(capturedWires[1]).expect("wire 2").messageId
|
|
check id1 != id2
|
|
|
|
## Receive side: identical content under distinct ids delivers twice.
|
|
let remotePeer =
|
|
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
|
|
for i in 1 .. 2:
|
|
let wire = (
|
|
await remotePeer.wrapOutgoingMessage(
|
|
appPayload, "unique-m" & $i, SdsChannelID(channelId)
|
|
)
|
|
).expect("wrap " & $i)
|
|
waku_message_events.MessageReceivedEvent.emit(
|
|
brokerCtx,
|
|
waku_message_events.MessageReceivedEvent(
|
|
messageHash: "unique-hash-" & $i, message: sdsWakuMessage(contentTopic, wire)
|
|
),
|
|
)
|
|
deadline = Moment.now() + 2.seconds
|
|
while Moment.now() < deadline and deliveries.len < 2:
|
|
await sleepAsync(5.milliseconds)
|
|
check deliveries.len == 2
|
|
|
|
(await waku.stop()).expect("stop")
|
|
|
|
asyncTest "manager rejects operations on unknown channels":
|
|
var waku: LogosDelivery
|
|
var manager: ReliableChannelManager
|
|
lockNewGlobalBrokerContext:
|
|
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
|
|
manager = waku.reliableChannelManager
|
|
|
|
check (await manager.send(ChannelId("no-such-channel"), "x".toBytes())).isErr()
|
|
check (await manager.closeChannel(ChannelId("no-such-channel"))).isErr()
|
|
|
|
(await waku.stop()).expect("stop")
|