Repository: Bitmessage/PyBitmessage

Issue:
Incorrect stream offset in `HandleDisseminatePreEncryptedMsg`
A user wanted to use `HandleDisseminatePreEncryptedMsg` API call and it didn't appear to work. After reviewing the code and specification, it looks like when decoding destination stream:

https://github.com/Bitmessage/PyBitmessage/blob/a5a56fe7f1af86f5b8c40ee7d6307df62e0e6b3e/src/api.py#L1313

it uses a protocol v2 specification, and not protocol v3. In v2, the offset for the stream for a message starts at byte 16, but in v3 it starts at a variable position (at the moment it usually is byte 21, which allows for a quick workaround). This API method was introduced around the same time that v3 came out so I guess someone wasn't paying attention.

References:
v2 specification from wiki: https://wiki.bitmessage.org/index.php?title=Protocol_specification&oldid=23575#msg
v3 specification from rtd: https://pybitmessage.readthedocs.io/en/v0.6/protocol.html#object


Discussion:

Resolving patch:
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -70,7 +70,7 @@
 import xmlrpclib
 from binascii import hexlify, unhexlify
 from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
-from struct import pack
+from struct import pack, unpack
 
 import defaults
 import helper_inbox
@@ -1275,7 +1275,7 @@ def ListSubscriptions(self):
         return {'subscriptions': data}
 
     @command('disseminatePreEncryptedMsg')
-    def HandleDisseminatePreEncryptedMsg(
+    def HandleDisseminatePreEncryptedMsg(  # pylint: disable=too-many-locals
         self, encryptedPayload, requiredAverageProofOfWorkNonceTrialsPerByte,
             requiredPayloadLengthExtraBytes):
         """Handle a request to disseminate an encrypted message"""
@@ -1286,13 +1286,22 @@ def HandleDisseminatePreEncryptedMsg(
         # to the rest of the Bitmessage network as if it had generated
         # the message itself. Please do not yet add this to the api doc.
         encryptedPayload = self._decode(encryptedPayload, "hex")
+        expiresTime = unpack('>Q', encryptedPayload[0:8])[0]
+        objectType = unpack('>I', encryptedPayload[8:12])[0]
+        TTL = expiresTime - time.time() + 300  # a bit of extra padding
         # Let us do the POW and attach it to the front
         target = 2**64 / (
-            (
-                len(encryptedPayload)
-                + requiredPayloadLengthExtraBytes
-                + 8
-            ) * requiredAverageProofOfWorkNonceTrialsPerByte)
+            requiredAverageProofOfWorkNonceTrialsPerByte * (
+                len(encryptedPayload) + 8
+                + requiredPayloadLengthExtraBytes + ((
+                    TTL * (
+                        len(encryptedPayload) + 8
+                        + requiredPayloadLengthExtraBytes
+                    )) / (2 ** 16))
+            ))
+        logger.debug("expiresTime: %s", expiresTime)
+        logger.debug("TTL: %s", TTL)
+        logger.debug("objectType: %s", objectType)
         logger.info(
             '(For msg message via API) Doing proof of work. Total  required'
             ' difficulty: %s\nRequired small message difficulty: %s',
@@ -1311,18 +1320,22 @@ def HandleDisseminatePreEncryptedMsg(
             nonce / (time.time() - powStartTime)
         )
         encryptedPayload = pack('>Q', nonce) + encryptedPayload
-        toStreamNumber = decodeVarint(encryptedPayload[16:26])[0]
+        parserPos = 20
+        _, objectVersionLength = decodeVarint(
+            encryptedPayload[parserPos:parserPos + 10])
+        parserPos += objectVersionLength
+        toStreamNumber, _ = decodeVarint(
+            encryptedPayload[parserPos:parserPos + 10])
         inventoryHash = calculateInventoryHash(encryptedPayload)
-        objectType = 2
-        TTL = 2.5 * 24 * 60 * 60
         Inventory()[inventoryHash] = (
             objectType, toStreamNumber, encryptedPayload,
-            int(time.time()) + TTL, ''
+            expiresTime, ''
         )
         logger.info(
             'Broadcasting inv for msg(API disseminatePreEncryptedMsg'
             ' command): %s', hexlify(inventoryHash))
         queues.invQueue.put((toStreamNumber, inventoryHash))
+        return hexlify(inventoryHash)
 
     @command('trashSentMessageByAckData')
     def HandleTrashSentMessageByAckDAta(self, ackdata):


Test patch: