Pārlūkot izejas kodu

document everything

Malte Kraus 8 gadi atpakaļ
vecāks
revīzija
c02f887982
18 mainītis faili ar 369 papildinājumiem un 142 dzēšanām
  1. 19 5
      doc/index.rst
  2. 0 7
      doc/tests.rst
  3. 5 1
      miner.py
  4. BIN
      src/__init__.pyc
  5. 35 2
      src/block.py
  6. 38 18
      src/blockchain.py
  7. 37 12
      src/chainbuilder.py
  8. 15 16
      src/crypto.py
  9. 14 6
      src/merkle.py
  10. 35 13
      src/mining.py
  11. 13 5
      src/mining_strategy.py
  12. 25 4
      src/proof_of_work.py
  13. 62 22
      src/protocol.py
  14. 55 23
      src/transaction.py
  15. 2 2
      tests/test_proto.py
  16. 3 1
      tests/utils.py
  17. 1 1
      tests/verifications.py
  18. 10 4
      wallet.py

+ 19 - 5
doc/index.rst

@@ -6,18 +6,26 @@
 Welcome to blockchain's documentation!
 ======================================
 
-Contents:
 
-.. toctree::
-   :maxdepth: 2
+Executables
+***********
 
-   tests
+.. list-table::
+    :stub-columns: 1
+    :widths: 10 90
 
+    * - miner
+      - .. automodule:: miner
+    * - wallet
+      - .. automodule:: wallet
+
+
+Source Code Documentation
+*************************
 
 .. autosummary::
     :toctree: _autosummary
 
-    src
     src.blockchain
     src.block
     src.chainbuilder
@@ -29,6 +37,12 @@ Contents:
     src.protocol
     src.transaction
 
+Tests
+*****
+Run the tests like this:
+
+    >>> python3 -m tests.test_proto
+    >>> python3 -m tests.verifications
 
 
 Indices and tables

+ 0 - 7
doc/tests.rst

@@ -1,7 +0,0 @@
-Tests
-=====
-
-
-Run the tests like this:
-
-    >>> python3 -m tests.test_proto

+ 5 - 1
miner.py

@@ -1,10 +1,14 @@
 #!/usr/bin/env python3
 
+""" The executable that participates in the P2P network and optionally mines new blocks. """
+
+__all__ = []
+
 import argparse
+import json
 from urllib.parse import urlparse
 
 import flask
-import json
 app = flask.Flask(__name__)
 
 from src.crypto import Signing

BIN
src/__init__.pyc


+ 35 - 2
src/block.py

@@ -1,3 +1,5 @@
+""" Definitions of blocks, and the genesis block. """
+
 from datetime import datetime
 from binascii import hexlify, unhexlify
 import json
@@ -5,12 +7,32 @@ import logging
 
 from .merkle import merkle_tree
 from .crypto import get_hasher
-from .proof_of_work import verify_proof_of_work, GENESIS_DIFFICULTY
 
 __all__ = ['Block', 'GENESIS_BLOCK', 'GENESIS_BLOCK_HASH']
 
 class Block:
-    """ A block. """
+    """
+    A block.
+
+    :ivar hash: The hash value of this block.
+    :vartype hash: bytes
+    :ivar prev_block_hash: The hash of the previous block.
+    :vartype prev_block_hash: bytes
+    :ivar merkle_root_hash: The hash of the merkle tree root of the transactions in this block.
+    :vartype merkle_root_hash: bytes
+    :ivar time: The time when this block was created.
+    :vartype time: datetime
+    :ivar nonce: The nonce in this block that was required to achieve the proof of work.
+    :vartype nonce: int
+    :ivar height: The height (accumulated difficulty) of this block.
+    :vartype height: int
+    :ivar received_time: The time when we received this block.
+    :vartype received_time: datetime
+    :ivar difficulty: The difficulty of this block.
+    :vartype difficulty: int
+    :ivar transactions: The list of transactions in this block.
+    :vartype transactions: List[Transaction]
+    """
 
     def __init__(self, hash_val, prev_block_hash, time, nonce, height, received_time, difficulty, transactions, merkle_root_hash=None):
         self.hash = hash_val
@@ -24,6 +46,7 @@ class Block:
         self.transactions = transactions
 
     def to_json_compatible(self):
+        """ Returns a JSON-serializable representation of this object. """
         val = {}
         val['hash'] = hexlify(self.hash).decode()
         val['prev_block_hash'] = hexlify(self.prev_block_hash).decode()
@@ -37,6 +60,7 @@ class Block:
 
     @classmethod
     def from_json_compatible(cls, val):
+        """ Create a new block from its JSON-serializable representation. """
         from .transaction import Transaction
         return cls(unhexlify(val['hash']),
                    unhexlify(val['prev_block_hash']),
@@ -68,6 +92,11 @@ class Block:
         return merkle_tree(self.transactions).get_hash() == self.merkle_root_hash
 
     def get_partial_hash(self):
+        """
+        Computes a hash over the contents of this block, except for the nonce. The proof of
+        work can use this partial hash to efficiently try different nonces. Other uses should
+        use :any:`get_hash` to get the complete hash.
+        """
         hasher = get_hasher()
         hasher.update(self.prev_block_hash)
         hasher.update(self.merkle_root_hash)
@@ -129,7 +158,11 @@ class Block:
             return self.hash == GENESIS_BLOCK_HASH
         return self.verify_difficulty() and self.verify_merkle() and self.verify_prev_block(chain) and self.verify_transactions(chain)
 
+from .proof_of_work import verify_proof_of_work, GENESIS_DIFFICULTY
+
 GENESIS_BLOCK = Block(b"", b"None", datetime(2017, 3, 3, 10, 35, 26, 922898),
                       0, 0, datetime.now(), GENESIS_DIFFICULTY, [], merkle_tree([]).get_hash())
 GENESIS_BLOCK_HASH = GENESIS_BLOCK.get_hash()
 GENESIS_BLOCK.hash = GENESIS_BLOCK_HASH
+
+from .blockchain import Blockchain

+ 38 - 18
src/blockchain.py

@@ -1,16 +1,27 @@
+""" Definition of block chains. """
+
 __all__ = ['Blockchain']
 import logging
+from typing import List, Dict, Optional
 
 class Blockchain:
-    def __init__(self, blocks: list):
+    """
+    A block chain: a ordrered list of blocks.
+
+    :ivar blocks: The blocks in this chain, oldest first.
+    :vartype blocks: List[Block]
+    :ivar block_indices: A dictionary allowing efficient lookup of the index of a block in this
+                         block chain by its hash value.
+    :vartype block_indices: Dict[bytes, int]
+    """
+
+    def __init__(self, blocks: 'List[Block]'):
         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: bytes):
-        """
-        Returns a transaction from its hash, or None.
-        """
+    def get_transaction_by_hash(self, hash_val: bytes) -> 'Optional[Transaction]':
+        """ Returns a transaction from its hash, or None. """
         # TODO: build a hash table with this info
         for block in self.blocks[::-1]:
             for trans in block.transactions:
@@ -18,11 +29,16 @@ class Blockchain:
                     return trans
         return None
 
-    def is_coin_still_valid(self, transaction_input: 'TransactionInput', prev_block: 'Block'=None):
+    def is_coin_still_valid(self, transaction_input: 'TransactionInput',
+                            prev_block: 'Block'=None) -> bool:
         """
         Validates that the coins that were sent in the transaction identified
         by `transaction_hash_val` to the nth receiver (n=output_idx) have not been
         spent before the given block.
+
+        :param transaction_input: The coin to check.
+        :param prev_block: The youngest block in this block chain that should be considered for
+                           the validation.
         """
         if prev_block is None:
             prev_block = self.head
@@ -35,40 +51,41 @@ class Blockchain:
                     return False
         return True
 
-    def get_block_by_hash(self, hash_val):
-        """
-        Returns a block by its hash value, or None if it cannot be found.
-        """
+    def get_block_by_hash(self, hash_val: bytes) -> 'Optional[Block]':
+        """ Returns a block by its hash value, or None if it cannot be found. """
         idx = self.block_indices.get(hash_val)
         if idx is None:
             return None
         return self.blocks[idx]
 
-    def verify_all_transactions(self):
-        """
-        Verify the transactions in all blocks in this chain.
-        """
+    def verify_all_transactions(self) -> bool:
+        """ Verify the transactions in all blocks in this chain. """
         for block in self.blocks:
             if not block.verify_transactions(self):
                 return False
         return True
 
-    def verify_all(self):
+    def verify_all(self) -> bool:
         """ Verify all blocks in this block chain. """
         return all(block.verify(self) for block in self.blocks)
 
     @property
     def head(self):
-        """ The head of this block chain. """
+        """
+        The head of this block chain.
+
+        :rtype: Block
+        """
         return self.blocks[-1]
 
-    def compute_difficulty(self):
+    def compute_difficulty(self) -> int:
         """ Compute the desired difficulty for the next block. """
         # TODO: dynamic calculation
         # TODO: verify difficulty in new blocks
         return self.head.difficulty
 
-    def compute_blockreward(self, prev_block):
+    def compute_blockreward(self, prev_block: 'Block') -> int:
+        """ Compute the block reward that is expected for the block following `prev_block`. """
         assert prev_block is not None
         reward = 1000
         l = self.block_indices[prev_block.hash]
@@ -76,3 +93,6 @@ class Blockchain:
             l = l - 10000
             reward = reward // 2
         return reward
+
+from .block import Block
+from .transaction import TransactionInput, Transaction

+ 37 - 12
src/chainbuilder.py

@@ -1,14 +1,36 @@
-from .block import GENESIS_BLOCK, GENESIS_BLOCK_HASH
-from .blockchain import Blockchain
+"""
+The chain builder maintains the current longest confirmed (primary) block chain as well as one
+candidate for an even longer chain that it attempts to download and verify.
+"""
 
 import threading
+from typing import List, Dict, Callable, Optional
+
+from .block import GENESIS_BLOCK, GENESIS_BLOCK_HASH
+from .blockchain import Blockchain
 
 __all__ = ['ChainBuilder']
 
 class ChainBuilder:
     """
-    Maintains the current longest confirmed (primary) block chain as well as one candidate for an even longer
-    chain that it attempts to download and verify.
+    Maintains the current longest confirmed (primary) block chain as well as one candidate for an
+    even longer chain that it attempts to download and verify.
+
+    :ivar primary_block_chain: The longest fully validated block chain we know of.
+    :vartype primary_block_chain: Blockchain
+    :ivar unconfirmed_block_chain: A list of blocks (ordered young to old) that might lead to a
+                                   new primary block chain after all predecessors are fully
+                                   downloaded and verified.
+    :vartype unconfirmed_transactions: List[Block]
+    :ivar block_cache: A cache of received blocks, not bound to any one specific block chain.
+    :vartype block_cache: Dict[bytes, Block]
+    :ivar unconfirmed_transactions: Known transactions that are not part of the primary block chain.
+    :vartype unconfirmed_transactions: Dict[bytes, Transaction]
+    :ivar chain_change_handlers: Event handlers that get called when we find out about a new primary
+                                 block chain.
+    :vartype chain_change_handlers: List[Callable]
+    :ivar protocol: The protocol instance used by this chain builder.
+    :vartype protocol: Protocol
     """
 
     def __init__(self, protocol):
@@ -34,11 +56,12 @@ class ChainBuilder:
             self._thread_id = threading.get_ident()
         assert self._thread_id == threading.get_ident()
 
-    def block_request_received(self, block_hash):
+    def block_request_received(self, block_hash: bytes) -> 'Optional[Block]':
+        """ Our event handler for block requests in the protocol. """
         self._assert_thread_safety()
         return self.block_cache.get(block_hash)
 
-    def new_transaction_received(self, transaction):
+    def new_transaction_received(self, transaction: 'Transaction'):
         """ Event handler that is called by the network layer when a transaction is received. """
         self._assert_thread_safety()
         hash_val = transaction.get_hash()
@@ -47,10 +70,8 @@ class ChainBuilder:
             self.unconfirmed_transactions[hash_val] = transaction
             self.protocol.broadcast_transaction(transaction)
 
-    def _new_primary_block_chain(self, chain):
-        """
-        Does all the housekeeping that needs to be done when a new longest chain is found.
-        """
+    def _new_primary_block_chain(self, chain: 'Blockchain'):
+        """ Does all the housekeeping that needs to be done when a new longest chain is found. """
         self._assert_thread_safety()
         self.primary_block_chain = chain
         todelete = set()
@@ -82,10 +103,11 @@ class ChainBuilder:
         else:
             self.protocol.send_block_request(unc[-1].prev_block_hash)
 
-    def new_block_received(self, block):
+    def new_block_received(self, block: 'Block'):
         """ Event handler that is called by the network layer when a block is received. """
         self._assert_thread_safety()
-        if not block.verify_difficulty() or not block.verify_merkle() or block.hash in self.block_cache:
+        if not block.verify_difficulty() or not block.verify_merkle() or \
+                block.hash in self.block_cache:
             return
         self.block_cache[block.hash] = block
 
@@ -101,3 +123,6 @@ class ChainBuilder:
             self.unconfirmed_block_chain = [block]
             self.get_next_unconfirmed_block()
 
+from .protocol import Protocol
+from .block import Block
+from .transaction import Transaction

+ 15 - 16
src/crypto.py

@@ -1,4 +1,4 @@
-""" Cryptographic primitives. """
+""" Generic functions for the cryptographic primitives used in this project. """
 
 from Crypto.Signature import PKCS1_PSS
 from Crypto.Hash import SHA512
@@ -7,10 +7,7 @@ from Crypto.PublicKey import RSA
 __all__ = ['get_hasher', 'Signing', 'MAX_HASH']
 
 def get_hasher():
-    """
-    Returns a object that you can use for hashing. Currently SHA512, swap it
-    out for something if you feel like it!
-    """
+    """ Returns a object that you can use for hashing, compatible to the `hashlib` interface. """
     return SHA512.new()
 
 
@@ -19,38 +16,40 @@ MAX_HASH = (1 << 512) - 1 # the largest possible hash value
 
 class Signing:
     """
-    Functionality for creating and verifying signatures, and their
-    public/private keys.
+    Functionality for creating and verifying signatures, and their public/private keys.
+
+    :param byte_repr: The bytes serialization of a public key.
     """
 
-    def __init__(self, byte_repr):
+    def __init__(self, byte_repr: bytes):
         self.rsa = RSA.importKey(byte_repr)
 
-    def verify_sign(self, hashed_value, signature):
-        """ Verify a signature for a already hashed value and a public key. """
+    def verify_sign(self, hashed_value: bytes, signature: bytes) -> bool:
+        """ Verify a signature for an already hashed value and a public key. """
         ver = PKCS1_PSS.new(self.rsa)
         h = get_hasher()
         h.update(hashed_value)
         return ver.verify(h, signature)
 
-    def sign(self, hashed_value):
-        """ Sign a hashed value with a private key. """
+    def sign(self, hashed_value: bytes) -> bytes:
+        """ Sign a hashed value with this private key. """
         signer = PKCS1_PSS.new(self.rsa)
         h = get_hasher()
         h.update(hashed_value)
         return signer.sign(h)
 
     @classmethod
-    def generatePrivateKey(cls):
+    def generate_private_key(cls):
+        """ Generate a new private key. """
         return Signing(RSA.generate(3072).exportKey())
 
-    def as_bytes(self, include_priv=False):
-        """ bytes representation of this key. """
+    def as_bytes(self, include_priv: bool=False) -> bytes:
+        """ Serialize this key to a `bytes` value. """
         if include_priv:
             return self.rsa.exportKey()
         else:
             return self.rsa.publickey().exportKey()
 
-    def __eq__(self, other):
+    def __eq__(self, other: 'Signing'):
         # TODO: it's possible that the same key has multiple different representations
         return self.as_bytes() == other.as_bytes()

+ 14 - 6
src/merkle.py

@@ -1,14 +1,22 @@
+""" Functionality for creating a Merkle tree. """
+
 import json
 from binascii import hexlify
-from treelib import Node, Tree
 from itertools import zip_longest
 
+from treelib import Node, Tree
+
 from .crypto import get_hasher
 
 __all__ = ['merkle_tree', 'MerkleNode']
 
 class MerkleNode:
-    """ A hash tree node, pointing to a leaf value or another node. """
+    """
+    A hash tree node, pointing to a leaf value or another node.
+
+    :ivar v1: The first child of this node.
+    :ivar v2: The second child of this node.
+    """
 
     def __init__(self, v1, v2):
         self.v1 = v1
@@ -16,7 +24,7 @@ class MerkleNode:
         self.v2 = v2
         self.v2_hash = b'' if v2 is None else v2.get_hash()
 
-    def get_hash(self):
+    def get_hash(self) -> bytes:
         """ Compute the hash of this node. """
         hasher = get_hasher()
         hasher.update(self.v1_hash)
@@ -40,11 +48,11 @@ class MerkleNode:
         self._get_tree(tree, None)
         return str(tree)
 
-def merkle_tree(values):
+def merkle_tree(values: list) -> MerkleNode:
     """
-    Constructs a merkle tree from a list of values.
+    Constructs a Merkle tree from a list of values.
 
-    All values need to support a method get_hash().
+    All `values` need to support a method `get_hash()`.
     """
 
     if not values:

+ 35 - 13
src/mining.py

@@ -1,31 +1,49 @@
-from .proof_of_work import ProofOfWork
-from .chainbuilder import ChainBuilder
-from . import mining_strategy
-from .protocol import Protocol
+""" Functionality for mining new blocks. """
 
 from threading import Thread
 from time import sleep
+from typing import Optional
+
+from .proof_of_work import ProofOfWork
+from .chainbuilder import ChainBuilder
+from . import mining_strategy
 
 __all__ = ['Miner']
 
+def _yield():
+    sleep(0)
+
 class Miner:
+    """
+    Management of a background thread that mines for new blocks.
+
+    :ivar proto: The protocol where newly mined blocks will be sent to.
+    :vartype proto: Protocol
+    :ivar chainbuilder: The chain builder used by :any:`start_mining` to find the primary chain.
+    :vartype chainbuilder: ChainBuilder
+    :ivar _cur_miner: The proof of work we're currently working on.
+    :vartype _cur_miner: Optional[ProofOfWork]
+    :ivar reward_pubkey: The public key to which mining fees and block rewards should be sent to.
+    :vartype reward_pubkey: Signing
+    """
+
     def __init__(self, proto, reward_pubkey):
         self.proto = proto
         self.chainbuilder = ChainBuilder(proto)
         self.chainbuilder.chain_change_handlers.append(self.start_mining)
-        self.is_mining = False
-        self.cur_miner = None
+        self._cur_miner = None
         self.reward_pubkey = reward_pubkey
         Thread(target=self._miner_thread, daemon=True).start()
 
     def _miner_thread(self):
         while True:
-            miner = self.cur_miner
+            miner = self._cur_miner
             if miner is None:
-                sleep(0)
+                # TODO: condition variable
+                _yield()
             else:
                 block = miner.run()
-                self.cur_miner = None
+                self._cur_miner = None
                 if block is not None:
                     self.proto.broadcast_primary_block(block)
 
@@ -36,10 +54,14 @@ class Miner:
         chain = self.chainbuilder.primary_block_chain
         transactions = self.chainbuilder.unconfirmed_transactions.values()
         block = mining_strategy.create_block(chain, transactions, self.reward_pubkey)
-        self.cur_miner = ProofOfWork(block)
+        self._cur_miner = ProofOfWork(block)
 
     def stop_mining(self):
         """ Stop all mining. """
-        if self.cur_miner:
-            self.cur_miner.abort()
-            self.cur_miner = None
+        if self._cur_miner:
+            self._cur_miner.abort()
+            self._cur_miner = None
+
+from .protocol import Protocol
+from .chainbuilder import ChainBuilder
+from .crypto import Signing

+ 13 - 5
src/mining_strategy.py

@@ -1,16 +1,21 @@
-from datetime import datetime
+""" Defines the contents of newly mined blocks. """
+
+from typing import List
 
 from .block import Block
-from .merkle import merkle_tree
 from .transaction import Transaction, TransactionTarget
 
-from Crypto.PublicKey import RSA
-
 __all__ = ['create_block']
 
-def create_block(blockchain, unconfirmed_transactions, reward_pubkey):
+def create_block(blockchain: 'Blockchain', unconfirmed_transactions: 'List[Transaction]',
+                 reward_pubkey: 'Signing') -> 'Block':
     """
     Creates a new block that can be mined.
+
+    :param blockchain: The blockchain on top of which the new block should fit.
+    :param unconfirmed_transactions: The transactions that should be considered for inclusion in
+                                     this block.
+    :param reward_pubkey: The key that should receive block rewards.
     """
     head = blockchain.head
 
@@ -26,3 +31,6 @@ def create_block(blockchain, unconfirmed_transactions, reward_pubkey):
     transactions.add(trans)
 
     return Block.create(blockchain, list(transactions))
+
+from .blockchain import Blockchain
+from .crypto import Signing

+ 25 - 4
src/proof_of_work.py

@@ -1,25 +1,44 @@
+""" Implementation and verification of the proof of work. """
+
+from typing import Optional
+
 from .crypto import MAX_HASH
 
 __all__ = ['verify_proof_of_work', 'GENESIS_DIFFICULTY', 'ProofOfWork']
 
-def verify_proof_of_work(block):
+def verify_proof_of_work(block: 'Block'):
     """ Verify the proof of work on a block. """
     return int.from_bytes(block.hash, byteorder='little', signed=False) > block.difficulty
 
 GENESIS_DIFFICULTY = MAX_HASH - (MAX_HASH // 10000)
+""" The difficulty of the genesis block. """
 
 class ProofOfWork:
-    def __init__(self, block):
+    """
+    Allows performing (and aborting) a proof of work.
+
+    :ivar stopped: A flag that is set to `True` to abort the `run` operation.
+    :vartype stopped: bool
+    :ivar block: The block on which the proof of work should be performed.
+    :param block: The block on which the proof of work should be performed.
+    :vartype block: Block
+    :ivar success: A flag indication whether the proof of work was successful or not.
+    :vartype success: bool
+    """
+
+    def __init__(self, block: 'Block'):
         self.stopped = False
         self.block = block
         self.success = False
 
     def abort(self):
+        """ Aborts execution of this proof of work. """
         self.stopped = True
 
-    def run(self):
+    def run(self) -> 'Optional[Block]':
         """
-        Perform the proof of work on a block, until cond.stopped becomes True or the proof of work was sucessful.
+        Perform the proof of work on a block, until `stopped` becomes True or the proof of
+        work was successful.
         """
         hasher = self.block.get_partial_hash()
         while not self.stopped:
@@ -31,3 +50,5 @@ class ProofOfWork:
                     return self.block
                 self.block.nonce += 1
         return None
+
+from .block import Block

+ 62 - 22
src/protocol.py

@@ -1,39 +1,44 @@
+""" Implementation of the P2P protocol. """
+
 import json
-from enum import Enum
 import socket
 import socketserver
-from threading import Thread, Lock
 import logging
+from threading import Thread, Lock
 from queue import Queue, PriorityQueue
 from binascii import unhexlify, hexlify
+from typing import Callable, List
 
-from .block import Block
-from .transaction import Transaction
 
-__all__ = ['Protocol', 'PeerConnection']
+__all__ = ['Protocol', 'PeerConnection', 'MAX_PEERS', 'HELLO_MSG']
 
 MAX_PEERS = 10
+""" The maximum number of peers that we connect to."""
+
 HELLO_MSG = b"bl0ckch41n"
+""" The hello message two peers use to make sure they are speaking the same protocol. """
 
+# TODO: set this centrally
 logging.basicConfig(level=logging.INFO)
-
 socket.setdefaulttimeout(30)
 
 class PeerConnection:
     """
     Handles the low-level socket connection to one other peer.
     :ivar peer_addr: The self-reported address one can use to connect to this peer.
+    :ivar param: The self-reported address one can use to connect to this peer.
     :ivar _sock_addr: The address our socket is or will be connected to.
     :ivar socket: The socket object we use to communicate with our peer.
+    :param sock: A socket object we should use to communicate with our peer.
     :ivar proto: The Protocol instance this peer connection belongs to.
     :ivar is_connected: A boolean indicating the current connection status.
     :ivar outgoing_msgs: A queue of messages we want to send to this peer.
     """
 
-    def __init__(self, peer_addr, proto, socket=None):
+    def __init__(self, peer_addr: tuple, proto: 'Protocol', sock: socket.socket=None):
         self.peer_addr = None
         self._sock_addr = peer_addr
-        self.socket = socket
+        self.socket = sock
         self.proto = proto
         self.is_connected = False
         self.outgoing_msgs = Queue()
@@ -69,7 +74,7 @@ class PeerConnection:
         Thread(target=self.reader_thread, daemon=True).start()
         self.writer_thread()
 
-    def close_on_error(fn):
+    def close_on_error(fn: Callable):
         """ A decorator that closes both threads if one dies. """
 
         def wrapper(self, *args, **kwargs):
@@ -83,6 +88,8 @@ class PeerConnection:
         return wrapper
 
     def close(self):
+        """ Closes the connection to this peer. """
+
         if not self.is_connected:
             return
 
@@ -95,7 +102,7 @@ class PeerConnection:
             self.proto.peers.remove(self)
         self.socket.close()
 
-    def send_msg(self, msg_type, msg_param):
+    def send_msg(self, msg_type: str, msg_param):
         """
         Sends a message to this peer.
 
@@ -122,7 +129,9 @@ class PeerConnection:
 
     @close_on_error
     def reader_thread(self):
-        """ The reader thread reads messages from the socket and passes them to the protocol to handle. """
+        """
+        The reader thread reads messages from the socket and passes them to the protocol to handle.
+        """
         while True:
             buf = b""
             while not buf or buf[-1] != ord('\n'):
@@ -158,8 +167,16 @@ class PeerConnection:
 
 
 class SocketServer(socketserver.TCPServer):
+    """
+    A TCP socketserver that calls does not close the connections on its own.
+    """
+
     allow_reuse_address = True
+    """ Make sure the server can be restarted without delays. """
+
     def serve_forever_bg(self):
+        """ Runs the server forever in a background thread. """
+
         logging.info("listening on %s", self.server_address)
         Thread(target=self.serve_forever, daemon=True).start()
 
@@ -173,9 +190,19 @@ class Protocol:
     """
     Manages connections to our peers. Allows sending messages to them and has event handlers
     for handling messages from other peers.
+
+    :ivar block_receive_handlers: Event handlers that get called when a new block is received.
+    :vartype block_receive_handlers: List[Callable]
+    :ivar trans_receive_handlers: Event handlers that get called when a new transaction is received.
+    :vartype trans_receive_handlers: List[Callable]
+    :ivar block_request_handlers: Event handlers that get called when a block request is received.
+    :vartype block_request_handlers: List[Callable]
+    :ivar peers: The peers we are connected to.
+    :vartype peers: List[PeerConnection]
     """
 
-    def __init__(self, bootstrap_peers, primary_block, listen_port=0, listen_addr=""):
+    def __init__(self, bootstrap_peers: 'List[tuple]',
+                 primary_block: 'Block', listen_port: int=0, listen_addr: str=""):
         """
         :param bootstrap_peers: network addresses of peers where we bootstrap the P2P network from
         :param primary_block: the head of the primary block chain
@@ -198,7 +225,8 @@ class Protocol:
             def handle(self):
                 logging.info("connection from peer %s", repr(self.client_address))
                 if len(self.proto.peers) > MAX_PEERS:
-                    logging.warn("too many connections: rejecting peer %s", repr(self.client_address))
+                    logging.warning("too many connections: rejecting peer %s",
+                                    repr(self.client_address))
                     self.request.close()
                     # TODO: separate limits for incoming and outgoing connections
                     return
@@ -213,38 +241,47 @@ class Protocol:
 
         Thread(target=self._main_thread, daemon=True).start()
 
-    def broadcast_primary_block(self, block: Block):
+    def broadcast_primary_block(self, block: 'Block'):
         """ Notifies all peers and local listeners of a new primary block. """
         self._primary_block = block.to_json_compatible()
         for peer in self.peers:
             peer.send_msg("block", self._primary_block)
         self.received('block', self._primary_block, None, 0)
 
-    def broadcast_transaction(self, trans: Transaction):
+    def broadcast_transaction(self, trans: 'Transaction'):
         """ Notifies all peers and local listeners of a new transaction. """
         for peer in self.peers:
             peer.send_msg("transaction", trans.to_json_compatible())
 
-    def received(self, msg_type, msg_param, peer, prio=1):
-        """ Called by a PeerConnection when a new message was received. """
+    def received(self, msg_type: str, msg_param, peer: PeerConnection, prio: int=1):
+        """
+        Called by a PeerConnection when a new message was received.
+
+        :param msg_type: The message type identifier.
+        :param msg_param: The JSON-compatible object that was received.
+        :param peer: The peer who sent us the message.
+        :param prio: The priority of the message. (Should be lower for locally generated events
+                     than for remote events, to make sure self-mined blocks get handled first.)
+        """
         with self._callback_counter_lock:
             counter = self._callback_counter + 1
             self._callback_counter = counter
         self._callback_queue.put((prio, counter, msg_type, msg_param, peer))
 
     def _main_thread(self):
+        """ The main loop of the one thread where all incoming events are handled. """
         while True:
             _, _, msg_type, msg_param, peer = self._callback_queue.get()
             try:
                 getattr(self, 'received_' + msg_type)(msg_param, peer)
-            except Exception:
+            except:
                 logging.exception("unhandled exception in event handler")
                 try:
                     peer.close()
                 except OSError:
                     pass
 
-    def received_peer(self, peer_addr, _):
+    def received_peer(self, peer_addr: list, _):
         """ Information about a peer has been received. """
 
         peer_addr = tuple(peer_addr)
@@ -258,7 +295,7 @@ class Protocol:
         # TODO: if the other peer also just learned of us, we can end up with two connections (one from each direction)
         self.peers.append(PeerConnection(peer_addr, self))
 
-    def received_getblock(self, block_hash, peer):
+    def received_getblock(self, block_hash: str, peer: PeerConnection):
         """ We received a request for a new block from a certain peer. """
         for handler in self.block_request_handlers:
             block = handler(unhexlify(block_hash))
@@ -266,12 +303,12 @@ class Protocol:
                 peer.send_msg("block", block.to_json_compatible())
                 break
 
-    def received_block(self, block, _):
+    def received_block(self, block: dict, _):
         """ Someone sent us a block. """
         for handler in self.block_receive_handlers:
             handler(Block.from_json_compatible(block))
 
-    def received_transaction(self, transaction, _):
+    def received_transaction(self, transaction: dict, _):
         """ Someone sent us a transaction. """
         for handler in self.trans_receive_handlers:
             handler(Transaction.from_json_compatible(transaction))
@@ -280,3 +317,6 @@ class Protocol:
         """ Sends a request for a block to all our peers. """
         for peer in self.peers:
             peer.send_msg("getblock", hexlify(block_hash).decode())
+
+from .block import Block
+from .transaction import Transaction

+ 55 - 23
src/transaction.py

@@ -1,38 +1,62 @@
+""" Defines transactions and their inputs and outputs. """
+
+import logging
 from collections import namedtuple
 from binascii import hexlify, unhexlify
-import logging
-
-from .blockchain import Blockchain
-from .block import Block
+from typing import List, Set
 
 from .crypto import get_hasher, Signing
 
 __all__ = ['TransactionTarget', 'TransactionInput', 'Transaction']
 
 TransactionTarget = namedtuple("TransactionTarget", ["recipient_pk", "amount"])
-""" The recipient of a transaction ('coin'). """
+"""
+The recipient of a transaction ('coin').
+
+:ivar recipient_pk: The public key of the recipient.
+:vartype recipient_pk: Signing
+:ivar amount: The amount sent to `recipient_pk`.
+:vartype amount: int
+"""
 
 TransactionInput = namedtuple("TransactionInput", ["transaction_hash", "output_idx"])
-""" One transaction input (pointer to 'coin'). """
+"""
+One transaction input (pointer to 'coin').
 
-from typing import List
-SigningListType = List[Signing]
+:ivar transaction_hash: The hash of the transaction that sent money to the sender.
+:vartype transaction_hash: bytes
+:ivar output_idx: The index into `Transaction.targets` of the `transaction_hash`.
+:vartype output_idx: int
+"""
 
 
 class Transaction:
-
-    def __init__(self, inputs: list, targets: list, signatures:list=None, iv:bytes=None):
+    """
+    A transaction.
+
+    :ivar inputs: The inputs of this transaction. Empty in the case of block reward transactions.
+    :vartype inputs: List[TransactionInput]
+    :ivar targets: The targets of this transaction.
+    :vartype targets: List[TransactionTarget]
+    :ivar signatures: Signatures for each input. Must be in the same order as `inputs`. Filled
+                      by :func:`sign`.
+    :vartype signatures: List[bytes]
+    :ivar iv: The IV is used to differentiate block reward transactions.  These have no inputs and
+              therefore would otherwise hash to the same value, when the target is identical.
+              Reuse of IVs leads to inaccessible coins.
+    :vartype iv: bytes
+    """
+
+    def __init__(self, inputs: 'List[TransactionInput]', targets: 'List[TransactionTarget]',
+                 signatures: 'List[bytes]'=None, iv: bytes=None):
         self.inputs = inputs
         self.targets = targets
         self.signatures = signatures or []
-        # The IV is used to differentiate block reward transactions.
-        # These have no inputs and therefore would otherwise hash to the
-        # same value, when the target is identical.
-        # Reuse of IVs leads to inaccessible coins.
         self.iv = iv
         self._hash = None
 
     def to_json_compatible(self):
+        """ Returns a JSON-serializable representation of this object. """
         val = {}
         val['inputs'] = []
         for inp in self.inputs:
@@ -55,6 +79,7 @@ class Transaction:
 
     @classmethod
     def from_json_compatible(cls, obj: dict):
+        """ Creates a new object of this class, from a JSON-serializable representation. """
         inputs = []
         for inp in obj['inputs']:
             inputs.append(TransactionInput(unhexlify(inp['transaction_hash']),
@@ -62,7 +87,7 @@ class Transaction:
         targets = []
         for targ in obj['targets']:
             targets.append(TransactionTarget(Signing(unhexlify(targ['recipient_pk'])),
-                                           int(targ['amount'])))
+                                             int(targ['amount'])))
         signatures = []
         for sig in obj['signatures']:
             signatures.append(unhexlify(sig))
@@ -71,7 +96,7 @@ class Transaction:
         return cls(inputs, targets, signatures, iv)
 
 
-    def get_hash(self):
+    def get_hash(self) -> bytes:
         """ Hash this transaction. Returns raw bytes. """
         if self._hash is None:
             h = get_hasher()
@@ -86,7 +111,7 @@ class Transaction:
             self._hash = h.digest()
         return self._hash
 
-    def sign(self, private_keys: SigningListType):
+    def sign(self, private_keys: 'List[Signing]'):
         """
         Sign this transaction with the given private keys. The private keys need
         to be in the same order as the inputs.
@@ -94,7 +119,7 @@ class Transaction:
         for private_key in private_keys:
             self.signatures.append(private_key.sign(self.get_hash()))
 
-    def _verify_signatures(self, chain: Blockchain):
+    def _verify_signatures(self, chain: 'Blockchain'):
         """ Verify that all inputs are signed and the signatures are valid. """
         if len(self.signatures) != len(self.inputs):
             logging.warning("wrong number of signatures")
@@ -105,7 +130,7 @@ class Transaction:
                 return False
         return True
 
-    def _verify_single_sig(self, sig: str, inp: TransactionInput, chain: Blockchain):
+    def _verify_single_sig(self, sig: str, inp: TransactionInput, chain: 'Blockchain') -> bool:
         """ Verifies the signature on a single input. """
         trans = chain.get_transaction_by_hash(inp.transaction_hash)
         if trans is None:
@@ -118,7 +143,8 @@ class Transaction:
         return True
 
 
-    def _verify_single_spend(self, chain: Blockchain, other_trans: set, prev_block: Block):
+    def _verify_single_spend(self, chain: 'Blockchain', other_trans: set,
+                             prev_block: 'Block') -> bool:
         """ Verifies that all inputs have not been spent yet. """
         inp_set = set(self.inputs)
         if len(self.inputs) != len(inp_set):
@@ -126,7 +152,8 @@ class Transaction:
             return False
         other_inputs = {i for t in other_trans for i in t.inputs}
         if other_inputs.intersection(inp_set):
-            logging.warning("Transaction may not spend the same coin as another transaction in the same block.")
+            logging.warning("Transaction may not spend the same coin as another transaction in the"
+                            " same block.")
             return False
 
         for i in self.inputs:
@@ -135,6 +162,11 @@ class Transaction:
                 return False
         return True
 
-    def verify(self, chain: Blockchain, other_trans:set, prev_block:Block=None):
+    def verify(self, chain: 'Blockchain', other_trans: 'Set[Transaction]',
+               prev_block: 'Block'=None) -> bool:
         """ Verifies that this transaction is completely valid. """
-        return self._verify_single_spend(chain, other_trans, prev_block) and self._verify_signatures(chain)
+        return self._verify_single_spend(chain, other_trans, prev_block) and \
+               self._verify_signatures(chain)
+
+from .blockchain import Blockchain
+from .block import Block

+ 2 - 2
tests/test_proto.py

@@ -6,7 +6,7 @@ from src.transaction import Transaction, TransactionInput, TransactionTarget
 
 from time import sleep
 
-reward_key = Signing.generatePrivateKey()
+reward_key = Signing.generate_private_key()
 
 proto1 = Protocol([], GENESIS_BLOCK, 1337)
 proto2 = Protocol([("127.0.0.1", 1337)], GENESIS_BLOCK, 1338)
@@ -18,7 +18,7 @@ miner1.start_mining()
 
 
 sleep(5)
-target_key = Signing.generatePrivateKey()
+target_key = Signing.generate_private_key()
 reward_trans = miner2.chainbuilder.primary_block_chain.blocks[20].transactions[0]
 trans_in = TransactionInput(reward_trans.get_hash(), 0)
 trans_targ = TransactionTarget(target_key, reward_trans.targets[0].amount)

+ 3 - 1
tests/utils.py

@@ -1,5 +1,7 @@
 import src.proof_of_work
+import src.block
 src.proof_of_work.verify_proof_of_work = lambda b: True
+src.block.verify_proof_of_work = src.proof_of_work.verify_proof_of_work
 
 from src.block import *
 from src.blockchain import *
@@ -23,7 +25,7 @@ def trans_as_input(trans, out_idx=0):
 
 def new_trans(old_trans, out_idx=0):
     amount = old_trans.targets[out_idx].amount
-    key = Signing.generatePrivateKey()
+    key = Signing.generate_private_key()
     trans = Transaction([trans_as_input(old_trans, out_idx)],
                         [TransactionTarget(key, amount)])
     trans.sign([old_trans.targets[out_idx].recipient_pk])

+ 1 - 1
tests/verifications.py

@@ -8,7 +8,7 @@ def trans_test(fn):
 
     gen_chain = Blockchain([GENESIS_BLOCK])
     assert gen_chain.verify_all()
-    key = Signing.generatePrivateKey()
+    key = Signing.generate_private_key()
     reward_trans = Transaction([], [TransactionTarget(key, gen_chain.compute_blockreward(gen_chain.head))])
     chain = extend_blockchain(gen_chain, [reward_trans])
     try:

+ 10 - 4
wallet.py

@@ -1,10 +1,15 @@
 #!/usr/bin/env python3
 
+"""
+The wallet allows a user to query account balance, send money, and get status information about a
+miner.
+"""
+
+__all__ = []
+
 import argparse
 import requests
 
-from binascii import hexlify, unhexlify
-
 from src.blockchain import Blockchain
 from src.block import Block
 from src.transaction import Transaction
@@ -28,8 +33,9 @@ def main():
     parser = argparse.ArgumentParser(description="Wallet.")
     parser.add_argument("--miner-port", default=40203, type=int,
                         help="The RPC port of the miner to connect to.")
-    parser.add_argument("--show-transactions", type=argparse.FileType("rb"), default=[], action="append",
-                        help="Shows all transactions involving the public key stored in the specified file.")
+    parser.add_argument("--show-transactions", type=argparse.FileType("rb"), default=[],
+                        action="append", help="Shows all transactions involving the public key "
+                                              "stored in the specified file.")
     parser.add_argument("--show-network", action="store_true", default=False,
                         help="Prints networking information about the miner.")
     args = parser.parse_args()