Example #1
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]
Example #2
0
    def get_block(cls, number: int = 0, hash: str = '') -> 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)
                return cls._decode_block(block[0]) if block else None
Example #3
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]
Example #4
0
    def validate_block_data(cls, block_data: dict):
        """
        Validates the block_data dictionary. 'block_data' should be a strict subset of the 'block' dictionary, keys for all
        columns in the block table EXCEPT 'number' and 'hash'. If any validation fails, an exception is raised.
        For a block_data dictionary to be valid, it must:
         - Have a key for each block data column specified in BLOCK_DATA_COLS (at top of blocks.py)
         - BlockContender successfully validates with the Merkle root (meaning all signatures in the BlockContender
           can be verified using the Merkle root as the message)
         - Merkle leaves contained in BlockContender (block_contender.nodes) match Merkle leaves in block_data dict
         - Merkle root is correct root if a Merkle tree is built from Merkle leaves
         - Masternode signature is valid (signature is valid using Merkle root as message and masternode_vk as vk)

        :param block_data: The dictionary containing a key for each column in BLOCK_DATA_COLS
        (ie 'merkle_root', 'prev_block_hash', .. ect)
        :raises: An BlockStorageValidationException (or subclass) if any validation fails
        """
        # Check block_data has all the necessary keys
        expected_keys = set(BLOCK_DATA_COLS.keys())
        actual_keys = set(block_data.keys())
        missing_keys = expected_keys - actual_keys
        extra_keys = actual_keys - expected_keys

        # Check for missing keys
        if len(missing_keys) > 0:
            raise BlockStorageValidationException("block_data keys {} missing key(s) {}".format(actual_keys, missing_keys))
        # Check for extra (unrecognized) keys
        if len(extra_keys) > 0:
            raise BlockStorageValidationException("block_data keys {} has unrecognized keys {}".format(actual_keys, extra_keys))

        # Validate Merkle Tree
        tree = MerkleTree.from_leaves_hex_str(block_data['merkle_leaves'])
        if tree.root_as_hex != block_data['merkle_root']:
            raise InvalidMerkleTreeException("Merkle Tree could not be validated for block_data {}".format(block_data))

        # Validate BlockContender nodes match merkle leaves
        block_leaves = block_data['block_contender'].merkle_leaves
        if len(block_leaves) != len(tree.leaves):
            raise InvalidBlockContenderException("Number of Merkle leaves on BlockContender {} does not match number of"
                                                 " leaves in MerkleTree {}".format(len(block_leaves), len(tree.leaves)))
        for block_leaf, merkle_leaf in zip(block_leaves, tree.leaves_as_hex):
            if block_leaf != merkle_leaf:
                raise InvalidBlockContenderException("BlockContender leaves do not match Merkle leaves\nblock leaves = "
                                                     "{}\nmerkle leaves = {}".format(block_leaves, tree.leaves_as_hex))

        # Validate MerkleSignatures inside BlockContender match Merkle leaves from raw transactions
        bc = block_data['block_contender']
        if not bc.validate_signatures():
            raise InvalidBlockContenderException("BlockContender signatures could not be validated! BC = {}".format(bc))

        # TODO validate MerkleSignatures are infact signed by valid delegates
        # this is tricky b/c we would need to know who the delegates were at the time of the block, not necessarily the
        # current delegates

        # Validate Masternode Signature
        if not is_valid_hex(block_data['masternode_vk'], length=64):
            raise InvalidBlockSignatureException("Invalid verifying key for field masternode_vk: {}"
                                                 .format(block_data['masternode_vk']))
        if not wallet.verify(block_data['masternode_vk'], bytes.fromhex(block_data['merkle_root']), block_data['masternode_signature']):
            raise InvalidBlockSignatureException("Could not validate Masternode's signature on block data")
Example #5
0
    def validate(self):
        # Validate field types and existence
        assert type(self._data) == dict, "BlockContender's _data must be a dict"
        assert BlockContender.SIGS in self._data, "signature field missing from data {}".format(self._data)
        assert BlockContender.LEAVES in self._data, "leaves field missing from data {}".format(self._data)

        # Ensure merkle leaves are valid hex
        for leaf in self.merkle_leaves:
            assert is_valid_hex(leaf, length=64), "Invalid Merkle leaf {} ... expected 64 char hex string".format(leaf)

        # Attempt to deserialize signatures by reading property (will raise exception if can't)
        self.signatures
Example #6
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
Example #7
0
    def get_raw_transactions_from_block(
            cls, block_hash: str) -> 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_hash:
        :return: A list of raw transactions each as bytes, or None if no block with the given hash can be found
        """
        assert is_valid_hex(
            block_hash, length=64
        ), "Expected block_hash to be 64 char hex str, not {}".format(
            block_hash)

        with DB() as db:
            transactions = db.tables.transactions
            rows = transactions.select().where(
                transactions.block_hash == block_hash).run(db.ex)
            if not rows:
                return None
            else:
                return [decode_tx(row['data']) for row in rows]
Example #8
0
    def get_raw_transaction(cls, tx_hash: str) -> bytes or None:
        """
        Retrieves a single raw transaction from its hash. Returns None if no transaction for that hash can be found
        :param tx_hash: The hash of the raw transaction to lookup. Should be a 64 character hex string
        :return: The raw transactions as bytes, or None if no transaction with that hash can be found
        """
        assert is_valid_hex(
            tx_hash, length=64
        ), "Expected tx_hash to be 64 char hex str, not {}".format(tx_hash)

        with DB() as db:
            transactions = db.tables.transactions
            rows = transactions.select().where(
                transactions.hash == tx_hash).run(db.ex)
            if not rows:
                return None
            else:
                assert len(
                    rows
                ) == 1, "Multiple Transactions found with has {}! BIG TIME DEVELOPMENT ERROR!!!".format(
                    tx_hash)
                return decode_tx(rows[0]['data'])
Example #9
0
    def store_block(cls,
                    block_contender: BlockContender,
                    raw_transactions: List[bytes],
                    publisher_sk: str,
                    timestamp: int = 0):
        """

        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.

        :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: None
        :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 = ED25519Wallet.get_vk(publisher_sk)
        publisher_sig = ED25519Wallet.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 and finally persist the data
        log.info(
            "Attempting to persist new block with hash {}".format(block_hash))
        block_data = cls._encode_block(block_data)

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

            # 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))