Exemple #1
0
    def __init__(self, base_db: BaseDB, header: BlockHeader = None) -> None:
        if not self.vm_configuration:
            raise ValueError(
                "The Chain class cannot be instantiated with an empty `vm_configuration`"
            )
        else:
            validate_vm_configuration(self.vm_configuration)

        self.chaindb = self.get_chaindb_class()(base_db)
        self.headerdb = HeaderDB(base_db)
        self.header = header
        if self.header is None:
            self.header = self.create_header_from_parent(
                self.get_canonical_head())
        if self.gas_estimator is None:
            self.gas_estimator = get_gas_estimator()  # type: ignore
Exemple #2
0
async def test_peer_pool_connect(monkeypatch, event_loop,
                                 receiver_server_with_dumb_peer):
    started_peers = []

    def mock_start_peer(peer):
        nonlocal started_peers
        started_peers.append(peer)

    monkeypatch.setattr(receiver_server_with_dumb_peer, '_start_peer',
                        mock_start_peer)
    # We need this to ensure the server can check if the peer pool is full for
    # incoming connections.
    monkeypatch.setattr(receiver_server_with_dumb_peer, 'peer_pool',
                        MockPeerPool())

    pool = PeerPool(DumbPeer, HeaderDB(MemoryDB()), NETWORK_ID,
                    INITIATOR_PRIVKEY, tuple())
    nodes = [RECEIVER_REMOTE]
    await pool.connect_to_nodes(nodes)
    # Give the receiver_server a chance to ack the handshake.
    await asyncio.sleep(0.1)

    assert len(started_peers) == 1
    assert len(pool.connected_nodes) == 1

    # Stop our peer to make sure its pending asyncio tasks are cancelled.
    await list(pool.connected_nodes.values())[0].cancel()
Exemple #3
0
async def test_peer_pool_connect(monkeypatch, event_loop,
                                 receiver_server_with_dumb_peer):
    started_peers = []

    def mock_start_peer(peer):
        nonlocal started_peers
        started_peers.append(peer)

    monkeypatch.setattr(receiver_server_with_dumb_peer, '_start_peer',
                        mock_start_peer)

    network_id = 1
    discovery = None
    pool = PeerPool(DumbPeer, HeaderDB(MemoryDB()), network_id,
                    INITIATOR_PRIVKEY, discovery)
    nodes = [RECEIVER_REMOTE]
    await pool._connect_to_nodes(nodes)
    # Give the receiver_server a chance to ack the handshake.
    await asyncio.sleep(0.1)

    assert len(started_peers) == 1
    assert len(pool.connected_nodes) == 1

    # Stop our peer to make sure its pending asyncio tasks are cancelled.
    await list(pool.connected_nodes.values())[0].cancel()
Exemple #4
0
def get_server(privkey, address, peer_class):
    base_db = MemoryDB()
    headerdb = HeaderDB(base_db)
    chaindb = ChainDB(base_db)
    chaindb.persist_header(ROPSTEN_GENESIS_HEADER)
    chain = RopstenChain(base_db)
    server = Server(
        privkey,
        address.tcp_port,
        chain,
        chaindb,
        headerdb,
        base_db,
        network_id=NETWORK_ID,
        peer_class=peer_class,
    )
    return server
Exemple #5
0
def get_server(privkey, address, peer_class):
    db = MemoryDB()
    headerdb = HeaderDB(db)
    chaindb = ChainDB(db)
    chaindb.persist_header(ROPSTEN_GENESIS_HEADER)
    chain = RopstenChain(chaindb)
    server = Server(
        privkey,
        address,
        chain,
        chaindb,
        headerdb,
        db,
        network_id=1,
        min_peers=1,
        peer_class=peer_class,
    )
    return server
Exemple #6
0
def headerdb(base_db):
    return HeaderDB(base_db)
Exemple #7
0
class Chain(BaseChain):
    """
    A Chain is a combination of one or more VM classes.  Each VM is associated
    with a range of blocks.  The Chain class acts as a wrapper around these other
    VM classes, delegating operations to the appropriate VM depending on the
    current block number.
    """
    logger = logging.getLogger("evm.chain.chain.Chain")
    header = None  # type: BlockHeader
    network_id = None  # type: int
    gas_estimator = None  # type: Callable

    chaindb_class = ChainDB  # type: Type[BaseChainDB]

    def __init__(self, base_db: BaseDB, header: BlockHeader = None) -> None:
        if not self.vm_configuration:
            raise ValueError(
                "The Chain class cannot be instantiated with an empty `vm_configuration`"
            )
        else:
            validate_vm_configuration(self.vm_configuration)

        self.chaindb = self.get_chaindb_class()(base_db)
        self.headerdb = HeaderDB(base_db)
        self.header = header
        if self.header is None:
            self.header = self.create_header_from_parent(
                self.get_canonical_head())
        if self.gas_estimator is None:
            self.gas_estimator = get_gas_estimator()  # type: ignore

    #
    # Helpers
    #
    @classmethod
    def get_chaindb_class(cls) -> Type[BaseChainDB]:
        if cls.chaindb_class is None:
            raise AttributeError("`chaindb_class` not set")
        return cls.chaindb_class

    #
    # Chain API
    #
    @classmethod
    def from_genesis(cls,
                     base_db: BaseDB,
                     genesis_params: Dict[str, HeaderParams],
                     genesis_state: AccountState = None) -> 'BaseChain':
        """
        Initializes the Chain from a genesis state.
        """
        genesis_vm_class = cls.get_vm_class_for_block_number(BlockNumber(0))

        account_db = genesis_vm_class.get_state_class().get_account_db_class()(
            base_db,
            BLANK_ROOT_HASH,
        )

        if genesis_state is None:
            genesis_state = {}

        # mutation
        apply_state_dict(account_db, genesis_state)
        account_db.persist()

        if 'state_root' not in genesis_params:
            # If the genesis state_root was not specified, use the value
            # computed from the initialized state database.
            genesis_params = assoc(genesis_params, 'state_root',
                                   account_db.state_root)
        elif genesis_params['state_root'] != account_db.state_root:
            # If the genesis state_root was specified, validate that it matches
            # the computed state from the initialized state database.
            raise ValidationError(
                "The provided genesis state root does not match the computed "
                "genesis state root.  Got {0}.  Expected {1}".format(
                    account_db.state_root,
                    genesis_params['state_root'],
                ))

        genesis_header = BlockHeader(**genesis_params)
        return cls.from_genesis_header(base_db, genesis_header)

    @classmethod
    def from_genesis_header(cls, base_db: BaseDB,
                            genesis_header: BlockHeader) -> 'BaseChain':
        """
        Initializes the chain from the genesis header.
        """
        chaindb = cls.get_chaindb_class()(base_db)
        chaindb.persist_header(genesis_header)
        return cls(base_db)

    def get_chain_at_block_parent(self, block: BaseBlock) -> BaseChain:
        """
        Returns a `Chain` instance with the given block's parent at the chain head.
        """
        try:
            parent_header = self.get_block_header_by_hash(
                block.header.parent_hash)
        except HeaderNotFound:
            raise ValidationError("Parent ({0}) of block {1} not found".format(
                block.header.parent_hash, block.header.hash))

        init_header = self.create_header_from_parent(parent_header)
        return type(self)(self.chaindb.db, init_header)

    #
    # VM API
    #
    def get_vm(self, header: BlockHeader = None) -> 'BaseVM':
        """
        Returns the VM instance for the given block number.
        """
        if header is None:
            header = self.header

        vm_class = self.get_vm_class_for_block_number(header.block_number)
        return vm_class(header=header, chaindb=self.chaindb)

    #
    # Header API
    #
    def create_header_from_parent(self, parent_header, **header_params):
        """
        Passthrough helper to the VM class of the block descending from the
        given header.
        """
        return self.get_vm_class_for_block_number(
            block_number=parent_header.block_number +
            1, ).create_header_from_parent(parent_header, **header_params)

    def get_block_header_by_hash(self, block_hash: Hash32) -> BlockHeader:
        """
        Returns the requested block header as specified by block hash.

        Raises BlockNotFound if there's no block header with the given hash in the db.
        """
        validate_word(block_hash, title="Block Hash")
        return self.chaindb.get_block_header_by_hash(block_hash)

    def get_canonical_head(self):
        """
        Returns the block header at the canonical chain head.

        Raises CanonicalHeadNotFound if there's no head defined for the canonical chain.
        """
        return self.chaindb.get_canonical_head()

    def get_score(self, block_hash):
        """
        Returns the difficulty score of the block with the given hash.

        Raises HeaderNotFound if there is no matching black hash.
        """
        return self.headerdb.get_score(block_hash)

    #
    # Block API
    #
    @to_tuple
    def get_ancestors(self,
                      limit: int,
                      header: BlockHeader = None) -> Iterator[BaseBlock]:
        """
        Return `limit` number of ancestor blocks from the current canonical head.
        """
        if header is None:
            header = self.header
        lower_limit = max(header.block_number - limit, 0)
        for n in reversed(range(lower_limit, header.block_number)):
            yield self.get_canonical_block_by_number(BlockNumber(n))

    def get_block(self) -> BaseBlock:
        """
        Returns the current TIP block.
        """
        return self.get_vm().block

    def get_block_by_hash(self, block_hash: Hash32) -> BaseBlock:
        """
        Returns the requested block as specified by block hash.
        """
        validate_word(block_hash, title="Block Hash")
        block_header = self.get_block_header_by_hash(block_hash)
        return self.get_block_by_header(block_header)

    def get_block_by_header(self, block_header):
        """
        Returns the requested block as specified by the block header.
        """
        vm = self.get_vm(block_header)
        return vm.block

    def get_canonical_block_by_number(self,
                                      block_number: BlockNumber) -> BaseBlock:
        """
        Returns the block with the given number in the canonical chain.

        Raises BlockNotFound if there's no block with the given number in the
        canonical chain.
        """
        validate_uint256(block_number, title="Block Number")
        return self.get_block_by_hash(
            self.chaindb.get_canonical_block_hash(block_number))

    def get_canonical_block_hash(self, block_number: BlockNumber) -> Hash32:
        """
        Returns the block hash with the given number in the canonical chain.

        Raises BlockNotFound if there's no block with the given number in the
        canonical chain.
        """
        return self.chaindb.get_canonical_block_hash(block_number)

    #
    # Transaction API
    #
    def get_canonical_transaction(self,
                                  transaction_hash: Hash32) -> BaseTransaction:
        """
        Returns the requested transaction as specified by the transaction hash
        from the canonical chain.

        Raises TransactionNotFound if no transaction with the specified hash is
        found in the main chain.
        """
        (block_num,
         index) = self.chaindb.get_transaction_index(transaction_hash)
        VM = self.get_vm_class_for_block_number(block_num)

        transaction = self.chaindb.get_transaction_by_index(
            block_num,
            index,
            VM.get_transaction_class(),
        )

        if transaction.hash == transaction_hash:
            return transaction
        else:
            raise TransactionNotFound(
                "Found transaction {} instead of {} in block {} at {}".format(
                    encode_hex(transaction.hash),
                    encode_hex(transaction_hash),
                    block_num,
                    index,
                ))

    def create_transaction(self, *args: Any, **kwargs: Any) -> BaseTransaction:
        """
        Passthrough helper to the current VM class.
        """
        return self.get_vm().create_transaction(*args, **kwargs)

    def create_unsigned_transaction(self, *args: Any,
                                    **kwargs: Any) -> BaseUnsignedTransaction:
        """
        Passthrough helper to the current VM class.
        """
        return self.get_vm().create_unsigned_transaction(*args, **kwargs)

    #
    # Execution API
    #
    def apply_transaction(self, transaction):
        """
        Applies the transaction to the current tip block.

        WARNING: Receipt and Transaction trie generation is computationally
        heavy and incurs significant perferomance overhead.
        """
        vm = self.get_vm()
        base_block = vm.block

        new_header, receipt, computation = vm.apply_transaction(
            base_block.header, transaction)

        # since we are building the block locally, we have to persist all the incremental state
        vm.state.account_db.persist()

        transactions = base_block.transactions + (transaction, )
        receipts = base_block.get_receipts(self.chaindb) + (receipt, )

        new_block = vm.set_block_transactions(base_block, new_header,
                                              transactions, receipts)

        self.header = new_block.header

        return new_block, receipt, computation

    def estimate_gas(self,
                     transaction: BaseTransaction,
                     at_header: BlockHeader = None) -> int:
        """
        Returns an estimation of the amount of gas the given transaction will
        use if executed on top of the block specified by the given header.
        """
        if at_header is None:
            at_header = self.get_canonical_head()
        with self.get_vm(at_header).state_in_temp_block() as state:
            return self.gas_estimator(state, transaction)

    def import_block(self,
                     block: BaseBlock,
                     perform_validation: bool = True) -> BaseBlock:
        """
        Imports a complete block.
        """
        if block.number > self.header.block_number:
            raise ValidationError(
                "Attempt to import block #{0}.  Cannot import block with number "
                "greater than current block #{1}.".format(
                    block.number,
                    self.header.block_number,
                ))

        parent_chain = self.get_chain_at_block_parent(block)
        imported_block = parent_chain.get_vm().import_block(block)

        # Validate the imported block.
        if perform_validation:
            ensure_imported_block_unchanged(imported_block, block)
            self.validate_block(imported_block)

        self.chaindb.persist_block(imported_block)
        self.header = self.create_header_from_parent(self.get_canonical_head())
        self.logger.debug(
            'IMPORTED_BLOCK: number %s | hash %s',
            imported_block.number,
            encode_hex(imported_block.hash),
        )
        return imported_block

    def mine_block(self, *args: Any, **kwargs: Any) -> BaseBlock:
        """
        Mines the current block. Proxies to the current Virtual Machine.
        See VM. :meth:`~evm.vm.base.VM.mine_block`
        """
        mined_block = self.get_vm().mine_block(*args, **kwargs)

        self.validate_block(mined_block)

        self.chaindb.persist_block(mined_block)
        self.header = self.create_header_from_parent(self.get_canonical_head())
        return mined_block

    #
    # Validation API
    #
    def validate_block(self, block: BaseBlock) -> None:
        """
        Performs validation on a block that is either being mined or imported.

        Since block validation (specifically the uncle validation must have
        access to the ancestor blocks, this validation must occur at the Chain
        level.
        """
        self.validate_seal(block.header)
        self.validate_uncles(block)
        self.validate_gaslimit(block.header)

    def validate_seal(self, header: BlockHeader) -> None:
        """
        Validate the seal on the given header.
        """
        vm = self.get_vm()
        vm.validate_seal(header)

    def validate_gaslimit(self, header: BlockHeader) -> None:
        """
        Validate the gas limit on the given header.
        """
        parent_header = self.get_block_header_by_hash(header.parent_hash)
        low_bound, high_bound = compute_gas_limit_bounds(parent_header)
        if header.gas_limit < low_bound:
            raise ValidationError(
                "The gas limit on block {0} is too low: {1}. It must be at least {2}"
                .format(encode_hex(header.hash), header.gas_limit, low_bound))
        elif header.gas_limit > high_bound:
            raise ValidationError(
                "The gas limit on block {0} is too high: {1}. It must be at most {2}"
                .format(encode_hex(header.hash), header.gas_limit, high_bound))

    def validate_uncles(self, block: BaseBlock) -> None:
        """
        Validate the uncles for the given block.
        """
        # Check for duplicates
        uncle_groups = groupby(operator.attrgetter('hash'), block.uncles)
        duplicate_uncles = tuple(
            sorted(hash for hash, twins in uncle_groups.items()
                   if len(twins) > 1))
        if duplicate_uncles:
            raise ValidationError("Block contains duplicate uncles:\n"
                                  " - {0}".format(
                                      ' - '.join(duplicate_uncles)))

        recent_ancestors = tuple(ancestor for ancestor in self.get_ancestors(
            MAX_UNCLE_DEPTH + 1, header=block.header))
        recent_ancestor_hashes = {
            ancestor.hash
            for ancestor in recent_ancestors
        }
        recent_uncle_hashes = _extract_uncle_hashes(recent_ancestors)

        for uncle in block.uncles:
            if uncle.hash == block.hash:
                raise ValidationError("Uncle has same hash as block")

            # ensure the uncle has not already been included.
            if uncle.hash in recent_uncle_hashes:
                raise ValidationError("Duplicate uncle: {0}".format(
                    encode_hex(uncle.hash)))

            # ensure that the uncle is not one of the canonical chain blocks.
            if uncle.hash in recent_ancestor_hashes:
                raise ValidationError(
                    "Uncle {0} cannot be an ancestor of {1}".format(
                        encode_hex(uncle.hash), encode_hex(block.hash)))

            # ensure that the uncle was built off of one of the canonical chain
            # blocks.
            if uncle.parent_hash not in recent_ancestor_hashes or (
                    uncle.parent_hash == block.header.parent_hash):
                raise ValidationError(
                    "Uncle's parent {0} is not an ancestor of {1}".format(
                        encode_hex(uncle.parent_hash), encode_hex(block.hash)))

            # Now perform VM level validation of the uncle
            self.validate_seal(uncle)

            try:
                uncle_parent = self.get_block_header_by_hash(uncle.parent_hash)
            except HeaderNotFound:
                raise ValidationError("Uncle ancestor not found: {0}".format(
                    uncle.parent_hash))

            uncle_vm = self.get_vm(uncle)
            uncle_vm.validate_uncle(block, uncle, uncle_parent)