diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c335676..c8037fb6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This release contains the following: ### Changes - GossipSub [prune backoff period](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#prune-backoff-and-peer-exchange) is now the recommended 1 minute +- Bridge now uses content topic format according to [23/WAKU2-TOPICS](https://rfc.vac.dev/spec/23/) #### General refactoring @@ -22,9 +23,11 @@ This release contains the following: #### API - ### Fixes +- Bridge no longer re-encodes already encoded payloads when publishing to V1 +- Bridge now populates WakuMessage timestamps when publishing to V2 + ## 2021-07-26 v0.5.1 This patch release contains the following fix: diff --git a/tests/v2/test_waku_bridge.nim b/tests/v2/test_waku_bridge.nim index 922b3ef6c..77a1ce159 100644 --- a/tests/v2/test_waku_bridge.nim +++ b/tests/v2/test_waku_bridge.nim @@ -50,11 +50,12 @@ procSuite "WakuBridge": v2NodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[] v2Node = WakuNode.new(v2NodeKey, ValidIpAddress.init("0.0.0.0"), Port(60002)) - contentTopic = ContentTopic("/waku/1/0x1a2b3c4d/rlp") + contentTopic = ContentTopic("/waku/1/0x1a2b3c4d/rfc26") topic = [byte 0x1a, byte 0x2b, byte 0x3c, byte 0x4d] payloadV1 = "hello from V1".toBytes() payloadV2 = "hello from V2".toBytes() - message = WakuMessage(payload: payloadV2, contentTopic: contentTopic) + encodedPayloadV2 = Payload(payload: payloadV2, dst: some(nodev1Key.pubKey)) + message = WakuMessage(payload: encodedPayloadV2.encode(1, rng[]).get(), contentTopic: contentTopic, version: 1) ######################## # Tests setup/teardown # @@ -74,14 +75,14 @@ procSuite "WakuBridge": # Expected cases check: - toV1Topic(ContentTopic("/waku/1/0x00000000/rlp")) == [byte 0x00, byte 0x00, byte 0x00, byte 0x00] - toV2ContentTopic([byte 0x00, byte 0x00, byte 0x00, byte 0x00]) == ContentTopic("/waku/1/0x00000000/rlp") - toV1Topic(ContentTopic("/waku/1/0xffffffff/rlp")) == [byte 0xff, byte 0xff, byte 0xff, byte 0xff] - toV2ContentTopic([byte 0xff, byte 0xff, byte 0xff, byte 0xff]) == ContentTopic("/waku/1/0xffffffff/rlp") - toV1Topic(ContentTopic("/waku/1/0x1a2b3c4d/rlp")) == [byte 0x1a, byte 0x2b, byte 0x3c, byte 0x4d] - toV2ContentTopic([byte 0x1a, byte 0x2b, byte 0x3c, byte 0x4d]) == ContentTopic("/waku/1/0x1a2b3c4d/rlp") + toV1Topic(ContentTopic("/waku/1/0x00000000/rfc26")) == [byte 0x00, byte 0x00, byte 0x00, byte 0x00] + toV2ContentTopic([byte 0x00, byte 0x00, byte 0x00, byte 0x00]) == ContentTopic("/waku/1/0x00000000/rfc26") + toV1Topic(ContentTopic("/waku/1/0xffffffff/rfc26")) == [byte 0xff, byte 0xff, byte 0xff, byte 0xff] + toV2ContentTopic([byte 0xff, byte 0xff, byte 0xff, byte 0xff]) == ContentTopic("/waku/1/0xffffffff/rfc26") + toV1Topic(ContentTopic("/waku/1/0x1a2b3c4d/rfc26")) == [byte 0x1a, byte 0x2b, byte 0x3c, byte 0x4d] + toV2ContentTopic([byte 0x1a, byte 0x2b, byte 0x3c, byte 0x4d]) == ContentTopic("/waku/1/0x1a2b3c4d/rfc26") # Topic conversion should still work where '0x' prefix is omitted from - toV1Topic(ContentTopic("/waku/1/1a2b3c4d/rlp")) == [byte 0x1a, byte 0x2b, byte 0x3c, byte 0x4d] + toV1Topic(ContentTopic("/waku/1/1a2b3c4d/rfc26")) == [byte 0x1a, byte 0x2b, byte 0x3c, byte 0x4d] # Invalid cases @@ -91,11 +92,11 @@ procSuite "WakuBridge": expect ValueError: # Content topic name too short - discard toV1Topic(ContentTopic("/waku/1/0x112233/rlp")) + discard toV1Topic(ContentTopic("/waku/1/0x112233/rfc26")) expect ValueError: # Content topic name not hex - discard toV1Topic(ContentTopic("/waku/1/my-content/rlp")) + discard toV1Topic(ContentTopic("/waku/1/my-content/rfc26")) asyncTest "Messages are bridged between Waku v1 and Waku v2": # Setup test @@ -103,7 +104,7 @@ procSuite "WakuBridge": waitFor bridge.start() waitFor v2Node.start() - v2Node.mountRelay(@[DefaultBridgeTopic]) + v2Node.mountRelay(@[DefaultBridgeTopic], triggerSelf = false) discard waitFor v1Node.rlpxConnect(newNode(bridge.nodev1.toENode())) waitFor v2Node.connectToNodes(@[bridge.nodev2.peerInfo]) @@ -134,12 +135,14 @@ procSuite "WakuBridge": # v1Node received message published by v2Node v1Node.protocolState(Waku).queue.items.len == 1 - let msg = v1Node.protocolState(Waku).queue.items[0] + let + msg = v1Node.protocolState(Waku).queue.items[0] + decodedPayload = msg.env.data.decode(some(nodev1Key.seckey), none[SymKey]()).get() check: # Message fields are as expected msg.env.topic == topic # Topic translation worked - string.fromBytes(msg.env.data).contains("from V2") + string.fromBytes(decodedPayload.payload).contains("from V2") # Test bridging from V1 to V2 check: diff --git a/waku/common/wakubridge.nim b/waku/common/wakubridge.nim index 44652340f..767e45cb1 100644 --- a/waku/common/wakubridge.nim +++ b/waku/common/wakubridge.nim @@ -65,7 +65,7 @@ proc containsOrAdd(sequence: var seq[hashes.Hash], hash: hashes.Hash): bool = proc toV2ContentTopic*(v1Topic: waku_protocol.Topic): ContentTopic = ## Convert a 4-byte array v1 topic to a namespaced content topic - ## with format `/waku/1//proto` + ## with format `/waku/1//rfc26` ## ## should be prefixed with `0x` @@ -74,13 +74,13 @@ proc toV2ContentTopic*(v1Topic: waku_protocol.Topic): ContentTopic = namespacedTopic.application = "waku" namespacedTopic.version = "1" namespacedTopic.topicName = "0x" & v1Topic.toHex() - namespacedTopic.encoding = "rlp" + namespacedTopic.encoding = "rfc26" return ContentTopic($namespacedTopic) proc toV1Topic*(contentTopic: ContentTopic): waku_protocol.Topic {.raises: [Defect, LPError, ValueError]} = ## Extracts the 4-byte array v1 topic from a content topic - ## with format `/waku/1//proto` + ## with format `/waku/1//rfc26` hexToByteArray(hexStr = NamespacedTopic.fromString(contentTopic).tryGet().topicName, N = 4) # Byte array length @@ -91,6 +91,7 @@ func toWakuMessage(env: Envelope): WakuMessage = # Translate a Waku v1 envelope to a Waku v2 message WakuMessage(payload: env.data, contentTopic: toV2ContentTopic(env.topic), + timestamp: float64(env.expiry - env.ttl), version: 1) proc toWakuV2(bridge: WakuBridge, env: Envelope) {.async.} = @@ -121,10 +122,16 @@ proc toWakuV1(bridge: WakuBridge, msg: WakuMessage) {.gcsafe, raises: [Defect, L # @TODO: use namespacing to map v2 contentTopics to v1 topics let v1TopicSeq = msg.contentTopic.toBytes()[0..3] - - discard bridge.nodev1.postMessage(ttl = DefaultTTL, - topic = toV1Topic(msg.contentTopic), - payload = msg.payload) + + case msg.version: + of 1: + discard bridge.nodev1.postEncoded(ttl = DefaultTTL, + topic = toV1Topic(msg.contentTopic), + encodedPayload = msg.payload) # The payload is already encoded according to https://rfc.vac.dev/spec/26/ + else: + discard bridge.nodev1.postMessage(ttl = DefaultTTL, + topic = toV1Topic(msg.contentTopic), + payload = msg.payload) ############## # Public API # diff --git a/waku/v1/protocol/waku_protocol.nim b/waku/v1/protocol/waku_protocol.nim index ed59d545c..be58a09a4 100644 --- a/waku/v1/protocol/waku_protocol.nim +++ b/waku/v1/protocol/waku_protocol.nim @@ -497,6 +497,55 @@ proc queueMessage(node: EthereumNode, msg: Message): bool = # Public EthereumNode calls ---------------------------------------------------- +proc postEncoded*(node: EthereumNode, ttl: uint32, + topic: Topic, encodedPayload: seq[byte], + powTime = 1'f, + powTarget = defaultMinPow, + targetPeer = none[NodeId]()): bool = + ## Post a message from pre-encoded payload on the message queue. + ## This will be processed at the next `messageInterval`. + ## The encodedPayload must be encoded according to RFC 26/WAKU-PAYLOAD + ## at https://rfc.vac.dev/spec/26/ + + var env = Envelope(expiry:epochTime().uint32 + ttl, + ttl: ttl, topic: topic, data: encodedPayload, nonce: 0) + + # Allow lightnode to post only direct p2p messages + if targetPeer.isSome(): + return node.sendP2PMessage(targetPeer.get(), [env]) + else: + # non direct p2p message can not have ttl of 0 + if env.ttl == 0: + return false + var msg = initMessage(env, powCalc = false) + # XXX: make this non blocking or not? + # In its current blocking state, it could be noticed by a peer that no + # messages are send for a while, and thus that mining PoW is done, and + # that next messages contains a message originated from this peer + # zah: It would be hard to execute this in a background thread at the + # moment. We'll need a way to send custom "tasks" to the async message + # loop (e.g. AD2 support for AsyncChannels). + if not msg.sealEnvelope(powTime, powTarget): + return false + + # need to check expiry after mining PoW + if not msg.env.valid(): + return false + + result = node.queueMessage(msg) + + # Allows light nodes to post via untrusted messages packet. + # Queue gets processed immediatly as the node sends only its own messages, + # so the privacy ship has already sailed anyhow. + # TODO: + # - Could be still a concern in terms of efficiency, if multiple messages + # need to be send. + # - For Waku Mode, the checks in processQueue are rather useless as the + # idea is to connect only to 1 node? Also refactor in that case. + if node.protocolState(Waku).config.isLightNode: + for peer in node.peers(Waku): + peer.processQueue() + proc postMessage*(node: EthereumNode, pubKey = none[PublicKey](), symKey = none[SymKey](), src = none[PrivateKey](), ttl: uint32, topic: Topic, payload: seq[byte], @@ -511,44 +560,7 @@ proc postMessage*(node: EthereumNode, pubKey = none[PublicKey](), let payload = encode(node.rng[], Payload( payload: payload, src: src, dst: pubKey, symKey: symKey, padding: padding)) if payload.isSome(): - var env = Envelope(expiry:epochTime().uint32 + ttl, - ttl: ttl, topic: topic, data: payload.get(), nonce: 0) - - # Allow lightnode to post only direct p2p messages - if targetPeer.isSome(): - return node.sendP2PMessage(targetPeer.get(), [env]) - else: - # non direct p2p message can not have ttl of 0 - if env.ttl == 0: - return false - var msg = initMessage(env, powCalc = false) - # XXX: make this non blocking or not? - # In its current blocking state, it could be noticed by a peer that no - # messages are send for a while, and thus that mining PoW is done, and - # that next messages contains a message originated from this peer - # zah: It would be hard to execute this in a background thread at the - # moment. We'll need a way to send custom "tasks" to the async message - # loop (e.g. AD2 support for AsyncChannels). - if not msg.sealEnvelope(powTime, powTarget): - return false - - # need to check expiry after mining PoW - if not msg.env.valid(): - return false - - result = node.queueMessage(msg) - - # Allows light nodes to post via untrusted messages packet. - # Queue gets processed immediatly as the node sends only its own messages, - # so the privacy ship has already sailed anyhow. - # TODO: - # - Could be still a concern in terms of efficiency, if multiple messages - # need to be send. - # - For Waku Mode, the checks in processQueue are rather useless as the - # idea is to connect only to 1 node? Also refactor in that case. - if node.protocolState(Waku).config.isLightNode: - for peer in node.peers(Waku): - peer.processQueue() + return node.postEncoded(ttl, topic, payload.get(), powTime, powTarget, targetPeer) else: error "Encoding of payload failed" return false