예제 #1
0
    def test_creates_new_db_only_once(self):
        """
        Tests that a DB instance is only created once for each process.
        """
        pid = os.getpid()
        lock1, lock2 = None, None
        before_creation = DBSingletonMeta._instances.copy(
        )  # Dict of DB instances before we call DB()

        with DB() as db:
            lock1 = db.lock

        after_creation1 = DBSingletonMeta._instances.copy()

        self.assertTrue(len(after_creation1) == len(before_creation) + 1)
        self.assertTrue(pid in after_creation1)

        # Invoke a second time ... this should not create anything new, and should return the same lock
        with DB() as db:
            lock2 = db.lock

        after_creation2 = DBSingletonMeta._instances.copy()

        self.assertTrue(len(after_creation1) == len(after_creation2))
        self.assertEqual(lock1, lock2)
예제 #2
0
    def test_store_block_inserts_transactions(self):
        num_txs = 4

        with DB() as db:
            initial_txs = len(db.tables.transactions.select().run(db.ex))

        mn_sk = TESTNET_MASTERNODES[0]['sk']
        timestamp = random.randint(0, pow(2, 32))
        raw_transactions = [build_test_transaction().serialize() for _ in range(num_txs)]

        tree = MerkleTree(raw_transactions)
        bc = build_test_contender(tree=tree)

        BlockStorageDriver.store_block(block_contender=bc, raw_transactions=raw_transactions, publisher_sk=mn_sk, timestamp=timestamp)
        block_hash = BlockStorageDriver.get_latest_block_hash()

        with DB() as db:
            transactions = db.tables.transactions
            all_tx_query = transactions.select().run(db.ex)

            # Ensure the correct number of transactions was inserted
            self.assertEquals(len(all_tx_query) - initial_txs, num_txs)

            # Ensure the transactions were correctly inserted
            for raw_tx in raw_transactions:
                tx_hash = Hasher.hash(raw_tx)

                rows = transactions.select().where(transactions.hash == tx_hash).run(db.ex)
                self.assertTrue(rows, "Expected there to be a row for inserted tx {}".format(raw_tx))

                tx_row = rows[0]
                self.assertEquals(tx_row['hash'], tx_hash, "Expected fetched tx to have hash equal to its hashed data")
                self.assertEquals(tx_row['data'], encode_tx(raw_tx), "Expected tx data col to equal encoded raw tx")
                self.assertEquals(tx_row['block_hash'], block_hash, "Expected inserted tx to reference last block")
예제 #3
0
    def store_block_from_meta(
            cls, block: BlockMetaData or NewBlockNotification) -> str:
        """
        Stores a block from a BlockMetaData object. This block must be the child of the current lastest block.
        :param block: The BlockMetaData object containing all of the block's data (excluding the raw transactions)
        :return: The hash of the stored block (as a string)
        :raises: A BlockStorageException (or specific subclass) if any validation or storage fails
        """
        assert issubclass(
            type(block), BlockMetaData
        ), "Can only store BlockMetaData objects or subclasses"

        # Ensure this block's previous hash matches the latest block hash in the DB
        if block.prev_block_hash != cls.get_latest_block_hash():
            raise InvalidBlockLinkException(
                "Attempted to store a block with previous_hash {} that does not match the "
                "database latest block hash {}".format(
                    block.prev_block_hash, cls.get_latest_block_hash()))

        with DB() as db:
            encoded_block_data = cls._encode_block(block.block_dict())
            res = db.tables.blocks.insert([encoded_block_data]).run(db.ex)
            if res:
                log.success2(
                    "Successfully inserted new block with number {} and hash {}"
                    .format(res['last_row_id'], block.block_hash))
            else:
                raise BlockStorageDatabaseException(
                    "Error inserting block! Got None/False result back "
                    "from insert query. Result={}".format(res))

            return block.block_hash
예제 #4
0
    def get_block(cls,
                  number: int = 0,
                  hash: str = '',
                  include_number=True) -> dict or None:
        """
        Retrieves a block by its hash, or autoincrement number. Returns a dictionary with a key for each column in the
        blocks table. Returns None if no block with the specified hash/number is found.
        :param number: The number of the block to fetch. The genesis block is number 1, and the first 'real' block
        is number 2, and so on.
        :param hash: The hash of the block to lookup. Must be valid 64 char hex string
        :return: A dictionary, containing a key for each column in the blocks table.
        """
        assert bool(number > 0) ^ bool(
            hash), "Either 'number' XOR 'hash' arg must be given"

        with DB() as db:
            blocks = db.tables.blocks

            if number > 0:
                block = blocks.select().where(blocks.number == number).run(
                    db.ex)
                return cls._decode_block(block[0]) if block else None

            elif hash:
                assert is_valid_hex(
                    hash, length=64), "Invalid block hash {}".format(hash)
                block = blocks.select().where(blocks.hash == hash).run(db.ex)

                if block:
                    b = cls._decode_block(block[0])
                    if not include_number:
                        del b['number']
                    return b
                else:
                    return None
예제 #5
0
    def _get_raw_transactions(cls, hashes: str or list, is_block_hashes=False):
        """
        Helper method to query raw transactions, either from a list of hashes or a single hash. If is_block_hashes=True,
        then the hashes are assumed to refer to block hashes, in which case all the transactions belonging to that
        particular block is returned.
        :param hashes: A single hash (as a string), or a list of hashes
        :param is_block_hashes: If true, 'hashes' is assumed to refer to block hashes, and all transactions belonging
        to the specified block hash(es) will be returned. If False, 'hashes' are assumed to be transaction hashes.
        :return: A list of raw transactions, each as bytes, or None if no block with the given hash can be found
        """
        assert isinstance(hashes, str) or isinstance(
            hashes, list), "Expected hashes to be a str or list"

        if isinstance(hashes, str):
            hashes = [hashes]

        for h in hashes:
            assert is_valid_hex(
                h, length=64
            ), "Expected hashes to be 64 char hex str, not {}".format(h)

        with DB() as db:
            transactions = db.tables.transactions
            comparison_col = transactions.block_hash if is_block_hashes else transactions.hash
            where_clause = or_(*[(comparison_col == h) for h in hashes])

            rows = transactions.select().where(where_clause).run(db.ex)
            if not rows:
                return None
            else:
                return [decode_tx(row['data']) for row in rows]
예제 #6
0
    def get_child_block_hashes(cls, parent_hash: str, limit=0) -> List[str] or None:
        """
        Retrieve a list of child block hashes from a given a parent block. In other words, this method gets the hashes
        for all blocks created "after" the specified block with hash 'parent_hash'.
        :param parent_hash: The hash of the parent block
        :param limit: If specified,
        :return: A list of hashes for the blocks that descend the parent block. These will be sorted by their order
        in the block chain, such that the first element is the block immediately after parent_hash, and the last element
        is the latest block in the block chain. Returns None if parent_hash is already the latest block, or if no
        block with 'parent_hash' can be found.
        """
        assert is_valid_hex(parent_hash, 64), "parent_hash {} is not valid 64 char hex str".format(parent_hash)
        assert limit >= 0, "Limit must be >= 0 (not {})".format(limit)

        # TODO implement functionality for limits
        if limit:
            raise NotImplementedError("Limiting logic not implemented")

        # TODO optimize this once we get more functionality in EasyDB ...
        # we would like to select all rows where the number >= the number associated with 'parent_hash', ordered
        # ascending by number. For now, we must do this in 2 queries
        with DB() as db:
            blocks = db.tables.blocks

            # Get block index number associated with 'parent_hash'
            row = blocks.select().where(blocks.hash == parent_hash).run(db.ex)
            if not row:
                log.warning("No block with hash {} could be found!".format(parent_hash))
                return None
            parent_number = row[0]['number']

            rows = blocks.select('hash').where(blocks.number > parent_number).order_by('number', desc=False).run(db.ex)
            if not rows:
                return None
            return [row['hash'] for row in rows]
예제 #7
0
    def test_validate_blockchain_invalid(self):
        reset_db()

        # Stuff a bunch of valid blocks in
        for _ in range(4):
            mn_sk = masternodes[0]['sk']
            timestamp = random.randint(0, pow(2, 32))
            raw_transactions = [
                build_test_transaction().serialize() for _ in range(19)
            ]

            tree = MerkleTree(raw_transactions)
            bc = build_test_contender(tree=tree)

            BlockStorageDriver.store_block(block_contender=bc,
                                           raw_transactions=raw_transactions,
                                           publisher_sk=mn_sk,
                                           timestamp=timestamp)

        # Stuff a sketch block in that doesn't link to the last
        sketch_block = self._build_block_data(
        )  # by default this has prev_block_hash = 'AAAAA...'
        sketch_block['hash'] = BlockStorageDriver.compute_block_hash(
            sketch_block)
        with DB() as db:
            db.tables.blocks.insert(
                [BlockStorageDriver._encode_block(sketch_block)]).run(db.ex)

        self.assertRaises(InvalidBlockLinkException,
                          BlockStorageDriver.validate_blockchain)
예제 #8
0
    def test_store_block_inserts(self):
        with DB() as db:
            initial_num_blocks = len(db.tables.blocks.select().run(db.ex))

        mn_sk = TESTNET_MASTERNODES[0]['sk']
        timestamp = random.randint(0, pow(2, 32))
        raw_transactions = [build_test_transaction().serialize() for _ in range(19)]

        tree = MerkleTree(raw_transactions)
        bc = build_test_contender(tree=tree)

        BlockStorageDriver.store_block(block_contender=bc, raw_transactions=raw_transactions, publisher_sk=mn_sk, timestamp=timestamp)

        with DB() as db:
            blocks = db.tables.blocks.select().run(db.ex)
            self.assertEquals(len(blocks) - initial_num_blocks, 1)
            self.assertEquals(blocks[-1]['timestamp'], timestamp)
예제 #9
0
    def run_witness(signing_key, ip, name='Witness', reset_db=False):
        with DB(should_reset=reset_db) as db:
            pass
        loop = asyncio.new_event_loop()

        w = NodeFactory._build_node(loop=loop,
                                    signing_key=signing_key,
                                    ip=ip,
                                    node_cls=Witness,
                                    name=name)

        w.start()
예제 #10
0
    def run_masternode(signing_key, ip, name='Masternode', reset_db=False):
        with DB(should_reset=reset_db) as db:
            pass

        loop = asyncio.new_event_loop()

        mn = NodeFactory._build_node(loop=loop,
                                     signing_key=signing_key,
                                     ip=ip,
                                     node_cls=Masternode,
                                     name=name)

        mn.start()
예제 #11
0
    def run_delegate(signing_key, ip, name='Delegate', reset_db=False):
        with DB(should_reset=reset_db) as db:
            pass

        loop = asyncio.new_event_loop()

        d = NodeFactory._build_node(loop=loop,
                                    signing_key=signing_key,
                                    ip=ip,
                                    node_cls=Delegate,
                                    name=name)

        d.start()
예제 #12
0
    def get_latest_block(cls, include_number=True) -> dict:
        """
        Retrieves the latest block published in the chain.
        :return: A dictionary representing the latest block, containing a key for each column in the blocks table.
        """
        with DB() as db:
            latest = db.tables.blocks.select().order_by('number', desc=True).limit(1).run(db.ex)
            assert latest, "No blocks found! There should be a genesis. Was the database properly seeded?"

            # TODO unit tests around include_number functionality
            block = cls._decode_block(latest[0])
            if not include_number:
                del block['number']
            return block
예제 #13
0
    def test_creates_new_db(self):
        """
        Tests that a new instance of DB is lazily created when DB() is invoked
        """
        pid = os.getpid()
        before_creation = DBSingletonMeta._instances.copy(
        )  # Dict of DB instances before we call DB()

        with DB() as db:
            pass

        after_creation = DBSingletonMeta._instances

        self.assertTrue(len(after_creation) == len(before_creation) + 1)
        self.assertTrue(pid in after_creation)
예제 #14
0
    def get_latest_block_hash(cls) -> str:
        """
        Looks into the DB, and returns the latest block's hash. If the latest block_hash is for whatever reason invalid,
        (ie. not valid 64 char hex string), then this method will raise an assertion.
        :return: A string, representing the latest (most recent) block's hash
        :raises: An assertion if the latest block hash is not vaild 64 character hex. If this happens, something was
        seriously messed up in the block storage process.

        # TODO this can perhaps be made more efficient by memoizing the new block_hash each time we store it
        """
        with DB() as db:
            row = db.tables.blocks.select('hash').order_by('number', desc=True).limit(1).run(db.ex)[0]
            last_hash = row['hash']

            assert is_valid_hex(last_hash, length=64), "Latest block hash is invalid 64 char hex! Got {}".format(last_hash)

            return last_hash
예제 #15
0
    def __init__(self):
        self.log = get_logger(self.__class__.__name__)
        self.queue = deque()
        self.heap = []

        self.max_delay_ms = max_queue_delay_ms
        self.ex = Executer('root', '', DB_NAME, '127.0.0.1')

        # Grab a reference to contracts table from DB singleton
        with DB() as db:
            self.contracts_table = db.tables.contracts

        self.loop = asyncio.get_event_loop()
        self.check_contract_future = None
        self.start()

        # Ensure contracts table was seeded properly
        assert self.contracts_table.select().run(self.ex), "Expected contracts table to be seeded with at least one row"
예제 #16
0
def run_contracts_standalone(num_contracts=100):
    ex = Executer('root', '', DB_NAME, '127.0.0.1')
    with DB() as db:
        contracts_table = db.tables.contracts

    count = 0
    start = time.time()
    log.info("Running contracts...")
    for _ in range(num_contracts):
        run_contract(ex,
                     contracts_table,
                     contract_id=None,
                     user_id=SENDER_VK,
                     code_str=CODE_STR)
        count += 1
        if count % checkpoint_freq == 0:
            log.notice("{} contracts run so far.".format(count))

    total_time = time.time() - start
    cps = num_contracts / total_time
    log.important("Ran {} contracts in {} seconds".format(
        num_contracts, round(total_time, 4)))
    log.important("{} contracts per second.".format(round(cps, 2)))
예제 #17
0
def _build_node(signing_key, name='', node_cls=None) -> tuple:
    assert node_cls and name, "This is an abstract class. Subclasses must pass in node_cls and name."
    assert node_cls in (
        Witness, Masternode, Delegate
    ), "node_cls must be Witness/Masternode/Delegate, not {}".format(node_cls)

    loop = asyncio.get_event_loop()
    asyncio.set_event_loop(loop)

    with DB(should_reset=True) as db:
        pass

    ip = os.getenv('HOST_IP', '127.0.0.1')

    node = NodeFactory._build_node(loop=loop,
                                   signing_key=signing_key,
                                   ip=ip,
                                   node_cls=node_cls,
                                   name=name)
    node.start(start_loop=False)

    tasks = node.tasks + [node.composer.interface._recv_messages()]

    return node, loop, tasks
예제 #18
0
 def _run_db_proc(shared_mem):
     with DB() as db:
         pid = os.getpid()
         shared_mem[pid] = id(db.lock)
예제 #19
0
    def store_block(cls,
                    block_contender: BlockContender,
                    raw_transactions: List[bytes],
                    publisher_sk: str,
                    timestamp: int = 0) -> str:
        """
        Persist a new block to the blockchain, along with the raw transactions associated with the block. An exception
        will be raised if an error occurs either validating the new block data, or storing the block. Thus, it is
        recommended that this method is wrapped in a try block. If the block was successfully stored, this method will
        return the hash of the stored block.

        :param block_contender: A BlockContender instance
        :param raw_transactions: A list of ordered raw transactions contained in the block
        :param publisher_sk: The signing key of the publisher (a Masternode) who is publishing the block
        :param timestamp: The time the block was published, in unix epoch time. If 0, time.time() is used
        :return: The hash of the stored block
        :raises: An assertion error if invalid args are passed into this function, or a BlockStorageValidationException
         if validation fails on the attempted block

        TODO -- think really hard and make sure that this is 'collision proof' (extremely unlikely, but still possible)
        - could there be a hash collision in the Merkle tree nodes?
        - hash collision in block hash space?
        - hash collision in transaction space?
        """
        assert isinstance(
            block_contender, BlockContender
        ), "Expected block_contender arg to be BlockContender instance"
        assert is_valid_hex(
            publisher_sk,
            64), "Invalid signing key {}. Expected 64 char hex str".format(
                publisher_sk)

        if not timestamp:
            timestamp = int(time.time())

        tree = MerkleTree.from_raw_transactions(raw_transactions)

        publisher_vk = wallet.get_vk(publisher_sk)
        publisher_sig = wallet.sign(publisher_sk, tree.root)

        # Build and validate block_data
        block_data = {
            'block_contender': block_contender,
            'timestamp': timestamp,
            'merkle_root': tree.root_as_hex,
            'merkle_leaves': tree.leaves_as_concat_hex_str,
            'prev_block_hash': cls.get_latest_block_hash(),
            'masternode_signature': publisher_sig,
            'masternode_vk': publisher_vk,
        }
        cls.validate_block_data(block_data)

        # Compute block hash
        block_hash = cls.compute_block_hash(block_data)

        # Encode block data for serialization
        log.info(
            "Attempting to persist new block with hash {}".format(block_hash))
        block_data = cls._encode_block(block_data)

        # Finally, persist the data
        with DB() as db:
            # Store block
            res = db.tables.blocks.insert([{
                'hash': block_hash,
                **block_data
            }]).run(db.ex)
            if res:
                log.success2(
                    "Successfully inserted new block with number {} and hash {}"
                    .format(res['last_row_id'], block_hash))
            else:
                raise BlockStorageDatabaseException(
                    "Error inserting block! Got None/False result back "
                    "from insert query. Result={}".format(res))

            # Store raw transactions
            log.info(
                "Attempting to store {} raw transactions associated with block hash {}"
                .format(len(raw_transactions), block_hash))
            tx_rows = [{
                'hash': Hasher.hash(raw_tx),
                'data': encode_tx(raw_tx),
                'block_hash': block_hash
            } for raw_tx in raw_transactions]

            res = db.tables.transactions.insert(tx_rows).run(db.ex)
            if res:
                log.info("Successfully inserted {} transactions".format(
                    res['row_count']))
            else:
                log.error(
                    "Error inserting raw transactions! Got None from insert query. Result={}"
                    .format(res))

            return block_hash
예제 #20
0
class BlockStorageDriver:
    """
    This class provides a high level functional API for storing/retrieving blockchain data. It interfaces with the
    database under the hood using the process-specific DB Singleton. This allows all methods on this class to be
    implemented as class methods, since database cursors are provided via the Singleton instead of stored as
    properties on the BlockStorageDriver class/instance.
    """
    def __init__(self):
        raise NotImplementedError(
            "Do not instantiate this class! Instead, use the class methods.")

    @classmethod
    def store_block(cls,
                    block_contender: BlockContender,
                    raw_transactions: List[bytes],
                    publisher_sk: str,
                    timestamp: int = 0) -> str:
        """
        Persist a new block to the blockchain, along with the raw transactions associated with the block. An exception
        will be raised if an error occurs either validating the new block data, or storing the block. Thus, it is
        recommended that this method is wrapped in a try block. If the block was successfully stored, this method will
        return the hash of the stored block.

        :param block_contender: A BlockContender instance
        :param raw_transactions: A list of ordered raw transactions contained in the block
        :param publisher_sk: The signing key of the publisher (a Masternode) who is publishing the block
        :param timestamp: The time the block was published, in unix epoch time. If 0, time.time() is used
        :return: The hash of the stored block
        :raises: An assertion error if invalid args are passed into this function, or a BlockStorageValidationException
         if validation fails on the attempted block

        TODO -- think really hard and make sure that this is 'collision proof' (extremely unlikely, but still possible)
        - could there be a hash collision in the Merkle tree nodes?
        - hash collision in block hash space?
        - hash collision in transaction space?
        """
        assert isinstance(
            block_contender, BlockContender
        ), "Expected block_contender arg to be BlockContender instance"
        assert is_valid_hex(
            publisher_sk,
            64), "Invalid signing key {}. Expected 64 char hex str".format(
                publisher_sk)

        if not timestamp:
            timestamp = int(time.time())

        tree = MerkleTree.from_raw_transactions(raw_transactions)

        publisher_vk = wallet.get_vk(publisher_sk)
        publisher_sig = wallet.sign(publisher_sk, tree.root)

        # Build and validate block_data
        block_data = {
            'block_contender': block_contender,
            'timestamp': timestamp,
            'merkle_root': tree.root_as_hex,
            'merkle_leaves': tree.leaves_as_concat_hex_str,
            'prev_block_hash': cls.get_latest_block_hash(),
            'masternode_signature': publisher_sig,
            'masternode_vk': publisher_vk,
        }
        cls.validate_block_data(block_data)

        # Compute block hash
        block_hash = cls.compute_block_hash(block_data)

        # Encode block data for serialization
        log.info(
            "Attempting to persist new block with hash {}".format(block_hash))
        block_data = cls._encode_block(block_data)

        # Finally, persist the data
        with DB() as db:
            # Store block
            res = db.tables.blocks.insert([{
                'hash': block_hash,
                **block_data
            }]).run(db.ex)
            if res:
                log.success2(
                    "Successfully inserted new block with number {} and hash {}"
                    .format(res['last_row_id'], block_hash))
            else:
                raise BlockStorageDatabaseException(
                    "Error inserting block! Got None/False result back "
                    "from insert query. Result={}".format(res))

            # Store raw transactions
            log.info(
                "Attempting to store {} raw transactions associated with block hash {}"
                .format(len(raw_transactions), block_hash))
            tx_rows = [{
                'hash': Hasher.hash(raw_tx),
                'data': encode_tx(raw_tx),
                'block_hash': block_hash
            } for raw_tx in raw_transactions]

            res = db.tables.transactions.insert(tx_rows).run(db.ex)
            if res:
                log.info("Successfully inserted {} transactions".format(
                    res['row_count']))
            else:
                log.error(
                    "Error inserting raw transactions! Got None from insert query. Result={}"
                    .format(res))

            return block_hash

    @classmethod
    def store_block_from_meta(
            cls, block: BlockMetaData or NewBlockNotification) -> str:
        """
        Stores a block from a BlockMetaData object. This block must be the child of the current lastest block.
        :param block: The BlockMetaData object containing all of the block's data (excluding the raw transactions)
        :return: The hash of the stored block (as a string)
        :raises: A BlockStorageException (or specific subclass) if any validation or storage fails
        """
        assert issubclass(
            type(block), BlockMetaData
        ), "Can only store BlockMetaData objects or subclasses"

        # Ensure this block's previous hash matches the latest block hash in the DB
        if block.prev_block_hash != cls.get_latest_block_hash():
            raise InvalidBlockLinkException(
                "Attempted to store a block with previous_hash {} that does not match the "
                "database latest block hash {}".format(
                    block.prev_block_hash, cls.get_latest_block_hash()))

        with DB() as db:
            encoded_block_data = cls._encode_block(block.block_dict())
            res = db.tables.blocks.insert([encoded_block_data]).run(db.ex)
            if res:
                log.success2(
                    "Successfully inserted new block with number {} and hash {}"
                    .format(res['last_row_id'], block.block_hash))
            else:
                raise BlockStorageDatabaseException(
                    "Error inserting block! Got None/False result back "
                    "from insert query. Result={}".format(res))

            return block.block_hash

    @classmethod
    def get_block(cls,
                  number: int = 0,
                  hash: str = '',
                  include_number=True) -> dict or None:
        """
        Retrieves a block by its hash, or autoincrement number. Returns a dictionary with a key for each column in the
        blocks table. Returns None if no block with the specified hash/number is found.
        :param number: The number of the block to fetch. The genesis block is number 1, and the first 'real' block
        is number 2, and so on.
        :param hash: The hash of the block to lookup. Must be valid 64 char hex string
        :return: A dictionary, containing a key for each column in the blocks table.
        """
        assert bool(number > 0) ^ bool(
            hash), "Either 'number' XOR 'hash' arg must be given"

        with DB() as db:
            blocks = db.tables.blocks

            if number > 0:
                block = blocks.select().where(blocks.number == number).run(
                    db.ex)
                return cls._decode_block(block[0]) if block else None

            elif hash:
                assert is_valid_hex(
                    hash, length=64), "Invalid block hash {}".format(hash)
                block = blocks.select().where(blocks.hash == hash).run(db.ex)

                if block:
                    b = cls._decode_block(block[0])
                    if not include_number:
                        del b['number']
                    return b
                else:
                    return None

    @classmethod
    def get_latest_block(cls, include_number=True) -> dict:
        """
        Retrieves the latest block published in the chain.
        :return: A dictionary representing the latest block, containing a key for each column in the blocks table.
        """
        with DB() as db:
            latest = db.tables.blocks.select().order_by(
                'number', desc=True).limit(1).run(db.ex)
            assert latest, "No blocks found! There should be a genesis. Was the database properly seeded?"

            # TODO unit tests around include_number functionality
            block = cls._decode_block(latest[0])
            if not include_number:
                del block['number']
            return block

    @classmethod
    def get_child_block_hashes(cls,
                               parent_hash: str,
                               limit=0) -> List[str] or None:
        """
        Retrieve a list of child block hashes from a given a parent block. In other words, this method gets the hashes
        for all blocks created "after" the specified block with hash 'parent_hash'.
        :param parent_hash: The hash of the parent block
        :param limit: If specified,
        :return: A list of hashes for the blocks that descend the parent block. These will be sorted by their order
        in the block chain, such that the first element is the block immediately after parent_hash, and the last element
        is the latest block in the block chain. Returns None if parent_hash is already the latest block, or if no
        block with 'parent_hash' can be found.
        """
        assert is_valid_hex(
            parent_hash,
            64), "parent_hash {} is not valid 64 char hex str".format(
                parent_hash)
        assert limit >= 0, "Limit must be >= 0 (not {})".format(limit)

        # TODO implement functionality for limits
        if limit:
            raise NotImplementedError("Limiting logic not implemented")

        # TODO optimize this once we get more functionality in EasyDB ...
        # we would like to select all rows where the number >= the number associated with 'parent_hash', ordered
        # ascending by number. For now, we must do this in 2 queries
        with DB() as db:
            blocks = db.tables.blocks

            # Get block index number associated with 'parent_hash'
            row = blocks.select().where(blocks.hash == parent_hash).run(db.ex)
            if not row:
                log.warning("No block with hash {} could be found!".format(
                    parent_hash))
                return None
            parent_number = row[0]['number']

            rows = blocks.select('hash').where(
                blocks.number > parent_number).order_by('number',
                                                        desc=False).run(db.ex)
            if not rows:
                return None
            return [row['hash'] for row in rows]

    @classmethod
    def get_latest_block_hash(cls) -> str:
        """
        Looks into the DB, and returns the latest block's hash. If the latest block_hash is for whatever reason invalid,
        (ie. not valid 64 char hex string), then this method will raise an assertion.
        :return: A string, representing the latest (most recent) block's hash
        :raises: An assertion if the latest block hash is not vaild 64 character hex. If this happens, something was
        seriously messed up in the block storage process.

        # TODO this can perhaps be made more efficient by memoizing the new block_hash each time we store it
        """
        with DB() as db:
            row = db.tables.blocks.select('hash').order_by(
                'number', desc=True).limit(1).run(db.ex)[0]
            last_hash = row['hash']

            assert is_valid_hex(
                last_hash, length=64
            ), "Latest block hash is invalid 64 char hex! Got {}".format(
                last_hash)

            return last_hash

    @classmethod
    def get_raw_transactions(cls, tx_hashes: str or list) -> bytes or None:
        """
        Retrieves a single raw transaction from its hash. Returns None if no transaction for that hash can be found
        :param tx_hashes: The hash of the raw transaction to lookup (as a str), or a list of hashes. Hashes should
        be 64 character hex strings.
        :return: The raw transactions as bytes, or None if no transaction with that hash can be found
        """
        return cls._get_raw_transactions(hashes=tx_hashes,
                                         is_block_hashes=False)

    @classmethod
    def get_raw_transactions_from_block(
            cls, block_hashes: str or list) -> List[bytes] or None:
        """
        Retrieves a list of raw transactions associated with a particular block. Returns None if no block with
        the given hash can be found.
        :param block_hashes: A single transaction hash (as a 64 char string), or a list of transaction hashes
        :return: A list of raw transactions each as bytes, or None if no block with the given hash can be found
        """
        return cls._get_raw_transactions(hashes=block_hashes,
                                         is_block_hashes=True)

    @classmethod
    def validate_blockchain(cls, async=False):
        """
        Validates the cryptographic integrity of the blockchain. See spec in docs folder for details on what defines a
        valid blockchain structure.
        # TODO docstring
        :param async: If true, run this in a separate process
        :raises: An exception if validation fails
        """
        start = time.time()

        if async:
            raise NotImplementedError()

        with DB() as db:
            blocks = db.tables.blocks.select().order_by('number',
                                                        desc=False).run(db.ex)
            assert blocks, "No blocks found! There should be a genesis. Was the database properly seeded?"

            for i in range(len(blocks) - 1):
                cls._validate_block_link(cls._decode_block(blocks[i]),
                                         cls._decode_block(blocks[i + 1]))

        log.info("Blockchain validation completed successfully in {} seconds.".
                 format(round(time.time() - start, 2)))