Browse Source

work in progress for P2P protocol

Malte Kraus 8 years ago
parent
commit
e5075afb1d
4 changed files with 192 additions and 19 deletions
  1. 29 10
      src/block.py
  2. 2 2
      src/blockchain.py
  3. 113 1
      src/protocol.py
  4. 48 6
      src/transaction.py

+ 29 - 10
src/block.py

@@ -1,5 +1,6 @@
 from datetime import datetime
-from binascii import hexlify
+from binascii import hexlify, unhexlify
+import json
 
 from .merkle import merkle_tree
 from .crypto import get_hasher
@@ -18,17 +19,35 @@ class Block:
         self.received_time = received_time
         self.difficulty = difficulty
         self.transactions = transactions
+        assert transactions is not None
+        # TODO: fix param order, make transactions non-optional
 
-    def __str__(self):
-        return """Block {}
-    Previous Block: {}
-    Merkle Hash: {}
-    Time: {}
-    Nonce: {:x}
-    Height: {}
-    Received Time: {}
-    Difficulty: {:x}""".format(hexlify(self.hash), hexlify(self.prev_block_hash), hexlify(self.merkle_root_hash), self.time, self.nonce, self.height, self.received_time, self.difficulty)
+    def to_json_compatible(self):
+        val = {}
+        val['hash'] = hexlify(self.hash).encode()
+        val['prev_block_hash'] = hexlify(self.prev_block_hash).encode()
+        val['merkle_root_hash'] = hexlify(self.merkle_root_hash).encode()
+        val['time'] = self.time.timestamp()
+        val['nonce'] = self.nonce
+        val['height'] = self.height
+        val['difficulty'] = self.difficulty
+        val['transactions'] = [t.to_json_compatible() for t in self.transactions]
+        return val
+
+    @classmethod
+    def from_json_compatible(cls):
+        return cls(unhexlify(val['hash']),
+                   unhexlify(val['prev_block_hash']),
+                   datetime.fromtimestamp(int(val['time'])),
+                   int(val['nonce']),
+                   int(val['height']),
+                   datetime.now()
+                   int(val['difficulty']),
+                   unhexlify(val['merkle_root_hash']),
+                   [Transaction.from_json_compatible(t) for t in list(val['transactions'])])
 
+    def __str__(self):
+        return json.dumps(self.to_json_compatible(), indent=4)
 
     def verify_merkle(self):
         """ Verify that the merkle root hash is correct for the transactions in this block. """

+ 2 - 2
src/blockchain.py

@@ -1,11 +1,11 @@
 
 class Blockchain:
-    def __init__(self, blocks):
+    def __init__(self, blocks: list):
         self.blocks = blocks
         assert self.blocks[0].height == 0
         self.block_indices = {block.hash: i for (i, block) in enumerate(blocks)}
 
-    def get_transaction_by_hash(self, hash_val):
+    def get_transaction_by_hash(self, hash_val: bytes):
         """
         Returns a transaction from its hash, or None.
         """

+ 113 - 1
src/protocol.py

@@ -1,2 +1,114 @@
+from src.mining import Miner
+import json
+from enum import Enum
+import socket
+from threading import Thread
+import logging
+from queue import Queue
+
+MAX_PEERS = 10
+HELLO_MSG = b"bl0ckch41n"
+
+socket.setdefaulttimeout(30)
+
+Messages = Enum("Messages", "peers getblock block transaction")
+
+class PeerConnection:
+    def __init__(self, peer, proto, socket=None):
+        self.peer = peer
+        self.socket = None
+        self.proto = proto
+        self.is_connected = False
+        self.outgoing_msgs = Queue()
+        Thread.start(target=self.run, daemon=True)
+
+    def run(self):
+        if self.socket is not None:
+            self.socket = socket.create_connection(self.peer)
+        self.socket.sendall(HELLO_MSG)
+        if self.socket.recv(len(HELLO_MSG)) != HELLO_MSG:
+            return
+        self.is_connected = True
+        Thread.start(target=self.close_on_error, args=(self.reader_thread,))
+        self.close_on_error(self.writer_thread)
+
+    def close_on_error(self, cmd):
+        try:
+            cmd()
+        except Exception:
+            logging.exception()
+        while not self.outgoing_msgs.empty():
+            self.outgoing_msgs.get_nowait()
+        self.outgoing_msgs.put(None)
+        self.is_connected = False
+        self.socket.close()
+
+    def writer_thread(self):
+        while True:
+            item = self.outgoing_msgs.get()
+            if item is None:
+                break
+            self.socket.sendall(str(len(item)).encode() + b"\n")
+            self.socket.sendall(item)
+            self.outgoing_msgs.task_done()
+
+    def reader_thread(self):
+        while True:
+            buf = b""
+            while not buf or buf[-1] != '\n':
+                tmp = self.socket.recv(1)
+                if not tmp:
+                    return
+                buf += tmp
+            length = int(buf)
+            buf = bytearray(length)
+            read = 0
+            while length > read:
+                tmp = self.socket.recv_into(buf[read:])
+                if not tmp:
+                    return
+                read += tmp
+
+            success = self.proto.received(buf, self)
+            if not success:
+                return
+
+class IncomingHandler(socketserver.BaseRequestHandler):
+    def handle(self):
+        pass
+
 class Protocol:
-    pass
+    def __init__(self, bootstrap_peer):
+        self.block_receive_handlers = []
+        self.trans_receive_handlers = []
+        socketserver.TCPServer((HOST, PORT), MyTCPHandler)
+        self.peers = [PeerConnection(bootstrap_peer)]
+
+    def block_received(self, block):
+        for handler in self.block_receive_handlers:
+            handler(block)
+
+    def broadcast_mined_block(self, block):
+        self.fake_block_received(block)
+
+    def received(self, msg, peer):
+        try:
+            obj = json.loads(msg.decode())
+            msg_type = obj['msg_type']
+            msg_param = obj['msg_params']
+        except KeyError:
+            return False
+        except UnicodeDecodeError:
+            return False
+        except json.JSONDecodeError:
+            return False
+        getattr(self, 'received_' + msg_type)(msg_param)
+        return True
+    def received_peers(self, peer_list):
+        pass
+    def received_getblock(self, block_hash):
+        pass
+    def received_block(self, block):
+        pass
+    def received_transaction(self, transaction):
+        pass

+ 48 - 6
src/transaction.py

@@ -1,4 +1,5 @@
 from collections import namedtuple
+from binascii import hexlify, unhexlify
 
 from .crypto import get_hasher, Signing
 
@@ -7,11 +8,13 @@ TransactionTarget = namedtuple("TransactionTarget", ["recipient_pk", "amount"])
 """ One transaction input (pointer to 'coin'). """
 TransactionInput = namedtuple("TransactionInput", ["transaction_hash", "output_idx"])
 
+from typing import List
+SigningListType = List[Signing]
 
 
 class Transaction:
 
-    def __init__(self, inputs, targets, signatures=None, iv=None):
+    def __init__(self, inputs: list, targets: list, signatures=None: list, iv=None: bytes):
         self.inputs = inputs
         self.targets = targets
         self.signatures = signatures or []
@@ -21,6 +24,45 @@ class Transaction:
         # Reuse of IVs leads to inaccessible coins.
         self.iv = iv
 
+    def to_json_compatible(self):
+        val = {}
+        val['inputs'] = []
+        for inp in self.inputs:
+            val['inputs'].append({
+                'recipient_pk': hexlify(inp.recipient_pk.to_bytes()).decode(),
+                'amount': inp.amount,
+            })
+        val['targets'] = []
+        for targ in self.targets:
+            val['targets'].append({
+                'transaction_hash': hexlify(targ.transaction_hash).decode(),
+                'output_idx': targ.output_idx,
+            })
+        val['signatures'] = []
+        for sig in self.signatures:
+            val['signatures'].append(sig)
+        val['iv'] = hexlify(self.iv)
+        return val
+
+    @classmethod
+    def from_json_compatible(cls, obj: dict):
+        inputs = []
+        for inp in obj['inputs']:
+            inputs.append(TransactionInput(Signing(unhexlify(inp['recipient_pk'])),
+                                           int(inp['amount'])))
+        targets = []
+        for targ in obj['targets']:
+            targets.append(TransactionTarget(unhexlify(inp['transaction_hash']),
+                                           int(inp['output_idx'])))
+        signatures = obj['signatures']
+        for sig in signatures:
+            if not isinstance(sig, str):
+                raise ValueError()
+
+        iv = unhexlify(obj['iv'])
+        return cls(inputs, targets, signatures, iv)
+
+
     def get_hash(self):
         """ Hash this transaction. Returns raw bytes. """
         h = get_hasher()
@@ -34,7 +76,7 @@ class Transaction:
             h.update(str(inp.output_idx).encode())
         return h.digest()
 
-    def sign(self, private_keys):
+    def sign(self, private_keys: SigningListType):
         """
         Sign this transaction with the given private keys. The private keys need
         to be in the same order as the inputs.
@@ -42,7 +84,7 @@ class Transaction:
         for private_key in private_keys:
             self.signatures.append(private_key.sign(self.get_hash()))
 
-    def _verify_signatures(self, chain):
+    def _verify_signatures(self, chain: Blockchain):
         """ Verify that all inputs are signed and the signatures are valid. """
         if len(self.signatures) != len(self.inputs):
             return False
@@ -52,20 +94,20 @@ class Transaction:
                 return False
         return True
 
-    def _verify_single_sig(self, sig, inp, chain):
+    def _verify_single_sig(self, sig: str, inp: TransactionInput, chain: Blockchain):
         """ Verifies the signature on a single input. """
         trans = chain.get_transaction_by_hash(inp.transaction_hash)
         sender_pk = trans.targets[inp.output_idx].recipient_pk
         return sender_pk.verify_sign(self.get_hash(), sig)
 
 
-    def _verify_single_spend(self, chain, prev_block):
+    def _verify_single_spend(self, chain: Blockchain, prev_block: Block):
         """ Verifies that all inputs have not been spent yet. """
         for i in self.inputs:
             if not chain.is_coin_still_valid(i, prev_block):
                 return False
         return True
 
-    def verify(self, chain, prev_block=None):
+    def verify(self, chain: Blockchain, prev_block=None: Block):
         """ Verifies that this transaction is completely valid. """
         return self._verify_single_spend(chain, prev_block) and self._verify_signatures(chain)