feat: running validators in /relay/v1/messages/{pubsubTopic} (#2373)

This commit is contained in:
gabrielmer 2024-02-01 18:16:10 +01:00 committed by GitHub
parent 3e65cc18f6
commit 59d8b6204f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 121 additions and 55 deletions

View File

@ -438,13 +438,15 @@ proc setupProtocols(node: WakuNode,
return err("failed to mount waku relay protocol: " & getCurrentExceptionMsg()) return err("failed to mount waku relay protocol: " & getCurrentExceptionMsg())
# Add validation keys to protected topics # Add validation keys to protected topics
var subscribedProtectedTopics : seq[ProtectedTopic]
for topicKey in conf.protectedTopics: for topicKey in conf.protectedTopics:
if topicKey.topic notin pubsubTopics: if topicKey.topic notin pubsubTopics:
warn "protected topic not in subscribed pubsub topics, skipping adding validator", warn "protected topic not in subscribed pubsub topics, skipping adding validator",
protectedTopic=topicKey.topic, subscribedTopics=pubsubTopics protectedTopic=topicKey.topic, subscribedTopics=pubsubTopics
continue continue
subscribedProtectedTopics.add(topicKey)
notice "routing only signed traffic", protectedTopic=topicKey.topic, publicKey=topicKey.key notice "routing only signed traffic", protectedTopic=topicKey.topic, publicKey=topicKey.key
node.wakuRelay.addSignedTopicValidator(Pubsubtopic(topicKey.topic), topicKey.key) node.wakuRelay.addSignedTopicsValidator(subscribedProtectedTopics)
# Enable Rendezvous Discovery protocol when Relay is enabled # Enable Rendezvous Discovery protocol when Relay is enabled
try: try:

View File

@ -20,7 +20,8 @@ const MessageWindowInSec = 5*60 # +- 5 minutes
import import
../../waku/waku_relay/protocol, ../../waku/waku_relay/protocol,
../../waku/waku_core ../../waku/waku_core,
./external_config
declarePublicCounter waku_msg_validator_signed_outcome, "number of messages for each validation outcome", ["result"] declarePublicCounter waku_msg_validator_signed_outcome, "number of messages for each validation outcome", ["result"]
@ -49,21 +50,28 @@ proc withinTimeWindow*(msg: WakuMessage): bool =
return true return true
return false return false
proc addSignedTopicValidator*(w: WakuRelay, topic: PubsubTopic, publicTopicKey: SkPublicKey) = proc addSignedTopicsValidator*(w: WakuRelay, protectedTopics: seq[ProtectedTopic]) =
debug "adding validator to signed topic", topic=topic, publicTopicKey=publicTopicKey debug "adding validator to signed topics"
proc validator(topic: string, msg: WakuMessage): Future[errors.ValidationResult] {.async.} = proc validator(topic: string, msg: WakuMessage): Future[errors.ValidationResult] {.async.} =
var outcome = errors.ValidationResult.Reject var outcome = errors.ValidationResult.Reject
for protectedTopic in protectedTopics:
if msg.timestamp != 0: if(protectedTopic.topic == topic):
if msg.withinTimeWindow(): if msg.timestamp != 0:
let msgHash = SkMessage(topic.msgHash(msg)) if msg.withinTimeWindow():
let recoveredSignature = SkSignature.fromRaw(msg.meta) let msgHash = SkMessage(topic.msgHash(msg))
if recoveredSignature.isOk(): let recoveredSignature = SkSignature.fromRaw(msg.meta)
if recoveredSignature.get.verify(msgHash, publicTopicKey): if recoveredSignature.isOk():
outcome = errors.ValidationResult.Accept if recoveredSignature.get.verify(msgHash, protectedTopic.key):
outcome = errors.ValidationResult.Accept
waku_msg_validator_signed_outcome.inc(labelValues = [$outcome]) if outcome != errors.ValidationResult.Accept:
return outcome debug "signed topic validation failed", topic=topic, publicTopicKey=protectedTopic.key
waku_msg_validator_signed_outcome.inc(labelValues = [$outcome])
return outcome
w.addValidator(topic, validator) return errors.ValidationResult.Accept
w.addValidator(validator, "signed topic validation failed")

View File

@ -297,14 +297,14 @@ suite "Waku Relay":
proc otherSimpleFutureHandler(topic: PubsubTopic, message: WakuMessage) {.async, gcsafe.} = proc otherSimpleFutureHandler(topic: PubsubTopic, message: WakuMessage) {.async, gcsafe.} =
otherHandlerFuture.complete((topic, message)) otherHandlerFuture.complete((topic, message))
otherNode.addValidator(pubsubTopic, len4Validator) otherNode.addValidator(len4Validator)
discard otherNode.subscribe(pubsubTopic, otherSimpleFutureHandler) discard otherNode.subscribe(pubsubTopic, otherSimpleFutureHandler)
await sleepAsync(500.millis) await sleepAsync(500.millis)
check: check:
otherNode.isSubscribed(pubsubTopic) otherNode.isSubscribed(pubsubTopic)
# Given a subscribed node with a validator # Given a subscribed node with a validator
node.addValidator(pubsubTopic, len4Validator) node.addValidator(len4Validator)
discard node.subscribe(pubsubTopic, simpleFutureHandler) discard node.subscribe(pubsubTopic, simpleFutureHandler)
await sleepAsync(500.millis) await sleepAsync(500.millis)
check: check:

View File

@ -163,7 +163,7 @@ suite "WakuNode - Relay":
completionFutValidatorAcc.complete(true) completionFutValidatorAcc.complete(true)
return ValidationResult.Accept return ValidationResult.Accept
node2.wakuRelay.addValidator(pubSubTopic, validator) node2.wakuRelay.addValidator(validator)
var completionFut = newFuture[bool]() var completionFut = newFuture[bool]()
proc relayHandler(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async, gcsafe.} = proc relayHandler(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async, gcsafe.} =

View File

@ -13,6 +13,7 @@ import
libp2p/multihash, libp2p/multihash,
secp256k1 secp256k1
import import
../../apps/wakunode2/external_config,
../../apps/wakunode2/wakunode2_validator_signed, ../../apps/wakunode2/wakunode2_validator_signed,
../../waku/waku_core, ../../waku/waku_core,
../../waku/node/peer_manager, ../../waku/node/peer_manager,
@ -42,8 +43,10 @@ suite "WakuNode2 - Validators":
# Add signed message validator to all nodes. They will only route signed messages # Add signed message validator to all nodes. They will only route signed messages
for node in nodes: for node in nodes:
var signedTopics : seq[ProtectedTopic]
for topic, publicKey in topicsPublicKeys: for topic, publicKey in topicsPublicKeys:
node.wakuRelay.addSignedTopicValidator(PubsubTopic(topic), publicKey) signedTopics.add(ProtectedTopic(topic: topic, key: publicKey))
node.wakuRelay.addSignedTopicsValidator(signedTopics)
# Connect the nodes in a full mesh # Connect the nodes in a full mesh
for i in 0..<5: for i in 0..<5:
@ -114,8 +117,10 @@ suite "WakuNode2 - Validators":
# Add signed message validator to all nodes. They will only route signed messages # Add signed message validator to all nodes. They will only route signed messages
for node in nodes: for node in nodes:
var signedTopics : seq[ProtectedTopic]
for topic, publicKey in topicsPublicKeys: for topic, publicKey in topicsPublicKeys:
node.wakuRelay.addSignedTopicValidator(PubsubTopic(topic), publicKey) signedTopics.add(ProtectedTopic(topic: topic, key: publicKey))
node.wakuRelay.addSignedTopicsValidator(signedTopics)
# Connect the nodes in a full mesh # Connect the nodes in a full mesh
for i in 0..<5: for i in 0..<5:
@ -232,8 +237,10 @@ suite "WakuNode2 - Validators":
# Add signed message validator to all nodes. They will only route signed messages # Add signed message validator to all nodes. They will only route signed messages
for node in nodes: for node in nodes:
var signedTopics : seq[ProtectedTopic]
for topic, publicKey in topicsPublicKeys: for topic, publicKey in topicsPublicKeys:
node.wakuRelay.addSignedTopicValidator(PubsubTopic(topic), publicKey) signedTopics.add(ProtectedTopic(topic: topic, key: publicKey))
node.wakuRelay.addSignedTopicsValidator(signedTopics)
# nodes[0] is connected only to nodes[1] # nodes[0] is connected only to nodes[1]
let connOk1 = await nodes[0].peerManager.connectRelay(nodes[1].switch.peerInfo.toRemotePeerInfo()) let connOk1 = await nodes[0].peerManager.connectRelay(nodes[1].switch.peerInfo.toRemotePeerInfo())

View File

@ -1,7 +1,7 @@
{.used.} {.used.}
import import
std/[sequtils,tempfiles], std/[sequtils, strformat, tempfiles],
stew/byteutils, stew/byteutils,
stew/shims/net, stew/shims/net,
testutils/unittests, testutils/unittests,
@ -21,7 +21,8 @@ import
../../waku/waku_relay, ../../waku/waku_relay,
../../../waku/waku_rln_relay, ../../../waku/waku_rln_relay,
../testlib/wakucore, ../testlib/wakucore,
../testlib/wakunode ../testlib/wakunode,
../resources/payloads
proc testWakuNode(): WakuNode = proc testWakuNode(): WakuNode =
let let
@ -480,6 +481,50 @@ suite "Waku v2 Rest API - Relay":
$response.contentType == $MIMETYPE_TEXT $response.contentType == $MIMETYPE_TEXT
response.data == "Failed to publish. Autosharding error: invalid format: topic must start with slash" response.data == "Failed to publish. Autosharding error: invalid format: topic must start with slash"
await restServer.stop()
await restServer.closeWait()
await node.stop()
asyncTest "Post a message larger than maximum size - POST /relay/v1/messages/{topic}":
# Given
let node = testWakuNode()
await node.start()
await node.mountRelay()
await node.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false,
rlnRelayCredIndex: some(1.uint),
rlnRelayTreePath: genTempPath("rln_tree", "wakunode_1")))
# RPC server setup
var restPort = Port(0)
let restAddress = parseIpAddress("0.0.0.0")
let restServer = RestServerRef.init(restAddress, restPort).tryGet()
restPort = restServer.server.address.port # update with bound port for client use
let cache = MessageCache.init()
installRelayApiHandlers(restServer.router, node, cache)
restServer.start()
let client = newRestHttpClient(initTAddress(restAddress, restPort))
node.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic))
require:
toSeq(node.wakuRelay.subscribedTopics).len == 1
# When
let response = await client.relayPostMessagesV1(DefaultPubsubTopic, RelayWakuMessage(
payload: base64.encode(getByteSequence(MaxWakuMessageSize)), # Message will be bigger than the max size
contentTopic: some(DefaultContentTopic),
timestamp: some(int64(2022))
))
# Then
check:
response.status == 400
$response.contentType == $MIMETYPE_TEXT
response.data == fmt"Failed to publish: Message size exceeded maximum of {DefaultMaxWakuMessageSizeStr}"
await restServer.stop() await restServer.stop()
await restServer.closeWait() await restServer.closeWait()
await node.stop() await node.stop()

View File

@ -987,7 +987,7 @@ proc mountRlnRelay*(node: WakuNode,
# register rln validator as default validator # register rln validator as default validator
debug "Registering RLN validator" debug "Registering RLN validator"
node.wakuRelay.addDefaultValidator(validator) node.wakuRelay.addValidator(validator, "RLN validation failed")
node.wakuRlnRelay = rlnRelay node.wakuRlnRelay = rlnRelay

View File

@ -135,16 +135,8 @@ proc installRelayApiHandlers*(router: var RestRouter, node: WakuNode, cache: Mes
if not success: if not success:
return RestApiResponse.internalServerError("Failed to publish: error appending RLN proof to message") return RestApiResponse.internalServerError("Failed to publish: error appending RLN proof to message")
# validate the message before sending it (await node.wakuRelay.validateMessage(pubsubTopic, message)).isOkOr:
let result = node.wakuRlnRelay.validateMessageAndUpdateLog(message) return RestApiResponse.badRequest("Failed to publish: " & error)
if result == MessageValidationResult.Invalid:
return RestApiResponse.internalServerError("Failed to publish: invalid RLN proof")
elif result == MessageValidationResult.Spam:
return RestApiResponse.badRequest("Failed to publish: limit exceeded, try again later")
elif result == MessageValidationResult.Valid:
debug "RLN proof validated successfully", pubSubTopic=pubSubTopic
else:
return RestApiResponse.internalServerError("Failed to publish: unknown RLN proof validation result")
# if we reach here its either a non-RLN message or a RLN message with a valid proof # if we reach here its either a non-RLN message or a RLN message with a valid proof
debug "Publishing message", pubSubTopic=pubSubTopic, rln=not node.wakuRlnRelay.isNil() debug "Publishing message", pubSubTopic=pubSubTopic, rln=not node.wakuRlnRelay.isNil()

View File

@ -8,6 +8,7 @@ else:
{.push raises: [].} {.push raises: [].}
import import
std/strformat,
stew/results, stew/results,
sequtils, sequtils,
chronos, chronos,
@ -124,14 +125,11 @@ type
WakuRelayHandler* = proc(pubsubTopic: PubsubTopic, message: WakuMessage): Future[void] {.gcsafe, raises: [Defect].} WakuRelayHandler* = proc(pubsubTopic: PubsubTopic, message: WakuMessage): Future[void] {.gcsafe, raises: [Defect].}
WakuValidatorHandler* = proc(pubsubTopic: PubsubTopic, message: WakuMessage): Future[ValidationResult] {.gcsafe, raises: [Defect].} WakuValidatorHandler* = proc(pubsubTopic: PubsubTopic, message: WakuMessage): Future[ValidationResult] {.gcsafe, raises: [Defect].}
WakuRelay* = ref object of GossipSub WakuRelay* = ref object of GossipSub
# a map of PubsubTopic's => seq[WakuValidatorHandler] that are # seq of tuples: the first entry in the tuple contains the validators are called for every topic
# called in order every time a message is received on a given pubsub topic # the second entry contains the error messages to be returned when the validator fails
wakuValidators: Table[PubsubTopic, seq[WakuValidatorHandler]] wakuValidators: seq[tuple[handler: WakuValidatorHandler, errorMessage: string]]
# a map that stores whether the ordered validator has been inserted # a map of validators to error messages to return when validation fails
# for a given PubsubTopic
validatorInserted: Table[PubsubTopic, bool] validatorInserted: Table[PubsubTopic, bool]
# seq of validators that are called for every pubsub topic
wakuDefaultValidators: seq[WakuValidatorHandler]
proc initProtocolHandler(w: WakuRelay) = proc initProtocolHandler(w: WakuRelay) =
proc handler(conn: Connection, proto: string) {.async.} = proc handler(conn: Connection, proto: string) {.async.} =
@ -180,14 +178,9 @@ proc new*(T: type WakuRelay,
return ok(w) return ok(w)
proc addValidator*(w: WakuRelay, proc addValidator*(w: WakuRelay,
topic: varargs[string], handler: WakuValidatorHandler,
handler: WakuValidatorHandler) {.gcsafe.} = errorMessage: string = "") {.gcsafe.} =
for t in topic: w.wakuValidators.add((handler, errorMessage))
w.wakuValidators.mgetOrPut(t, @[]).add(handler)
proc addDefaultValidator*(w: WakuRelay,
handler: WakuValidatorHandler) {.gcsafe.} =
w.wakuDefaultValidators.add(handler)
method start*(w: WakuRelay) {.async.} = method start*(w: WakuRelay) {.async.} =
debug "start" debug "start"
@ -216,19 +209,38 @@ proc generateOrderedValidator*(w: WakuRelay): auto {.gcsafe.} =
let msg = msgRes.get() let msg = msgRes.get()
# now sequentially validate the message # now sequentially validate the message
for validator in w.wakuDefaultValidators: for (validator, _) in w.wakuValidators:
let validatorRes = await validator(pubsubTopic, msg) let validatorRes = await validator(pubsubTopic, msg)
if validatorRes != ValidationResult.Accept: if validatorRes != ValidationResult.Accept:
return validatorRes return validatorRes
if w.wakuValidators.hasKey(pubsubTopic):
for validator in w.wakuValidators[pubsubTopic]:
let validatorRes = await validator(pubsubTopic, msg)
if validatorRes != ValidationResult.Accept:
return validatorRes
return ValidationResult.Accept return ValidationResult.Accept
return wrappedValidator return wrappedValidator
proc isValidSize(message: WakuMessage): Future[Result[void, string]] {.async.} =
let messageSizeBytes = uint64(message.encode().buffer.len)
if(messageSizeBytes > MaxWakuMessageSize):
let message = fmt"Message size exceeded maximum of {DefaultMaxWakuMessageSizeStr}"
debug "Invalid Waku Message", error=message
return err(message)
return ok()
proc validateMessage*(w: WakuRelay, pubsubTopic: string, msg: WakuMessage):
Future[Result[void, string]] {.async.} =
(await msg.isValidSize()).isOkOr:
return err(error)
for (validator, message) in w.wakuValidators:
let validatorRes = await validator(pubsubTopic, msg)
if validatorRes != ValidationResult.Accept:
if message.len > 0:
return err(message)
else:
return err("Validator failed")
return ok()
proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandler): TopicHandler = proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandler): TopicHandler =
debug "subscribe", pubsubTopic=pubsubTopic debug "subscribe", pubsubTopic=pubsubTopic