Exploring P2P messages

(in Dogecoin)

By Lola

P2P Network

Full or SPV nodes communicate using a peer to peer network protocol via TCP.

They send each other messages to retrieve new blocks, transactions, peers, ...

It is not ... JSON RPC

 

The RPC API a user to talk to their node.

{
    "jsonrpc": "1.0",
    "id": "curltest",
    "method": "getblock",
    "params": ["00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09"]
}

All the messages types

  • version
  • verack
  • addr
  • inv
  • getdata
  • notfound
  • getblocks
  • getheaders
  • tx
  • block
  • headers
  • getaddr
  • mempool
  • ping
  • pong
  • reject
  • filterload
  • filteradd
  • filterclear
  • merkleblock
  • sendheaders
  • feefilters
  • ...

Message structure

Field size Description Data type
4 magic bytes Unsigned Integer (32 bits)
12 command String (12 char)
4 length Unsigned Integer (32 bits)
4 checksum (double sha 256) Unsigned Integer (32 bits)
? payload Bytes

Example

C0C0C0C076657261636B000000000000000000005DF6E0E2
^ "verack" message

C0C0C0C0 <-- Dogecoin mainnet magic bytes
76657261636B000000000000 <-- "verack" command
00000000 <-- payload length 
5DF6E0E2 <-- checksum of payload

Writting our custom Dogecoin node in python

What the node will do :

- Connect to another node

- Ask for blocks

- Deserialize blocks

Connect to a node

Connect to a node

import socket
import struct
import sys
import time

MSG_BLOCK_TYPE = 2
HOST = "51.158.74.86"
PORT = 18444
GENESIS_BLOCK_HASH = "3d2160a3b5dc4a9d62e7e66a295f70313ac808440ef7400d6c0772171ce973a5"

print("Attempt to connect to node")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

# Prepare Version Message
version_message = prepareVersionMessage(HOST, PORT)
payload = preparePayload(version_message, b'version')
# Send Version Message
s.send(payload)

# Received Version Message
wait_for(s, b'version')

payload = preparePayload(b'', b'verack')
s.send(payload)
wait_for(s, b'verack')

Version message

Field size Description Data type
4 version Integer (32 bits)
8 services Unsigned Integer (64 bits)
8 timestamp
26 remote node address
26 local node address
8 nonce unsigned integer (64bits)
? user agent String
4 start height Integer (32 bits)
1 relay bool

Getting blocks

getblocks message

Field size Description Data type
4 version Unsigned Integer (32 bits)
1+ hash count Compact Size
32+ hashes Char[32]
32 stop hash Char[32]

inv/getdata message

Field size Description Data type
1+ count Compact Size
36 x ? inventory (hash+type)

block message

Field Size Description Data type
4 version Integer (32 bit)
32 previous block Char[32]
32 merkle root Char[32]
4 timestamp Unsigned Integer (32 bits)
4 bits Unsigned Integer (32 bits)
4 nonce Unsigned Integer (32 bits)
1+ transaction count Compact Size
? transactions Transaction
blocks_count = 0
next_hash = GENESIS_BLOCK_HASH

print("Asking for a new batch")
get_blocks_message = prepareGetBlockMessage(next_hash)
payload = preparePayload(get_blocks_message, b'getblocks')
s.send(payload)

l = 0
while l <= 1:
    # if 1 count it means it just tell us about a new block
    inv_message = wait_for(s, b'inv')[24:]
    l, offset = getCompactSize(inv_message[0:9])

print("l value is {}".format(l))

if l == 501:
    # There are giving us an extra block (probably the latest block discover)
    l = 500

for i in range(l):
    (type, hash) = struct.unpack('I32s', inv_message[offset+(36*i):offset+(36*(i+1))])
    # do we really need that ?
    assert(type == MSG_BLOCK_TYPE)
    if i == l-1:
        next_hash = hash[::-1].hex()

# We can send the message now
payload = preparePayload(inv_message, b'getdata')
s.send(payload)
count = l
log = ''

# Wait for 500 responses !
while count > 0:

    # Received Inv Message
    data = wait_for(s, b'block')

    if data == 0:
        print("ERROR : missing block")
        sys.exit()

    # verify data
    magic, command, m_length, chcksm = struct.unpack('4s12sI4s', data[0:24])
    if b'block' not in command:
        print("ERROR : wrong command")
        sys.exit()

    if len(data[24:]) != m_length:
        print("ERROR : Wrong length")
        sys.exit()


    block_message = data[24:]
    count -= 1
    hash = checksum(block_message[0:80])[::-1].hex()
    print("Count : {} - Block hash {}".format(blocks_count + 500 - count, hash))
    log += "----- {} -----\n".format(hash)

Getting all the blocks

Unpacking blocks

    print("Count : {} - Block hash {}".format(blocks_count + 500 - count, hash))
    log += "----- {} -----\n".format(hash)


    txs = unpackBlock(block_message, False)


    for tx in txs:
        log += "{}\n".format(tx.hex())


print(log)

The unpackBlock functions return all the transactions in the block.

What can you do next:

- Parse AuxPoW (auxiliary proof of work) block to go on testnet/mainnet

- Collect data from nodes

- Follow your dream

Find the transaction with the OP_RETURN code in its script.

 

Send the flag associated to it for a surprise.

Made with Slides.com