mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-07-25 05:53:11 +00:00
Live messages carry an RLN proof, but archives dropped it at insert time - so history handed over via store-sync transfer or store resume arrived proofless and had to be accepted on faith, sidestepping the spam limiter exactly where it should be load-bearing. A malicious peer could invent 'history you missed' and a victim would swallow and re-serve it. - archive: proof column (sqlite schema v11, postgres v8; nullable, so existing rows migrate as a no-op and in-memory archives need no migration); insert/select thread message.proof through both drivers; the queue driver stores whole messages and needed no change - transfer and store resume serve proof-bearing messages automatically (the wire codecs already carry the field) and both re-verify the original author's proof on arrival: the receiving node's FIRST verification of a message it missed on relay, not a repeat - same work the relay path would have done, deferred to catch-up - store resume gains the same validator the sync transfer uses, since a store node could also serve invented history - new --store-sync-require-proof flag (default off): the rollout lever; once the fleet persists and serves proofs, flipping it makes nodes reject proofless synced history when RLN is enabled - reconciliation untouched: proofs are not part of the message hash, so identity, fingerprints and cursors are unchanged Tested end-to-end: a proof survives full-node -> full-node sync transfer and the store-query -> resume -> archive trip intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
204 lines
6.0 KiB
Nim
204 lines
6.0 KiB
Nim
{.used.}
|
|
|
|
## Integration test for the "store as a startup-only dependency" experiment:
|
|
## two full nodes (no store service) wired exactly like `mountStoreSync` —
|
|
## reconciliation + transfer sharing one peer manager and the three async
|
|
## queues, backed by small in-memory archives — must recover missed
|
|
## messages from each other end to end.
|
|
|
|
import results, testutils/unittests, chronos, chronicles
|
|
import
|
|
../../logos_delivery/waku/[
|
|
node/peer_manager,
|
|
waku_core,
|
|
waku_core/message/digest,
|
|
waku_store_sync/common,
|
|
waku_store_sync/reconciliation,
|
|
waku_store_sync/transfer,
|
|
waku_archive/archive,
|
|
waku_archive/driver,
|
|
waku_archive/common,
|
|
],
|
|
../testlib/[wakucore, testasync],
|
|
../waku_archive/archive_utils
|
|
|
|
type FullNode = object
|
|
switch: Switch
|
|
driver: ArchiveDriver
|
|
archive: WakuArchive
|
|
recon: SyncReconciliation
|
|
transfer: SyncTransfer
|
|
peerInfo: RemotePeerInfo
|
|
peerManager: PeerManager
|
|
|
|
proc newFullNode(
|
|
msgValidator: Opt[TransferValidator] = Opt.none(TransferValidator)
|
|
): Future[FullNode] {.async.} =
|
|
## Mirrors the wiring of WakuNode.mountStoreSync: one peer manager and
|
|
## three shared channels connect reconciliation and transfer.
|
|
let switch = newTestSwitch()
|
|
await switch.start()
|
|
|
|
let driver = newSqliteArchiveDriver()
|
|
let archive = newWakuArchive(driver)
|
|
let peerManager = PeerManager.new(switch)
|
|
|
|
let idsChannel = newAsyncQueue[(SyncID, PubsubTopic, ContentTopic)]()
|
|
let wantsChannel = newAsyncQueue[PeerId]()
|
|
let needsChannel = newAsyncQueue[(PeerId, WakuMessageHash)]()
|
|
|
|
let recon = (
|
|
await SyncReconciliation.new(
|
|
pubsubTopics = @[],
|
|
contentTopics = @[],
|
|
peerManager = peerManager,
|
|
wakuArchive = archive,
|
|
relayJitter = 0.seconds,
|
|
idsRx = idsChannel,
|
|
localWantsTx = wantsChannel,
|
|
remoteNeedsTx = needsChannel,
|
|
)
|
|
).valueOr:
|
|
raiseAssert error
|
|
|
|
await recon.start()
|
|
switch.mount(recon)
|
|
|
|
let transfer = SyncTransfer.new(
|
|
peerManager = peerManager,
|
|
wakuArchive = archive,
|
|
idsTx = idsChannel,
|
|
localWantsRx = wantsChannel,
|
|
remoteNeedsRx = needsChannel,
|
|
msgValidator = msgValidator,
|
|
)
|
|
|
|
await transfer.start()
|
|
switch.mount(transfer)
|
|
|
|
return FullNode(
|
|
switch: switch,
|
|
driver: driver,
|
|
archive: archive,
|
|
recon: recon,
|
|
transfer: transfer,
|
|
peerInfo: switch.peerInfo.toRemotePeerInfo(),
|
|
peerManager: peerManager,
|
|
)
|
|
|
|
proc stop(node: FullNode) {.async.} =
|
|
await node.transfer.stop()
|
|
await node.recon.stop()
|
|
await node.switch.stop()
|
|
|
|
proc insertMessage(node: FullNode, msg: WakuMessage) =
|
|
## Live-ingest path: relay would archive the message and feed the
|
|
## reconciliation storage (subscription_manager archive + sync handlers).
|
|
discard node.driver.put(DefaultPubsubTopic, @[msg])
|
|
node.recon.messageIngress(DefaultPubsubTopic, msg)
|
|
|
|
proc hasMessage(node: FullNode, hash: WakuMessageHash): Future[bool] {.async.} =
|
|
var query = ArchiveQuery()
|
|
query.includeData = true
|
|
query.hashes = @[hash]
|
|
|
|
let response = (await node.archive.findMessages(query)).valueOr:
|
|
raiseAssert $error
|
|
|
|
return response.messages.len > 0
|
|
|
|
suite "Waku Sync: full node miss recovery":
|
|
var nodeA {.threadvar.}: FullNode
|
|
var nodeB {.threadvar.}: FullNode
|
|
|
|
asyncSetup:
|
|
nodeA = await newFullNode()
|
|
nodeB = await newFullNode()
|
|
|
|
nodeA.peerManager.addPeer(nodeB.peerInfo)
|
|
nodeB.peerManager.addPeer(nodeA.peerInfo)
|
|
|
|
asyncTeardown:
|
|
await nodeA.stop()
|
|
await nodeB.stop()
|
|
|
|
asyncTest "node recovers a missed message from a full-node peer":
|
|
let msg = fakeWakuMessage(contentTopic = DefaultContentTopic)
|
|
let hash = computeMessageHash(DefaultPubsubTopic, msg)
|
|
|
|
nodeA.insertMessage(msg)
|
|
|
|
check not await nodeB.hasMessage(hash)
|
|
|
|
let res = await nodeB.recon.storeSynchronization(Opt.some(nodeA.peerInfo))
|
|
assert res.isOk(), $res.error
|
|
|
|
# transfer of the missing message happens asynchronously after the
|
|
# reconciliation session ends
|
|
await sleepAsync(1.seconds)
|
|
|
|
check await nodeB.hasMessage(hash)
|
|
|
|
asyncTest "one session converges both nodes to the union":
|
|
let msgA = fakeWakuMessage(payload = @[byte 1], contentTopic = DefaultContentTopic)
|
|
let msgB = fakeWakuMessage(payload = @[byte 2], contentTopic = DefaultContentTopic)
|
|
let hashA = computeMessageHash(DefaultPubsubTopic, msgA)
|
|
let hashB = computeMessageHash(DefaultPubsubTopic, msgB)
|
|
|
|
nodeA.insertMessage(msgA)
|
|
nodeB.insertMessage(msgB)
|
|
|
|
let res = await nodeB.recon.storeSynchronization(Opt.some(nodeA.peerInfo))
|
|
assert res.isOk(), $res.error
|
|
|
|
await sleepAsync(1.seconds)
|
|
|
|
check:
|
|
await nodeA.hasMessage(hashB)
|
|
await nodeB.hasMessage(hashA)
|
|
|
|
asyncTest "synced messages keep the original author's proof":
|
|
let proof = @[byte 4, 2, 4, 2]
|
|
let msg = fakeWakuMessage(contentTopic = DefaultContentTopic, proof = proof)
|
|
let hash = computeMessageHash(DefaultPubsubTopic, msg)
|
|
|
|
nodeA.insertMessage(msg)
|
|
|
|
let res = await nodeB.recon.storeSynchronization(Opt.some(nodeA.peerInfo))
|
|
assert res.isOk(), $res.error
|
|
|
|
await sleepAsync(1.seconds)
|
|
|
|
var query = ArchiveQuery()
|
|
query.includeData = true
|
|
query.hashes = @[hash]
|
|
|
|
let response = (await nodeB.archive.findMessages(query)).valueOr:
|
|
raiseAssert $error
|
|
|
|
check:
|
|
response.messages.len == 1
|
|
response.messages[0].proof == proof
|
|
|
|
asyncTest "messages failing transfer validation are not archived":
|
|
await nodeB.stop()
|
|
|
|
let rejectAll: TransferValidator = proc(msg: WakuMessage): Future[bool] {.async.} =
|
|
return false
|
|
|
|
nodeB = await newFullNode(msgValidator = Opt.some(rejectAll))
|
|
nodeA.peerManager.addPeer(nodeB.peerInfo)
|
|
nodeB.peerManager.addPeer(nodeA.peerInfo)
|
|
|
|
let msg = fakeWakuMessage(contentTopic = DefaultContentTopic)
|
|
let hash = computeMessageHash(DefaultPubsubTopic, msg)
|
|
|
|
nodeA.insertMessage(msg)
|
|
|
|
let res = await nodeB.recon.storeSynchronization(Opt.some(nodeA.peerInfo))
|
|
assert res.isOk(), $res.error
|
|
|
|
await sleepAsync(1.seconds)
|
|
|
|
check not await nodeB.hasMessage(hash)
|