When establishing connection, we first need to send a version message to the node we wish to connect to. But keeping our connection alive will require the use of 3 more messages: ping, pong and VerAck.
The VerAck message is the most simple type of message, it’s basically an empty message, it has no payload, only a header. Alexis and I have decided that any type of message will have its own file to construct and parse its payload, even if that message doesn’t have any payload. It helps us to maintain the structure of our project.
class EncodeVerack: def __init__(self): self.command_name = "verack" # Verack messages have an empty payload def forge(self): return "" # No need this class because, like GetAddr, there is no payload class DecodeVerack: def __init__(self, payload=""): pass
As you can see, the function
forge will return an empty string and the class
DecodeVerAck won’t do anything.
Pay attention that even though the VerAck payload is empty, this message still needs to have a header. The header will contain 4 bytes for the Magic number (or starting string), 12 bytes for the command name (VerAck), 4 bytes for the size of the payload (which in this case will be zero) and another 4 bytes for the checksum (which in this case will always be the same).
Ping and Pong
One node can always send a ping request to the other. That is how peers on the network can check if the connection is still alive. Ping request is a nothing more than a message, just like any other Bitcoin message, that contains one field:
|Size (Bytes)||Name||Data type||Description|
|8||nonce||uint_64||A random number|
Alice sends Bob a ping message. This ping message will contain 8 bytes of random number. Bob will receive this ping message from Alice, and will acknowledge her request by sending her a pong message, this pong message will contain the same random number that Alice sent to Bob.
Let’s have a look at the code implementation
import random from Utils.dataTypes import * class EncodePing: def __init__(self): self.command_name = "ping" self.nonce = to_uint64(random.getrandbits(64)) def forge(self): return self.nonce class DecodePing: def __init__(self, payload): self.nonce = payload.read(8)
When we want to construct a ping message we’ll use the class
EncodePing which will set the field
nonce to be 8 bytes long random number (64 bit = 8 bytes). When we’ll receive a ping message, we’ll use the class
DecodePing with the payload of the incoming message to find out the
nonce of that ping message (We’re only reading the nonce in the ping message, we haven’t used it yet). We’ll use this
nonce when we’ll construct our returning (pong) message.
from Utils.dataTypes import * class EncodePong: def __init__(self, ping_received): self.command_name = "pong" self.nonce = ping_received def forge(self): return self.nonce class DecodedPong: def __init__(self, pong_received): self.command_name = "pong" self.nonce = read_uint64(pong_received.read(8)) def get_decoded_info(self): return "\npong :\t\t %s" % self.nonce
EncodePong will take the payload of the incoming ping message and will assign it to be the nonce of the pong message (the message that we’ll send back). The
DecodedPong class will receive the payload of the pong and will assign it to the self variable
nonce. We can use the
get_decoded_info function to display this nonce number. Because the pong is just a returning message, there’s nothing else we need to do with it.