Commit 7d015edd authored by Romain Bignon's avatar Romain Bignon

initial commit

parents
__pycache__
*.pyc
from ipaddress import ip_address
import struct
import time
from copy import deepcopy
from collections import OrderedDict
from termcolor import colored
class Field:
_creation_counter = 0
def __init__(self):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
self.data = b''
def get(self):
return self.unserialize(self.data)[0]
def parse(self, data):
length = self.unserialize(data)[1]
self.data = data[:length]
return length
def set(self, val):
self.data = self.serialize(val)
def repr(self):
return self.get()
@classmethod
def serialize(cls, val):
raise NotImplementedError()
@classmethod
def unserialize(cls, val):
raise NotImplementedError()
class MessageMetaClass(type):
def __new__(cls, name, bases, attrs):
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in list(attrs.items()) if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
new_class = type.__new__(cls, name, bases, attrs)
if new_class._fields is None:
new_class._fields = OrderedDict()
else:
new_class._fields = deepcopy(new_class._fields)
new_class._fields.update(fields)
return new_class
class Message(metaclass=MessageMetaClass):
_fields = None
def __init__(self):
self._fields = deepcopy(self._fields)
def __getattr__(self, name):
if name in self._fields:
return self._fields[name].get()
else:
raise AttributeError("'%s' object has no attribute '%s'" % (
self.__class__.__name__, name))
def __setattr__(self, name, value):
try:
attr = (self._fields or {})[name]
except KeyError:
object.__setattr__(self, name, value)
else:
attr.set(value)
def __str__(self):
return '<%s%s%s>' % (type(self).__name__,
' ' if self._fields else '',
' '.join('%s=%s' % (name, colored(value.repr(), 'magenta')) for name, value in self._fields.items()))
def dump(self):
data = b''
for field in self._fields.values():
data += field.data
return data
@classmethod
def parse(cls, func):
def inner(self, data):
msg = cls()
for field in msg._fields.values():
size = field.parse(data)
data = data[size:]
print(colored('<<<<<', 'green'), msg)
return func(self, msg)
return inner
class Struct(Field):
format = None
repr_format = 'hexa'
def __init__(self, repr_format=None):
super(Struct, self).__init__()
if repr_format:
self.repr_format = repr_format
def repr(self):
if self.repr_format == 'hexa':
return '0x%x' % self.get()
else:
return self.get()
@classmethod
def serialize(cls, val):
return struct.pack(cls.format, val)
@classmethod
def unserialize(cls, data):
length = struct.calcsize(cls.format)
ret = struct.unpack(cls.format, data[:length])
if len(ret) == 1:
ret = ret[0]
return ret, length
class UInt32(Struct): format = 'I'
class Int32(Struct): format = 'i'
class UInt64(Struct): format = 'Q'
class Int64(Struct): format = 'q'
class Bool(Struct): format = '?'
class Char(Struct): format = 'c'; repr_format=None
class InventoryVector(Struct):
format = 'I32s'
@classmethod
def unserialize(cls, payload):
(type, hash), length = super(InventoryVector, cls).unserialize(payload)
return Hash(type, int(hash.hex(), 16)), length
class Hash:
MSG_TX = 1
MSG_BLOCK = 2
MSG_FILTERED_BLOCK = 3
MSG_CMPCT_BLOCK = 4
def __init__(self, type, hash):
self.type = type
self.hash = hash
def __repr__(self):
return '0x%x' % self.hash
class VarInt(Field):
@classmethod
def serialize(cls, n):
if n < 0xfd:
return struct.pack('<B', n)
elif n < 0xffff:
return struct.pack('<cH', '\xfd', n)
elif n < 0xffffffff:
return struct.pack('<cI', '\xfe', n)
else:
return struct.pack('<cQ', '\xff', n)
@classmethod
def unserialize(cls, payload):
n0 = payload[0]
if n0 < 0xfd:
return n0, 1
elif n0 == 0xfd:
return struct.unpack('<H', payload[1:3])[0], 3
elif n0 == 0xfe:
return struct.unpack('<L', payload[1:5])[0], 5
else:
return struct.unpack('<Q', payload[1:5])[0], 7
class VarStr(Field):
@classmethod
def serialize(cls, val):
if isinstance(val, str):
val = val.encode()
return VarInt.serialize(len(val)) + val
@classmethod
def unserialize(cls, payload):
n, length = VarInt.unserialize(payload)
return payload[length:length+n].decode(), length + n
class NetAddr(Field):
@classmethod
def serialize(cls, val):
ipaddr, port = val
ipaddr_packed = ip_address(ipaddr).packed
if len(ipaddr_packed) < 16:
ipaddr_packed = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff' + ipaddr_packed
services = 1
return struct.pack('<Q', services) + ipaddr_packed + struct.pack('!H', port)
@classmethod
def unserialize(cls, payload):
assert len(payload) >= 26
services, ipaddr_packed, port = struct.unpack('!Q16sH', payload[:26])
if ipaddr_packed[:12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
ipaddr = ip_address(ipaddr_packed[12:])
else:
ipaddr = ip_address(ipaddr_packed)
return (ipaddr, port), 26
def repr(self):
ipaddr, port = self.get()
return '%s:%s' % (ipaddr, port)
class TimeNetAddr(NetAddr):
@classmethod
def serialize(cls, val):
data = NetAddr.serialize(val)
return struct.pack('I', int(time.time())) + data
@classmethod
def unserialize(cls, data):
un, length = NetAddr.unserialize(data[4:])
return un, length+4
class List(Field):
def __init__(self, klass):
super(List, self).__init__()
self.klass = klass
def serialize(self, elements):
data = VarInt.serialize(len(elements))
for el in elements:
data += self.klass.serialize(el)
return data
def unserialize(self, payload):
n, length = VarInt.unserialize(payload)
payload = payload[length:]
elements = []
for i in range(n):
el, el_length = self.klass.unserialize(payload)
elements.append(el)
payload = payload[el_length:]
return elements, length + n*el_length
from random import choice
import dns.resolver
class DNSSeed:
SERVERS = [
'seed.bitcoin.sipa.be',
'dnsseed.bluematt.me',
'dnsseed.bitcoin.dashjr.org',
'seed.bitcoinstats.com',
'seed.bitcoin.jonasschnelli.ch',
'seed.btc.petertodd.org',
'seed.bitcoin.sprovoost.nl',
'dnsseed.emzy.de',
]
def find_nodes(self):
server = choice(self.SERVERS)
answers = dns.resolver.query(server, 'A')
for rdata in answers:
yield rdata.address
import sys
import time
from contextlib import contextmanager
from termcolor import colored
@contextmanager
def log(message, success='done'):
print('%s... ' % message, end='', flush=True)
start = time.time()
try:
yield
except KeyboardInterrupt:
print(colored('abort', 'red'))
sys.exit(1)
except Exception as e:
print(colored('fail: %s' % e, 'red'))
raise
else:
print('%s %s' % (colored(success, 'green'),
colored('(%.2fs)' % (time.time() - start), 'blue')))
import asyncio
from .dnsseed import DNSSeed
from .node import OutboundNode
from .log import log
class Network:
def __init__(self):
self.nodes = []
async def start(self):
node = await self.connect_to_first_node()
if not node:
return
self.nodes.append(node)
node.send_version()
await asyncio.sleep(3600)
import pdb; pdb.set_trace()
async def connect_to_first_node(self):
dnsseed = DNSSeed()
loop = asyncio.get_running_loop()
for ipaddr in dnsseed.find_nodes():
try:
with log('Trying to connect to %s:8333' % ipaddr):
transport, protocol = await loop.create_connection(lambda: OutboundNode(), ipaddr, 8333)
except OSError:
continue
return protocol
import asyncio
import random
import time
import struct
import hashlib
from termcolor import colored
from .protocol import Version, VerAck, Reject, Addr, Inv
class Node(asyncio.Protocol):
magic = 0xd9b4bef9
header_size = 24
version = 70015
version_string = '/pytcoin:0.0/'
transport = None
queue = b''
def connection_made(self, transport):
self.transport = transport
def data_received(self, data):
self.queue += data
self.parse_read_buffer()
def parse_read_buffer(self):
while len(self.queue) >= self.header_size:
magic, command, payload_len, checksum = struct.unpack('I12sI4s', self.queue[:self.header_size])
if magic != self.magic:
print('Oops, received a message from a non bitcoin client??')
return
if len(self.queue) < self.header_size+payload_len:
# Not enough data... for now.
return
payload = self.queue[self.header_size:self.header_size+payload_len]
self.queue = self.queue[self.header_size+payload_len:]
command = command.decode().strip('\0')
try:
func = getattr(self, 'handle_%s' % command)
except AttributeError:
print('Unable to handle command "%s"' % command)
continue
func(payload)
@Version.parse
def handle_version(self, msg):
pass
@VerAck.parse
def handle_verack(self, payload):
self.send_message('verack')
self.send_message('getaddr')
@Reject.parse
def handle_reject(self, msg):
pass
@Addr.parse
def handle_addr(self, msg):
pass
@Inv.parse
def handle_inv(self, msg):
pass
def send_message(self, command, message=None):
print(colored('>>>>>', 'red'), message or ('<%s>' % command.title()))
data = self.make_message(command, message)
self.transport.write(data)
def make_message(self, command, message):
payload = message.dump() if message else b''
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[0:4]
return struct.pack('I12sI4s', self.magic, command.encode(), len(payload), checksum) + payload
def send_version(self):
msg = Version()
msg.version = self.version
msg.services = 1
msg.timestamp = int(time.time())
msg.addr_recv = '127.0.0.1', 8333
msg.addr_from = self.transport.get_extra_info("peername")
msg.nonce = random.getrandbits(64)
msg.user_agent = self.version_string
msg.start_height = 0
msg.relay = True
return self.send_message('version', msg)
class OutboundNode(Node):
pass
class InboundNode(Node):
pass
from .datastruct import Message, Int32, Int64, UInt64, NetAddr, VarStr, Bool, Char, List, TimeNetAddr, InventoryVector
class Version(Message):
version = Int32(repr_format='int')
services = UInt64()
timestamp = Int64(repr_format='int')
addr_recv = NetAddr()
addr_from = NetAddr()
nonce = UInt64()
user_agent = VarStr()
start_height = Int32(repr_format='int')
relay = Bool()
class VerAck(Message):
pass
class Reject(Message):
message = VarStr()
ccode = Char()
reason = VarStr()
class Inv(Message):
inventory = List(InventoryVector)
class Addr(Message):
addr_list = List(TimeNetAddr)
#!/usr/bin/env python3
import asyncio
from pytcoin.net import Network
if __name__ == '__main__':
net = Network()
asyncio.run(net.start())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment