Repository: Bitmessage/PyBitmessage

Issue:
PyBitmessage does not wait for "verack" and has non specified timeout.
I found a (small) bug in PyBitmessage Version 0.4.2 (or in the protocol specification)

Bug description:

The protocol specification states:
... No futher communication is possible until both peers have exchanged their version.

But PyBitmessage Version 0.4.2 sends an "addr" or an "inv" 

message, without having received my "verack" (I'm sure I've not send it because I've not yet implemented it)

It's not a "showstopper" type of bug. But as PyBitmessage is assumed to be the "reference implementation" we should clearify what's the correct behaviour. Maybe the "verack" message is optional? Than this should be mentioned in the protocol specification.

In addition to that, PyBitmessage closes the connection after 20 seconds. That looks like a type of "timeout". Why? There is none specified (maybe we should specify one).

Trace:

``````
First run:

Message type Version:
127.0.0.1:36271 -> 127.0.0.1:8444
0x000000: e9 be b4 d9 76 65 72 73 69 6f 6e 00 00 00 00 00 ....version.....
0x000010: 00 00 00 7f 0e 80 43 17 00 00 00 02 00 00 00 00 ......C.........
0x000020: 00 00 00 01 00 00 00 00 53 60 e8 23 00 00 00 00 ........S`.#....
0x000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ................
0x000040: 01 00 00 7f fc 20 00 00 00 00 00 00 00 01 00 00 ..... ..........
0x000050: 00 00 00 00 00 00 00 00 ff ff 01 00 00 7f af 8d ................
0x000060: e2 2b a4 e2 64 b6 ad 16 2c 68 74 74 70 73 3a 2f .+..d...,https:/
0x000070: 2f 67 69 74 68 75 62 2e 63 6f 6d 2f 54 68 6f 6d /github.com/Thom
0x000080: 61 73 2d 41 73 74 61 64 65 2f 62 69 74 6d 65 73 as-Astade/bitmes
0x000090: 73 61 67 65 64 01 01                            saged..         
expect a message of size: 24
Received Message:
0x000000: e9 be b4 d9 76 65 72 61 63 6b 00 00 00 00 00 00 ....verack......
0x000010: 00 00 00 00 cf 83 e1 35                         .......5        
expect a message of size: 127
Received Message:
0x000000: e9 be b4 d9 76 65 72 73 69 6f 6e 00 00 00 00 00 ....version.....
0x000010: 00 00 00 67 1d df c0 28 00 00 00 02 00 00 00 00 ...g...(........
0x000020: 00 00 00 01 00 00 00 00 53 60 e8 23 00 00 00 00 ........S`.#....
0x000030: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ff ff ................
0x000040: 7f 00 00 01 8d af 00 00 00 00 00 00 00 01 00 00 ................
0x000050: 00 00 00 00 00 00 00 00 ff ff 7f 00 00 01 20 fc .............. .
0x000060: 8d b3 73 9e f3 19 67 b1 14 2f 50 79 42 69 74 6d ..s...g../PyBitm
0x000070: 65 73 73 61 67 65 3a 30 2e 34 2e 32 2f 01 01    essage:0.4.2/.. 
expect a message of size: 63
Received Message:
0x000000: e9 be b4 d9 61 64 64 72 00 00 00 00 00 00 00 00 ....addr........
0x000010: 00 00 00 27 9c cb ff 00 01 00 00 00 00 53 60 e8 ...'.........S`.
0x000020: 28 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 (...............
0x000030: 00 00 00 00 00 00 00 ff ff 4d bf 34 58 20 fc    .........M.4X . 


Next run:

Message type Version:
127.0.0.1:37291 -> 127.0.0.1:8444
0x000000: e9 be b4 d9 76 65 72 73 69 6f 6e 00 00 00 00 00 ....version.....
0x000010: 00 00 00 7f 24 04 31 3a 00 00 00 02 00 00 00 00 ....$.1:........
0x000020: 00 00 00 01 00 00 00 00 53 60 ee ba 00 00 00 00 ........S`......
0x000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ................
0x000040: 01 00 00 7f fc 20 00 00 00 00 00 00 00 01 00 00 ..... ..........
0x000050: 00 00 00 00 00 00 00 00 ff ff 01 00 00 7f ab 91 ................
0x000060: a0 ff 0e a0 99 74 2d 95 2c 68 74 74 70 73 3a 2f .....t-.,https:/
0x000070: 2f 67 69 74 68 75 62 2e 63 6f 6d 2f 54 68 6f 6d /github.com/Thom
0x000080: 61 73 2d 41 73 74 61 64 65 2f 62 69 74 6d 65 73 as-Astade/bitmes
0x000090: 73 61 67 65 64 01 01                            saged..         
expect a message of size: 24
Received Message:
0x000000: e9 be b4 d9 76 65 72 61 63 6b 00 00 00 00 00 00 ....verack......
0x000010: 00 00 00 00 cf 83 e1 35                         .......5        
expect a message of size: 127
Received Message:
0x000000: e9 be b4 d9 76 65 72 73 69 6f 6e 00 00 00 00 00 ....version.....
0x000010: 00 00 00 67 1e f8 8e 49 00 00 00 02 00 00 00 00 ...g...I........
0x000020: 00 00 00 01 00 00 00 00 53 60 ee ba 00 00 00 00 ........S`......
0x000030: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ff ff ................
0x000040: 7f 00 00 01 91 ab 00 00 00 00 00 00 00 01 00 00 ................
0x000050: 00 00 00 00 00 00 00 00 ff ff 7f 00 00 01 20 fc .............. .
0x000060: 8d b3 73 9e f3 19 67 b1 14 2f 50 79 42 69 74 6d ..s...g../PyBitm
0x000070: 65 73 73 61 67 65 3a 30 2e 34 2e 32 2f 01 01    essage:0.4.2/.. 
expect a message of size: 89
Received Message:
0x000000: e9 be b4 d9 69 6e 76 00 00 00 00 00 00 00 00 00 ....inv.........
0x000010: 00 00 00 41 63 1f 9c 6c 02 58 18 4c a5 ef 61 c6 ...Ac..l.X.L..a.
0x000020: 90 90 52 03 9f 57 1d 56 3a 93 fa a6 45 3a 36 2c ..R..W.V:...E:6,
0x000030: e8 2b 90 bf b4 61 86 a5 29 da e5 66 b4 9b 38 85 .+...a..)..f..8.
0x000040: 31 c3 76 01 f1 35 11 66 a2 41 02 d6 ba 56 41 26 1.v..5.f.A...VA&
0x000050: 18 ec 96 40 fa d5 3b ef 25                      ...@..;.%       

```]
``````



Discussion:

Resolving patch:
diff --git a/src/class_receiveDataThread.py b/src/class_receiveDataThread.py
--- a/src/class_receiveDataThread.py
+++ b/src/class_receiveDataThread.py
@@ -147,9 +147,9 @@ def processData(self):
         with shared.printLock:
             print 'remoteCommand', repr(remoteCommand.replace('\x00', '')), ' from', self.peer
 
-        if remoteCommand == 'version\x00\x00\x00\x00\x00':
+        if remoteCommand == 'version\x00\x00\x00\x00\x00' and not self.connectionIsOrWasFullyEstablished:
             self.recversion(self.data[24:self.payloadLength + 24])
-        elif remoteCommand == 'verack\x00\x00\x00\x00\x00\x00':
+        elif remoteCommand == 'verack\x00\x00\x00\x00\x00\x00' and not self.connectionIsOrWasFullyEstablished:
             self.recverack()
         elif remoteCommand == 'addr\x00\x00\x00\x00\x00\x00\x00\x00' and self.connectionIsOrWasFullyEstablished:
             self.recaddr(self.data[24:self.payloadLength + 24])
@@ -237,7 +237,12 @@ def recverack(self):
             self.connectionFullyEstablished()
 
     def connectionFullyEstablished(self):
+        if self.connectionIsOrWasFullyEstablished:
+            # there is no reason to run this function a second time
+            return
         self.connectionIsOrWasFullyEstablished = True
+        # Command the corresponding sendDataThread to set its own connectionIsOrWasFullyEstablished variable to True also
+        self.sendDataThreadQueue.put((0, 'connectionIsOrWasFullyEstablished', 'no data'))
         if not self.initiatedConnection:
             shared.clientHasReceivedIncomingConnections = True
             shared.UISignalQueue.put(('setStatusIcon', 'green'))
@@ -301,8 +306,9 @@ def sendBigInv(self):
             self.sendinvMessageToJustThisOnePeer(
                 numberOfObjectsInInvMessage, payload)
 
-    # Self explanatory. Notice that there is also a broadcastinv function for
-    # broadcasting invs to everyone in our stream.
+    # Used to send a big inv message when the connection with a node is 
+    # first fully established. Notice that there is also a broadcastinv 
+    # function for broadcasting invs to everyone in our stream.
     def sendinvMessageToJustThisOnePeer(self, numberOfObjects, payload):
         payload = encodeVarint(numberOfObjects) + payload
         headerData = '\xe9\xbe\xb4\xd9'  # magic bits, slighly different from Bitcoin's magic bits.
@@ -665,7 +671,10 @@ def recaddr(self, data):
             print 'knownNodes currently has', len(shared.knownNodes[self.streamNumber]), 'nodes for this stream.'
 
 
-    # Send a big addr message to our peer
+    # Send a huge addr message to our peer. This is only used 
+    # when we fully establish a connection with a 
+    # peer (with the full exchange of version and verack 
+    # messages).
     def sendaddr(self):
         addrsInMyStream = {}
         addrsInChildStreamLeft = {}
@@ -750,58 +759,68 @@ def recversion(self, data):
         if len(data) < 83:
             # This version message is unreasonably short. Forget it.
             return
-        elif not self.verackSent:
-            self.remoteProtocolVersion, = unpack('>L', data[:4])
-            if self.remoteProtocolVersion <= 1:
-                shared.broadcastToSendDataQueues((0, 'shutdown', self.peer))
-                with shared.printLock:
-                    print 'Closing connection to old protocol version 1 node: ', self.peer
-                return
-            # print 'remoteProtocolVersion', self.remoteProtocolVersion
-            self.myExternalIP = socket.inet_ntoa(data[40:44])
-            # print 'myExternalIP', self.myExternalIP
-            self.remoteNodeIncomingPort, = unpack('>H', data[70:72])
-            # print 'remoteNodeIncomingPort', self.remoteNodeIncomingPort
-            useragentLength, lengthOfUseragentVarint = decodeVarint(
-                data[80:84])
-            readPosition = 80 + lengthOfUseragentVarint
-            useragent = data[readPosition:readPosition + useragentLength]
-            readPosition += useragentLength
-            numberOfStreamsInVersionMessage, lengthOfNumberOfStreamsInVersionMessage = decodeVarint(
-                data[readPosition:])
-            readPosition += lengthOfNumberOfStreamsInVersionMessage
-            self.streamNumber, lengthOfRemoteStreamNumber = decodeVarint(
-                data[readPosition:])
+        if self.verackSent:
+            """
+            We must have already processed the remote node's version message.
+            There might be a time in the future when we Do want to process
+            a new version message, like if the remote node wants to update
+            the streams in which they are interested. But for now we'll
+            ignore this version message
+            """ 
+            return
+        self.remoteProtocolVersion, = unpack('>L', data[:4])
+        if self.remoteProtocolVersion <= 1:
+            shared.broadcastToSendDataQueues((0, 'shutdown', self.peer))
             with shared.printLock:
-                print 'Remote node useragent:', useragent, '  stream number:', self.streamNumber
+                print 'Closing connection to old protocol version 1 node: ', self.peer
+            return
+        # print 'remoteProtocolVersion', self.remoteProtocolVersion
+        self.myExternalIP = socket.inet_ntoa(data[40:44])
+        # print 'myExternalIP', self.myExternalIP
+        self.remoteNodeIncomingPort, = unpack('>H', data[70:72])
+        # print 'remoteNodeIncomingPort', self.remoteNodeIncomingPort
+        useragentLength, lengthOfUseragentVarint = decodeVarint(
+            data[80:84])
+        readPosition = 80 + lengthOfUseragentVarint
+        useragent = data[readPosition:readPosition + useragentLength]
+        readPosition += useragentLength
+        numberOfStreamsInVersionMessage, lengthOfNumberOfStreamsInVersionMessage = decodeVarint(
+            data[readPosition:])
+        readPosition += lengthOfNumberOfStreamsInVersionMessage
+        self.streamNumber, lengthOfRemoteStreamNumber = decodeVarint(
+            data[readPosition:])
+        with shared.printLock:
+            print 'Remote node useragent:', useragent, '  stream number:', self.streamNumber
 
-            if self.streamNumber != 1:
-                shared.broadcastToSendDataQueues((0, 'shutdown', self.peer))
-                with shared.printLock:
-                    print 'Closed connection to', self.peer, 'because they are interested in stream', self.streamNumber, '.'
-                return
-            shared.connectedHostsList[
-                self.peer.host] = 1  # We use this data structure to not only keep track of what hosts we are connected to so that we don't try to connect to them again, but also to list the connections count on the Network Status tab.
-            # If this was an incoming connection, then the sendData thread
-            # doesn't know the stream. We have to set it.
-            if not self.initiatedConnection:
-                shared.broadcastToSendDataQueues((
-                    0, 'setStreamNumber', (self.peer, self.streamNumber)))
-            if data[72:80] == shared.eightBytesOfRandomDataUsedToDetectConnectionsToSelf:
-                shared.broadcastToSendDataQueues((0, 'shutdown', self.peer))
-                with shared.printLock:
-                    print 'Closing connection to myself: ', self.peer
-                return
-            self.sendDataThreadQueue.put((0, 'setRemoteProtocolVersion', self.remoteProtocolVersion))
+        if self.streamNumber != 1:
+            shared.broadcastToSendDataQueues((0, 'shutdown', self.peer))
+            with shared.printLock:
+                print 'Closed connection to', self.peer, 'because they are interested in stream', self.streamNumber, '.'
+            return
+        shared.connectedHostsList[
+            self.peer.host] = 1  # We use this data structure to not only keep track of what hosts we are connected to so that we don't try to connect to them again, but also to list the connections count on the Network Status tab.
+        # If this was an incoming connection, then the sendData thread
+        # doesn't know the stream. We have to set it.
+        if not self.initiatedConnection:
+            self.sendDataThreadQueue.put((0, 'setStreamNumber', self.streamNumber))
+        if data[72:80] == shared.eightBytesOfRandomDataUsedToDetectConnectionsToSelf:
+            shared.broadcastToSendDataQueues((0, 'shutdown', self.peer))
+            with shared.printLock:
+                print 'Closing connection to myself: ', self.peer
+            return
+        
+        # The other peer's protocol version is of interest to the sendDataThread but we learn of it
+        # in this version message. Let us inform the sendDataThread.
+        self.sendDataThreadQueue.put((0, 'setRemoteProtocolVersion', self.remoteProtocolVersion))
 
-            shared.knownNodesLock.acquire()
-            shared.knownNodes[self.streamNumber][shared.Peer(self.peer.host, self.remoteNodeIncomingPort)] = int(time.time())
-            shared.needToWriteKnownNodesToDisk = True
-            shared.knownNodesLock.release()
+        shared.knownNodesLock.acquire()
+        shared.knownNodes[self.streamNumber][shared.Peer(self.peer.host, self.remoteNodeIncomingPort)] = int(time.time())
+        shared.needToWriteKnownNodesToDisk = True
+        shared.knownNodesLock.release()
 
-            self.sendverack()
-            if self.initiatedConnection == False:
-                self.sendversion()
+        self.sendverack()
+        if self.initiatedConnection == False:
+            self.sendversion()
 
     # Sends a version message
     def sendversion(self):
diff --git a/src/class_sendDataThread.py b/src/class_sendDataThread.py
--- a/src/class_sendDataThread.py
+++ b/src/class_sendDataThread.py
@@ -25,6 +25,7 @@ def __init__(self, sendDataThreadQueue):
         self.data = ''
         self.objectHashHolderInstance = objectHashHolder(self.sendDataThreadQueue)
         self.objectHashHolderInstance.start()
+        self.connectionIsOrWasFullyEstablished = False
 
 
     def setup(
@@ -71,16 +72,6 @@ def run(self):
                     if data == self.peer or data == 'all':
                         with shared.printLock:
                             print 'sendDataThread (associated with', self.peer, ') ID:', id(self), 'shutting down now.'
-
-                        try:
-                            self.sock.shutdown(socket.SHUT_RDWR)
-                            self.sock.close()
-                        except:
-                            pass
-                        shared.sendDataQueues.remove(self.sendDataThreadQueue)
-                        with shared.printLock:
-                            print 'len of sendDataQueues', len(shared.sendDataQueues)
-
                         break
                 # When you receive an incoming connection, a sendDataThread is
                 # created even though you don't yet know what stream number the
@@ -89,12 +80,9 @@ def run(self):
                 # will continue on with the connection and will set the
                 # streamNumber of this send data thread here:
                 elif command == 'setStreamNumber':
-                    peerInMessage, specifiedStreamNumber = data
-                    if peerInMessage == self.peer:
-                        with shared.printLock:
-                            print 'setting the stream number in the sendData thread (ID:', id(self), ') to', specifiedStreamNumber
-
-                        self.streamNumber = specifiedStreamNumber
+                    self.streamNumber = data
+                    with shared.printLock:
+                        print 'setting the stream number in the sendData thread (ID:', id(self), ') to', self.streamNumber 
                 elif command == 'setRemoteProtocolVersion':
                     specifiedRemoteProtocolVersion = data
                     with shared.printLock:
@@ -103,6 +91,9 @@ def run(self):
                 elif command == 'advertisepeer':
                     self.objectHashHolderInstance.holdPeer(data)
                 elif command == 'sendaddr':
+                    if not self.connectionIsOrWasFullyEstablished:
+                        # not sending addr because we haven't sent and heard a verack from the remote node yet
+                        return
                     numberOfAddressesInAddrMessage = len(
                         data)
                     payload = ''
@@ -127,17 +118,13 @@ def run(self):
                         self.lastTimeISentData = int(time.time())
                     except:
                         print 'sendaddr: self.sock.sendall failed'
-                        try:
-                            self.sock.shutdown(socket.SHUT_RDWR)
-                            self.sock.close()
-                        except:
-                            pass
-                        shared.sendDataQueues.remove(self.sendDataThreadQueue)
-                        print 'sendDataThread thread (ID:', str(id(self)) + ') ending now. Was connected to', self.peer
                         break
                 elif command == 'advertiseobject':
                     self.objectHashHolderInstance.holdHash(data)
                 elif command == 'sendinv':
+                    if not self.connectionIsOrWasFullyEstablished:
+                        # not sending inv because we haven't sent and heard a verack from the remote node yet
+                        return
                     payload = ''
                     for hash in data:
                         if hash not in self.someObjectsOfWhichThisRemoteNodeIsAlreadyAware:
@@ -153,13 +140,6 @@ def run(self):
                             self.lastTimeISentData = int(time.time())
                         except:
                             print 'sendinv: self.sock.sendall failed'
-                            try:
-                                self.sock.shutdown(socket.SHUT_RDWR)
-                                self.sock.close()
-                            except:
-                                pass
-                            shared.sendDataQueues.remove(self.sendDataThreadQueue)
-                            print 'sendDataThread thread (ID:', str(id(self)) + ') ending now. Was connected to', self.peer
                             break
                 elif command == 'pong':
                     self.someObjectsOfWhichThisRemoteNodeIsAlreadyAware.clear() # To save memory, let us clear this data structure from time to time. As its function is to help us keep from sending inv messages to peers which sent us the same inv message mere seconds earlier, it will be fine to clear this data structure from time to time.
@@ -174,29 +154,28 @@ def run(self):
                             self.lastTimeISentData = int(time.time())
                         except:
                             print 'send pong failed'
-                            try:
-                                self.sock.shutdown(socket.SHUT_RDWR)
-                                self.sock.close()
-                            except:
-                                pass
-                            shared.sendDataQueues.remove(self.sendDataThreadQueue)
-                            print 'sendDataThread thread', self, 'ending now. Was connected to', self.peer
                             break
                 elif command == 'sendRawData':
                     try:
                         self.sock.sendall(data)
                         self.lastTimeISentData = int(time.time())
                     except:
-                        try:
-                            self.sock.shutdown(socket.SHUT_RDWR)
-                            self.sock.close()
-                        except:
-                            pass
-                        shared.sendDataQueues.remove(self.sendDataThreadQueue)
                         print 'Sending of data to', self.peer, 'failed. sendDataThread thread', self, 'ending now.' 
                         break
+                elif command == 'connectionIsOrWasFullyEstablished':
+                    with shared.printLock:
+                        print 'sendDataThread (associated with', self.peer, ') ID:', id(self), 'setting connectionIsOrWasFullyEstablished to True.'
+                    self.connectionIsOrWasFullyEstablished = True
             else:
                 with shared.printLock:
                     print 'sendDataThread ID:', id(self), 'ignoring command', command, 'because the thread is not in stream', deststream
 
+        try:
+            self.sock.shutdown(socket.SHUT_RDWR)
+            self.sock.close()
+        except:
+            pass
+        shared.sendDataQueues.remove(self.sendDataThreadQueue)
+        with shared.printLock:
+            print 'Number of queues remaining in sendDataQueues:', len(shared.sendDataQueues)
         self.objectHashHolderInstance.close()
diff --git a/src/defaultKnownNodes.py b/src/defaultKnownNodes.py
--- a/src/defaultKnownNodes.py
+++ b/src/defaultKnownNodes.py
@@ -11,17 +11,17 @@ def createDefaultKnownNodes(appdata):
     ############## Stream 1 ################
     stream1 = {}
 
-    stream1[shared.Peer('176.31.246.114', 8444)] = int(time.time())
-    stream1[shared.Peer('109.229.197.133', 8444)] = int(time.time())
-    stream1[shared.Peer('174.3.101.111', 8444)] = int(time.time())
-    stream1[shared.Peer('90.188.238.79', 7829)] = int(time.time())
-    stream1[shared.Peer('184.75.69.2', 8444)] = int(time.time())
-    stream1[shared.Peer('60.225.209.243', 8444)] = int(time.time())
-    stream1[shared.Peer('5.145.140.218', 8444)] = int(time.time())
-    stream1[shared.Peer('5.19.255.216', 8444)] = int(time.time())
-    stream1[shared.Peer('193.159.162.189', 8444)] = int(time.time())
-    stream1[shared.Peer('86.26.15.171', 8444)] = int(time.time())
-
+    #stream1[shared.Peer('2604:2000:1380:9f:82e:148b:2746:d0c7', 8080)] = int(time.time())
+    stream1[shared.Peer('68.33.0.104', 8444)] = int(time.time())
+    stream1[shared.Peer('97.77.34.35', 8444)] = int(time.time())
+    stream1[shared.Peer('71.232.195.131', 8444)] = int(time.time())
+    stream1[shared.Peer('192.241.231.39', 8444)] = int(time.time())
+    stream1[shared.Peer('75.66.0.116', 8444)] = int(time.time())
+    stream1[shared.Peer('182.169.23.102', 8444)] = int(time.time())
+    stream1[shared.Peer('75.95.134.9', 8444)] = int(time.time())
+    stream1[shared.Peer('46.236.100.108', 48444)] = int(time.time())
+    stream1[shared.Peer('66.108.53.42', 8080)] = int(time.time())
+    
     ############# Stream 2 #################
     stream2 = {}
     # None yet


Test patch: