浏览代码

bug fixes and major update

Bernardo Magri 7 年之前
父节点
当前提交
396271c92b

+ 24 - 4
README.md

@@ -12,18 +12,21 @@ from urllib.parse import urlparse
 from typing import Tuple
 
 import logging
+
 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
 
-from src.crypto import Signing
+from src.config import *
+from src.crypto import Key
 from src.protocol import Protocol
-from src.block import GENESIS_BLOCK
+from src.blockchain import GENESIS_BLOCK
 from src.chainbuilder import ChainBuilder
 from src.mining import Miner
 from src.persistence import Persistence
 from src.rpc_server import rpc_server
 
+
 def parse_addr_port(val: str) -> Tuple[str, int]:
-    """ Parse a user-specified "host:port" value to a tuple. """
+    """ Parse a user-specified `host:port` value to a tuple. """
     url = urlparse("//" + val)
     assert url.scheme == ''
     assert url.path == ''
@@ -36,6 +39,15 @@ def parse_addr_port(val: str) -> Tuple[str, int]:
 
 
 def main():
+    """
+    Takes arguments:
+    `listen-address`: The IP address where the P2P server should bind to. Default is: `\"\"`
+    `listen-port`: The port where the P2P server should listen. Defaults a dynamically assigned port. Default is: `0`
+    `mining-pubkey`: The public key where mining rewards should be sent to. No mining is performed if this is left unspecified.
+    `bootstrap-peer`: Addresses of other P2P peers in the network. Default is: `[]`
+    `rpc-port`: The port number where the wallet can find an RPC server. Default is: `40203`
+    `persist-path`: The file where data is persisted.
+    """
     parser = argparse.ArgumentParser(description="Blockchain Miner.")
     parser.add_argument("--listen-address", default="",
                         help="The IP address where the P2P server should bind to.")
@@ -54,7 +66,7 @@ def main():
 
     proto = Protocol(args.bootstrap_peer, GENESIS_BLOCK, args.listen_port, args.listen_address)
     if args.mining_pubkey is not None:
-        pubkey = Signing(args.mining_pubkey.read())
+        pubkey = Key(args.mining_pubkey.read())
         args.mining_pubkey.close()
         miner = Miner(proto, pubkey)
         miner.start_mining()
@@ -73,5 +85,13 @@ def main():
 
     rpc_server(args.rpc_port, chainbuilder, persist)
 
+
+def start_listener(rpc_port: int, bootstrap_peer: str, listen_port: int, listen_address: str):
+    """ Starts the RPC Server and initializes the protocol. """
+    proto = Protocol([parse_addr_port(bootstrap_peer)], GENESIS_BLOCK, listen_port, listen_address)
+    chainbuilder = ChainBuilder(proto)
+    rpc_server(rpc_port, chainbuilder, None)
+
+
 if __name__ == '__main__':
     main()

+ 3 - 2
requirements.txt

@@ -3,6 +3,7 @@ Babel==2.3.4
 click==6.7
 docutils==0.13.1
 Flask==0.12
+Flask_API==1.0
 imagesize==0.7.1
 itsdangerous==0.24
 Jinja2==2.9.5
@@ -16,8 +17,8 @@ pytz==2016.10
 requests==2.13.0
 six==1.10.0
 snowballstemmer==1.2.1
-Sphinx==1.5.3
-sphinx-autodoc-typehints==1.1.0
+Sphinx==1.7.1
+sphinx-autodoc-typehints==1.2.5
 treelib==1.3.5
 urwid==1.3.1
 Werkzeug==0.12

+ 0 - 0
src/__init__.py


+ 62 - 57
src/block.py

@@ -1,16 +1,19 @@
 """ Definitions of blocks, and the genesis block. """
 
-from datetime import datetime, timedelta
+from datetime import datetime
 from binascii import hexlify, unhexlify
-from struct import pack
+
 import json
 import logging
-import math
 
+import src.utils as utils
+
+from .config import *
 from .merkle import merkle_tree
 from .crypto import get_hasher
 
-__all__ = ['Block', 'GENESIS_BLOCK', 'GENESIS_BLOCK_HASH']
+__all__ = ['Block']
+
 
 class Block:
     """
@@ -21,6 +24,8 @@ class Block:
 
     :ivar hash: The hash value of this block.
     :vartype hash: bytes
+    :ivar id: The ID of this block. Genesis-Block has the ID '0'.
+    :vartype id: int
     :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.
@@ -39,7 +44,9 @@ class Block:
     :vartype transactions: List[Transaction]
     """
 
-    def __init__(self, prev_block_hash, time, nonce, height, received_time, difficulty, transactions, merkle_root_hash=None):
+    def __init__(self, prev_block_hash, time, nonce, height, received_time, difficulty, transactions,
+                 merkle_root_hash=None, id=None):
+        self.id = id
         self.prev_block_hash = prev_block_hash
         self.merkle_root_hash = merkle_root_hash
         self.time = time
@@ -53,6 +60,8 @@ class Block:
     def to_json_compatible(self):
         """ Returns a JSON-serializable representation of this object. """
         val = {}
+        val['id'] = self.id
+        val['hash'] = hexlify(self.hash).decode()
         val['prev_block_hash'] = hexlify(self.prev_block_hash).decode()
         val['merkle_root_hash'] = hexlify(self.merkle_root_hash).decode()
         val['time'] = self.time.strftime("%Y-%m-%dT%H:%M:%S.%f UTC")
@@ -73,33 +82,27 @@ class Block:
                    datetime.utcnow(),
                    int(val['difficulty']),
                    [Transaction.from_json_compatible(t) for t in list(val['transactions'])],
-                   unhexlify(val['merkle_root_hash']))
+                   unhexlify(val['merkle_root_hash']),
+                   int(val['id']))
 
     @classmethod
-    def create(cls, blockchain: 'Blockchain', transactions: list, ts=None):
+    def create(cls, chain_difficulty: int, prev_block: 'Block', transactions: list, ts=None):
         """
         Create a new block for a certain blockchain, containing certain transactions.
         """
         tree = merkle_tree(transactions)
-        difficulty = blockchain.compute_difficulty_next_block()
+        difficulty = chain_difficulty
+        id = prev_block.height + 1
         if ts is None:
             ts = datetime.utcnow()
-        if ts <= blockchain.head.time:
-            ts = blockchain.head.time + timedelta(microseconds=1)
-        return Block(blockchain.head.hash, ts, 0, blockchain.head.height + difficulty,
-                     None, difficulty, transactions, tree.get_hash())
+        if ts <= prev_block.time:
+            ts = prev_block.time + timedelta(microseconds=1)
+        return Block(prev_block.hash, ts, 0, prev_block.height + 1,
+                     None, difficulty, transactions, tree.get_hash(), id)
 
     def __str__(self):
         return json.dumps(self.to_json_compatible(), indent=4)
 
-    @staticmethod
-    def _int_to_bytes(val: int) -> bytes:
-        """ Turns an (arbitrarily long) integer into a bytes sequence. """
-        l = val.bit_length() + 1
-        # we need to include the length in the hash in some way, otherwise e.g.
-        # the numbers (0xffff, 0x00) would be encoded identically to (0xff, 0xff00)
-        return pack("<Q", l) + val.to_bytes(l, 'little', signed=True)
-
     def get_partial_hash(self):
         """
         Computes a hash over the contents of this block, except for the nonce. The proof of
@@ -110,7 +113,7 @@ class Block:
         hasher.update(self.prev_block_hash)
         hasher.update(self.merkle_root_hash)
         hasher.update(self.time.strftime("%Y-%m-%dT%H:%M:%S.%f UTC").encode())
-        hasher.update(self._int_to_bytes(self.difficulty))
+        hasher.update(utils.int_to_bytes(self.difficulty))
         return hasher
 
     def finish_hash(self, hasher):
@@ -119,7 +122,7 @@ class Block:
         work can use this function to efficiently try different nonces. Other uses should
         use `hash` to get the complete hash in one step.
         """
-        hasher.update(self._int_to_bytes(self.nonce))
+        hasher.update(utils.int_to_bytes(self.nonce))
         return hasher.digest()
 
     def _get_hash(self):
@@ -131,52 +134,62 @@ class Block:
         """ Verify that the merkle root hash is correct for the transactions in this block. """
         return merkle_tree(self.transactions).get_hash() == self.merkle_root_hash
 
+    def verify_proof_of_work(self):
+        """ Verify the proof of work on a block. """
+        return int.from_bytes(self.hash, 'big') < self.difficulty
+
     def verify_difficulty(self):
         """ Verifies that the hash value is correct and fulfills its difficulty promise. """
-        if self.hash == GENESIS_BLOCK_HASH:
+        if self.height == 0:  # can be removed?!
             return True
-        if not verify_proof_of_work(self):
+        if not self.verify_proof_of_work():
             logging.warning("block does not satisfy proof of work")
             return False
         return True
 
-    def verify_prev_block(self, chain: 'Blockchain'):
+    def verify_prev_block(self, prev_block: 'Block', chain_difficulty: int):
         """ Verifies that the previous block pointer points to the head of the given block chain and difficulty and height are correct. """
-        if chain.head.hash != self.prev_block_hash:
+        if prev_block.hash != self.prev_block_hash:
             logging.warning("Previous block is not head of the block chain.")
             return False
-        if self.difficulty != chain.compute_difficulty_next_block():
+        if self.difficulty != chain_difficulty:
             logging.warning("Block has wrong difficulty.")
             return False
-        if chain.head.height + self.difficulty != self.height:
+        if prev_block.height + 1 != self.height:
             logging.warning("Block has wrong height.")
             return False
         return True
 
-    def verify_transactions(self, chain: 'Blockchain'):
+    def verify_block_transactions(self, unspent_coins: dict, reward: int):
         """ Verifies that all transaction in this block are valid in the given block chain. """
-        mining_reward = None
+        mining_rewards = []
+        all_inputs = []
 
-        trans_set = set(self.transactions)
         for t in self.transactions:
+
+            all_inputs += t.inputs
             if not t.inputs:
-                if mining_reward is not None:
-                    logging.warning("block has more than one reward transaction")
+                if len(mining_rewards) > 1:
+                    logging.warning("block has more than one coinbase transaction")
                     return False
-                mining_reward = t
+                mining_rewards.append(t)
+
+            if not t.validate_tx(unspent_coins):
+                return False
 
-            if not t.verify(chain, trans_set - {t}):
+            if reward != sum(t.amount for t in mining_rewards[0].targets):
+                logging.warning("reward is different than specified")
                 return False
-        if mining_reward is not None:
-            fees = sum(t.get_transaction_fee(chain) for t in self.transactions)
-            reward = chain.compute_blockreward_next_block()
-            used = sum(t.amount for t in mining_reward.targets)
-            if used > fees + reward:
-                logging.warning("mining reward is too large")
+
+            if not self._verify_input_consistency(all_inputs):
                 return False
+
         return True
 
-    def verify_time(self, chain: 'Blockchain'):
+    def _verify_input_consistency(self, tx_inputs: 'List[TransactionInputs]'):
+        return len(tx_inputs) == len(set(tx_inputs))
+
+    def verify_time(self, head_time: datetime):
         """
         Verifies that blocks are not from far in the future, but a bit younger
         than the head of `chain`.
@@ -184,30 +197,22 @@ class Block:
         if self.time - timedelta(hours=2) > datetime.utcnow():
             logging.warning("discarding block because it is from the far future")
             return False
-        if self.time <= chain.head.time:
+        if self.time <= head_time:
             logging.warning("discarding block because it is younger than its predecessor")
             return False
         return True
 
-    def verify(self, chain: 'Blockchain'):
+    def verify(self, prev_block: 'Block', chain_difficulty: int, unspent_coins: dict, chain_indices: dict, reward: int):
         """
         Verifies that this block contains only valid data and can be applied on top of the block
         chain `chain`.
         """
-        assert self.hash not in chain.block_indices
+        assert self.hash not in chain_indices
         if self.height == 0:
             logging.warning("only the genesis block may have height=0")
             return False
-        return self.verify_difficulty() and self.verify_merkle() and self.verify_prev_block(chain) \
-                and self.verify_transactions(chain) and self.verify_time(chain)
-
-from .proof_of_work import verify_proof_of_work, GENESIS_DIFFICULTY, DIFFICULTY_BLOCK_INTERVAL, \
-        DIFFICULTY_TARGET_TIMEDELTA
-
-
-GENESIS_BLOCK = Block("None; {} {}".format(DIFFICULTY_BLOCK_INTERVAL,
-        DIFFICULTY_TARGET_TIMEDELTA).encode(), datetime(2017, 3, 3, 10, 35, 26, 922898), 0, 0,
-        datetime.utcnow(), GENESIS_DIFFICULTY, [], merkle_tree([]).get_hash())
-GENESIS_BLOCK_HASH = GENESIS_BLOCK.hash
 
-from .blockchain import Blockchain
+        return self.verify_difficulty() and self.verify_merkle() and self.verify_prev_block(prev_block,
+                                                                                            chain_difficulty) \
+               and self.verify_time(prev_block
+                                    .time) and self.verify_block_transactions(unspent_coins, reward)

+ 50 - 35
src/blockchain.py

@@ -1,22 +1,32 @@
 """ Definition of block chains. """
 
-__all__ = ['Blockchain']
+__all__ = ['Blockchain', 'GENESIS_BLOCK']
 import logging
-from fractions import Fraction
-from typing import List, Dict, Optional
 
-from .proof_of_work import DIFFICULTY_BLOCK_INTERVAL, DIFFICULTY_TARGET_TIMEDELTA
+from typing import Optional
 
-GENESIS_REWARD = 1000
-""" The reward that is available for the first `REWARD_HALF_LIFE` blocks, starting with the genesis block. """
+from datetime import datetime
+
+from .merkle import merkle_tree
+from .block import Block
+
+from .config import *
+from .utils import compute_blockreward_next_block
+
+# If you want to add transactions in the genesis block you can create a Transaction object and include it in the list below (after GENESIS_DIFFICULTY)
+GENESIS_BLOCK = Block("None; {} {}".format(DIFFICULTY_BLOCK_INTERVAL,
+                                           DIFFICULTY_TARGET_TIMEDELTA).encode(),
+                      datetime(2017, 3, 3, 10, 35, 26, 922898), 0, 0,
+                      datetime.utcnow(), GENESIS_DIFFICULTY, [], merkle_tree([]).get_hash(),
+                      0)
+
+GENESIS_BLOCK_HASH = GENESIS_BLOCK.hash
 
-REWARD_HALF_LIFE = 10000
-""" The number of blocks until the block reward is halved. """
 
 class Blockchain:
     """
-    A block chain: a ordered, immutable list of valid blocks. The only ways to create a blockchain
-    instance are the constructor, which will create a block chain containing only the genesis block,
+    A block chain: a ordered, immutable list of valid blocks. The only way to create a blockchain
+    instance is the constructor, which will create a block chain containing only the genesis block,
     and the `try_append` method which creates a new block chain only if the given block is valid on
     top of `self`.
 
@@ -34,32 +44,38 @@ class Blockchain:
         self.blocks = [GENESIS_BLOCK]
         assert self.blocks[0].height == 0
         self.block_indices = {GENESIS_BLOCK_HASH: 0}
-        assert not GENESIS_BLOCK.transactions
         self.unspent_coins = {}
+        self.total_difficulty = GENESIS_BLOCK.difficulty
 
     def try_append(self, block: 'Block') -> 'Optional[Blockchain]':
         """
-        If `block` is valid on top of this chain, returns a new block chain including that block.
+        If `block` is valid on top of this chain, returns a new blockchain including that block.
         Otherwise, it returns `None`.
         """
 
-        if not block.verify(self):
+        if not block.verify(self.head, self.compute_difficulty_next_block(), self.unspent_coins, self.block_indices,
+                            compute_blockreward_next_block(self.head.height)):
             return None
 
         unspent_coins = self.unspent_coins.copy()
 
         for t in block.transactions:
             for inp in t.inputs:
-                assert inp in unspent_coins, "Aborting computation of unspent transactions because a transaction spent an unavailable coin."
-                del unspent_coins[inp]
+                try:
+                    del unspent_coins[inp.transaction_hash, inp.output_idx]
+                except KeyError:
+                    logging.info("Input was already spent in this block!")
+                    return None
             for i, target in enumerate(t.targets):
-                unspent_coins[TransactionInput(t.get_hash(), i)] = target
+                if target.is_pay_to_pubkey or target.is_pay_to_pubkey_lock:
+                    unspent_coins[(t.get_hash(), i)] = target
 
         chain = Blockchain()
         chain.unspent_coins = unspent_coins
         chain.blocks = self.blocks + [block]
         chain.block_indices = self.block_indices.copy()
         chain.block_indices[block.hash] = len(self.blocks)
+        chain.total_difficulty = self.total_difficulty + block.difficulty
 
         return chain
 
@@ -81,33 +97,32 @@ class Blockchain:
 
     def compute_difficulty_next_block(self) -> int:
         """ Compute the desired difficulty for the block following this chain's `head`. """
-        target_timedelta = Fraction(int(DIFFICULTY_TARGET_TIMEDELTA.total_seconds() * 1000 * 1000))
+        should_duration = DIFFICULTY_TARGET_TIMEDELTA.total_seconds()
 
-        block_idx = len(self.blocks)
-        if block_idx % DIFFICULTY_BLOCK_INTERVAL != 0:
+        if (self.head.height % DIFFICULTY_BLOCK_INTERVAL != 0) or (self.head.height == 0):
             return self.head.difficulty
 
-        duration = self.head.time - self.blocks[block_idx - DIFFICULTY_BLOCK_INTERVAL].time
-        duration = Fraction(int(duration.total_seconds() * 1000 * 1000))
+        last_duration = (
+                self.head.time - self.blocks[self.head.height - DIFFICULTY_BLOCK_INTERVAL].time).total_seconds()
+        diff_adjustment_factor = last_duration / should_duration
+        prev_difficulty = self.head.difficulty
 
-        prev_difficulty = Fraction(self.head.difficulty)
-        hash_rate = prev_difficulty * DIFFICULTY_BLOCK_INTERVAL / duration
-
-        new_difficulty = hash_rate * target_timedelta / DIFFICULTY_BLOCK_INTERVAL
+        new_difficulty = prev_difficulty * diff_adjustment_factor
 
         # the genesis difficulty was very easy, dropping below it means there was a pause
         # in mining, so let's start with a new difficulty!
-        if new_difficulty < self.blocks[0].difficulty:
+        if new_difficulty > self.blocks[0].difficulty:
             new_difficulty = self.blocks[0].difficulty
 
         return int(new_difficulty)
 
-    def compute_blockreward_next_block(self) -> int:
-        """ Compute the block reward that is expected for the block following this chain's `head`. """
-        half_lives = len(self.blocks) // REWARD_HALF_LIFE
-        reward = GENESIS_REWARD // (2 ** half_lives)
-
-        return reward
-
-from .block import Block, GENESIS_BLOCK, GENESIS_BLOCK_HASH
-from .transaction import TransactionInput, Transaction
+    # def get_key_for_tx(self, tx:Transaction) -> Key:
+    #     inp = tx.inputs[0]
+    #     for block in self.blocks:
+    #         for transaction in block.transactions:
+    #             if transaction.get_hash() == inp.transaction_hash:
+    #                 target = transaction.targets[inp.output_idx]
+    #                 logging.info("REWARD TO")
+    #                 logging.info(target.get_pubkey)
+    #                 return target.get_pubkey
+    #     return None

+ 39 - 33
src/chainbuilder.py

@@ -27,17 +27,21 @@ most checkpoints being relatively recent. There also is always one checkpoint wi
 block.
 """
 
+import binascii
 import threading
 import logging
 import math
 from typing import List, Dict, Callable, Optional
-from datetime import datetime, timedelta
+from datetime import datetime
 
-from .block import GENESIS_BLOCK, GENESIS_BLOCK_HASH, Block
-from .blockchain import Blockchain
+from .config import *
+from .block import Block
+from .blockchain import Blockchain, GENESIS_BLOCK, GENESIS_BLOCK_HASH
+from .protocol import Protocol
 
 __all__ = ['ChainBuilder']
 
+
 class BlockRequest:
     """
     Stores information about a pending block request and the partial chains that depend on it.
@@ -51,12 +55,6 @@ class BlockRequest:
     :vartype _request_count: int
     """
 
-    BLOCK_REQUEST_RETRY_INTERVAL = timedelta(minutes=1)
-    """ The approximate interval after which a block request will be retried. """
-    BLOCK_REQUEST_RETRY_COUNT = 3
-    """ The number of failed requests of a block until we give up and delete the depending partial chains. """
-
-
     def __init__(self):
         self.partial_chains = [[]]
         self.clear()
@@ -71,11 +69,12 @@ class BlockRequest:
         self._request_count += 1
         self._last_update = datetime.utcnow()
         protocol.send_block_request(self.partial_chains[0][-1].prev_block_hash)
-        logging.debug("asking for another block %d (attempt %d)", max(len(r) for r in self.partial_chains), self._request_count)
+        logging.debug("asking for another block %d (attempt %d)", max(len(r) for r in self.partial_chains),
+                      self._request_count)
 
     def timeout_reached(self) -> bool:
         """ Returns a bool indicating whether all attempts to download this block have failed. """
-        return self._request_count > self.BLOCK_REQUEST_RETRY_COUNT
+        return self._request_count > BLOCK_REQUEST_RETRY_COUNT
 
     def checked_retry(self, protocol: 'Protocol'):
         """
@@ -83,12 +82,13 @@ class BlockRequest:
         request was sent yet.
         """
 
-        if self._last_update + self.BLOCK_REQUEST_RETRY_INTERVAL < datetime.utcnow():
-            if self._request_count >= self.BLOCK_REQUEST_RETRY_COUNT:
+        if self._last_update + BLOCK_REQUEST_RETRY_INTERVAL < datetime.utcnow():
+            if self._request_count >= BLOCK_REQUEST_RETRY_COUNT:
                 self._request_count += 1
             else:
                 self.send_request(protocol)
 
+
 class ChainBuilder:
     """
     The chain builder maintains the current longest confirmed (primary) block chain as well as
@@ -106,7 +106,7 @@ class ChainBuilder:
     :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.
+                                 block chain.unconfirmed_transactions
     :vartype chain_change_handlers: List[Callable]
     :ivar transaction_change_handlers: Event handlers that get called when we find out about a new
                                        transaction.
@@ -118,10 +118,17 @@ class ChainBuilder:
     def __init__(self, protocol: 'Protocol'):
         self.primary_block_chain = Blockchain()
         self._block_requests = {}
-        self._blockchain_checkpoints = { GENESIS_BLOCK_HASH: self.primary_block_chain }
+        self._blockchain_checkpoints = {GENESIS_BLOCK_HASH: self.primary_block_chain}
 
-        self.block_cache = { GENESIS_BLOCK_HASH: GENESIS_BLOCK }
+        self.block_cache = {GENESIS_BLOCK_HASH: GENESIS_BLOCK}
         self.unconfirmed_transactions = {}
+
+        # Adding the tx from Genesis block to unspent coins
+        for tx in GENESIS_BLOCK.transactions:
+            for i, target in enumerate(tx.targets):
+                if target.is_pay_to_pubkey or target.is_pay_to_pubkey_lock:
+                    self.primary_block_chain.unspent_coins[(tx.get_hash(), i)] = target
+
         # TODO: we want this to be sorted by some function of rewards and age
         # TODO: we want two lists, one with known valid, unapplied transactions, the other with all known transactions (with some limit)
 
@@ -151,8 +158,8 @@ class ChainBuilder:
         hash_val = transaction.get_hash()
 
         def input_ok(inp):
-            return inp in self.primary_block_chain.unspent_coins or \
-                    inp.transaction_hash in self.unconfirmed_transactions
+            return (inp.transaction_hash, inp.output_idx) in self.primary_block_chain.unspent_coins or \
+                   inp.transaction_hash in self.unconfirmed_transactions
 
         if hash_val not in self.unconfirmed_transactions and \
                 all(input_ok(inp) for inp in transaction.inputs):
@@ -163,12 +170,13 @@ class ChainBuilder:
 
     def _new_primary_block_chain(self, chain: 'Blockchain'):
         """ Does all the housekeeping that needs to be done when a new longest chain is found. """
-        logging.info("new primary block chain with height %d with current difficulty %d", len(chain.blocks), chain.head.difficulty)
+        logging.info("new primary block chain with height %d with current difficulty %d", len(chain.blocks),
+                     chain.head.difficulty)
         self._assert_thread_safety()
         self.primary_block_chain = chain
         todelete = set()
         for (hash_val, trans) in self.unconfirmed_transactions.items():
-            if not trans.verify(chain, set()):
+            if not trans.validate_tx(chain.unspent_coins):
                 todelete.add(hash_val)
         for hash_val in todelete:
             del self.unconfirmed_transactions[hash_val]
@@ -201,6 +209,7 @@ class ChainBuilder:
             if next_chain is None:
                 logging.warning("invalid block")
                 break
+                # TODO we need to figure out why the miner stops after an invalid block!
             chain = next_chain
             checkpoints[chain.head.hash] = chain
 
@@ -213,7 +222,6 @@ class ChainBuilder:
         self._blockchain_checkpoints = checkpoints
         self._new_primary_block_chain(chain)
 
-
     def _retry_expired_requests(self):
         """ Sends new block requests to our peers for unanswered pending requests. """
         for request in self._block_requests.values():
@@ -243,28 +251,30 @@ class ChainBuilder:
     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 block.hash in self.block_cache or not block.verify_difficulty() or \
-                not block.verify_merkle():
+
+        if (block.hash in self.block_cache) or (not block.verify_difficulty()) or (not block.verify_merkle()):
             return
+
         self.block_cache[block.hash] = block
 
         self._retry_expired_requests()
 
         if block.hash not in self._block_requests:
-            if block.height > self.primary_block_chain.head.height:
-                if block.hash not in self._block_requests:
-                    self._block_requests[block.hash] = BlockRequest()
-            else:
-                return
+            self._block_requests[block.hash] = BlockRequest()
+        else:
+            return
 
         request = self._block_requests[block.hash]
         del self._block_requests[block.hash]
+
         while True:
             for partial_chain in request.partial_chains:
                 partial_chain.append(block)
-            if block.prev_block_hash not in self.block_cache or block.prev_block_hash in self._blockchain_checkpoints:
+            if (block.prev_block_hash not in self.block_cache) or (
+                    block.prev_block_hash in self._blockchain_checkpoints):
                 break
             block = self.block_cache[block.prev_block_hash]
+
         if block.prev_block_hash in self._block_requests:
             chains = request.partial_chains
             request = self._block_requests[block.prev_block_hash]
@@ -279,7 +289,3 @@ class ChainBuilder:
             for partial_chain in request.partial_chains:
                 self._build_blockchain(checkpoint, partial_chain[::-1])
         request.checked_retry(self.protocol)
-
-from .protocol import Protocol
-from .block import Block
-from .transaction import Transaction

+ 26 - 0
src/config.py

@@ -0,0 +1,26 @@
+from datetime import timedelta
+
+GENESIS_REWARD = 1000
+""" The reward that is available for the first `REWARD_HALF_LIFE` blocks, starting with the genesis block. """
+
+REWARD_HALF_LIFE = 10000
+""" The number of blocks until the block reward is halved. """
+
+BLOCK_REQUEST_RETRY_INTERVAL = timedelta(minutes=1)
+""" The approximate interval after which a block request will be retried. """
+BLOCK_REQUEST_RETRY_COUNT = 3
+""" The number of failed requests of a block until we give up and delete the depending partial chains. """
+
+GENESIS_DIFFICULTY = (1 << 256) - 1
+"""
+The difficulty of the genesis block.
+
+It is directly used for comparison with the hash and thus we start with the smallest possible difficulty which is the maximal possible hash value.
+Thus, everything bellow is a valid proof of work
+"""
+
+DIFFICULTY_BLOCK_INTERVAL = 5
+""" The number of blocks between difficulty changes. """
+DIFFICULTY_TARGET_TIMEDELTA = timedelta(seconds=60)
+""" The time span that it should approximately take to mine `DIFFICULTY_BLOCK_INTERVAL` blocks.  """
+

+ 17 - 11
src/crypto.py

@@ -3,25 +3,31 @@
 import os
 import os.path
 import tempfile
+import random
+import string
 from binascii import hexlify, unhexlify
 from typing import Iterator, Iterable
 
 from Crypto.Signature import PKCS1_PSS
-from Crypto.Hash import SHA512
+from Crypto.Hash import SHA256
 from Crypto.PublicKey import RSA
 
-__all__ = ['get_hasher', 'Signing', 'MAX_HASH']
+# TODO upgrade to ecdsa at https://pypi.org/project/fastecdsa/
+
+__all__ = ['get_hasher', 'Key']
+
 
 def get_hasher():
     """ Returns a object that you can use for hashing, compatible to the `hashlib` interface. """
-    return SHA512.new()
+    return SHA256.new()
 
 
-MAX_HASH = (1 << 512) - 1
-""" The largest possible hash value, when interpreted as an unsigned int. """
+def get_random_int(length: int) -> int:
+    rnd = random.SystemRandom()
+    return rnd.randint(0, (2 ** length) - 1)
 
 
-class Signing:
+class Key:
     """
     Functionality for creating and verifying signatures, and their public/private keys.
 
@@ -48,7 +54,7 @@ class Signing:
     @classmethod
     def generate_private_key(cls):
         """ Generate a new private key. """
-        return Signing(RSA.generate(3072).exportKey())
+        return Key(RSA.generate(1024).exportKey())
 
     @classmethod
     def from_file(cls, path):
@@ -56,7 +62,7 @@ class Signing:
         with open(path, 'rb') as f:
             return cls(f.read())
 
-    def as_bytes(self, include_priv: bool=False) -> bytes:
+    def as_bytes(self, include_priv: bool = False) -> bytes:
         """ Serialize this key to a `bytes` value. """
         if include_priv:
             return self.rsa.exportKey()
@@ -72,7 +78,7 @@ class Signing:
         """ Creates a new object of this class, from a JSON-serializable representation. """
         return cls(unhexlify(obj))
 
-    def __eq__(self, other: 'Signing'):
+    def __eq__(self, other: 'Key'):
         return self.rsa.e == other.rsa.e and self.rsa.n == other.rsa.n
 
     def __hash__(self):
@@ -87,7 +93,7 @@ class Signing:
         return self.rsa.has_private()
 
     @classmethod
-    def read_many_private(cls, file_contents: bytes) -> 'Iterator[Signing]':
+    def read_many_private(cls, file_contents: bytes) -> 'Iterator[Key]':
         """ Reads many private keys from the (binary) contents of a file written with `write_many_private`. """
         end = b"-----END RSA PRIVATE KEY-----"
         for key in file_contents.strip().split(end):
@@ -98,7 +104,7 @@ class Signing:
             yield cls(key)
 
     @staticmethod
-    def write_many_private(path: str, keys: 'Iterable[Signing]'):
+    def write_many_private(path: str, keys: 'Iterable[Key]'):
         """ Writes the private keys in `keys` to the file at `path`. """
         dirname = os.path.dirname(path) or "."
         with tempfile.NamedTemporaryFile("wb", delete=False, dir=dirname) as fp:

+ 1662 - 0
src/labscript.py

@@ -0,0 +1,1662 @@
+#! /usr/bin/env/python3
+import hashlib
+import logging
+from .crypto import *
+from binascii import hexlify, unhexlify
+from datetime import datetime
+from .transaction import *
+
+global transaction
+global prog_stack
+global validity
+global control_flow_stack
+global alt_stack
+
+#The actual interpreter for our scriptlanguage
+def interpreter(inputscript: str, outputscript: str, tx):
+
+    global transaction
+    #get nLockTime from transaction for OP_CHECKLOCKTIME / OP_CHECKLOCKTIMEVERIFY / OP_NOP2
+    transaction = tx
+
+    #parse script-strings to LabScript
+    scriptSig = LabScript(inputscript + " " + outputscript)
+    #scriptPubKey = LabScript(outputscript)
+    
+    #create Scriptstack
+    stack = LabStack()
+
+    #execute scripts and merge the results
+    return scriptSig.execute_script(stack)
+    #return scriptPubKey.execute_script(stack)
+
+def invalidate():
+    """
+    Makes the script invalid. 
+    """
+    global validity
+    validity = False
+
+class LabStack:
+    """
+    Labscript uses a stack for imperative control flow. LabStack is a simple 
+    implementation of a stack. The LabStack is used by the LabScript 
+    interpreter to store intermediate values. A newly initialized LabStack is
+    always empty. 
+    """
+
+    def __init__(self):
+        self.items = []    
+        self.sp = 0
+        return  
+
+    def is_empty(self):
+        """
+        Tests whether the stack is empty and returns true if yes, false 
+        otherwise.
+        """
+        return self.sp == 0
+
+    def size(self):
+        """
+        Returns the number of items currently in the stack.
+        """
+        return self.sp
+
+    def peek(self):
+        """
+        Returns the item at the top of the stack without removing it from the
+        stack. If the stack is empty, None is returned.
+        """
+        if self.is_empty():
+            return None
+        
+        return self.items[self.sp-1]
+
+    def push(self, item):
+        """
+        Appends a new item to the top of the stack and increases the stack 
+        pointer by 1.
+        """
+        self.items.append(item)
+        self.sp += 1
+        return
+
+    def pop(self):
+        """
+        Removes the item at the top of the stack and returns it and the stack
+        pointer is decreased by 1. If the stack is empty, None is returned.
+        """
+        if self.is_empty():
+            return None
+        
+        self.sp -= 1
+        return self.items.pop()
+
+    def set_sp(pos):
+        """
+        Sets the stack pointer to pos and alters the stack accordingly:
+        If pos is greater than the current sp, the stack is made bigger and
+        the new items are initialized with None. If pos is less than the 
+        current sp, the stack is made smaller and all items above the current
+        sp are simply removed from the stack.
+        """
+        while(sp != pos):
+            if pos > self.sp:
+                self.push(None)
+            else:
+                self.pop()
+        return   
+
+    def print_stack(self):
+        """
+        Prints all items on the stack, from top to bottom.
+        """
+        for i in self.items[::-1]:
+            print(str(i))
+        return
+
+    
+class LabScript:
+    """
+    LabScript is a simple imperative stack-based script language. This class
+    implements a script as a List of commands to be executed from left to
+    right. The items in the list of commands can be any data or an operation.
+
+    USAGE:
+    The constructor is called using a string that represents the script. 
+    This string is a long chain consisting of commands, which are arbitrary
+    substrings, each separated by a whitespace. If a command substring matches
+    an opcode string, as specified in the OPLIST below, the interpreter will
+    parse opcode into an operation and execute its behavior. Any other command
+    will be simply pushed onto the stack as data.
+    """
+
+    """
+    Following is an overview  of all implemented operations sorted after area
+    of application.
+    For more information go to https://en.bitcoin.it/wiki/Script
+    or read the explanation within the op-implementation below:
+
+        Constants:
+
+            OP_0
+            OP_FALSE
+            OP_1NEGATE
+            OP_1
+            OP_TRUE
+
+        Flow Control:
+
+            OP_NOP
+            OP_IF
+            OP_NOTIF
+            OP_ELSE
+            OP_ENDIF
+            OP_VERIFY
+            OP_RETURN
+
+        Stack:
+
+            OP_TOALTSTACK
+            OP_FROMALTSTACK
+            OP_IFDUP
+            OP_DROP
+            OP_DUP
+            OP_NIP
+            OP_OVER
+            OP_PICK
+            OP_ROLL
+            OP_ROT
+            OP_SWAP
+            OP_TUCK
+            OP_2DROP
+            OP_2DUP
+            OP_3DUP
+            OP_2OVER
+            OP_2ROT
+            OP_2SWAP
+
+        Splice:
+
+            -
+
+        Bitwise logic:
+
+            OP_INVERT
+            OP_AND
+            OP_OR
+            OP_XOR
+            OP_EQUAL
+            OP_EQUALVERIFY
+
+        Arithmetic:
+
+            OP_1ADD
+            OP_1SUB
+            OP_2MUL
+            OP_2DIV
+            OP_NOT
+            OP_0NOTEQUAL
+            OP_ADD
+            OP_SUB
+            OP_MUL
+            OP_DIV
+            OP_MOD
+            OP_BOOLAND
+            OP_BOOLOR
+            OP_NUMEQUAL
+            OP_NUMEQUALVERIFY
+            OP_NUMNOTEQUAL
+            OP_LESSTHAN
+            OP_GREATERTHAN
+            OP_LESSTHANOREQUAL
+            OP_GREATERTHANOREQUAL
+            OP_MIN
+            OP_MAX
+            OP_WITHIN
+
+        Crypto:
+
+            OP_RIPDEM160
+            OP_SHA1
+            OP_SHA256
+            OP_HASH160
+            OP_HASH256
+            OP_CHECKSIG
+            OP_CHECKSIGVERIFY
+            #OP_CHECKMULTISIG ???
+            #OP_CHECKMULTISIGVERIFY ???
+
+        Locktime:
+
+            OP_CHECKLOCKTIME
+            OP_CHECKLOCKTIMEVERIFY
+            OP_NOP2
+
+        Reserved words:
+
+            OP_NOP1
+            OP_NOP4
+            OP_NOP5
+            OP_NOP6
+            OP_NOP7
+            OP_NOP8
+            OP_NOP9
+            OP_NOP10
+    """
+
+    def __init__(self, list_of_commands: str):
+        self.prog_queue = list_of_commands.split()
+        self.pc = 1 # program counter
+        self.pend = len(self.prog_queue) # end of program
+
+        self.valid = True # is the script valid? if not, better not use it
+                            # for anything important.
+
+        self.if_endif_syntax_check()
+        return
+
+
+    def to_string(self):
+        schtring_aeeh_string = ""
+        for i in self.prog_queue:
+            schtring_aeeh_string+=str(i)+" "
+
+        schtring_aeeh_string = schtring_aeeh_string[:-1]
+        return schtring_aeeh_string 
+
+
+
+    # operation implementations
+
+    def op_ripemd160():
+        #The input is hashed using RIPEMD160.
+        #DONE
+
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        ripemd160 = hashlib.new('ripemd160')
+        ripemd160.update(str(prog_stack.pop()).encode('utf-8'))
+        ripemd160 = hexlify(ripemd160.digest())
+        prog_stack.push(ripemd160.decode('utf-8'))
+        return
+
+    def op_sha1():
+        #The input is hashed using SHA-1.
+        #DONE
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        sha1 = hashlib.sha1()
+        sha1.update(str(prog_stack.pop()).encode('utf-8'))
+        sha1 = hexlify(sha1.digest())
+        prog_stack.push(sha1.decode('utf-8'))
+        return
+    def op_sha256():
+        #The input is hashed using SHA-256.
+        #DONE
+
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        sha256 = hashlib.sha256()
+        sha256.update(str(prog_stack.pop()).encode('utf-8'))
+        sha256 = hexlify(sha256.digest())
+        prog_stack.push(sha256.decode('utf-8'))
+        return
+
+    def op_hash256():
+        #The input is hashed two times with SHA-256.
+        #DONE
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        sha256 = hashlib.sha256()
+        sha256.update(str(prog_stack.pop()).encode('utf-8'))
+        sha256 = hexlify(sha256.digest())
+        prog_stack.push(sha256.decode('utf-8'))
+
+        sha256 = hashlib.sha256()
+        sha256.update(str(prog_stack.pop()).encode('utf-8'))
+        sha256 = hexlify(sha256.digest())
+        prog_stack.push(sha256.decode('utf-8'))
+        return
+
+    def op_hash160():
+        #The input is hashed twice: first with SHA-256 and then with
+        # RIPEMD-160.
+        #DONE
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        sha256 = hashlib.sha256()
+        sha256.update(str(prog_stack.pop()).encode('utf-8'))
+        sha256 = hexlify(sha256.digest())
+        prog_stack.push(sha256.decode('utf-8'))
+
+        ripemd160 = hashlib.new('ripemd160')
+        ripemd160.update(str(prog_stack.pop()).encode('utf-8'))
+        ripemd160 = hexlify(ripemd160.digest())
+        prog_stack.push(ripemd160.decode('utf-8'))
+        return
+
+    def op_checksig():
+        # The signature used by OP_CHECKSIG must be a valid signature for
+        # this hash and public key.
+        #If it is, 1 is returned, 0 otherwise.
+
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            prog_stack.push(str(0))
+            return
+
+        pubKey = Key.from_json_compatible(prog_stack.pop())
+
+        tx_hash = transaction.get_hash()
+
+        sig = unhexlify(prog_stack.pop())
+
+        if pubKey.verify_sign(tx_hash, sig):
+            prog_stack.push(str(1))
+            return
+
+        logging.warning("Signature not verified")
+        prog_stack.push(str(0))
+        return
+
+    def op_checksigverify():
+        #runs checksig and verify afterwards
+        
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            prog_stack.push(str(0))
+            return
+
+        mystring = "-----BEGIN PUBLIC KEY-----" + prog_stack.pop() + "-----END PUBLIC KEY-----"
+        pubKey = bytes(mystring, 'utf-8')
+        sig = unhexlify(prog_stack.pop())
+        #sig = bytes(prog_stack.pop(), 'utf-8')
+
+        signing_service = Key(pubKey)
+
+        hash = transaction.get_hash()
+
+        #verify signature
+        if signing_service.verify_sign(hash, sig):
+            prog_stack.push(str(1))
+            return
+
+        logging.warning("Signature not verified")
+        prog_stack.push(str(0))
+
+        #verify
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        if(not(int(prog_stack.pop()))):
+            logging.warning('Transaction not valid!')
+            invalidate()
+        return
+        
+
+    def op_nop():
+        #Does nothing
+        #DONE
+        pass
+        return
+
+    def op_equal():
+        #Returns 1 if the inputs are exactly equal, 0 otherwise.
+        #DONE
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop() 
+        x2 = prog_stack.pop()
+        
+        if(x1 == x2):
+            prog_stack.push(str(1))
+        else:
+            prog_stack.push(str(0))
+
+        return
+
+    def op_verify():
+        #Marks transaction as invalid if top stack value is not true. The
+        # top stack value is removed.
+        #DONE
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        if(not(int(prog_stack.pop()))):
+            logging.warning('Transaction not valid!')
+            invalidate()
+        return
+
+    def op_if():
+        #DONE
+        # If the top stack value is not False, the statements are executed.
+        # The top stack value is removed. 
+
+            # this operation pushes a new conditional flag onto the
+            # control flow stack according to this scheme:
+            #
+            # control_flow_stack, prog_stack:   push flag
+            # -----------------------------------------------
+            # ALLOW_ALL, TRUE:      push ALLOW_ALL
+            # ALLOW_ALL, FALSE:     push ALLOW_IF_ELSE
+            #
+            # ALLOW_IF_ELSE, TRUE:  push ALLOW_IF
+            # ALLOW_IF_ELSE, FALSE: push ALLOW_IF
+            #
+            # ALLOW_IF, TRUE:       push ALLOW_IF
+            # ALLOW_IF, FALSE:      push ALLOW_IF 
+
+        ALLOW_ALL=0 # Any operation can be executed.
+        ALLOW_IF_ELSE=1 # Nothing but ifs, elses and endifs can be executed.
+        ALLOW_IF=2  # Nothing but ifs and endifs can be executed. 
+
+        flag = control_flow_stack.peek()
+
+        if flag == ALLOW_ALL:
+
+            if(prog_stack.is_empty()):
+                logging.warning("Stack is empty")
+                invalidate()
+                return
+
+            if int(prog_stack.pop()):
+                control_flow_stack.push(ALLOW_ALL)
+            else: 
+                control_flow_stack.push(ALLOW_IF_ELSE)
+        else:
+            control_flow_stack.push(ALLOW_IF)
+
+        return
+
+    def op_notif():
+        #DONE
+        # If the top stack value is False, the statements are executed.
+        # The top stack value is removed. 
+
+            # this operation pushes a new conditional flag onto the
+            # control flow stack. Any operations that follow are only 
+            # executed if the current flag is True. 
+
+        ALLOW_ALL=0 # Any operation can be executed.
+        ALLOW_IF_ELSE=1 # Nothing but ifs, elses and endifs can be executed.
+        ALLOW_IF=2  # Nothing but ifs and endifs can be executed.
+
+        flag = control_flow_stack.peek()
+
+        if flag == ALLOW_ALL:
+
+            if(prog_stack.is_empty()):
+                logging.warning("Not enough arguments")
+                invalidate()
+                return
+
+            if int(prog_stack.pop()):
+                control_flow_stack.push(ALLOW_IF_ELSE)
+            else:
+                control_flow_stack.push(ALLOW_ALL)
+        else:
+            control_flow_stack.push(ALLOW_IF)
+        
+        return
+
+    def op_else():
+        #DONE
+        # If the preceding OP_IF or OP_NOTIF or OP_ELSE was not executed
+        # then these statements are and if the preceding OP_IF or OP_NOTIF
+        # or OP_ELSE was executed then these statements are not. 
+
+            # this operation changes the current conditional flag onto the
+            # control flow stack according to this scheme:
+            #
+            # control_flow_stack, prog_stack:   push flag
+            # -----------------------------------------------
+            # ALLOW_ALL     -> ALLOW_IF_ELSE    
+            # ALLOW_IF_ELSE -> ALLOW_ALL
+            # if the flag is ALLOW_IF, this is never executed.
+
+        ALLOW_ALL=0 # Any operation can be executed.
+        ALLOW_IF_ELSE=1 # Nothing but ifs, elses and endifs can be executed.
+        ALLOW_IF=2  # Nothing but ifs and endifs can be executed.
+
+        flag = control_flow_stack.pop()
+        if flag == ALLOW_ALL:
+            control_flow_stack.push(ALLOW_IF_ELSE)
+        if flag == ALLOW_IF_ELSE:
+            control_flow_stack.push(ALLOW_ALL)
+
+        return
+
+    def op_endif():
+        #DONE
+        # Ends an if/else block.
+        control_flow_stack.pop()
+        return
+
+    def op_return():
+
+        """Marks transaction as invalid.
+        A standard way of attaching extra data to transactions is to add a zero-value output with a
+        scriptPubKey consisting of OP_RETURN followed by exactly one pushdata op. Such outputs are
+        provably unspendable, reducing their cost to the network.
+        Currently it is usually considered non-standard (though valid) for a transaction to have more
+        than one OP_RETURN output or an OP_RETURN output with more than one pushdata op. """
+
+        #DONE
+        prog_stack.push(str(0))
+        logging.warning('Transaction not valid!')
+        invalidate()
+        return           
+
+    def op_dup():
+        #Duplicates the top stack item.
+        #DONE
+
+        if(prog_stack.peek()!=None):
+            prog_stack.push(prog_stack.peek())
+            return
+
+        logging.warning("Stack is empty")
+        invalidate()
+        return
+
+    def op_drop():
+        #Removes the top stack item.
+        #DONE
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is already empty")
+            invalidate()
+            return
+
+        prog_stack.pop()
+        return
+
+    def op_checklocktime():
+
+        global transaction
+
+        #Error Indicator
+        errno = 0
+
+        #Error if Stack is empty
+        if(prog_stack.is_empty() or prog_stack.size() < 2):
+            errno = 1
+
+
+        #if top stack item is greater than the transactions nLockTime field ERROR
+        temp = float(prog_stack.pop())
+        try:
+            timestamp = datetime.fromtimestamp(temp)
+        except TypeError:
+            logging.error("A timestamp needs to be supplied after the OP_CHECKLOCKTIME operation!")
+            invalidate()
+            return
+
+        #TODO we need to make sure that the timezones are being taken care of
+        if(timestamp > datetime.utcnow()):
+            print("You need to wait at least " + str(timestamp - datetime.utcnow()) + " to spend this Tx")
+            errno = 3
+
+        if(errno):
+            #errno = 1 Stack is empty
+            if(errno == 1):
+                logging.warning('Stack is empty!')
+                invalidate()
+                return
+
+            #errno = 2 Top-Stack-Value < 0
+            if(errno == 2):
+                logging.warning('Top-Stack-Value < 0')
+                invalidate()
+                return
+
+            #errno = 3 top stack item is greater than the transactions
+            if(errno == 3):
+                #logging.warning('you need to wait more to unlock the funds!')
+                invalidate()
+                return
+        return
+
+    def op_equalverify():
+        #Same as OP_EQUAL, but runs OP_VERIFY afterward.
+        #DONE
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop() 
+        x2 = prog_stack.pop()
+        
+        if(x1 == x2):
+            prog_stack.push(str(1))
+        else:
+            prog_stack.push(str(0))
+
+        #verify
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        if(not(int(prog_stack.pop()))):
+            logging.warning('Transaction not valid!')
+            invalidate()
+        return
+
+    def op_invert():
+        #Flips all of the bits in the input. disabled at BITCOIN.
+        #DONE
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        prog_stack.push(str(~ int(prog_stack.pop())))
+        return
+
+    def op_and():
+        #Boolean and between each bit in the inputs. disabled at BITCOIN.
+        #DONE
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        x2 = int(prog_stack.pop())
+
+        prog_stack.push(str(x1 & x2))
+
+        return
+
+    def op_or():
+        #Boolean or between each bit in the inputs. disabled at BITCOIN.
+        #DONE
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        x2 = int(prog_stack.pop())
+
+        prog_stack.push(str(x1 | x2))
+
+        return
+
+    def op_xor():
+        #Boolean exclusive or between each bit in the inputs. disabled at BITCOIN.
+        #DONE
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        x2 = int(prog_stack.pop())
+
+        prog_stack.push(str(x1 ^ x2))
+
+        return
+
+    def op_true():
+        #op_1()
+        #The number 1 is pushed onto the stack. 
+        #DONE
+        prog_stack.push(str(1))
+        return
+
+    def op_false():
+        #op_0()
+        #The number 0 is pushed onto the stack.
+        #DONE
+        prog_stack.push(str(0))
+        return
+
+    def op_1negate():
+        #The number -1 is pushed onto the stack.
+        #DONE
+        prog_stack.push(str(-1))
+        return
+
+    def op_toaltstack():
+        #Puts the input onto the top of the alt stack.
+        #Removes it from the main stack.
+        #DONE
+
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+
+        alt_stack.push(prog_stack.pop())
+        return
+
+    def op_fromaltstack():
+        #Puts the input onto the top of the main stack.
+        #Removes it from the alt stack.
+        #DONE
+
+        if(alt_stack.is_empty()):
+            logging.warning("Alt_stack is empty")
+            invalidate()
+            return
+
+        prog_stack.push(alt_stack.pop())
+        return
+
+    def op_ifdup():
+        #If the top stack value is not 0, duplicate it.
+        #DONE
+
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        if(int(prog_stack.peek()) != 0):
+            op_dup()
+
+        return
+
+    def op_nip():
+        #Removes the second-to-top stack item.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        prog_stack.pop()
+        prog_stack.push(x1)
+
+        return
+
+    def op_over():
+        #Copies the second-to-top stack item to the top.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.peek()
+        prog_stack.push(x1)
+        prog_stack.push(x2)
+        return
+
+    def op_pick():
+        #The item n back in the stack is copied to the top.
+
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        n = int(prog_stack.pop())
+
+        if(prog_stack.size() < n):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        stack_list = list()
+
+        for i in range(n):
+            stack_list.append(prog_stack.pop())
+
+        for i in range(n):
+            prog_stack.push(stack_list[n - 1 - i])
+
+        prog_stack.push(stack_list[n-1])
+
+        return
+
+    def op_roll():
+        #The item n back in the stack is moved to the top.
+
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        n = int(prog_stack.pop())
+
+        if(prog_stack.size() < n):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        stack_list = list()
+
+        for i in range(n):
+            stack_list.append(prog_stack.pop())
+
+        for i in range (n-1):
+            prog_stack.push(stack_list[n - 2 - i])
+
+        prog_stack.push(stack_list[n-1])
+
+        return
+
+    def op_rot():
+        #The top three items on the stack are rotated to the left.
+
+        if(prog_stack.size() < 3):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        x3 = prog_stack.pop()
+        
+        prog_stack.push(x1)
+        prog_stack.push(x3)
+        prog_stack.push(x2)
+
+        return
+
+    def op_swap():
+        #The top two items on the stack are swapped.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        
+        prog_stack.push(x1)
+        prog_stack.push(x2)
+
+        return
+
+    def op_tuck():
+        #The item at the top of the stack is copied
+        #and inserted before the second-to-top item.
+        #like cuddling :3 OwO
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        
+        prog_stack.push(x1)
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+
+        return
+
+    def op_2drop():
+        #Removes the top two stack items.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        prog_stack.pop()
+        prog_stack.pop()
+        return
+
+    def op_2dup():
+        #Duplicates the top two stack items.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+
+        return
+
+    def op_3dup():
+        #Duplicates the top three stack items.
+        if(prog_stack.size() < 3):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        x3 = prog_stack.pop()
+        
+        prog_stack.push(x3)
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+        prog_stack.push(x3)
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+
+        return
+
+    def op_2over():
+        #Copies the pair of items two spaces back in the stack to the front.
+        if(prog_stack.size() < 4):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        x3 = prog_stack.pop()
+        x4 = prog_stack.pop()
+        
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+        prog_stack.push(x4)
+        prog_stack.push(x3)
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+
+        return
+
+    def op_2rot():
+        #The fifth and sixth items back are moved to the top of the stack.
+        if(prog_stack.size() < 6):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        x3 = prog_stack.pop()
+        x4 = prog_stack.pop()
+        x5 = prog_stack.pop()
+        x6 = prog_stack.pop()
+        
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+        prog_stack.push(x6)
+        prog_stack.push(x5)
+        prog_stack.push(x4)
+        prog_stack.push(x3)
+
+        return
+
+    def op_2swap():
+        #Swaps the top two pairs of items.
+        if(prog_stack.size() < 4):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x1 = prog_stack.pop()
+        x2 = prog_stack.pop()
+        x3 = prog_stack.pop()
+        x4 = prog_stack.pop()
+        
+        prog_stack.push(x2)
+        prog_stack.push(x1)
+        prog_stack.push(x4)
+        prog_stack.push(x3)
+
+        return
+
+    def op_1add():
+        #1 is added to the input.
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        x1 += 1
+        prog_stack.push(str(x1))
+        return
+
+    def op_1sub():
+        #1 is subtracted from the input.
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        x1 -= 1
+        prog_stack.push(str(x1))
+        return
+
+    def op_2mul():
+        #The input is multiplied by 2. disabled at BITCOIN.
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        x1 *= 2
+        prog_stack.push(str(x1))
+        return
+
+    def op_2div():
+        #The input is divided by 2. disabled at BITCOIN.
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        x1 /= 2
+        prog_stack.push(str(x1))
+        return
+
+    def op_not():
+        #If the input is 0 or 1, it is flipped. Otherwise the output will be 0.
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        
+        if(x1 == 0):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_0notequal():
+        #Returns 0 if the input is 0. 1 otherwise.
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        x1 = int(prog_stack.pop())
+        
+        if(x1 == 0):
+            prog_stack.push(str(0))
+            return
+
+        prog_stack.push(str(1))
+        return
+
+    def op_add():
+        #a is added to b.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        prog_stack.push(str(a + b))
+        return
+
+    def op_sub():
+        #b is subtracted from a.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        prog_stack.push(str(a - b))
+        return
+
+    def op_mul():
+        #a is multiplied by b. disabled at BITCOIN.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        prog_stack.push(str(a * b))
+        return
+
+    def op_div():
+        #a is divided by b. disabled at BITCOIN.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        prog_stack.push(str(a / b))
+        return
+
+    def op_mod():
+        #Returns the remainder after dividing a by b. disabled at BITCOIN.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        prog_stack.push(str(a % b))
+        return
+
+    def op_booland():
+        #If both a and b are not "" (null string), the output is 1. Otherwise 0.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = prog_stack.pop()
+        b = prog_stack.pop()
+
+        if(a != "" and b != ""):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_boolor():
+        #If a or b is not "" (null string), the output is 1. Otherwise 0.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = prog_stack.pop()
+        b = prog_stack.pop()
+
+        if(a != "" or b != ""):
+            prog_stack.push(str(1))
+            return
+
+        op_false()
+        return
+
+    def op_numequal():
+        #Returns 1 if the numbers are equal, 0 otherwise.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a == b):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_numequalverify():
+        #Same as OP_NUMEQUAL, but runs OP_VERIFY afterward.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a == b):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+
+        #verify
+        if(prog_stack.is_empty()):
+            logging.warning("Stack is empty")
+            invalidate()
+            return
+
+        if(not(int(prog_stack.pop()))):
+            logging.warning('Transaction not valid!')
+            invalidate()
+        return
+
+    def op_numnotequal():
+        #Returns 1 if the numbers are not equal, 0 otherwise.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a != b):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_lessthan():
+        #Returns 1 if a is less than b, 0 otherwise.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a < b):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_greaterthan():
+        #Returns 1 if a is greater than b, 0 otherwise.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a > b):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_lessthanorequal():
+        #Returns 1 if a is less than or equal to b, 0 otherwise.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a <= b):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_greaterthanorequal():
+        #Returns 1 if a is greater than or equal to b, 0 otherwise.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a >= b):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+    def op_min():
+        #Returns the smaller of a and b.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a < b):
+            prog_stack.push(str(a))
+            return
+
+        prog_stack.push(str(b))
+        return
+
+    def op_max():
+        #Returns the larger of a and b.
+        if(prog_stack.size() < 2):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        a = int(prog_stack.pop())
+        b = int(prog_stack.pop())
+
+        if(a > b):
+            prog_stack.push(str(a))
+            return
+
+        prog_stack.push(str(b))
+        return
+
+    def op_within():
+        #Returns 1 if x is within the specified range (left-inclusive), 0 otherwise.
+        if(prog_stack.size() < 3):
+            logging.warning("Not enough arguments")
+            invalidate()
+            return
+
+        x = int(prog_stack.pop())
+        mini = int(prog_stack.pop())
+        maxi = int(prog_stack.pop())
+
+        if(mini <= x and x < maxi):
+            prog_stack.push(str(1))
+            return
+
+        prog_stack.push(str(0))
+        return
+
+
+    
+
+    """
+    OPLIST:
+    This is the dictionary used by the fetch-and-execute loop to parse
+    opcode strings into proper operations. Proper operations will then be
+    executed. Any command string that matches one of the opcodes will be
+    treated like an operation.
+    """
+    global operations
+    operations = {
+        # opcode                     # proper operation
+        'OP_RIPEMD160':               op_ripemd160,
+        'OP_SHA1':                    op_sha1,
+        'OP_SHA256':                  op_sha256,
+        'OP_HASH256':                 op_hash256,
+        'OP_HASH160':                 op_hash160,
+        'OP_VERIFYSIG':               op_checksig,
+        'OP_CHECKSIG':                op_checksig,
+        'OP_CHECKSIGVERIFY':          op_checksigverify,
+
+        'OP_NOP':                     op_nop,
+        'OP_EQUAL':                   op_equal,
+        'OP_VERIFY':                  op_verify,
+        'OP_IF':                      op_if,
+        'OP_NOTIF':                   op_notif,
+        'OP_ELSE':                    op_else,
+        'OP_ENDIF':                   op_endif,
+        'OP_RETURN':                  op_return,
+
+        'OP_DUP':                     op_dup,
+        'OP_DROP':                    op_drop,
+
+        'OP_CHECKLOCKTIME':           op_checklocktime,
+        'OP_CHECKLOCKTIMEVERIFY':     op_checklocktime,
+        'OP_NOP2':                    op_checklocktime,
+
+        'OP_EQUALVERIFY':             op_equalverify,
+        'OP_INVERT':                  op_invert,
+        'OP_AND':                     op_and,
+        'OP_OR':                      op_or,
+        'OP_XOR':                     op_xor,
+        'OP_TRUE':                    op_true,
+        'OP_1':                       op_true,
+        'OP_FALSE':                   op_false,
+        'OP_0':                       op_false,
+        'OP_1NEGATE':                 op_1negate,
+
+        'OP_TOALTSTACK':              op_toaltstack,
+        'OP_FROMALTSTACK':            op_fromaltstack,
+        'OP_IFDUP':                   op_ifdup,
+        'OP_NIP':                     op_nip,
+        'OP_OVER':                    op_over,
+        'OP_PICK':                    op_pick,
+        'OP_ROLL':                    op_roll,
+        'OP_ROT':                     op_rot,
+        'OP_SWAP':                    op_swap,
+        'OP_TUCK':                    op_tuck,
+        'OP_2DROP':                   op_2drop,
+        'OP_2DUP':                    op_2dup,
+        'OP_3DUP':                    op_3dup,
+        'OP_2OVER':                   op_2over,
+        'OP_2ROT':                    op_2rot,
+        'OP_2SWAP':                   op_2swap,
+
+        'OP_1ADD':                    op_1add,
+        'OP_1SUB':                    op_1sub,
+        'OP_2MUL':                    op_2mul,
+        'OP_2DIV':                    op_2div,
+        'OP_NOT':                     op_not,
+        'OP_0NOTEQUAL':               op_0notequal,
+        'OP_ADD':                     op_add,
+        'OP_SUB':                     op_sub,
+        'OP_MUL':                     op_mul,
+        'OP_DIV':                     op_div,
+        'OP_MOD':                     op_mod,
+        'OP_BOOLAND':                 op_booland,
+        'OP_BOOLOR':                  op_boolor,
+        'OP_NUMEQUAL':                op_numequal,
+        'OP_NUMEQUALVERIFY':          op_numequalverify,
+        'OP_NUMNOTEQUAL':             op_numnotequal,
+        'OP_LESSTHAN':                op_lessthan,
+        'OP_GREATERTHAN':             op_greaterthan,
+        'OP_LESSTHANOREQUAL':         op_lessthanorequal,
+        'OP_GREATERTHANOREQUAL':      op_greaterthanorequal,
+        'OP_MIN':                     op_min,
+        'OP_MAX':                     op_max,
+        'OP_WITHIN':                  op_within,
+
+        'OP_NOP1':                    op_nop,
+        'OP_NOP4':                    op_nop,
+        'OP_NOP5':                    op_nop,
+        'OP_NOP6':                    op_nop,
+        'OP_NOP7':                    op_nop,
+        'OP_NOP8':                    op_nop,
+        'OP_NOP9':                    op_nop,
+        'OP_NOP10':                   op_nop,
+        }
+
+    def if_endif_syntax_check(self):
+        """
+        Checks whether the program contains proper if/endif syntax as
+        specified in the documentation. 
+        """
+        global operations
+
+        counter = 0
+
+        pq_copy = self.prog_queue.copy()
+        # go through the program queue and convert all opcodes into
+            # proper operations
+
+        ifs = {'OP_IF', 'OP_NOTIF'}
+        elses = {'OP_ELSE'}
+        endifs = {'OP_ENDIF'}
+
+        # Ifs increase the counter, endifs decrease it. Program is only valid
+        # when the counter is 0 at the end and all existing elses occured
+        # when the counter was greater than zero.
+        for command in pq_copy:
+            if command in ifs:
+                counter += 1
+            if command in endifs:
+                counter -= 1
+            if command in elses and counter == 0:
+                invalidate()
+
+        if counter != 0:
+            invalidate()
+
+
+    def execute_script(self, prog_stacky: LabStack, printstack = 0):
+        """
+        Calling this instance method upon a LabScript instance executes the
+        script. 
+        """
+        global prog_stack
+        prog_stack = prog_stacky
+        global validity
+        validity = True
+
+        global alt_stack
+        alt_stack = LabStack()  # program memory
+
+        global control_flow_stack
+        control_flow_stack = LabStack()
+
+        """
+            stack used for control flow. The topmost entry informs the program
+            whether the oncoming flow of operations can be executed, as allowed
+            or disallowed by any recent conditional operations. this acts like
+            a state machine. We need these flags to prevent execution of else
+            statements across layered if-blocks.
+        """
+
+        ALLOW_ALL=0 # Any operation can be executed.
+        ALLOW_IF_ELSE=1 # Nothing but ifs, elses and endifs can be executed.
+        ALLOW_IF=2  # Nothing but ifs and endifs can be executed. 
+        # To understand what all of this crap means, execute this code on
+        # paper and write down what flags are pushed when:
+        """    
+             OP_IF (true)
+             OP_NOP
+             OP_ELSE
+             OP_IF
+             OP_NOP
+             OP_ELSE
+             OP_NOP
+             OP_ENDIF
+             OP_ELSE
+             OP_NOP
+             OP_ENDIF
+        """
+        
+        control_flow_stack.push(ALLOW_ALL)
+
+        
+        #This loop keeps fetching commands from the prog_queue and tries to
+        #interpret and execute them, until end of program is reached.
+        while(self.pc <= self.pend):
+
+            #global validity
+            self.valid = validity
+            # Check for validity. If invalid, stop executing.
+            if not self.valid:
+                logging.warning("[!] Error: Invalid.")
+                return 0
+
+            next_item = self.prog_queue[self.pc-1] # Fetch next item
+            operationstring = next_item
+
+            pushed = 0
+
+            # Check if item is data or opcode. If data, push onto stack.
+            if (next_item not in operations):
+                if control_flow_stack.peek() == ALLOW_ALL:
+                    prog_stack.push(next_item)
+                    pushed = 1
+
+                #print
+                if(printstack):
+                    print("--------------------------")
+                    print("SCRIPT: \"" + self.to_string() + "\"")
+                    print("--------------------------")
+                    if(pushed):
+                        print("PUSHED:")
+                    else:
+                        print("DIDN'T PUSH:")
+                    print(str(operationstring) + "\n")
+                    print("COUNTER:")
+                    print("programm_end: " + str(self.pend))
+                    print("programm_step: " + str(self.pc))
+                    print("\n")
+                    print("STACK:")
+                    prog_stack.print_stack()
+                    print("\nCONTROL_FLOW_STACK:")
+                    control_flow_stack.print_stack()
+                    print("--------------------------")
+                    #input()
+
+                self.pc = self.pc + 1
+
+                continue
+            
+            op = operations[next_item] # Proper operation to be executed
+
+            # Check if op_code is op_if or op_endif. Always execute
+            # these operations.
+            if operationstring in {'OP_IF', 'OP_ENDIF'}:
+                op() # EXECUTION
+
+            # Check if op_code is op_else. Only allow this operation if
+            # the control flow flag is ALLOW_ELSE.
+            elif control_flow_stack.peek() == ALLOW_IF_ELSE:
+                if operationstring == 'OP_ELSE':
+                    op() # EXECUTION
+
+            # If op_code is any other proper operation, execute it only
+            # if the current control flow allows it. It won't get performed
+            # if we are in an if/else-block that was evaluated to false.
+            elif control_flow_stack.peek() == ALLOW_ALL:
+                op() # EXECUTION
+
+            #if the printstack argument is given, the stack is printed after each step.
+
+            if(printstack):
+                print("--------------------------")
+                print("SCRIPT: \"" + self.to_string() + "\"")
+                print("--------------------------")
+                print("OPERATION:")
+                print(str(operationstring) + "\n")
+                print("COUNTER:")
+                print("programm_end: " + str(self.pend))
+                print("programm_step: " + str(self.pc) + "\n")
+                print("STACK:")
+                prog_stack.print_stack()
+                print("\nCONTROL_FLOW_STACK:")
+                control_flow_stack.print_stack()
+                print("--------------------------")
+                #input()
+
+            self.pc= self.pc + 1
+
+        # If Progstack is empty or TRUE is on top return true otherwise
+        # invalidate and return FALSE
+        if (prog_stack.is_empty() or (prog_stack.size() == 1 and prog_stack.peek() == '1')):
+            return 1
+        else:
+            invalidate()
+            logging.warning("[!] Error: Invalid Tx.")
+            return 0
+
+        

+ 9 - 12
src/merkle.py

@@ -4,10 +4,10 @@ import json
 import os
 import sys
 import signal
-import time
+
 import select
 from threading import Thread, Condition
-from typing import Optional, Callable, Tuple, List
+from typing import Callable, Tuple, List
 
 from .proof_of_work import ProofOfWork
 from .chainbuilder import ChainBuilder
@@ -16,10 +16,9 @@ from . import mining_strategy
 
 __all__ = ['Miner']
 
-
-
 signal.signal(signal.SIGCHLD, signal.SIG_IGN)
 
+
 def exit_on_pipe_close(pipe):
     """ Waits until the pipe `pipe` can no longer be used, then kills this process. """
     poller = select.poll()
@@ -38,11 +37,11 @@ def start_process(func: Callable) -> Tuple[int, int]:
     """
     rx, wx = os.pipe()
     pid = os.fork()
-    if pid == 0: # child
+    if pid == 0:  # child
         try:
             os.close(0)
             os.closerange(3, wx)
-            os.closerange(wx + 1, 2**16)
+            os.closerange(wx + 1, 2 ** 16)
 
             Thread(target=exit_on_pipe_close, args=(wx,), daemon=True).start()
 
@@ -55,10 +54,11 @@ def start_process(func: Callable) -> Tuple[int, int]:
             traceback.print_exc()
             os._exit(1)
         os._exit(0)
-    else: # parent
+    else:  # parent
         os.close(wx)
         return rx, pid
 
+
 def wait_for_result(pipes: List[int], cls: type):
     """
     Waits for one of the pipes in `pipes` to become ready, reads a JSON object from that pipe
@@ -77,6 +77,7 @@ def wait_for_result(pipes: List[int], cls: type):
     with os.fdopen(ready[0], "r") as fp:
         return cls.from_json_compatible(json.load(fp))
 
+
 class Miner:
     """
     Management of a background process that mines for new blocks.
@@ -102,7 +103,7 @@ class Miner:
     :ivar _cur_miner_pids: Process ids of our worker processes.
     :vartype _cur_miner_pids: List[int]
     :ivar reward_pubkey: The public key to which mining fees and block rewards should be sent to.
-    :vartype reward_pubkey: Signing
+    :vartype reward_pubkey: Key
     """
 
     def __init__(self, proto, reward_pubkey):
@@ -187,7 +188,3 @@ class Miner:
             self._stop_mining_for_now()
             self._miner_cond.notify()
         self.chainbuilder.chain_change_handlers.remove(self._chain_changed)
-
-from .protocol import Protocol
-from .chainbuilder import ChainBuilder
-from .crypto import Signing

+ 20 - 12
src/mining_strategy.py

@@ -3,12 +3,18 @@
 from typing import List
 
 from .block import Block
-from .transaction import Transaction, TransactionTarget
+
+from .labscript import *
+from .utils import compute_blockreward_next_block
+
+from .blockchain import Blockchain
+from .crypto import Key
 
 __all__ = ['create_block']
 
+
 def create_block(blockchain: 'Blockchain', unconfirmed_transactions: 'List[Transaction]',
-                 reward_pubkey: 'Signing') -> 'Block':
+                 reward_pubkey: 'Key') -> 'Block':
     """
     Creates a new block that can be mined.
 
@@ -17,19 +23,21 @@ def create_block(blockchain: 'Blockchain', unconfirmed_transactions: 'List[Trans
                                      this block.
     :param reward_pubkey: The key that should receive block rewards.
     """
+
+    # sort the uncorfirmed transactions by the transaction fee amount
+    sorted_unconfirmed_tx = sorted(unconfirmed_transactions,
+                                   key=lambda tx: tx.get_transaction_fee(blockchain.unspent_coins), reverse=True)
     transactions = set()
-    for t in unconfirmed_transactions:
-        if t.verify(blockchain, transactions):
-            # TODO: choose most profitable of conflicting transactions
+
+    for t in sorted_unconfirmed_tx:
+        if t.validate_tx(blockchain.unspent_coins) and not t.check_tx_collision(transactions):
             transactions.add(t)
 
+    reward = compute_blockreward_next_block(blockchain.head.height)
+    fees = sum(t.get_transaction_fee(blockchain.unspent_coins) for t in transactions)
 
-    reward = blockchain.compute_blockreward_next_block()
-    fees = sum(t.get_transaction_fee(blockchain) for t in transactions)
-    trans = Transaction([], [TransactionTarget(reward_pubkey, reward + fees)], [], iv=blockchain.head.hash)
+    trans = Transaction([], [TransactionTarget(TransactionTarget.pay_to_pubkey(reward_pubkey), fees + reward)],
+                        datetime.utcnow(), iv=blockchain.head.hash)
     transactions.add(trans)
 
-    return Block.create(blockchain, list(transactions))
-
-from .blockchain import Blockchain
-from .crypto import Signing
+    return Block.create(blockchain.compute_difficulty_next_block(), blockchain.head, list(transactions))

+ 9 - 18
src/persistence.py

@@ -1,27 +1,18 @@
 """ Implementation and verification of the proof of work. """
 
+import logging
+
 from datetime import timedelta
 from typing import Optional
+from datetime import datetime
 
-from .crypto import MAX_HASH
-
-__all__ = ['verify_proof_of_work', 'GENESIS_DIFFICULTY', 'ProofOfWork']
+from .block import 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) > (MAX_HASH - MAX_HASH // block.difficulty)
+from .config import *
 
-GENESIS_DIFFICULTY = 1000
-"""
-The difficulty of the genesis block.
 
-Right now this is the average required number of hashes to compute one valid block.
-"""
+__all__ = ['GENESIS_DIFFICULTY', 'ProofOfWork']
 
-DIFFICULTY_BLOCK_INTERVAL = 30
-""" The number of blocks between difficulty changes. """
-DIFFICULTY_TARGET_TIMEDELTA = timedelta(minutes=1)
-""" The time span that it should approximately take to mine `DIFFICULTY_BLOCK_INTERVAL` blocks.  """
 
 class ProofOfWork:
     """
@@ -40,6 +31,7 @@ class ProofOfWork:
         self.stopped = False
         self.block = block
         self.success = False
+        self.init_time = 0
 
     def abort(self):
         """ Aborts execution of this proof of work. """
@@ -50,13 +42,12 @@ class ProofOfWork:
         Perform the proof of work on a block, until `stopped` becomes True or the proof of
         work was successful.
         """
+        self.init_time = datetime.now()
         hasher = self.block.get_partial_hash()
         while not self.stopped:
             for _ in range(1000):
                 self.block.hash = self.block.finish_hash(hasher.copy())
-                if verify_proof_of_work(self.block):
+                if self.block.verify_proof_of_work():
                     return self.block
                 self.block.nonce += 1
         return None
-
-from .block import Block

+ 14 - 10
src/protocol.py

@@ -24,11 +24,10 @@ from collections import namedtuple
 from threading import Thread, Lock
 from queue import Queue, PriorityQueue
 from binascii import unhexlify, hexlify
-from uuid import UUID, uuid4
+from uuid import uuid4
 from typing import Callable, List, Optional
 
-from .block import GENESIS_BLOCK_HASH
-
+from .blockchain import GENESIS_BLOCK_HASH
 
 __all__ = ['Protocol', 'PeerConnection', 'MAX_PEERS', 'HELLO_MSG']
 
@@ -45,6 +44,7 @@ succeed.
 SOCKET_TIMEOUT = 30
 """ The socket timeout for P2P connections. """
 
+
 class PeerConnection:
     """
     Handles the low-level socket connection to one other peer.
@@ -59,7 +59,7 @@ class PeerConnection:
     :ivar outgoing_msgs: A queue of messages we want to send to this peer.
     """
 
-    def __init__(self, peer_addr: tuple, proto: 'Protocol', sock: socket.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 = sock
@@ -214,6 +214,7 @@ class SocketServer(socketserver.TCPServer):
     def shutdown_request(self, request):
         pass
 
+
 class Protocol:
     """
     Manages connections to our peers. Allows sending messages to them and has event handlers
@@ -235,7 +236,7 @@ class Protocol:
     """
 
     def __init__(self, bootstrap_peers: 'List[tuple]',
-                 primary_block: 'Block', listen_port: int=0, listen_addr: str=""):
+                 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
@@ -245,6 +246,7 @@ class Protocol:
 
         self.block_receive_handlers = []
         self.trans_receive_handlers = []
+        self.opening_receive_handlers = []
         self.block_request_handlers = []
         self._primary_block = primary_block.to_json_compatible()
         self.peers = []
@@ -252,10 +254,10 @@ class Protocol:
         self._callback_counter = 0
         self._callback_counter_lock = Lock()
 
-
         class IncomingHandler(socketserver.BaseRequestHandler):
             """ Handler for incoming P2P connections. """
             proto = self
+
             def handle(self):
                 logging.info("connection from peer %s", repr(self.client_address))
                 if len(self.proto.peers) > MAX_PEERS:
@@ -267,6 +269,7 @@ class Protocol:
 
                 conn = PeerConnection(self.client_address, self.proto, self.request)
                 self.proto.peers.append(conn)
+
         self.server = SocketServer((listen_addr, listen_port), IncomingHandler)
         self.server.serve_forever_bg()
 
@@ -296,7 +299,7 @@ class Protocol:
         for peer in self.peers:
             peer.send_msg("transaction", trans.to_json_compatible())
 
-    def received(self, msg_type: str, msg_param, peer: Optional[PeerConnection], prio: int=1):
+    def received(self, msg_type: str, msg_param, peer: Optional[PeerConnection], prio: int = 1):
         """
         Called by a PeerConnection when a new message was received.
 
@@ -390,10 +393,10 @@ class Protocol:
 
     def received_transaction(self, transaction: dict, sender: PeerConnection):
         """ Someone sent us a transaction. """
-        transaction = Transaction.from_json_compatible(transaction)
-        logging.debug("%s < transaction %s", sender.peer_addr, hexlify(transaction.get_hash()))
+        tx = Transaction.from_json_compatible(transaction)
+        logging.debug("%s < transaction %s", sender.peer_addr, hexlify(tx.get_hash()))
         for handler in self.trans_receive_handlers:
-            handler(transaction)
+            handler(tx)
 
     def received_disconnected(self, _, peer: PeerConnection):
         """
@@ -411,5 +414,6 @@ class Protocol:
         for peer in self.peers:
             peer.send_msg("getblock", hexlify(block_hash).decode())
 
+
 from .block import Block
 from .transaction import Transaction

+ 25 - 17
src/rpc_client.py

@@ -1,12 +1,14 @@
 """ The RPC functionality used by the wallet to talk to the miner application. """
 
 import json
+import sys
 from typing import List, Tuple, Iterator
+from datetime import datetime
 
 import requests
 
 from .transaction import Transaction, TransactionTarget, TransactionInput
-from .crypto import Signing
+from .crypto import Key
 
 
 class RPCClient:
@@ -19,7 +21,7 @@ class RPCClient:
     def send_transaction(self, transaction: Transaction):
         """ Sends a transaction to the miner. """
         resp = self.sess.put(self.url + 'new-transaction', data=json.dumps(transaction.to_json_compatible()),
-                        headers={"Content-Type": "application/json"})
+                             headers={"Content-Type": "application/json"})
         resp.raise_for_status()
 
     def network_info(self) -> List[Tuple[str, int]]:
@@ -28,40 +30,46 @@ class RPCClient:
         resp.raise_for_status()
         return [tuple(peer) for peer in resp.json()]
 
-    def get_transactions(self, pubkey: Signing) -> List[Transaction]:
+    def get_transactions(self, pubkey: Key) -> List[Transaction]:
         """ Returns all transactions involving a certain public key. """
         resp = self.sess.post(self.url + 'transactions', data=pubkey.as_bytes(),
-                         headers={"Content-Type": "application/json"})
+                              headers={"Content-Type": "application/json"})
         resp.raise_for_status()
         return [Transaction.from_json_compatible(t) for t in resp.json()]
 
-    def show_balance(self, pubkeys: List[Signing]) -> Iterator[Tuple[Signing, int]]:
+    def show_balance(self, pubkeys: List[Key]) -> Iterator[Tuple[Key, int]]:
         """ Returns the balance of a number of public keys. """
         resp = self.sess.post(self.url + "show-balance", data=json.dumps([pk.to_json_compatible() for pk in pubkeys]),
-                         headers={"Content-Type": "application/json"})
+                              headers={"Content-Type": "application/json"})
         resp.raise_for_status()
         return zip(pubkeys, resp.json())
 
-    def build_transaction(self, source_keys: List[Signing], targets: List[TransactionTarget],
-                          change_key: Signing, transaction_fee: int) -> Transaction:
+    def build_transaction(self, source_keys: List[Key], targets: List[TransactionTarget],
+                          change_key: Key, transaction_fee: int, timestamp: datetime) -> Transaction:
         """
         Builds a transaction sending money from `source_keys` to `targets`, sending any change to the
         key `change_key` and a transaction fee `transaction_fee` to the miner.
         """
         resp = self.sess.post(self.url + "build-transaction", data=json.dumps({
-                "sender-pubkeys": [k.to_json_compatible() for k in source_keys],
-                "amount": sum(t.amount for t in targets) + transaction_fee,
-            }), headers={"Content-Type": "application/json"})
+            "sender-pubkeys": [k.to_json_compatible() for k in source_keys],
+            "amount": sum(t.amount for t in targets) + transaction_fee,
+        }), headers={"Content-Type": "application/json"})
         resp.raise_for_status()
         resp = resp.json()
         remaining = resp['remaining_amount']
         if remaining < 0:
-            print("You do not have sufficient funds for this transaction. ({} missing)".format(-remaining), file=sys.stderr)
+            print("You do not have sufficient funds for this transaction. ({} missing)".format(-remaining),
+                  file=sys.stderr)
             sys.exit(2)
         elif remaining > 0:
-            targets = targets + [TransactionTarget(change_key, remaining)]
+            targets = targets + [TransactionTarget(TransactionTarget.pay_to_pubkey(change_key), remaining)]
 
-        inputs = [TransactionInput.from_json_compatible(i) for i in resp['inputs']]
-        trans = Transaction(inputs, targets)
-        trans.sign([source_keys[idx] for idx in resp['key_indices']])
-        return trans
+        temp_inputs = [TransactionInput.from_json_compatible(i) for i in resp['inputs']]
+        temp_trans = Transaction(temp_inputs, targets, timestamp)
+
+        keyidx = resp['key_indices']
+
+        inputs = [(TransactionInput(inp.transaction_hash, inp.output_idx, temp_trans.sign(source_keys[keyidx[i]])))
+                  for i, inp in enumerate(temp_inputs)]
+
+        return Transaction(inputs, targets, timestamp)

+ 557 - 77
src/rpc_server.py

@@ -1,93 +1,573 @@
-""" The RPC functionality the miner provides for the wallet. """
+""" The RPC functionality the miner provides for the wallet and the blockchain explorer.
+All REST-API calls are defined here. """
 
+import binascii
 import json
+import time
+from binascii import hexlify
+from datetime import datetime
+from sys import maxsize
 
 import flask
+from flask_api import status
 
 from .chainbuilder import ChainBuilder
+from .crypto import Key
 from .persistence import Persistence
-from .crypto import Signing
+from .config import DIFFICULTY_BLOCK_INTERVAL
 from .transaction import TransactionInput
 
+time_format = "%d.%m.%Y %H:%M:%S"  # Defines the format string of timestamps in local time.
+app = flask.Flask(__name__)
+cb = None
+pers = None
+QUERY_PARAMETER_LIMIT = maxsize
+
+
+def datetime_from_utc_to_local(utc_datetime):
+    """ Converts UTC timestamp to local timezone. """
+    now_timestamp = time.time()
+    offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp(now_timestamp)
+    return utc_datetime + offset
+
+
 def rpc_server(port: int, chainbuilder: ChainBuilder, persist: Persistence):
     """ Runs the RPC server (forever). """
+    global cb
+    cb = chainbuilder
+    global pers
+    pers = persist
+
+    app.run(port=port)
+
+
+@app.route("/network-info", methods=['GET'])
+def get_network_info():
+    """ Returns the connected peers.
+    Route: `\"/network-info\"`.
+    HTTP Method: `'GET'`
+    """
+    return json.dumps([list(peer.peer_addr)[:2] for peer in cb.protocol.peers if peer.is_connected])
+
+
+@app.route("/new-transaction", methods=['PUT'])
+def send_transaction():
+    """
+    Sends a transaction to the network, and uses it for mining.
+    Route: `\"/new-transaction\"`.
+    HTTP Method: `'PUT'`
+    """
+    cb.protocol.received("transaction", flask.request.json, None, 0)
+    return b""
+
+
+@app.route("/show-balance", methods=['POST'])
+def show_balance():
+    """
+    Returns the balance of a number of public keys.
+    Route: `\"/show-balance\"`.
+    HTTP Method: `'POST'`
+    """
+    pubkeys = {Key.from_json_compatible(pk): i for (i, pk) in enumerate(flask.request.json)}
+    amounts = [0 for _ in pubkeys.values()]
+    for output in cb.primary_block_chain.unspent_coins.values():
+        if output.get_pubkey in pubkeys:
+            amounts[pubkeys[output.get_pubkey]] += output.amount
 
-    app = flask.Flask(__name__)
-
-    @app.route("/network-info", methods=['GET'])
-    def get_network_info():
-        """ Returns the connected peers. """
-        return json.dumps([list(peer.peer_addr)[:2] for peer in chainbuilder.protocol.peers if peer.is_connected])
-
-    @app.route("/new-transaction", methods=['PUT'])
-    def send_transaction():
-        """ Sends a transaction to the network, and uses it for mining. """
-        chainbuilder.protocol.received("transaction", flask.request.json, None, 0)
-        return b""
-
-    @app.route("/show-balance", methods=['POST'])
-    def show_balance():
-        """ Returns the balance of a number of public keys. """
-        pubkeys = {Signing.from_json_compatible(pk): i for (i, pk) in enumerate(flask.request.json)}
-        amounts = [0 for _ in pubkeys.values()]
-        for output in chainbuilder.primary_block_chain.unspent_coins.values():
-            if output.recipient_pk in pubkeys:
-                amounts[pubkeys[output.recipient_pk]] += output.amount
-
-        return json.dumps(amounts)
-
-    @app.route("/build-transaction", methods=['POST'])
-    def build_transaction():
-        """
-        Returns the transaction inputs that can be used to build a transaction with a certain
-        amount from some public keys.
-        """
-        sender_pks = {
-                Signing.from_json_compatible(o): i
-                    for i, o in enumerate(flask.request.json['sender-pubkeys'])
-        }
-        amount = flask.request.json['amount']
+    return json.dumps(amounts)
 
+
+@app.route("/build-transaction", methods=['POST'])
+def build_transaction():
+    """
+    Returns the transaction inputs that can be used to build a transaction with a certain
+    amount from some public keys.
+    Route: `\"/build-transaction\"`.
+    HTTP Method: `'POST'`
+    """
+    sender_pks = {
+        Key.from_json_compatible(o): i
+        for i, o in enumerate(flask.request.json['sender-pubkeys'])
+    }
+    amount = flask.request.json['amount']
+
+    # TODO maybe give preference to the coins that are already unlocked  when creating a transaction!
+
+    inputs = []
+    used_keys = []
+    for (inp, output) in cb.primary_block_chain.unspent_coins.items():
+        if (output.get_pubkey in sender_pks) and (
+        not output.is_locked):  # here we check is the amount is not locked before creating a Tx
+            amount -= output.amount
+            temp_input = TransactionInput(inp[0], inp[1], "empty sig_script")
+            inputs.append(temp_input.to_json_compatible())
+            used_keys.append(sender_pks[output.get_pubkey])
+            if amount <= 0:
+                break
+
+    if amount > 0:
         inputs = []
         used_keys = []
-        for (inp, output) in chainbuilder.primary_block_chain.unspent_coins.items():
-            if output.recipient_pk in sender_pks:
-                amount -= output.amount
-                inputs.append(inp.to_json_compatible())
-                used_keys.append(sender_pks[output.recipient_pk])
-                if amount <= 0:
-                    break
-
-        if amount > 0:
-            inputs = []
-            used_keys = []
-
-        return json.dumps({
-            "inputs": inputs,
-            "remaining_amount": -amount,
-            "key_indices": used_keys,
-        })
-
-
-    @app.route("/transactions", methods=['POST'])
-    def get_transactions_for_key():
-        """ Returns all transactions involving a certain public key. """
-        key = Signing(flask.request.data)
-        transactions = set()
-        outputs = set()
-        chain = chainbuilder.primary_block_chain
-        for b in chain.blocks:
-            for t in b.transactions:
-                for i, target in enumerate(t.targets):
-                    if target.recipient_pk == key:
-                        transactions.add(t)
-                        outputs.add(TransactionInput(t.get_hash(), i))
-        for b in chain.blocks:
-            for t in b.transactions:
-                for inp in t.inputs:
-                    if inp in outputs:
-                        transactions.add(t)
-
-        return json.dumps([t.to_json_compatible() for t in transactions])
 
-    app.run(port=port)
+    return json.dumps({
+        "inputs": inputs,
+        "remaining_amount": -amount,
+        "key_indices": used_keys,
+    })
+
+
+@app.route("/transactions", methods=['POST'])
+def get_transactions_for_key():
+    """
+    Returns all transactions involving a certain public key.
+    Route: `\"/transactions\"`.
+    HTTP Method: `'POST'`
+    """
+    key = Key(flask.request.data)
+    transactions = set()
+    outputs = set()
+    chain = cb.primary_block_chain
+    for b in chain.blocks:
+        for t in b.transactions:
+            for i, target in enumerate(t.targets):
+                if target.get_pubkey == key:
+                    transactions.add(t)
+                    outputs.add((t.get_hash(), i))
+
+    for b in chain.blocks:
+        for t in b.transactions:
+            for inp in t.inputs:
+                if (inp.transaction_hash, inp.output_idx) in outputs:
+                    transactions.add(t)
+
+    return json.dumps([t.to_json_compatible() for t in transactions])
+
+
+@app.route("/explorer/sortedtransactions/<string:key>", methods=['GET'])
+def get_sorted_transactions_for_key(key):
+    """
+    Returns all transactions involving a certain public key.
+    Route: `\"/explorer/sortedtransactions/<string:key>\"`.
+    HTTP Method: `'GET'`
+    """
+    key = Key(binascii.unhexlify(key))
+    all_transactions = {}
+    received_transactions = []
+    sent_transactions = []
+
+    outputs = set()
+    chain = cb.primary_block_chain
+    for b in chain.blocks:
+        for t in b.transactions:
+            for i, target in enumerate(t.targets):
+                if target.get_pubkey == key:
+                    received_transactions.append(t.to_json_compatible())
+                    outputs.add((t.get_hash(), i))
+
+    for b in chain.blocks:
+        for t in b.transactions:
+            for inp in t.inputs:
+                if (inp.transaction_hash, inp.output_idx) in outputs:
+                    sent_transactions.append(t.to_json_compatible())
+
+    for t in sent_transactions:
+        t['timestamp'] = datetime_from_utc_to_local(datetime.strptime(t['timestamp'],
+                                                                      "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+            time_format)
+
+    for t in received_transactions:
+        t['timestamp'] = datetime_from_utc_to_local(datetime.strptime(t['timestamp'],
+                                                                      "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+            time_format)
+
+    all_transactions["sent"] = sent_transactions
+    all_transactions["received"] = received_transactions
+
+    return json.dumps(all_transactions)
+
+
+@app.route("/explorer/addresses", methods=['GET'])
+def get_addresses():
+    """
+    Returns all addresses in the blockchain.
+    Route: `\"/explorer/addresses\"`.
+    HTTP Method: `'GET'`
+    """
+    addresses = set()
+    chain = cb.primary_block_chain
+    for b in chain.blocks:
+        for t in b.transactions:
+            for i, target in enumerate(t.targets):
+                addresses.add(hexlify(target.get_pubkey.as_bytes()).decode())
+    if len(addresses) != 0:
+        return json.dumps([a for a in addresses])
+
+    return json.dumps("Resource not found."), status.HTTP_404_NOT_FOUND
+
+
+@app.route("/explorer/show-balance", methods=['POST'])
+def show_single_balance():
+    """
+    Returns the balance of a public key.
+    Route: `\"/explorer/show-balance\"`
+    HTTP Method: `'POST'`
+    """
+    key = Key(flask.request.data)
+    amount = 0
+    for output in cb.primary_block_chain.unspent_coins.values():
+        if output.get_pubkey == key:
+            amount += output.amount
+    result = {"credit": amount}
+
+    return json.dumps(result)
+
+
+@app.route("/explorer/lasttransactions/<int:amount>", methods=['GET'])
+def get_last_transactions(amount):
+    """
+    Returns last transactions. Number is specified in `amount`.
+    Route: `\"/explorer/lasttransactions/<int:amount>\"`
+    HTTP Method: `'GET'`
+    """
+    last_transactions = []
+    counter = 0
+
+    unconfirmed_tx = cb.unconfirmed_transactions
+
+    for (key, value) in unconfirmed_tx.items():
+        if counter < amount:
+            val = value.to_json_compatible()
+            val['block_id'] = "Pending.."
+            val['block_hash'] = ""
+            val['number_confirmations'] = 0
+            val['timestamp'] = datetime_from_utc_to_local(datetime.strptime(val['timestamp'],
+                                                                            "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+                time_format)
+            last_transactions.append(val)
+            counter += 1
+        else:
+            break
+
+    last_confirmed_transactions = []
+    chain = cb.primary_block_chain
+    for b in reversed(chain.blocks):
+        if not counter < amount:
+            break
+        for t in reversed(b.transactions):
+            if counter < amount:
+                trans = t.to_json_compatible()
+                block = b.to_json_compatible()
+                trans['block_id'] = block['id']
+                trans['block_hash'] = block['hash']
+                trans['number_confirmations'] = chain.head.id - int(block['id'])
+                trans['timestamp'] = datetime_from_utc_to_local(datetime.strptime(trans['timestamp'],
+                                                                                  "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+                    time_format)
+
+                last_confirmed_transactions.append(trans)
+                counter += 1
+            else:
+                break
+
+    last_transactions.extend(last_confirmed_transactions)
+    return json.dumps(last_transactions)
+
+
+@app.route("/explorer/transactions", methods=['GET'])
+def get_transactions():
+    """
+    Returns all transactions.
+    Route: `\"/explorer/transactions\"`
+    HTTP Method: `'GET'`
+    """
+    transactions = []
+    chain = cb.primary_block_chain
+    for b in reversed(chain.blocks):
+        for t in reversed(b.transactions):
+            trans = t.to_json_compatible()
+            block = b.to_json_compatible()
+            trans['block_id'] = block['id']
+            trans['block_hash'] = block['hash']
+            trans['number_confirmations'] = chain.head.id - int(block['id'])
+            transactions.append(trans)
+    for t in transactions:
+        t['timestamp'] = datetime_from_utc_to_local(datetime.strptime(t['timestamp'],
+                                                                      "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+            time_format)
+    return json.dumps(transactions)
+
+
+@app.route("/explorer/transaction/<string:hash>", methods=['GET'])
+def get_transaction_from_hash(hash):
+    """
+    Returns a transaction with specified hash.
+    Route: `\"/explorer/transaction/<string:hash>\"`
+    HTTP Method: `'GET'`
+    """
+    chain = cb.primary_block_chain
+    for b in chain.blocks:
+        for t in b.transactions:
+            if hexlify(t.get_hash()).decode() == hash:
+                trans = t.to_json_compatible()
+                block = b.to_json_compatible()
+                trans['block_id'] = block['id']
+                trans['block_hash'] = block['hash']
+                trans['number_confirmations'] = chain.head.id - int(block['id'])
+                trans['timestamp'] = datetime_from_utc_to_local(datetime.strptime(trans['timestamp'],
+                                                                                  "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+                    time_format)
+                trans['fee'] = t.get_past_transaction_fee(chain)
+                return json.dumps(trans)
+
+    unconfirmed_tx = cb.unconfirmed_transactions
+    for (key, value) in unconfirmed_tx.items():
+        if hexlify(key).decode() == hash:
+            trans = value.to_json_compatible()
+            trans['block_id'] = ""
+            trans['block_hash'] = "Pending..."
+            trans['timestamp'] = datetime_from_utc_to_local(datetime.strptime(trans['timestamp'],
+                                                                              "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+                time_format)
+            trans['fee'] = t.get_past_transaction_fee(chain)
+            return json.dumps(trans)
+
+    return json.dumps("Resource not found."), status.HTTP_404_NOT_FOUND
+
+
+@app.route("/explorer/blocks", methods=['GET'])
+def get_blocks():
+    """
+    Returns all blocks in the blockchain.
+    Route: `\"/explorer/blocks\"`
+    HTTP Method: `'GET'`
+    """
+    chain = cb.primary_block_chain
+    result = []
+    for o in reversed(chain.blocks):
+        block = o.to_json_compatible()
+        block['time'] = datetime_from_utc_to_local(datetime.strptime(block['time'],
+                                                                     "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+            time_format)
+        result.append(block)
+    return json.dumps(result)
+
+
+@app.route("/explorer/lastblocks/<int:amount>", methods=['GET'])
+def get_blocks_amount(amount):
+    """
+    Returns the latest number of blocks in the blockchain.
+    Route: `\"/explorer/lastblocks/<int:amount>\"`
+    HTTP Method: `'GET'`
+    """
+    result = []
+    chain = cb.primary_block_chain
+    counter = 0
+    for b in reversed(chain.blocks):
+        block = b.to_json_compatible()
+        block['time'] = datetime_from_utc_to_local(datetime.strptime(block['time'],
+                                                                     "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+            time_format)
+        result.append(block)
+        counter += 1
+        if counter >= amount:
+            break
+    return json.dumps(result)
+
+
+@app.route("/explorer/blockat/<int:at>", methods=['GET'])
+def get_block_at(at):
+    """
+    Returns block at postion from zero in the blockchain.
+    Route: `\"/explorer/blockat/<int:at>\"`
+    HTTP Method: `'GET'`
+    """
+    chain = cb.primary_block_chain
+    result = chain.blocks[at].to_json_compatible()
+
+    result['time'] = datetime_from_utc_to_local(datetime.strptime(result['time'],
+                                                                  "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+        time_format)
+
+    return json.dumps(result)
+
+
+@app.route("/explorer/block/<string:hash>", methods=['GET'])
+def get_blocks_hash(hash):
+    """
+    Returns block with given hash from the blockchain.
+    Route: `\"/explorer/block/<string:hash>\"`
+    HTTP Method: `'GET'`
+    """
+    chain = cb.primary_block_chain
+    for b in chain.blocks:
+        if hexlify(b.hash).decode() == hash:
+            block = b.to_json_compatible()
+            block['time'] = datetime_from_utc_to_local(datetime.strptime(block['time'],
+                                                                         "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
+                time_format)
+            return json.dumps(block)
+
+    return json.dumps("Resource not found."), status.HTTP_404_NOT_FOUND
+
+
+@app.route("/explorer/statistics/hashrate", methods=['GET'])
+def get_hashrate():
+    """
+    Returns the total amount of blocks.
+    Route: `\"/explorer/statistics/hashrate\"`
+    HTTP Method: `'GET'`
+    """
+    parameter = flask.request.args.get('length')
+    if (parameter != None and isinstance(parameter, int)) and parameter > 0 and parameter < QUERY_PARAMETER_LIMIT:
+        user_input_length = parameter
+    else:
+        user_input_length = DIFFICULTY_BLOCK_INTERVAL
+    chain = cb.primary_block_chain
+
+    if chain.head.id <= user_input_length:
+        if (len(chain.blocks)) <= 2:
+            return json.dumps(0)
+        user_input_length = len(chain.blocks) - 1
+
+    block_hashrate = []
+
+    for i in range(user_input_length):
+        first_block = chain.blocks[-i - 1]
+        second_block = chain.blocks[-i - 2]
+        first_time = first_block.time
+        second_time = second_block.time
+        difficulty = first_block.difficulty
+        time_difference = abs((first_time - second_time).seconds)
+        if time_difference == 0:
+            time_difference = 1
+        hashrate = (difficulty / time_difference)
+        block_hashrate.append(hashrate)
+
+    block_hashrate_sum = 0
+    for i in block_hashrate:
+        block_hashrate_sum += i
+
+    block_hashrate_avg = block_hashrate_sum / len(block_hashrate)
+
+    if block_hashrate_avg >= 1000000000:
+        return json.dumps("%.1f" % (block_hashrate_avg / 1000000000) + " Gh/s")
+    if block_hashrate_avg >= 1000000:
+        return json.dumps("%.1f" % (block_hashrate_avg / 1000000) + " Mh/s")
+    if block_hashrate_avg >= 1000:
+        return json.dumps("%.1f" % (block_hashrate_avg / 1000) + " Kh/s")
+
+    return json.dumps("%.2f" % block_hashrate_avg)  # Returns float formatted with only 2 decimals
+
+
+@app.route("/explorer/statistics/tps", methods=['GET'])
+def get_tps():
+    """
+    Returns the average transaction rate over the last <length>- query parameter blocks.
+    Route: `\"/explorer/statistics/tps\"`
+    HTTP Method: `'GET'`
+    """
+    parameter = flask.request.args.get('length')
+    if (parameter != None and isinstance(parameter, int)) and parameter > 0 and parameter < QUERY_PARAMETER_LIMIT:
+        user_input_length = parameter
+    else:
+        user_input_length = DIFFICULTY_BLOCK_INTERVAL
+
+    chain = cb.primary_block_chain
+    if chain.head.id <= user_input_length:
+        # if only genesis block exists, no transacions have been made
+        if (len(chain.blocks)) <= 1:
+            return json.dumps(0)
+        first_block = chain.head
+        # use block after genesis block, because genesis block has hard-coded timestamp
+        second_block = chain.blocks[1]
+        first_time = first_block.time
+        second_time = second_block.time
+    else:
+        first_block = chain.head
+        second_block = chain.blocks[- 1 - user_input_length]
+        first_time = first_block.time
+        second_time = second_block.time
+
+    transactions = 0
+    for i in range(user_input_length):
+        transactions += len(chain.blocks[-1 - i].transactions)
+
+    time_difference = abs((first_time - second_time).seconds)
+    if time_difference == 0:
+        time_difference = 1
+
+    tps = transactions / time_difference
+
+    return json.dumps("%.2f" % tps)  # Returns float formatted with only 2 decimals
+
+
+@app.route("/explorer/statistics/totalblocks", methods=['GET'])
+def get_total_blocks():
+    """
+    Returns the total amount of blocks.
+    Route: `\"/explorer/statistics/totalblocks\"`
+    HTTP Method: `'GET'`
+    """
+    chain = cb.primary_block_chain
+    total_blocks = len(chain.blocks)
+
+    return json.dumps(total_blocks)
+
+
+@app.route("/explorer/statistics/difficulty", methods=['GET'])
+def get_difficulty():
+    """
+    Returns the current difficulty.
+    Route: `\"/explorer/statistics/difficulty\"`
+    HTTP Method: `'GET'`
+    """
+    chain = cb.primary_block_chain
+    current_difficulty = chain.head.difficulty
+
+    return json.dumps(current_difficulty)
+
+
+@app.route("/explorer/statistics/blocktime", methods=['GET'])
+def get_blocktime():
+    """
+    Returns the average time between two blocks.
+    Route: `\"/explorer/statistics/blocktime\"`
+    HTTP Method: `'GET'`
+    """
+    chain = cb.primary_block_chain
+    current_block = chain.head
+
+    if len(chain.blocks) < 2:
+        return json.dumps(0)
+
+    second_block = chain.blocks[1]
+
+    current_timestamp = current_block.time
+    second_timestamp = second_block.time
+    time_difference = abs((current_timestamp - second_timestamp).seconds)
+    total_blocks = int(get_total_blocks()) - 1
+
+    blocktime = time_difference / total_blocks
+
+    return json.dumps("%.2f" % blocktime)  # Returns float formatted with only 2 decimals
+
+
+@app.route('/shutdown', methods=['POST', 'GET'])
+def shutdown():
+    """
+    Shuts down the RPC-Server.
+    Route: `\"/shutdown\"`
+    HTTP Method: `'GET'/'POST'`
+    """
+    shutdown_server()
+    return 'Server shutting down...'
+
+
+def shutdown_server():
+    """
+    Shuts down flask. Needed for pytests.
+    """
+    func = flask.request.environ.get('werkzeug.server.shutdown')
+    if func is None:
+        raise RuntimeError('Not running with the Werkzeug Server')
+    func()

+ 233 - 206
src/transaction.py

@@ -1,206 +1,233 @@
-""" Defines transactions and their inputs and outputs. """
-
-import logging
-from collections import namedtuple
-from binascii import hexlify, unhexlify
-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').
-
-:ivar recipient_pk: The public key of the recipient.
-:vartype recipient_pk: Signing
-:ivar amount: The amount sent to `recipient_pk`.
-:vartype amount: int
-"""
-
-
-class TransactionInput(namedtuple("TransactionInput", ["transaction_hash", "output_idx"])):
-    """
-    One transaction input (pointer to 'coin').
-
-    :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
-    """
-
-    @classmethod
-    def from_json_compatible(cls, obj):
-        """ Creates a new object of this class, from a JSON-serializable representation. """
-        return cls(unhexlify(obj['transaction_hash']), int(obj['output_idx']))
-
-    def to_json_compatible(self):
-        """ Returns a JSON-serializable representation of this object. """
-        return {
-            'transaction_hash': hexlify(self.transaction_hash).decode(),
-            'output_idx': self.output_idx,
-        }
-
-
-class Transaction:
-    """
-    A transaction as it was received from the network or a block. To check that this transaction is
-    valid on top of a block chain and in combination with a set of different transactions (from the
-    same block), you can call the `verify` method.
-
-    :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 []
-        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:
-            val['inputs'].append(inp.to_json_compatible())
-        val['targets'] = []
-        for targ in self.targets:
-            val['targets'].append({
-                'recipient_pk': targ.recipient_pk.to_json_compatible(),
-                'amount': targ.amount,
-            })
-        val['signatures'] = []
-        for sig in self.signatures:
-            val['signatures'].append(hexlify(sig).decode())
-        if self.iv is not None:
-            val['iv'] = hexlify(self.iv).decode()
-        return val
-
-    @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.from_json_compatible(inp))
-        targets = []
-        for targ in obj['targets']:
-            if targ['amount'] <= 0:
-                raise ValueError("invalid amount")
-            targets.append(TransactionTarget(Signing.from_json_compatible(targ['recipient_pk']),
-                                             int(targ['amount'])))
-        signatures = []
-        for sig in obj['signatures']:
-            signatures.append(unhexlify(sig))
-
-        iv = unhexlify(obj['iv']) if 'iv' in obj else None
-        return cls(inputs, targets, signatures, iv)
-
-
-    def get_hash(self) -> bytes:
-        """ Hash this transaction. Returns raw bytes. """
-        if self._hash is None:
-            h = get_hasher()
-            if self.iv is not None:
-                h.update(self.iv)
-
-            h.update(Block._int_to_bytes(len(self.targets)))
-            for target in self.targets:
-                h.update(Block._int_to_bytes(target.amount))
-                h.update(target.recipient_pk.as_bytes())
-
-            h.update(Block._int_to_bytes(len(self.inputs)))
-            for inp in self.inputs:
-                h.update(inp.transaction_hash)
-                h.update(Block._int_to_bytes(inp.output_idx))
-
-            self._hash = h.digest()
-        return self._hash
-
-    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.
-        """
-        for private_key in private_keys:
-            self.signatures.append(private_key.sign(self.get_hash()))
-
-    def _verify_signatures(self, chain: 'Blockchain'):
-        """ Verifies that all inputs are signed and the signatures are valid. """
-        if len(self.signatures) != len(self.inputs):
-            logging.warning("wrong number of signatures")
-            return False
-
-        for (s, i) in zip(self.signatures, self.inputs):
-            if not self._verify_single_sig(s, i, chain):
-                return False
-        return True
-
-    def _verify_single_sig(self, sig: bytes, inp: TransactionInput, chain: 'Blockchain') -> bool:
-        """ Verifies the signature on a single input. """
-        outp = chain.unspent_coins.get(inp)
-        if outp is None:
-            logging.warning("Referenced transaction input could not be found.")
-            return False
-        if not outp.recipient_pk.verify_sign(self.get_hash(), sig):
-            logging.warning("Transaction signature does not verify.")
-            return False
-        return True
-
-    def _verify_single_spend(self, chain: 'Blockchain', other_trans: set) -> bool:
-        """ Verifies that all inputs have not been spent yet. """
-        inp_set = set(self.inputs)
-        if len(self.inputs) != len(inp_set):
-            logging.warning("Transaction may not spend the same coin twice.")
-            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.")
-            return False
-
-        if any(i not in chain.unspent_coins for i in self.inputs):
-            logging.debug("Transaction refers to a coin that was already spent.")
-            return False
-        return True
-
-    def get_transaction_fee(self, chain: 'Blockchain'):
-        """ Computes the transaction fees this transaction provides. """
-        if not self.inputs:
-            return 0 # block reward transaction pays no fees
-
-        input_amount = sum(chain.unspent_coins[inp].amount for inp in self.inputs)
-        output_amount = sum(outp.amount for outp in self.targets)
-        return input_amount - output_amount
-
-    def _verify_amounts(self, chain: 'Blockchain') -> bool:
-        """
-        Verifies that transaction fees are non-negative and output amounts are positive.
-        """
-        if self.get_transaction_fee(chain) < 0:
-            logging.warning("Transferred amounts are larger than the inputs.")
-            return False
-        if any(outp.amount <= 0 for outp in self.targets):
-            logging.warning("Transferred amounts must be positive.")
-            return False
-        return True
-
-    def verify(self, chain: 'Blockchain', other_trans: 'Set[Transaction]') -> bool:
-        """ Verifies that this transaction is completely valid. """
-        return self._verify_single_spend(chain, other_trans) and \
-               self._verify_signatures(chain) and self._verify_amounts(chain)
-
-from .blockchain import Blockchain
-from .block import Block
+""" Defines transactions and their inputs and outputs. """
+
+import logging
+
+from datetime import datetime, timezone
+from collections import namedtuple
+from binascii import hexlify, unhexlify
+from typing import List
+
+import src.utils as utils
+
+from .labscript import interpreter
+
+from .crypto import get_hasher, Key
+
+__all__ = ['TransactionTarget', 'TransactionInput', 'Transaction']
+
+
+class TransactionTarget(namedtuple("TransactionTarget", ["pubkey_script", "amount"])):
+    """
+    The recipient of a transaction ('coin').
+    
+    :ivar pubkey_script: The output script of a transaction.
+    :vartype pubkey_script: string
+    :ivar amount: The amount sent.
+    :vartype amount: int
+    """
+
+    @classmethod
+    def from_json_compatible(cls, obj):
+        """ Creates a new object of this class, from a JSON-serializable representation. """
+        return cls(str(obj['pubkey_script']), int(obj['amount']))
+
+    def to_json_compatible(self):
+        """ Returns a JSON-serializable representation of this object. """
+        return {
+            'pubkey_script': self.pubkey_script,
+            'amount': self.amount
+        }
+
+    @classmethod
+    def pay_to_pubkey(self, recipient_pk: Key) -> str:
+        """ Returns a standard pay-to-pubkey script """
+        keystr = recipient_pk.to_json_compatible()
+        return keystr + " OP_CHECKSIG"
+
+    @classmethod
+    def pay_to_pubkey_lock(self, recipient_pk: Key, lock_time: datetime) -> str:
+        """ Returns a pay-to-pubkey script with a lock-time """
+        keystr = recipient_pk.to_json_compatible()
+        return str(lock_time.replace(tzinfo=timezone.utc).timestamp()) + " OP_CHECKLOCKTIME " + keystr + " OP_CHECKSIG"
+
+    @property
+    def get_pubkey(self) -> Key:
+        """ Returns the public key of the target for a standard PAY_TO_PUBKEY transaction"""
+        if self.is_pay_to_pubkey_lock:
+            return Key.from_json_compatible(self.pubkey_script[
+                                            self.pubkey_script.find("OP_CHECKLOCKTIME") + 17:self.pubkey_script.find(
+                                                "OP_CHECKSIG") - 1])
+        elif self.is_pay_to_pubkey:
+            return Key.from_json_compatible(self.pubkey_script[:self.pubkey_script.find(" ")])
+
+        return None
+
+    @property
+    def is_pay_to_pubkey(self) -> bool:
+        op = self.pubkey_script[self.pubkey_script.find(" ") + 1:]
+        return op == "OP_CHECKSIG"
+
+    @property
+    def is_pay_to_pubkey_lock(self) -> bool:
+        # TODO SHOULD! not maybe! SHOULD!  fix this later! it needs to check is the strings are in the correct position within the script
+        # op1 = self.pubkey_script[:self.pubkey_script.find(" "):]
+        # op2 = self.pubkey_script[self.pubkey_script.find("OP_CHECKLOCKTIME"):]
+        return ("OP_CHECKSIG" in self.pubkey_script) and ("OP_CHECKLOCKTIME" in self.pubkey_script)
+
+    @property
+    def is_locked(self) -> bool:
+        if self.is_pay_to_pubkey_lock:
+            timestamp = datetime.utcfromtimestamp(float(self.pubkey_script[:self.pubkey_script.find(" ")]))
+            return timestamp > datetime.utcnow()
+        return False
+
+
+class TransactionInput(namedtuple("TransactionInput", ["transaction_hash", "output_idx", "sig_script"])):
+    """
+    One transaction input (pointer to 'coin').
+
+    :ivar transaction_hash: The hash of the transaction that you are trying to spend.
+    :vartype transaction_hash: bytes
+    :ivar output_idx: The index into `Transaction.targets` of the `transaction_hash`.
+    :vartype output_idx: int
+    :ivar sig_script: The script redeeming the output point by output_idx of the transaction pointed by transaction_hash
+    :vartype sig_script: string
+    """
+
+    @classmethod
+    def from_json_compatible(cls, obj):
+        """ Creates a new object of this class, from a JSON-serializable representation. """
+        return cls(unhexlify(obj['transaction_hash']), int(obj['output_idx']), str(obj['sig_script']))
+
+    def to_json_compatible(self):
+        """ Returns a JSON-serializable representation of this object. """
+        return {
+            'transaction_hash': hexlify(self.transaction_hash).decode(),
+            'output_idx': self.output_idx,
+            'sig_script': self.sig_script
+        }
+
+
+class Transaction:
+    """
+    A transaction as it was received from the network or a block. To check that this transaction is
+    valid on top of a block chain and in combination with a set of different transactions (from the
+    same block), you can call the `verify` method.
+
+    :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 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
+    :ivar timestamp: The time when the transaction was created.
+    :vartype timestamp: datetime
+    """
+
+    def __init__(self, inputs: 'List[TransactionInput]', targets: 'List[TransactionTarget]', timestamp: 'datetime',
+                 iv: bytes = None):
+        self.inputs = inputs
+        self.targets = targets
+        self.timestamp = timestamp
+        self.iv = iv
+        self._hash = None
+
+    def to_json_compatible(self):
+        """ Returns a JSON-serializable representation of this object. """
+        val = {}
+        val['hash'] = hexlify(self.get_hash()).decode()
+        val['inputs'] = []
+        for inp in self.inputs:
+            val['inputs'].append(inp.to_json_compatible())
+        val['targets'] = []
+        for targ in self.targets:
+            val['targets'].append(targ.to_json_compatible())
+        val['timestamp'] = self.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f UTC")
+        if self.iv is not None:
+            val['iv'] = hexlify(self.iv).decode()
+        return val
+
+    @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.from_json_compatible(inp))
+        targets = []
+        for targ in obj['targets']:
+            targets.append(TransactionTarget.from_json_compatible(targ))
+        timestamp = datetime.strptime(obj['timestamp'], "%Y-%m-%dT%H:%M:%S.%f UTC")
+        iv = unhexlify(obj['iv']) if 'iv' in obj else None
+        return cls(inputs, targets, timestamp, iv)
+
+    def get_hash(self) -> bytes:
+        """ Hash this transaction. Returns raw bytes. """
+        if self._hash is None:
+            h = get_hasher()
+            if self.iv is not None:
+                h.update(self.iv)
+
+            h.update(utils.int_to_bytes(len(self.targets)))
+            for target in self.targets:
+                h.update(utils.int_to_bytes(target.amount))
+                h.update(target.pubkey_script.encode())
+
+            h.update(utils.int_to_bytes(len(self.inputs)))
+            for inp in self.inputs:
+                h.update(inp.transaction_hash)
+                h.update(utils.int_to_bytes(inp.output_idx))
+
+            self._hash = h.digest()
+        return self._hash
+
+    def sign(self, signing_key: Key):
+        return hexlify(signing_key.sign(self.get_hash())).decode()
+
+    def get_transaction_fee(self, unspent_coins: dict):
+        """ Computes the transaction fees this transaction provides. """
+        if not self.inputs:
+            return 0  # block reward transaction pays no fees
+        try:
+            input_amount = sum(unspent_coins[(inp.transaction_hash, inp.output_idx)].amount for inp in self.inputs)
+            output_amount = sum(outp.amount for outp in self.targets)
+            return input_amount - output_amount
+        except:
+            logging.warning("Transaction input is not in unspent coins. Transaction is invalid or spent.")
+            # print(unspent_coins.items())
+            raise ValueError('Transaction input not found.')
+
+    def _verify_amounts(self) -> bool:
+        """
+        Verifies that transaction fees are non-negative and output amounts are positive.
+        """
+        if any(outp.amount < 0 for outp in self.targets):
+            return False
+        return True
+
+    def validate_tx(self, unspent_coins: dict) -> bool:
+        """
+        Validate the transaction
+        """
+
+        if not (self._verify_amounts()):
+            return False
+
+        for inp in self.inputs:
+            if ((inp.transaction_hash, inp.output_idx) not in unspent_coins):
+                return False  # ("The input is not in the unspent transactions database!")
+        for inp in self.inputs:
+            if not (
+            interpreter(inp.sig_script, unspent_coins[(inp.transaction_hash, inp.output_idx)].pubkey_script, self)):
+                return False  # ("Could not validate the Tx!")
+
+        return True
+
+    def check_tx_collision(self, other_tx):
+        for tx in other_tx:
+            for inp_other in tx.inputs:
+                for inp_self in self.inputs:
+                    if inp_self == inp_other:
+                        return True
+        return False

+ 23 - 0
src/utils.py

@@ -0,0 +1,23 @@
+from struct import pack
+from .config import *
+
+import datetime
+
+
+def int_to_bytes(val: int) -> bytes:
+    """ Turns an (arbitrarily long) integer into a bytes sequence. """
+    l = val.bit_length() + 1
+    # we need to include the length in the hash in some way, otherwise e.g.
+    # the numbers (0xffff, 0x00) would be encoded identically to (0xff, 0xff00)
+    return pack("<Q", l) + val.to_bytes(l, 'little', signed=True)
+
+
+def compute_blockreward_next_block(block_num: int) -> int:
+    """ Compute the block reward that is expected for the block following this chain's `head`. """
+    half_lives = block_num // REWARD_HALF_LIFE
+    reward = GENESIS_REWARD // (2 ** half_lives)
+    return reward
+
+
+def compute_lock_time(seconds_to_wait: int) -> datetime:
+    return datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds_to_wait)

+ 32 - 0
templates/about.html

@@ -0,0 +1,32 @@
+{% extends "layout.html" %}
+{% block body %}
+<div class=mycard>
+    <div class="mycard-header">
+        <h1>About the project</h1>
+    </div>
+    <div class="mycard-body about_mycard">
+        This project is a completely new blockchain-based coin, with P2P networking, a consensus mechanism and a wallet
+        interface.<br>
+        The goal of the project is to provide a framework that is easy to modify for people who want to develop
+        proof-of-concepts for blockchain-based technology.<br><br>
+        DO NOT USE THIS AS A REAL CURRENCY TO SEND, RETRIEVE, OR STORE ACTUAL MONEY! <br>
+        While we do not currently know of any way to do so, there are almost certainly bugs in this implementation that
+        would allow anyone to create money out of the blue or take yours away from you.
+        <br>
+        <br>
+        You can find the sourcecode on <a href="https://github.com/ChaAC-FAU/labchain">GitHub.</a>
+        And the documentation on <a href="http://labchain.readthedocs.io/en/latest/">Readthedocs.io.</a>
+        <br>
+        <br>
+        <a target="_blank" href="https://www.fau.de//"><img class="img-fluid" width="40%" src="{{ url_for('static', filename='Friedrich-Alexander-Universität_Erlangen-Nürnberg_logo.svg')}}"></a>
+        <a target="_blank" href="https://www.chaac.tf.fau.de/"><img class="img-fluid" width="40%" src="{{ url_for('static', filename='ChaAC-logo-black.png')}}"></a>
+        <br>
+        <br>
+        LabChain Explorer made by:<br>
+        ©2018 Patrick Dolinic, Jonas Heinrich, Lukas Mahlmeister, Martin Mecklenburg<br>
+        <br>
+        Based on the ChaAC Coin by:<br>
+        ©2017 Jonas Herzig, Malte Kraus
+    </div>
+</div>
+{% endblock %}

+ 103 - 0
templates/address.html

@@ -0,0 +1,103 @@
+{% from "macros.html" import make_short %}
+{% extends "layout.html" %}
+{% block body %}
+<div class=mycard>
+    <div class="mycard-header"><h1>Address</h1></div>
+    <table class=mycard-body>
+        <tr>
+            <th>Hash</th>
+            <td>{{make_short(data.hash[142:])}}</td>
+        </tr>
+        <tr>
+           <th>Credit</th> 
+           <td>{{data.credit}} CHaC</td>
+        </tr>
+        <tr>
+            <td colspan=2>
+                <div>
+                {% for transaction in data.sent -%}
+                <div class="mycard">
+                    <div class="mycard-header">
+                        <h3><a href="/transaction/{{transaction.hash}}">Sent</a></h3>
+                    </div>
+                    <table>
+                        {% if transaction.senders | length > 1%}
+                        <tr>
+                            <th>From:</th>
+                            <td>
+                                {% if transaction.senders %}
+                                    {% for sender in transaction.senders -%}
+                                    {{make_short(sender,"address")}}
+                                    {%- endfor %}
+                                {% else %}
+                                    Mining Reward
+                                {%endif %}
+                            </td>
+                        </tr>
+                        {% endif %}
+                        <tr>
+                            <th>Value:</th>
+                            <th>Address:</th>
+                            
+                        </tr>
+                        {% for target in transaction.targets -%}
+                        <tr>
+                            <td>{{target.amount}}</td>    
+                            <td>{{make_short(target.recipient_pk,"address")}}</td>
+                        </tr>
+                        {%- endfor %}
+                    </table>
+                </div>
+                    
+            {%- endfor %}
+            
+            </div>
+            
+            <div>
+                {% for transaction in data.received -%}
+                <div class="mycard">
+                        <div class="mycard-header">
+                            <h3><a href="/transaction/{{transaction.hash}}">Received</a></h3>
+                        </div>
+                        <table class="mycard-body">
+                                <tr>
+                                    <th>From:</th>
+                                    <td>
+                                        {% if transaction.senders %}
+                                            {% for sender in transaction.senders -%}
+                                            {{make_short(sender,"address")}}
+            
+                                            {%- endfor %}
+                                        {% else %}
+                                            Mining Reward
+                                        {%endif %}
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <th>Value:</th>
+                                    <th>Address:</th>
+                                    
+                                </tr>
+                                    {% for target in transaction.targets -%}
+                                <tr>
+                                    <td>
+                                        {{target.amount}}
+                                    </td>
+                                    <td>
+                                        {{make_short(target.recipient_pk,"address")}}
+                                    </td>
+                                </tr>
+                                {%- endfor %}
+                            </table>
+                        </div>
+            
+                    {%- endfor %}
+                </div>
+            </td>
+        </tr>
+    </table>
+</div>
+    
+
+    
+{% endblock %}

+ 16 - 0
templates/addresses.html

@@ -0,0 +1,16 @@
+{% extends "layout.html" %}
+{% from "macros.html" import make_short %}
+{% block body %}
+
+{% for address in data -%}
+<div class="mycard">
+    <div class="mycard-header">
+        <h3>Address</h3>
+    </div>
+    <div class="card-body">
+        {{make_short(address,"address")}}
+    </div>
+</div>
+{%- endfor %}
+
+{% endblock %}

+ 56 - 0
templates/block.html

@@ -0,0 +1,56 @@
+{% extends "layout.html" %}
+{% block body %}
+
+    <div class="mycard">
+        <div class="mycard-header">
+            <h3>Block</h3>
+        </div>
+        <table class="mycard-body">
+            <tr>
+                <th>ID</th>
+                <td>{{data.id}}</td>
+            </tr>
+            <tr>
+                <th>Hash</th>
+                <td>{{data.hash}}</td>
+            </tr>
+            <tr>
+                <th>Previous Block</th>
+                <td><a href = "{{data.prev_block_hash}}">{{data.prev_block_hash}}</a></td>
+            </tr>
+            <tr>
+                <th>Time</th>
+                <td>{{data.time}}</td>
+            </tr>
+            <tr>
+                <th>Nonce</th>
+                <td>{{data.nonce}}</td>
+            </tr>    
+            <tr>
+                <th>Merkle Root</th>
+                <td>{{data.merkle_root_hash}}</td>
+            </tr>    
+            <tr>
+                <th>Height</th>
+                <td>{{data.height}}</td>
+            </tr>
+            <tr>
+                <th>Difficulty</th>
+                <td>{{data.difficulty}}</td>
+            </tr>
+        </table>       
+    </div>
+    
+    <div>
+        {% for transaction in data.transactions -%}
+    {% if not transaction.senders %}
+        {% include  "mining_transaction.html" %}
+    {% else %}
+        {% include "from_to_transaction.html" %}
+    {%endif %}
+
+        {%- endfor %}
+
+    </div>
+
+    {% endblock %}

+ 16 - 0
templates/blocklist.html

@@ -0,0 +1,16 @@
+{% extends "layout.html" %}
+{% block body %}
+{% for item in data -%}
+<div class="mycard mb-3">
+    <div class="mycard-header">Block {{item.id}}:</div>
+    <div class="card-body">
+        <a href = "/block/{{item.hash}}" ><p class="card-text">{{item.hash}}</p></a>
+    </div>
+</div>
+{%- endfor %}
+{% if nextamount %}
+<div class="showmore mycard">
+    <div class=mycard-header><a href="/blocks/{{nextamount}}"><h1>Show more</h1></a></div>
+</div>
+{% endif %}
+{% endblock %}

+ 76 - 0
templates/from_to_transaction.html

@@ -0,0 +1,76 @@
+{% from "macros.html" import make_short %}
+<div class="transaction mycard">
+        <div class="mycard-header">
+            <h3>Transaction</h3>
+        </div>
+        <table class="mycard-body"> 
+            <tr> 
+                <th>Hash</th>
+                <td>{{make_short(transaction.hash,"transaction")}}</td> 
+            </tr>
+            {% if transaction.block_id %}
+            <tr> 
+                <th>Block ID</th>
+                <td>{{transaction.block_id}}</td> 
+            </tr>
+            {%endif%}
+            <tr> 
+                <th>Block hash</th>
+                    <td>{{make_short(transaction.block_hash,"block")}}</td> 
+                </tr>
+            <tr> 
+                <th>Number of confirmations</th>
+                <td>{{transaction.number_confirmations}}</td> 
+            </tr>
+            {% if transaction.fee %}
+            <tr>
+                <th>Transaction Fee</th>
+                <td>{{transaction.fee}}</td>
+            </tr>
+            {%endif%}
+            <tr>
+                <th>Timestamp</th>
+                <td>{{transaction.timestamp}}</td>
+            </tr>
+            <tr> 
+                <th colspan="2">Sender</th>
+
+            </tr>
+    
+            {% for sender in transaction.senders-%}
+            <tr>
+                <td colspan="2">
+                    {{ make_short(sender,'address') }}
+                </td>
+            </tr>
+    
+            {%- endfor %}
+    
+    
+            <tr>
+                <th>Input Transaction</th>
+                <th>Transaction Signature</th>
+            </tr>
+        {% for i in transaction.inp-%}
+            <tr>
+                <td>{{make_short(i.input.transaction_hash,"transaction")}}</td>
+                <td>{{make_short(i.signature)}}</td>
+            <tr>
+        {%- endfor %}
+    
+    
+            <tr>
+                <th>Recipient Address</th>
+                <th>Amount</th>
+            </tr>
+        {% for target in transaction.targets-%}
+            <tr>
+                <td>{{make_short(target.recipient_pk,"address")}}</td>
+                <td>{{target.amount}}</td>
+            <tr>
+        {%- endfor %}
+    
+        </table>
+            
+    </div>
+<div >

+ 48 - 0
templates/index.html

@@ -0,0 +1,48 @@
+{% extends "layout.html" %}
+{% block body %}
+<div class="mycard">
+    <div class="mycard-header"><h1>Welcome to the LabChain Explorer</h1></div>
+</div>
+
+<div class="statistics mycard">
+    <div class="mycard-header"><h2>Statistics</h2></div>
+    <div class="mycard-body">
+        <div class="mycard stat"><div class="mycard-header">Block Time Average</div> <div class="card-body"> {{data.statistics.blocktime}} Seconds</div></div>
+        <div class="mycard stat"><div class="mycard-header">Total Blocks</div> <div class="card-body">{{data.statistics.totalblocks}}</div></div>
+        <div class="mycard stat"><div class="mycard-header">Current Difficulty</div> <div class="card-body">{{data.statistics.difficulty}}</div></div>
+        <div class="mycard stat"><div class="mycard-header">Hash Rate</div> <div class="card-body">{{data.statistics.hashrate}} Hashes per Second</div></div>
+        <div class="mycard stat"><div class="mycard-header">Transactions per Second</div> <div class="card-body">{{data.statistics.tps}}</div></div>
+    </div>
+</div>
+
+<div class=HorizontalGroup>
+    <div class=half>
+            <div = class="blocks mycard">
+                    <div class="mycard-header"><a href="/blocks"><h2>Recent Blocks</h2></a></div>
+                    {% for item in data.blocks -%}
+                    <div class="mycard mb-3">
+                        <div class="mycard-header">Block {{item.id}}:</div>
+                        <div class="card-body">
+                            <a href = "/block/{{item.hash}}" ><p class="card-text">{{item.hash}}</p></a>
+                        </div>
+                    </div>
+                    {%- endfor %}
+                </div>
+    </div>
+    <div class=half>
+            <div class="transactions mycard">
+                    <div class="mycard-header"><a href="/transactions"><h2>Recent Transactions</h2></a></div>
+                    <div class="mycard-body">
+                        {%for transaction in data.transactions -%}
+                            {% if not transaction.senders %}
+                                {% include "mining_transaction.html" %}
+                            {% else %}
+                                {% include "from_to_transaction.html" %}
+                            {%endif %}
+                        {%- endfor %}
+                    </div>
+                </div>
+    </div>
+</div>
+
+{% endblock %}

+ 39 - 0
templates/layout.html

@@ -0,0 +1,39 @@
+<!doctype html>
+
+<head>
+    <title>LabChain</title>
+    <meta http-equiv="refresh" content="5">
+    <link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='style.css') }}">
+    <link rel="stylesheet" href="{{url_for('static',filename='bootstrap-4.0.0-dist/css/bootstrap.min.css')}}">
+    <script type=text/javascript src="{{ url_for('static', filename='jquery-3.3.1.js') }}"></script>
+    <script src="{{url_for('static',filename='bootstrap-4.0.0-dist/js/bootstrap.min.js')}}"></script>
+</head>
+<body>
+    <div class=page>
+            <nav class="mynavbar navbar navbar-expand-lg navbar-dark navcolor">
+                <a target="_blank" href="https://www.chaac.tf.fau.de/"><img src="{{ url_for('static', filename='ChaAC-logo-white.png')}}"></a><a class="navbar-brand" href="/">LabChain</a>
+                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
+                    <span class="navbar-toggler-icon"></span>
+                </button>
+
+                <div class="collapse navbar-collapse" id="navbarColor01">
+                    <ul class="navbar-nav mr-auto">
+                        <li class="nav-item"><a class="nav-link" href="/blocks">Blocks</a></li> <!-- haben wir-->
+                        <li class="nav-item"><a class="nav-link" href="/transactions">Transactions</a></li> <!-- haben wir-->
+                        <li class="nav-item"><a class="nav-link" href="/addresses">Adresses</a></li>
+                        <li class="nav-item"><a class="nav-link" href="/about">About</a></li>
+
+                        <!--<li class="nav-item"><a class="nav-link" href="/">Wallet</a></li>-->
+                    </ul>
+                </div>
+            </nav>
+
+            <div class=content>
+                {% block body %}{% endblock %}
+            </div>
+    </div>
+    <footer class="footer">
+        ©2018 Patrick Dolinic, Jonas Heinrich, Lukas Mahlmeister, Martin Mecklenburg | The ChaAC Coin Blockchain Explorer
+        | <a class="footer" href="https://github.com/ChaAC-FAU/labchain">GitHub</a> | <a class="footer" href="http://labchain.readthedocs.io/en/latest/">Docs</a>
+    </footer>
+</body>

+ 12 - 0
templates/macros.html

@@ -0,0 +1,12 @@
+{% macro make_short(hash,address) %}
+{% set max_hash_length = 80 %}
+{% if address %}
+    {%if address == "address"%}
+        <a href = "/{{address}}/{{hash}}" >{{hash[142:]|truncate(max_hash_length)}}</a>
+    {%else%}
+        <a href = "/{{address}}/{{hash}}" >{{hash|truncate(max_hash_length)}}</a>
+    {%endif%}
+{% else %}
+    {{hash|truncate(max_hash_length)}}</a>
+{% endif %}
+{% endmacro %}

+ 29 - 0
templates/mining_transaction.html

@@ -0,0 +1,29 @@
+{% from "macros.html" import make_short %}
+
+
+{% if not transaction.senders %}
+<div class="transaction reward mycard" >
+    <div class="mycard-header"><h3>Mining Reward</h3> </div>
+    <table class="mycard-body"> 
+        <tr> 
+            <th>Hash</th>
+            <td>{{make_short(transaction.hash,"transaction")}}</td>
+        </tr>
+        <tr> 
+            <th>Timestamp</th>
+            <td>{{transaction.timestamp}}</td> 
+        </tr>
+        {% for target in transaction.targets-%}
+        <tr> 
+            <th>Miner</th>
+            <td>{{make_short(target.recipient_pk,"address")}}</td>
+        </tr>
+        <tr> 
+            <th>Amount</th>
+            <td>{{target.amount}}</td> 
+        </tr>
+        {%- endfor %}
+    </table>
+</div>
+
+{%endif %}

+ 6 - 0
templates/not_found.html

@@ -0,0 +1,6 @@
+{% extends "layout.html" %}
+{% block body %}
+<div >
+    {{data.type}} <b>{{data.hash}}</b> not found!
+</div>
+{% endblock %}

+ 10 - 0
templates/transaction.html

@@ -0,0 +1,10 @@
+{% extends "layout.html" %}
+{% block body %}
+<div id="transaction-display" class="display-all">
+    {% if not transaction.senders %}
+        {% include  "mining_transaction.html" %}
+    {% else %}
+        {% include "from_to_transaction.html" %}
+    {%endif %}
+</div>
+{% endblock %}

+ 19 - 0
templates/transactions.html

@@ -0,0 +1,19 @@
+{% extends "layout.html" %}
+{% block body %}
+
+<div id="transaction-display">
+{%for transaction in data_array -%}
+    {% if not transaction.senders %}
+        {% include  "mining_transaction.html" %}
+    {% else %}
+        {% include "from_to_transaction.html" %}
+    {%endif %}
+{%- endfor %}
+</div>
+
+{% if nextamount %}
+<div class="showmore mycard">
+    <div class=mycard-header><a href="/transactions/{{nextamount}}"><h1>Show more</h1></a></div>
+</div>
+{% endif %}
+{% endblock %}

+ 46 - 0
templates/unconfirmed_transactions.html

@@ -0,0 +1,46 @@
+
+{% extends "layout.html" %}
+{% block body %}
+
+
+<div>
+        {% for key, transaction in data.items() -%}
+          <div>
+              <h3><a href = "/transaction/{{transaction.hash}}" >{{transaction.hash}}</a></h3>
+          </div>
+
+        <table>
+              <tr>
+                  <th>From:</th>
+                  <th>To:</th>
+                  <th>Value:</th>
+              </tr>
+              <tr>
+                  <td>
+                      {% if transaction.senders %}
+                          {% for sender in transaction.senders -%}
+                                <a href = "/address/{{sender}}" >{{sender}}</a><br>
+                          {%- endfor %}
+                      {% else %}
+                            Mining Reward
+                      {%endif %}
+
+                  </td>
+                  <td>
+                      {% for target in transaction.targets -%}
+                            <a href = "/address/{{target.recipient_pk}}" >{{target.recipient_pk}}</a> <br>
+                      {%- endfor %}
+                  </td>
+                  <td>
+                      {% for target in transaction.targets -%}
+                            {{target.amount}} <br>
+                      {%- endfor %}
+                  </td>
+              </tr>
+             </table>
+
+        {%- endfor %}
+    </div>
+
+
+{% endblock %}

+ 11 - 6
tests/__init__.py

@@ -5,11 +5,12 @@ import logging
 from src.protocol import Protocol
 from src.mining import Miner
 from src.block import GENESIS_BLOCK
-from src.crypto import Signing
+from src.crypto import Key
 from src.transaction import Transaction, TransactionInput, TransactionTarget
+from datetime import datetime
 
 def test_proto():
-    reward_key = Signing.generate_private_key()
+    reward_key = Key.generate_private_key()
 
     proto1 = Protocol([], GENESIS_BLOCK, 1337)
     proto2 = Protocol([("127.0.0.1", 1337)], GENESIS_BLOCK, 1338)
@@ -20,18 +21,22 @@ def test_proto():
 
     try:
         sleep(5)
-        target_key = Signing.generate_private_key()
+        target_key = Key.generate_private_key()
         chain = miner1.chainbuilder.primary_block_chain
         reward_trans = chain.blocks[20].transactions[0]
+
         trans_in = TransactionInput(reward_trans.get_hash(), 0)
         trans_targ = TransactionTarget(target_key, reward_trans.targets[0].amount)
 
-        trans = Transaction([trans_in], [trans_targ])
+        trans = Transaction([trans_in], [trans_targ], datetime.utcnow())
         trans.sign([reward_key])
+
         assert trans.verify(chain, set()), "transaction should be valid"
 
-        proto2.received('transaction', trans.to_json_compatible(), None)
-        sleep(5)
+        proto1.received('transaction', trans.to_json_compatible(), None)
+        print("============Transaction=============")
+
+        sleep(10)
 
         chain_len1 = len(miner1.chainbuilder.primary_block_chain.blocks)
         chain_len2 = len(miner2.chainbuilder.primary_block_chain.blocks)

+ 186 - 0
tests/test_rest_api.py

@@ -0,0 +1,186 @@
+from datetime import datetime
+
+
+from src.protocol import Protocol
+from src.chainbuilder import ChainBuilder
+from src.rpc_server import rpc_server
+from _thread import start_new_thread
+from tests.utils import *
+from src.rpc_server import shutdown_server
+from tests.test_verifications import block_test
+
+from Crypto.PublicKey import RSA
+
+import requests
+
+sess = requests.Session()
+host = "http://localhost:{}/"
+port = 2345
+
+NUMBER_FIELDS_IN_BLOCK = 9
+
+def start_server(chain):
+    """builds a chainbuilder from a Blockchain, appends and starts a rpc server"""
+    proto = Protocol([], GENESIS_BLOCK, 1337)
+    chainbuilder = ChainBuilder(proto)
+
+    chainbuilder.primary_block_chain = chain
+
+    return start_new_thread(rpc_server, (port, chainbuilder, None))
+
+def rpc_test(count):
+    """starts the rpc server, runs a test and stopps the server"""
+    def decorator(fn):
+        @block_test()
+        def wrapper(gen_chain):
+            chain = gen_chain
+            key = Key.generate_private_key()
+            for i in range(count):
+                reward_trans = Transaction([], [TransactionTarget(key, chain.compute_blockreward_next_block())], datetime.now())
+                chain = extend_blockchain(chain, [reward_trans])
+            start_server(chain)
+            fn(chain)
+            res = sess.get(host.format(port) + 'shutdown')
+            assert res.text == 'Server shutting down...'
+        return wrapper
+    return decorator
+
+
+def get_path(path):
+    """endpoint link"""
+    res = sess.get(host.format(port) + path)
+    assert (res.status_code == 200), "Could not reach endpoint " + path
+    return res
+
+def get_explorer(path):
+    """adds path to explorer"""
+    return get_path("explorer/"+path)
+
+def get_statistics(path):
+    """adds statistics to path"""
+    return get_explorer("statistics/"+path)
+
+@rpc_test(10)
+def test_explorer_availability(chain):
+    """explorer check for addresses, transcactions, blocks, lasttransactions, lastblocks, blockat"""
+    get_explorer('addresses')
+    get_explorer('transactions')
+    get_explorer('blocks')
+
+    get_explorer('lasttransactions/10')
+    get_explorer('lastblocks/10')
+    get_explorer('blockat/10')
+
+@rpc_test(10)
+def test_statistics_availability(chain):
+    """statistics check for hashrate, tps, totalblocks, difficulty, blocktime"""
+    get_statistics('hashrate')
+    get_statistics('tps')
+    get_statistics('totalblocks')
+    get_statistics('difficulty')
+    get_statistics('blocktime')
+    
+
+  
+
+def check_address_data(address):
+    """check for sender and receiver"""
+    assert 'received' in address, "response object does not contain a received"
+    assert 'sent' in address, "response object does not contain a sent"
+
+
+
+@rpc_test(1)
+def test_address_data(chain):
+    """gets the first address and checks the closer information"""
+    hash = get_explorer("addresses").json()[0]
+    res = get_explorer("sortedtransactions/" + hash)
+
+    res_json = res.json()
+
+    check_address_data(res_json)
+
+    
+def checkblock_data(block):
+    """blockcheck for nonce, id, difficulty, height, prev_block_hash, transactions, time, merkle_root_hash, hash"""
+    assert len(block) == NUMBER_FIELDS_IN_BLOCK, "Wrong number of Fields in response object"
+    assert 'nonce' in block, "response object does not contain a nonce"
+    assert 'id' in block, "response object does not contain a id"
+    assert 'difficulty' in block, "response object does not contain a difficulty"
+    assert 'height' in block, "response object does not contain a height"
+    assert 'prev_block_hash' in block, "response object does not contain a prev_block_hash"
+    assert 'transactions' in block, "response object does not contain a transactions"
+    assert 'time' in block, "response object does not contain a time"
+    assert 'merkle_root_hash' in block, "response object does not contain a merkle_root_hash"
+    assert 'hash' in block, "response object does not contain a hash"
+
+
+@rpc_test(1)
+def test_lastblocks_block_data(chain):
+    """checks the blockdata"""
+    res = get_explorer('lastblocks/10')
+
+    res_json = res.json()
+    assert len(res_json) == 2, "Incorrect count of Blocks"
+
+    block = res_json[0]
+    checkblock_data(block)
+
+
+
+@rpc_test(1)
+def test_transactions_data(chain):
+    """check in transaction for hash, block_id, targets, timestamp, number_confirmations, block_hash, signatures, inputs"""
+    res = sess.get(host.format(port) + 'explorer/transactions')
+    assert (res.status_code == 200)
+
+    res_json = res.json()
+    assert len(res_json) == 1, "Incorrect count of transactions"
+
+    transaction = res_json[0]
+    assert len(transaction) == 8, "Wrong number of Fields in resonse object"
+
+    assert 'hash' in transaction, "response object does not contain a hash"
+    assert 'block_id' in transaction, "response object does not contain a block_id"
+    assert 'targets' in transaction, "response object does not contain targets"
+    assert 'timestamp' in transaction, "response object does not contain a timestamp"
+    assert 'number_confirmations' in transaction, "response object does not contain a number_confirmations"
+    assert 'block_hash' in transaction, "response object does not contain a block_hash"
+    assert 'signatures' in transaction, "response object does not contain signatures"
+    assert 'inputs' in transaction, "response object does not contain inputs"
+
+    assert len(transaction["inputs"]) == len(transaction["signatures"]), "Difference in amount of signatures and inputs"
+
+
+
+def check_data_count(subpath):
+    """checks the correctness of the count from the subpath"""
+    @rpc_test(10)
+    def inner(chain):
+        path = "explorer/"+subpath
+        url = host.format(port) + path + "/" +str(5)
+        res = sess.get(url)
+        assert (res.status_code == 200), url
+
+        res_json = res.json()
+        assert len(res_json) == 5, "Incorrect count of " + subpath
+
+        url = host.format(port) + path + "/" + str(20)
+        res = sess.get(url)
+        assert (res.status_code == 200), url
+
+        res_json = res.json()
+        if(subpath == "lastblocks"):
+            assert len(res_json) == 11, "Incorrect count of " + subpath
+        if(subpath == "lasttransactions"):
+            assert len(res_json) == 10, "Incorrect count of " + subpath
+    inner()
+
+def test_blocks_count():
+    check_data_count("lastblocks")
+
+def test_transactions_count():
+    check_data_count("lasttransactions")
+
+
+

+ 19 - 19
tests/test_verifications.py

@@ -1,4 +1,4 @@
-from .utils import *
+from tests.utils import *
 
 def block_test(proof_of_work_res=True):
     """ Immediately runs a test that requires a blockchain. """
@@ -24,8 +24,8 @@ def trans_test(fn):
 
     @block_test()
     def wrapper(gen_chain):
-        key = Signing.generate_private_key()
-        reward_trans = Transaction([], [TransactionTarget(key, gen_chain.compute_blockreward_next_block())])
+        key = Key.generate_private_key()
+        reward_trans = Transaction([], [TransactionTarget(key, gen_chain.compute_blockreward_next_block())],datetime.now())
         chain = extend_blockchain(gen_chain, [reward_trans])
 
         fn(chain, reward_trans)
@@ -50,7 +50,7 @@ def test_double_spend2(chain, reward_trans):
 
 @trans_test
 def test_double_spend3(chain, reward_trans):
-    trans1 = Transaction([trans_as_input(reward_trans), trans_as_input(reward_trans)], [])
+    trans1 = Transaction([trans_as_input(reward_trans), trans_as_input(reward_trans)], [], datetime.now())
     key = reward_trans.targets[0].recipient_pk
     trans1.sign([key, key])
     extend_blockchain(chain, [trans1], verify_res=False)
@@ -60,7 +60,7 @@ def test_create_money1(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
     # create a transaction where the receiver gets 1 more coin than the sender puts in
     target = TransactionTarget(key, reward_trans.targets[0].amount + 1)
-    trans1 = Transaction([trans_as_input(reward_trans)], [target])
+    trans1 = Transaction([trans_as_input(reward_trans)], [target], datetime.now())
     trans1.sign([key])
     extend_blockchain(chain, [trans1], verify_res=False)
 
@@ -71,7 +71,7 @@ def test_create_money2(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
     target1 = TransactionTarget(key, -10)
     target2 = TransactionTarget(key, reward_trans.targets[0].amount + 10)
-    trans1 = Transaction([trans_as_input(reward_trans)], [target1, target2])
+    trans1 = Transaction([trans_as_input(reward_trans)], [target1, target2],datetime.now())
     trans1.sign([key])
     extend_blockchain(chain, [trans1], verify_res=False)
 
@@ -79,15 +79,15 @@ def test_create_money2(chain, reward_trans):
 def test_dupl_block_reward(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
     target1 = TransactionTarget(key, 1)
-    trans1 = Transaction([], [target1], iv=b"1")
-    trans2 = Transaction([], [target1], iv=b"2")
+    trans1 = Transaction([], [target1], datetime.now(), iv=b"1")
+    trans2 = Transaction([], [target1], datetime.now(), iv=b"2")
     extend_blockchain(chain, [trans1, trans2], verify_res=False)
 
 @trans_test
 def test_negative_block_reward(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
     target1 = TransactionTarget(key, -1)
-    trans1 = Transaction([], [target1], iv=b"1")
+    trans1 = Transaction([], [target1],datetime.now(), iv=b"1")
     extend_blockchain(chain, [trans1], verify_res=False)
 
 @trans_test
@@ -98,24 +98,24 @@ def test_zero_block_reward(chain, reward_trans):
 def test_too_large_block_reward(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
     target1 = TransactionTarget(key, chain.compute_blockreward_next_block() + 1)
-    trans1 = Transaction([], [target1], iv=b"1")
+    trans1 = Transaction([], [target1], datetime.now(), iv=b"1")
     extend_blockchain(chain, [trans1], verify_res=False)
 
     trans2 = new_trans(reward_trans, fee=1)
     target2 = TransactionTarget(key, chain.compute_blockreward_next_block() + 2)
-    trans3 = Transaction([], [target2], iv=b"2")
+    trans3 = Transaction([], [target2], datetime.now(), iv=b"2")
     extend_blockchain(chain, [trans2, trans3], verify_res=False)
 
 @trans_test
 def test_max_block_reward(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
     target1 = TransactionTarget(key, chain.compute_blockreward_next_block())
-    trans1 = Transaction([], [target1], iv=b"1")
+    trans1 = Transaction([], [target1], datetime.now(), iv=b"1")
     extend_blockchain(chain, [trans1], verify_res=True)
 
     trans2 = new_trans(reward_trans, fee=1)
     target2 = TransactionTarget(key, chain.compute_blockreward_next_block() + 1)
-    trans3 = Transaction([], [target2], iv=b"2")
+    trans3 = Transaction([], [target2], datetime.now(), iv=b"2")
     extend_blockchain(chain, [trans2, trans3], verify_res=True)
 
 @trans_test
@@ -128,12 +128,12 @@ def test_spend_too_much(chain, reward_trans):
 def test_spend_unknown_coin(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
     inp1 = TransactionInput(reward_trans.get_hash(), len(reward_trans.targets))
-    trans1 = Transaction([inp1], [])
+    trans1 = Transaction([inp1], [], datetime.now(),)
     trans1.sign([key])
     extend_blockchain(chain, [trans1], verify_res=False)
 
     inp2 = TransactionInput(b"invalid", 0)
-    trans2 = Transaction([inp2], [])
+    trans2 = Transaction([inp2], [], datetime.now(),)
     trans2.sign([key])
     extend_blockchain(chain, [trans2], verify_res=False)
 
@@ -151,22 +151,22 @@ def test_invalid_signature(chain, reward_trans):
     extend_blockchain(chain, [trans1], verify_res=False)
     extend_blockchain(chain, [trans2], verify_res=False)
 
-    trans3 = Transaction(trans1.inputs, trans1.targets, signatures=trans1.signatures+trans2.signatures)
+    trans3 = Transaction(trans1.inputs, trans1.targets, datetime.now(), signatures=trans1.signatures+trans2.signatures)
     extend_blockchain(chain, [trans3], verify_res=False)
 
-    trans4 = Transaction(trans1.inputs, trans1.targets, signatures=[])
+    trans4 = Transaction(trans1.inputs, trans1.targets, datetime.now(), signatures=[])
     extend_blockchain(chain, [trans4], verify_res=False)
 
     # too few signatures:
     key = reward_trans.targets[0].recipient_pk
     target1 = TransactionTarget(key, 1)
     target2 = TransactionTarget(key, 1)
-    trans5 = Transaction(trans1.inputs, [target1, target2])
+    trans5 = Transaction(trans1.inputs, [target1, target2],datetime.now())
     trans5.sign([key])
     extend_blockchain(chain, [trans5], verify_res=True)
     input1 = TransactionInput(trans5.get_hash(), 0)
     input2 = TransactionInput(trans5.get_hash(), 1)
-    trans6 = Transaction([input1, input2], [])
+    trans6 = Transaction([input1, input2], [], datetime.now())
     trans6.sign([key, key])
     trans6.signatures.pop()
     extend_blockchain(chain, [trans6], verify_res=False)

+ 2 - 2
tests/utils.py

@@ -35,8 +35,8 @@ def trans_as_input(trans, out_idx=0):
 
 def new_trans(old_trans, out_idx=0, fee=0):
     amount = old_trans.targets[out_idx].amount - fee
-    key = Signing.generate_private_key()
+    key = Key.generate_private_key()
     trans = Transaction([trans_as_input(old_trans, out_idx)],
-                        [TransactionTarget(key, amount)])
+                        [TransactionTarget(key, amount)],datetime.now())
     trans.sign([old_trans.targets[out_idx].recipient_pk])
     return trans

+ 39 - 30
wallet.py

@@ -9,43 +9,48 @@ __all__ = []
 
 import argparse
 import sys
+from datetime import datetime
 from binascii import hexlify
 from io import IOBase
 from typing import List, Union, Callable, Tuple, Optional
 
 import logging
+
 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
 
-from src.block import Block
-from src.blockchain import Blockchain
-from src.transaction import Transaction, TransactionTarget, TransactionInput
-from src.crypto import Signing
+from src.transaction import TransactionTarget
+from src.crypto import Key
 from src.rpc_client import RPCClient
 
-def parse_targets() -> Callable[[str], Union[Signing, int]]:
+
+def parse_targets() -> Callable[[str], Union[Key, int]]:
     """
     Parses transaction targets from the command line: the first value is a path to a key, the
     second an amount and so on.
     """
     start = True
+
     def parse(val):
         nonlocal start
         if start:
-            val = Signing.from_file(val)
+            val = Key.from_file(val)
         else:
             val = int(val)
         start = not start
         return val
+
     return parse
 
-def private_signing(path: str) -> Signing:
+
+def private_signing(path: str) -> Key:
     """ Parses a path to a private key from the command line. """
-    val = Signing.from_file(path)
+    val = Key.from_file(path)
     if not val.has_private:
         raise ValueError("The specified key is not a private key.")
     return val
 
-def wallet_file(path: str) -> Tuple[List[Signing], str]:
+
+def wallet_file(path: str) -> Tuple[List[Key], str]:
     """
     Parses the wallet from the command line.
 
@@ -57,13 +62,14 @@ def wallet_file(path: str) -> Tuple[List[Signing], str]:
             contents = f.read()
     except FileNotFoundError:
         return [], path
-    return list(Signing.read_many_private(contents)), path
+    return list(Key.read_many_private(contents)), path
+
 
 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("--wallet", type=wallet_file, default=([],None),
+    parser.add_argument("--wallet", type=wallet_file, default=([], None),
                         help="The wallet file containing the private keys to use.")
     subparsers = parser.add_subparsers(dest="command")
 
@@ -75,12 +81,12 @@ def main():
     balance = subparsers.add_parser("show-balance",
                                     help="Shows the current balance of the public key "
                                          "stored in the specified file.")
-    balance.add_argument("key", nargs="*", type=Signing.from_file)
+    balance.add_argument("key", nargs="*", type=Key.from_file)
 
     trans = subparsers.add_parser("show-transactions",
                                   help="Shows all transactions involving the public key "
                                        "stored in the specified file.")
-    trans.add_argument("key", nargs="*", type=Signing.from_file)
+    trans.add_argument("key", nargs="*", type=Key.from_file)
 
     subparsers.add_parser("show-network",
                           help="Prints networking information about the miner.")
@@ -89,31 +95,32 @@ def main():
     transfer.add_argument("--private-key", type=private_signing,
                           default=[], action="append", required=False,
                           help="The private key(s) whose coins should be used for the transfer.")
-    transfer.add_argument("--change-key", type=Signing.from_file, required=False,
+    transfer.add_argument("--change-key", type=Key.from_file, required=False,
                           help="The private key where any remaining coins are sent to.")
-    transfer.add_argument("--transaction-fee", type=int, default=0,
+    transfer.add_argument("--transaction-fee", type=int, default=1,
                           help="The transaction fee you want to pay to the miner.")
     transfer.add_argument("target", nargs='*', metavar=("TARGET_KEY AMOUNT"),
                           type=parse_targets(),
                           help="The private key(s) whose coins should be used for the transfer.")
+
     args = parser.parse_args()
 
     rpc = RPCClient(args.miner_port)
 
-    def show_transactions(keys: List[Signing]):
+    def show_transactions(keys: List[Key]):
         for key in keys:
             for trans in rpc.get_transactions(key):
                 print(trans.to_json_compatible())
             print()
 
-    def create_address(wallet_keys: List[Signing], wallet_path: str, output_files: List[IOBase]):
-        keys = [Signing.generate_private_key() for _ in output_files]
-        Signing.write_many_private(wallet_path, wallet_keys + keys)
+    def create_address(wallet_keys: List[Key], wallet_path: str, output_files: List[IOBase]):
+        keys = [Key.generate_private_key() for _ in output_files]
+        Key.write_many_private(wallet_path, wallet_keys + keys)
         for fp, key in zip(output_files, keys):
             fp.write(key.as_bytes())
             fp.close()
 
-    def show_balance(keys: List[Signing]):
+    def show_balance(keys: List[Key]):
         total = 0
         for pubkey, balance in rpc.show_balance(keys):
             print("{}: {}".format(hexlify(pubkey.as_bytes()), balance))
@@ -125,17 +132,18 @@ def main():
         for k, v in rpc.network_info():
             print("{}\t{}".format(k, v))
 
-    def transfer(targets: List[TransactionTarget], change_key: Optional[Signing],
-                 wallet_keys: List[Signing], wallet_path: str, priv_keys: List[Signing]):
-        if not change_key:
-            change_key = Signing.generate_private_key()
-            Signing.write_many_private(wallet_path, wallet_keys + [change_key])
+    def transfer(tx_targets: List[TransactionTarget], change_key: Optional[Key],
+                 wallet_keys: List[Key], wallet_path: str, priv_keys: List[Key]):
 
-        trans = rpc.build_transaction(priv_keys, targets, change_key, args.transaction_fee)
-        rpc.send_transaction(trans)
+        if not change_key:
+            change_key = Key.generate_private_key()
+            Key.write_many_private(wallet_path, wallet_keys + [change_key])
 
+        timestamp = datetime.utcnow()
+        tx = rpc.build_transaction(priv_keys, tx_targets, change_key, args.transaction_fee, timestamp)
+        rpc.send_transaction(tx)
 
-    def get_keys(keys: List[Signing]) -> List[Signing]:
+    def get_keys(keys: List[Key]) -> List[Key]:
         """
         Returns a combined list of keys from the `keys` and the wallet. Shows an error if empty.
         """
@@ -151,7 +159,6 @@ def main():
         if not args.wallet[1]:
             print("no wallet specified", file=sys.stderr)
             parser.parse_args(["--help"])
-
         create_address(*args.wallet, args.file)
     elif args.command == 'show-balance':
         show_balance(get_keys(args.key))
@@ -164,11 +171,13 @@ def main():
         if not args.change_key and not args.wallet[0]:
             print("You need to specify either --wallet or --change-key.\n", file=sys.stderr)
             parser.parse_args(["--help"])
-        targets = [TransactionTarget(k, a) for k, a in zip(args.target[::2], args.target[1::2])]
+        targets = [TransactionTarget(TransactionTarget.pay_to_pubkey(k), a) for k, a in
+                   zip(args.target[::2], args.target[1::2])]
         transfer(targets, args.change_key, *args.wallet, get_keys(args.private_key))
     else:
         print("You need to specify what to do.\n", file=sys.stderr)
         parser.parse_args(["--help"])
 
+
 if __name__ == '__main__':
     main()

+ 235 - 0
website.py

@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+
+"""
+The website starts the blockchain explorer, reachable as default under `localhost:80`. The blockchain explorer launches
+its own non-mining miner to get access to the blockchain.
+"""
+
+import argparse
+import requests
+import binascii
+import miner
+from flask import Flask, render_template
+from _thread import start_new_thread
+from flask import abort
+
+app = Flask(__name__)
+sess = requests.Session()
+host = "http://localhost:{}/"
+url = ""
+QUERY_PARAMETER_AVERAGE_LENGTH = 10
+
+
+def main():
+    """
+    Takes arguments: `rpc-port`: The port number where the Blockchain Explorer can find an RPC server. Default is: `40203`
+    `bootstrap-peer`: Address of other P2P peers in the network. If not supplied, no non-mining miner will be started.
+    `listen-address`: The IP address where the P2P server should bind to. `listen-port`: The port where the P2P server should listen.
+    Defaults a dynamically assigned port.
+    """
+    parser = argparse.ArgumentParser(description="Blockchain Explorer.")
+    parser.add_argument("--rpc-port", type=int, default=40203,
+                        help="The port number where the Blockchain Explorer can find an RPC server.")
+    parser.add_argument("--bootstrap-peer",
+                        help="Address of other P2P peers in the network.")
+    parser.add_argument("--listen-address", default="",
+                        help="The IP address where the P2P server should bind to.")
+    parser.add_argument("--listen-port", default=0, type=int,
+                        help="The port where the P2P server should listen. Defaults a dynamically assigned port.")
+
+    args = parser.parse_args()
+
+    global url
+    url = host.format(args.rpc_port)
+
+    if (args.bootstrap_peer):
+        start_new_thread(miner.start_listener, (args.rpc_port, args.bootstrap_peer, args.listen_port, args.listen_address))  # Starts the miner.
+        app.run(host='0.0.0.0', port=80)  # Starts the flask server for the blockchain explorer
+    else:
+        parser.parse_args(["--help"])
+
+
+def append_sender_to_transaction(transaction):
+    """ Reads the transaction inputs for the supplied transaction and adds the senders to the JSON objects. """
+    pks = []
+    for inp in transaction["inputs"]:
+        res = sess.get(url + 'explorer/transaction/' + inp["transaction_hash"])
+        output_idx = inp["output_idx"]
+        pks.append(res.json()["targets"][output_idx]["recipient_pk"])
+
+    counter = 0
+    result = []
+    for inp in transaction["inputs"]:
+        dict = {"input": inp, "signature": transaction["signatures"][counter]}
+        result.append(dict)
+        counter += 1
+
+    transaction["inp"] = result
+    transaction["senders"] = pks
+
+
+@app.route("/")
+def index():
+    """ Index page of the blockchain explorer. Shows statistics, last blocks and last transactions.
+        Route: `\"/\"`. """
+    data = {}
+    data["blocks"] = sess.get(url + 'explorer/lastblocks/10').json()
+    data["statistics"] = get_statistics()
+    transactions = sess.get(url + 'explorer/lasttransactions/5').json()
+    for transaction in transactions:
+        append_sender_to_transaction(transaction)
+    data["transactions"] = transactions
+    return render_template('index.html', data=data)
+
+
+@app.route("/blocks")
+@app.route("/blocks/<int:amount>")
+def blocklist(amount=10):
+    """ Lists all blocks from the blockchain. Optional takes argument as amount of blocks to just return
+     the last amount blocks.
+        Route: `\"/blocks/<optional: int:amount>\"`. """
+    blocks = sess.get(url + 'explorer/lastblocks/' + str(amount)).json()
+    if len(blocks) < amount:
+        return render_template('blocklist.html', data=blocks)
+    else:
+        return render_template('blocklist.html', data=blocks, nextamount=amount * 2)
+
+
+@app.route("/block/<string:hash>")
+def block(hash):
+    """ Lists information about the specified block.
+    Route: `\"/block/<string:hash>\"`. """
+    resp = sess.get(url + 'explorer/block/' + hash)
+    if resp.status_code == 404:
+        return render_template('not_found.html', data={'type': 'Block', 'hash': hash})
+    resp.raise_for_status()
+    json_obj = resp.json()
+
+    for transaction in json_obj["transactions"]:
+        append_sender_to_transaction(transaction)
+
+    return render_template('block.html', data=json_obj)
+
+
+@app.route("/addresses/")
+def addresses():
+    """ Lists all addresses found in the blockchain as sender or receiver of a transaction.
+    Route: `\"/addresses\"`. """
+    resp = sess.get(url + 'explorer/addresses')
+    resp.raise_for_status()
+
+    return render_template('addresses.html', data=resp.json())
+
+
+def try_get_json(addr):
+    try:
+        resp = sess.get(url + addr)
+    except:
+        abort(404)
+        # return render_template('not_found.html', data={'type': 'Address', 'hash': addr})
+
+    if resp.status_code == 404:
+        abort(404)
+        # return render_template('not_found.html', data={'type': 'Address', 'hash': addr})
+    resp.raise_for_status()
+    json_obj = resp.json()
+    return json_obj
+
+
+@app.route("/address/<string:addr>")
+def address(addr):
+    """ Lists information about the specified address. """
+
+    json_obj = try_get_json('explorer/sortedtransactions/' + addr)
+
+    for tr in json_obj["sent"]:
+        append_sender_to_transaction(tr)
+
+    for tr in json_obj["received"]:
+        for target in tr["targets"]:
+            if (target["recipient_pk"]) != addr:
+                tr["targets"].remove(target)
+        append_sender_to_transaction(tr)
+
+    resp_credit = sess.post(url + 'explorer/show-balance', data=binascii.unhexlify(addr),
+                            headers={"Content-Type": "application/json"})
+    resp_credit.raise_for_status()
+
+    json_obj["credit"] = resp_credit.json()["credit"]
+    json_obj["hash"] = addr
+    return render_template('address.html', data=json_obj)
+
+
+@app.route("/transactions")
+@app.route("/transactions/<int:amount>")
+def transactions(amount=10):
+    """ Lists all transactions from the blockchain. Optional takes argument as amount of transactions to just return
+     the last amount transactions.
+    Route: `\"/transactions/<optional: int:amount>\"`. """
+    resp = sess.get(url + 'explorer/lasttransactions/' + str(amount))
+    resp.raise_for_status()
+    transactions = resp.json()
+
+    for transaction in transactions:
+        append_sender_to_transaction(transaction)
+
+    if (len(transactions) < amount):
+        return render_template('transactions.html', data_array=transactions)
+    else:
+        return render_template('transactions.html', data_array=transactions, nextamount=amount * 2)
+
+
+@app.route("/transaction/<string:hash>")
+def transaction(hash):
+    """ Lists information about the specified transaction.
+    Route: `\"/transaction/<string:hash>\"`. """
+    resp = sess.get(url + 'explorer/transaction/' + hash)
+    if resp.status_code == 404:
+        return render_template('not_found.html', data={'type': 'Transaction', 'hash': hash})
+
+    resp.raise_for_status()
+
+    json_obj = resp.json()
+
+    append_sender_to_transaction(json_obj)
+
+    return render_template('transaction.html', transaction=json_obj)
+
+
+def get_statistics():
+    """ Lists all calculated statistics about the blockchain and its network. """
+    resp_blocktime = sess.get(url + 'explorer/statistics/blocktime')
+    resp_blocktime.raise_for_status()
+
+    resp_totalblocks = sess.get(url + 'explorer/statistics/totalblocks')
+    resp_totalblocks.raise_for_status()
+
+    resp_difficulty = sess.get(url + 'explorer/statistics/difficulty')
+    resp_difficulty.raise_for_status()
+
+    resp_hashrate = sess.get(url + 'explorer/statistics/hashrate?length=' + str(QUERY_PARAMETER_AVERAGE_LENGTH))
+    resp_hashrate.raise_for_status()
+
+    resp_tps = sess.get(url + 'explorer/statistics/tps?length=' + str(QUERY_PARAMETER_AVERAGE_LENGTH))
+    resp_tps.raise_for_status()
+
+    result = {"blocktime": resp_blocktime.json(), "totalblocks": resp_totalblocks.json(),
+              "difficulty": resp_difficulty.json(), "hashrate": resp_hashrate.json(), "tps": resp_tps.json()}
+    return result
+
+
+@app.route("/statistics")
+def statistics():
+    """ Shows all calculated statistics about the blockchain and its network.
+    Route: `\"/statistics\"`. """
+    return render_template('statistics.html', data=get_statistics())
+
+
+@app.route("/about")
+def about():
+    """ Shows the 'About'-Page.
+    Route: `\"/about\"`. """
+    return render_template('about.html')
+
+if __name__ == '__main__':
+    main()