Example #1
0
def get_discv5_topic(trinity_config: TrinityConfig,
                     protocol: Type[Protocol]) -> bytes:
    db = DBClient.connect(trinity_config.database_ipc_path)

    header_db = HeaderDB(db)
    genesis_hash = header_db.get_canonical_block_hash(
        BlockNumber(GENESIS_BLOCK_NUMBER))

    return get_v5_topic(protocol, genesis_hash)
Example #2
0
 def from_headerdb(cls, headerdb: HeaderDB,
                   **kwargs: Any) -> ETHHandshakeParams:
     head = headerdb.get_canonical_head()
     head_score = headerdb.get_score(head.hash)
     genesis = headerdb.get_canonical_block_header_by_number(
         GENESIS_BLOCK_NUMBER)
     return cls(head_hash=head.hash,
                genesis_hash=genesis.hash,
                total_difficulty=head_score,
                **kwargs)
def test_header_chain_initialization_header_already_persisted(base_db, genesis_header):
    headerdb = HeaderDB(base_db)
    headerdb.persist_header(genesis_header)

    # sanity check that the header is persisted
    assert_headers_eq(headerdb.get_canonical_head(), genesis_header)

    header_chain = HeaderChain.from_genesis_header(base_db, genesis_header)

    head = header_chain.get_canonical_head()
    assert_headers_eq(head, genesis_header)
Example #4
0
 def from_headerdb(cls, headerdb: HeaderDB,
                   **kwargs: Any) -> ETHHandshakeParams:
     head = headerdb.get_canonical_head()
     head_score = headerdb.get_score(head.hash)
     # TODO: https://github.com/ethereum/py-evm/issues/1847
     genesis = headerdb.get_canonical_block_header_by_number(
         BlockNumber(GENESIS_BLOCK_NUMBER))
     return cls(head_hash=head.hash,
                genesis_hash=genesis.hash,
                total_difficulty=head_score,
                **kwargs)
Example #5
0
    def __init__(self, base_db: BaseDB) -> 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)
        if self.gas_estimator is None:
            self.gas_estimator = get_gas_estimator()  # type: ignore
Example #6
0
    def __init__(self, base_db: AtomicDatabaseAPI) -> 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.consensus_context = self.consensus_context_class(self.chaindb.db)
        self.headerdb = HeaderDB(base_db)
        if self.gas_estimator is None:
            self.gas_estimator = get_gas_estimator()
Example #7
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()
def create_db_server_manager(trinity_config: TrinityConfig,
                             base_db: BaseAtomicDB) -> BaseManager:

    chain_config = trinity_config.get_chain_config()
    chaindb = ChainDB(base_db)

    if not is_database_initialized(chaindb):
        initialize_database(chain_config, chaindb, base_db)

    headerdb = HeaderDB(base_db)

    class DBManager(BaseManager):
        pass

    DBManager.register('get_db',
                       callable=lambda: TracebackRecorder(base_db),
                       proxytype=AsyncDBProxy)

    DBManager.register(
        'get_chaindb',
        callable=lambda: TracebackRecorder(chaindb),
        proxytype=AsyncChainDBProxy,
    )

    DBManager.register(
        'get_headerdb',
        callable=lambda: TracebackRecorder(headerdb),
        proxytype=AsyncHeaderDBProxy,
    )

    manager = DBManager(address=str(
        trinity_config.database_ipc_path))  # type: ignore
    return manager
Example #9
0
    def chain_for_eth1_config(self, trinity_config: TrinityConfig,
                              eth1_app_config: Eth1AppConfig) -> AsyncChainAPI:
        chain_config = eth1_app_config.get_chain_config()

        db = DBClient.connect(trinity_config.database_ipc_path)

        if eth1_app_config.database_mode is Eth1DbMode.LIGHT:
            header_db = HeaderDB(db)
            event_bus_light_peer_chain = EventBusLightPeerChain(self.event_bus)
            return chain_config.light_chain_class(
                header_db, peer_chain=event_bus_light_peer_chain)
        elif eth1_app_config.database_mode is Eth1DbMode.FULL:
            return chain_config.full_chain_class(db)
        else:
            raise Exception(
                f"Unsupported Database Mode: {eth1_app_config.database_mode}")
Example #10
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
Example #11
0
def chain_for_eth1_config(trinity_config: TrinityConfig,
                          eth1_app_config: Eth1AppConfig,
                          event_bus: EndpointAPI) -> Iterator[AsyncChainAPI]:
    chain_config = eth1_app_config.get_chain_config()

    db = DBClient.connect(trinity_config.database_ipc_path)

    with db:
        if eth1_app_config.database_mode is Eth1DbMode.LIGHT:
            header_db = HeaderDB(db)
            event_bus_light_peer_chain = EventBusLightPeerChain(event_bus)
            yield chain_config.light_chain_class(
                header_db, peer_chain=event_bus_light_peer_chain
            )
        elif eth1_app_config.database_mode is Eth1DbMode.FULL:
            yield chain_config.full_chain_class(db)
        else:
            raise Exception(f"Unsupported Database Mode: {eth1_app_config.database_mode}")
Example #12
0
def create_db_server_manager(trinity_config: TrinityConfig,
                             base_db: BaseAtomicDB) -> BaseManager:

    eth1_app_config = trinity_config.get_app_config(Eth1AppConfig)
    chain_config = eth1_app_config.get_chain_config()
    chaindb = ChainDB(base_db)

    if not is_database_initialized(chaindb):
        initialize_database(chain_config, chaindb, base_db)

    headerdb = HeaderDB(base_db)

    # This enables connection when clients launch from another process on the shell
    multiprocessing.current_process().authkey = AUTH_KEY

    class DBManager(BaseManager):
        pass

    DBManager.register('get_db',
                       callable=lambda: TracebackRecorder(base_db),
                       proxytype=AsyncDBProxy)

    DBManager.register(
        'get_chaindb',
        callable=lambda: TracebackRecorder(chaindb),
        proxytype=AsyncChainDBProxy,
    )

    DBManager.register(
        'get_headerdb',
        callable=lambda: TracebackRecorder(headerdb),
        proxytype=AsyncHeaderDBProxy,
    )

    manager = DBManager(address=str(
        trinity_config.database_ipc_path))  # type: ignore
    return manager
Example #13
0
    def setup_eth1_modules(
            self, trinity_config: TrinityConfig) -> Tuple[BaseRPCModule, ...]:
        db_manager = create_db_consumer_manager(
            trinity_config.database_ipc_path)

        eth1_app_config = trinity_config.get_app_config(Eth1AppConfig)
        chain_config = trinity_config.get_chain_config()

        chain: BaseAsyncChain

        if eth1_app_config.database_mode is Eth1DbMode.LIGHT:
            db = db_manager.get_db()  # type: ignore
            header_db = HeaderDB(db)
            event_bus_light_peer_chain = EventBusLightPeerChain(self.event_bus)
            chain = chain_config.light_chain_class(
                header_db, peer_chain=event_bus_light_peer_chain)
        elif eth1_app_config.database_mode is Eth1DbMode.FULL:
            db = db_manager.get_db()  # type: ignore
            chain = chain_config.full_chain_class(db)
        else:
            raise Exception(
                f"Unsupported Database Mode: {eth1_app_config.database_mode}")

        return initialize_eth1_modules(chain, self.event_bus)
Example #14
0
def test_true_chain_canonicalized_regardless_of_order(genesis_header, data):
    class ActionEnum(enum.Enum):
        PERSIST_CHAIN = enum.auto()
        CHECKPOINT = enum.auto()

    # Setup
    headerdb = HeaderDB(AtomicDB())
    CHAIN_A, CHAIN_B = 0, 1
    chain_length = data.draw(st.integers(min_value=2, max_value=20))
    headerdb.persist_header(genesis_header)
    chain_a_headers = mk_header_chain(genesis_header, length=chain_length)
    headers = [
        # chain A: eventually canonical; the only source for checkpoints
        chain_a_headers,
        # chain B: non-canonical
        mk_header_chain(genesis_header, length=chain_length),
    ]
    missing_indices = [
        set(range(chain_length)),  # chain A
        set(range(chain_length)),  # chain B
    ]

    @to_tuple
    def _get_valid_extensions(of_missing_indices):
        """
        Find the headers that have available parents, and so are valid to persist
        """
        # For each chain
        for chain_index, header_indices in enumerate(of_missing_indices):
            # yield each header number that has a parent that's not missing
            # Sorting header_indices is important for hypothesis consistency
            for header_index in sorted(header_indices):
                if header_index == 0 or header_index - 1 not in header_indices:
                    yield (chain_index, header_index)

    last_checkpoint = None

    while len(missing_indices[CHAIN_A]):
        _validate_consecutive_canonical_links(headerdb, chain_length)
        _validate_gap_invariants(headerdb.get_header_chain_gaps())

        action = data.draw(st.sampled_from(ActionEnum))
        if action == ActionEnum.CHECKPOINT:
            checkpoint_index = data.draw(
                st.sampled_from(list(sorted(missing_indices[CHAIN_A]))))
            checkpoint = chain_a_headers[checkpoint_index]
            checkpoint_score = get_score(
                genesis_header, chain_a_headers[:checkpoint_index + 1])
            headerdb.persist_checkpoint_header(checkpoint, checkpoint_score)

            # keep track of whether any checkpoints were added, so we eventually
            # guarantee A as canonical
            last_checkpoint = checkpoint_index

            missing_indices[CHAIN_A].discard(checkpoint_index)

        elif action == ActionEnum.PERSIST_CHAIN:
            # choose the series of headers to add
            valid_extensions = _get_valid_extensions(missing_indices)
            chain_idx, next_chain_start = data.draw(
                st.sampled_from(valid_extensions))
            next_chain_len = data.draw(
                st.integers(min_value=1, max_value=chain_length))
            chain_range_end = next_chain_start + next_chain_len
            next_headers = headers[chain_idx][next_chain_start:chain_range_end]

            # persist them to chain
            try:
                headerdb.persist_header_chain(next_headers)
            except CheckpointsMustBeCanonical:
                assert chain_idx == CHAIN_B, "Only chain B should fail to decanonize checkpoint"
                # Persist failed, so retry different action
                continue

            # mark persisted headers as not missing
            for inserted_index in range(next_chain_start, chain_range_end):
                missing_indices[chain_idx].discard(inserted_index)

    _validate_consecutive_canonical_links(headerdb, chain_length)
    _validate_gap_invariants(headerdb.get_header_chain_gaps())

    if last_checkpoint is None:
        # Force canonicalization of chain a by adding a bonus header at the end of
        #   all the chain A headers.
        (subsequent_header, ) = mk_header_chain(chain_a_headers[-1], length=1)
        headerdb.persist_checkpoint_header(
            subsequent_header,
            get_score(genesis_header, chain_a_headers + (subsequent_header, )),
        )

        assert_is_canonical_chain(headerdb,
                                  chain_a_headers + (subsequent_header, ))
    else:
        if headerdb.get_canonical_head() != chain_a_headers[-1]:
            child_headers = mk_header_chain(chain_a_headers[-1], length=1)
            headerdb.persist_header_chain(child_headers)
            assert_is_canonical_chain(headerdb,
                                      chain_a_headers + child_headers)
        else:
            assert_is_canonical_chain(headerdb, chain_a_headers)

    _validate_consecutive_canonical_links(headerdb, chain_length)
    _validate_gap_invariants(headerdb.get_header_chain_gaps())
Example #15
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("eth.chain.chain.Chain")
    gas_estimator = None  # type: Callable

    chaindb_class = ChainDB  # type: Type[BaseChainDB]

    def __init__(self, base_db: BaseDB) -> 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)
        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)

    #
    # VM API
    #
    def get_vm(self, at_header: BlockHeader=None) -> 'BaseVM':
        """
        Returns the VM instance for the given block number.
        """
        header = self.ensure_header(at_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)

    def ensure_header(self, header: BlockHeader=None) -> BlockHeader:
        """
        Return ``header`` if it is not ``None``, otherwise return the header
        of the canonical head.
        """
        if header is None:
            head = self.get_canonical_head()
            return self.create_header_from_parent(head)
        else:
            return header

    #
    # Block API
    #
    def get_ancestors(self, limit: int, header: BlockHeader) -> Tuple[BaseBlock, ...]:
        """
        Return `limit` number of ancestor blocks from the current canonical head.
        """
        ancestor_count = min(header.block_number, limit)

        # We construct a temporary block object
        vm_class = self.get_vm_class_for_block_number(header.block_number)
        block_class = vm_class.get_block_class()
        block = block_class(header=header, uncles=[])

        ancestor_generator = iterate(compose(
            self.get_block_by_hash,
            operator.attrgetter('parent_hash'),
            operator.attrgetter('header'),
        ), block)
        # we peel off the first element from the iterator which will be the
        # temporary block object we constructed.
        next(ancestor_generator)

        return tuple(take(ancestor_count, ancestor_generator))

    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)

    def build_block_with_transactions(self, transactions, parent_header=None):
        """
        Generate a block with the provided transactions. This does *not* import
        that block into your chain. If you want this new block in your chain,
        run :meth:`~import_block` with the result block from this method.

        :param transactions: an iterable of transactions to insert to the block
        :param parent_header: parent of the new block -- or canonical head if ``None``
        :return: (new block, receipts, computations)
        """
        base_header = self.ensure_header(parent_header)
        vm = self.get_vm(base_header)

        new_header, receipts, computations = vm.apply_all_transactions(transactions, base_header)
        new_block = vm.set_block_transactions(vm.block, new_header, transactions, receipts)

        return new_block, receipts, computations

    #
    # 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 get_transaction_result(
            self,
            transaction: Union[BaseTransaction, SpoofTransaction],
            at_header: BlockHeader) -> bytes:
        """
        Return the result of running the given transaction.
        This is referred to as a `call()` in web3.
        """
        with self.get_vm(at_header).state_in_temp_block() as state:
            computation = state.costless_execute_transaction(transaction)

        computation.raise_if_error()
        return computation.output

    def estimate_gas(
            self,
            transaction: Union[BaseTransaction, SpoofTransaction],
            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
                     ) -> Tuple[BaseBlock, Tuple[BaseBlock, ...], Tuple[BaseBlock, ...]]:
        """
        Imports a complete block and returns a 3-tuple

        - the imported block
        - a tuple of blocks which are now part of the canonical chain.
        - a tuple of blocks which are were canonical and now are no longer canonical.
        """

        try:
            parent_header = self.get_block_header_by_hash(block.header.parent_hash)
        except HeaderNotFound:
            raise ValidationError(
                "Attempt to import block #{}.  Cannot import block {} before importing "
                "its parent block at {}".format(
                    block.number,
                    block.hash,
                    block.header.parent_hash,
                )
            )

        base_header_for_import = self.create_header_from_parent(parent_header)
        imported_block = self.get_vm(base_header_for_import).import_block(block)

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

        (
            new_canonical_hashes,
            old_canonical_hashes,
        ) = self.chaindb.persist_block(imported_block)

        self.logger.debug(
            'IMPORTED_BLOCK: number %s | hash %s',
            imported_block.number,
            encode_hex(imported_block.hash),
        )

        new_canonical_blocks = tuple(
            self.get_block_by_hash(header_hash)
            for header_hash
            in new_canonical_hashes
        )
        old_canonical_blocks = tuple(
            self.get_block_by_hash(header_hash)
            for header_hash
            in old_canonical_hashes
        )

        return imported_block, new_canonical_blocks, old_canonical_blocks

    #
    # Validation API
    #
    def validate_receipt(self, receipt: Receipt, at_header: BlockHeader) -> None:
        VM = self.get_vm_class(at_header)
        VM.validate_receipt(receipt)

    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.

        Cannot be used to validate genesis block.
        """
        if block.is_genesis:
            raise ValidationError("Cannot validate genesis block this way")
        VM = self.get_vm_class_for_block_number(BlockNumber(block.number))
        parent_block = self.get_block_by_hash(block.header.parent_hash)
        VM.validate_header(block.header, parent_block.header, check_seal=True)
        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_class_for_block_number(BlockNumber(header.block_number))
        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.
        """
        if block.header.uncles_hash == EMPTY_UNCLE_HASH and len(block.uncles) == 0:
            # optimization to avoid checking ancestors if the block has no uncles
            return

        # 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_class = self.get_vm_class_for_block_number(uncle.block_number)
            uncle_vm_class.validate_uncle(block, uncle, uncle_parent)

    def validate_chain(
            self,
            parent: BlockHeader,
            chain: Tuple[BlockHeader, ...],
            seal_check_random_sample_rate: int = 1) -> None:

        all_indices = list(range(len(chain)))
        if seal_check_random_sample_rate == 1:
            headers_to_check_seal = set(all_indices)
        else:
            sample_size = len(all_indices) // seal_check_random_sample_rate
            headers_to_check_seal = set(random.sample(all_indices, sample_size))

        for i, header in enumerate(chain):
            if header.parent_hash != parent.hash:
                raise ValidationError(
                    "Invalid header chain; {} has parent {}, but expected {}".format(
                        header, header.parent_hash, parent.hash))
            vm_class = self.get_vm_class_for_block_number(header.block_number)
            if i in headers_to_check_seal:
                vm_class.validate_header(header, parent, check_seal=True)
            else:
                vm_class.validate_header(header, parent, check_seal=False)
            parent = header
Example #16
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("eth.chain.chain.Chain")
    gas_estimator: StaticMethod[Callable[[StateAPI, SignedTransactionAPI],
                                         int]] = None

    chaindb_class: Type[ChainDatabaseAPI] = ChainDB

    def __init__(self, base_db: AtomicDatabaseAPI) -> 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)
        if self.gas_estimator is None:
            self.gas_estimator = get_gas_estimator()

    #
    # Helpers
    #
    @classmethod
    def get_chaindb_class(cls) -> Type[ChainDatabaseAPI]:
        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: AtomicDatabaseAPI,
                     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))

        pre_genesis_header = BlockHeader(difficulty=0,
                                         block_number=-1,
                                         gas_limit=0)
        chain_context = ChainContext(cls.chain_id)
        state = genesis_vm_class.build_state(base_db, pre_genesis_header,
                                             chain_context)

        if genesis_state is None:
            genesis_state = {}

        # mutation
        apply_state_dict(state, genesis_state)
        state.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',
                                   state.state_root)
        elif genesis_params['state_root'] != state.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 "
                f"genesis state root.  Got {state.state_root}.  "
                f"Expected {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: AtomicDatabaseAPI,
                            genesis_header: BlockHeaderAPI) -> 'BaseChain':
        """
        Initializes the chain from the genesis header.
        """
        chaindb = cls.get_chaindb_class()(base_db)
        chaindb.persist_header(genesis_header)
        return cls(base_db)

    #
    # VM API
    #
    def get_vm(self, at_header: BlockHeaderAPI = None) -> VirtualMachineAPI:
        """
        Returns the VM instance for the given block number.
        """
        header = self.ensure_header(at_header)
        vm_class = self.get_vm_class_for_block_number(header.block_number)
        chain_context = ChainContext(self.chain_id)
        return vm_class(header=header,
                        chaindb=self.chaindb,
                        chain_context=chain_context)

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

    def get_block_header_by_hash(self, block_hash: Hash32) -> BlockHeaderAPI:
        """
        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) -> BlockHeaderAPI:
        """
        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: Hash32) -> int:
        """
        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)

    def ensure_header(self, header: BlockHeaderAPI = None) -> BlockHeaderAPI:
        """
        Return ``header`` if it is not ``None``, otherwise return the header
        of the canonical head.
        """
        if header is None:
            head = self.get_canonical_head()
            return self.create_header_from_parent(head)
        else:
            return header

    #
    # Block API
    #
    def get_ancestors(self, limit: int,
                      header: BlockHeaderAPI) -> Tuple[BlockAPI, ...]:
        """
        Return `limit` number of ancestor blocks from the current canonical head.
        """
        ancestor_count = min(header.block_number, limit)

        # We construct a temporary block object
        vm_class = self.get_vm_class_for_block_number(header.block_number)
        block_class = vm_class.get_block_class()
        block = block_class(header=header, uncles=[])

        ancestor_generator = iterate(
            compose(
                self.get_block_by_hash,
                operator.attrgetter('parent_hash'),
                operator.attrgetter('header'),
            ), block)
        # we peel off the first element from the iterator which will be the
        # temporary block object we constructed.
        next(ancestor_generator)

        return tuple(take(ancestor_count, ancestor_generator))

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

    def get_block_by_hash(self, block_hash: Hash32) -> BlockAPI:
        """
        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: BlockHeaderAPI) -> BlockAPI:
        """
        Returns the requested block as specified by the block header.
        """
        vm = self.get_vm(block_header)
        return vm.get_block()

    def get_canonical_block_by_number(self,
                                      block_number: BlockNumber) -> BlockAPI:
        """
        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)

    def build_block_with_transactions(
        self,
        transactions: Sequence[SignedTransactionAPI],
        parent_header: BlockHeaderAPI = None
    ) -> Tuple[BlockAPI, Tuple[ReceiptAPI, ...], Tuple[ComputationAPI, ...]]:
        """
        Generate a block with the provided transactions. This does *not* import
        that block into your chain. If you want this new block in your chain,
        run :meth:`~import_block` with the result block from this method.

        :param transactions: an iterable of transactions to insert to the block
        :param parent_header: parent of the new block -- or canonical head if ``None``
        :return: (new block, receipts, computations)
        """
        base_header = self.ensure_header(parent_header)
        vm = self.get_vm(base_header)

        new_header, receipts, computations = vm.apply_all_transactions(
            transactions, base_header)
        new_block = vm.set_block_transactions(vm.get_block(), new_header,
                                              transactions, receipts)

        return new_block, receipts, computations

    #
    # Transaction API
    #
    def get_canonical_transaction(
            self, transaction_hash: Hash32) -> SignedTransactionAPI:
        """
        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_class = self.get_vm_class_for_block_number(block_num)

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

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

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

    def create_unsigned_transaction(self, *, nonce: int, gas_price: int,
                                    gas: int, to: Address, value: int,
                                    data: bytes) -> UnsignedTransactionAPI:
        """
        Passthrough helper to the current VM class.
        """
        return self.get_vm().create_unsigned_transaction(
            nonce=nonce,
            gas_price=gas_price,
            gas=gas,
            to=to,
            value=value,
            data=data,
        )

    def get_transaction_receipt(self, transaction_hash: Hash32) -> ReceiptAPI:
        transaction_block_number, transaction_index = self.chaindb.get_transaction_index(
            transaction_hash, )
        receipt = self.chaindb.get_receipt_by_index(
            block_number=transaction_block_number,
            receipt_index=transaction_index,
        )

        return receipt

    #
    # Execution API
    #
    def get_transaction_result(self, transaction: SignedTransactionAPI,
                               at_header: BlockHeaderAPI) -> bytes:
        """
        Return the result of running the given transaction.
        This is referred to as a `call()` in web3.
        """
        with self.get_vm(at_header).state_in_temp_block() as state:
            computation = state.costless_execute_transaction(transaction)

        computation.raise_if_error()
        return computation.output

    def estimate_gas(self,
                     transaction: SignedTransactionAPI,
                     at_header: BlockHeaderAPI = 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: BlockAPI,
        perform_validation: bool = True
    ) -> Tuple[BlockAPI, Tuple[BlockAPI, ...], Tuple[BlockAPI, ...]]:
        """
        Imports a complete block and returns a 3-tuple

        - the imported block
        - a tuple of blocks which are now part of the canonical chain.
        - a tuple of blocks which were canonical and now are no longer canonical.
        """

        try:
            parent_header = self.get_block_header_by_hash(
                block.header.parent_hash)
        except HeaderNotFound:
            raise ValidationError(
                f"Attempt to import block #{block.number}.  "
                f"Cannot import block {block.hash} before importing "
                f"its parent block at {block.header.parent_hash}")

        base_header_for_import = self.create_header_from_parent(parent_header)
        imported_block = self.get_vm(base_header_for_import).import_block(
            block)

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

        (
            new_canonical_hashes,
            old_canonical_hashes,
        ) = self.chaindb.persist_block(imported_block)

        self.logger.debug(
            'IMPORTED_BLOCK: number %s | hash %s',
            imported_block.number,
            encode_hex(imported_block.hash),
        )

        new_canonical_blocks = tuple(
            self.get_block_by_hash(header_hash)
            for header_hash in new_canonical_hashes)
        old_canonical_blocks = tuple(
            self.get_block_by_hash(header_hash)
            for header_hash in old_canonical_hashes)

        return imported_block, new_canonical_blocks, old_canonical_blocks

    #
    # Validation API
    #
    def validate_receipt(self, receipt: ReceiptAPI,
                         at_header: BlockHeaderAPI) -> None:
        VM_class = self.get_vm_class(at_header)
        VM_class.validate_receipt(receipt)

    def validate_block(self, block: BlockAPI) -> 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.

        Cannot be used to validate genesis block.
        """
        if block.is_genesis:
            raise ValidationError("Cannot validate genesis block this way")
        VM_class = self.get_vm_class_for_block_number(BlockNumber(
            block.number))
        parent_header = self.get_block_header_by_hash(block.header.parent_hash)
        VM_class.validate_header(block.header, parent_header, check_seal=True)
        self.validate_uncles(block)
        self.validate_gaslimit(block.header)

    def validate_seal(self, header: BlockHeaderAPI) -> None:
        """
        Validate the seal on the given header.
        """
        VM_class = self.get_vm_class_for_block_number(
            BlockNumber(header.block_number))
        VM_class.validate_seal(header)

    def validate_gaslimit(self, header: BlockHeaderAPI) -> 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(
                f"The gas limit on block {encode_hex(header.hash)} "
                f"is too low: {header.gas_limit}. "
                f"It must be at least {low_bound}")
        elif header.gas_limit > high_bound:
            raise ValidationError(
                f"The gas limit on block {encode_hex(header.hash)} "
                f"is too high: {header.gas_limit}. "
                f"It must be at most {high_bound}")

    def validate_uncles(self, block: BlockAPI) -> None:
        """
        Validate the uncles for the given block.
        """
        has_uncles = len(block.uncles) > 0
        should_have_uncles = block.header.uncles_hash != EMPTY_UNCLE_HASH

        if not has_uncles and not should_have_uncles:
            # optimization to avoid loading ancestors from DB, since the block has no uncles
            return
        elif has_uncles and not should_have_uncles:
            raise ValidationError(
                "Block has uncles but header suggests uncles should be empty")
        elif should_have_uncles and not has_uncles:
            raise ValidationError(
                "Header suggests block should have uncles but block has none")

        # 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"
                                  f" - {' - '.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(
                    f"Duplicate uncle: {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(
                    f"Uncle {encode_hex(uncle.hash)} cannot be an ancestor "
                    f"of {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(
                    f"Uncle's parent {encode_hex(uncle.parent_hash)} "
                    f"is not an ancestor of {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(
                    f"Uncle ancestor not found: {uncle.parent_hash}")

            uncle_vm_class = self.get_vm_class_for_block_number(
                uncle.block_number)
            uncle_vm_class.validate_uncle(block, uncle, uncle_parent)
Example #17
0
class Chain(BaseChain):
    logger = logging.getLogger("eth.chain.chain.Chain")
    gas_estimator: StaticMethod[Callable[[StateAPI, SignedTransactionAPI], int]] = None

    chaindb_class: Type[ChainDatabaseAPI] = ChainDB
    consensus_context_class: Type[ConsensusContextAPI] = ConsensusContext

    def __init__(self, base_db: AtomicDatabaseAPI) -> 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.consensus_context = self.consensus_context_class(self.chaindb.db)
        self.headerdb = HeaderDB(base_db)
        if self.gas_estimator is None:
            self.gas_estimator = get_gas_estimator()

    #
    # Helpers
    #
    @classmethod
    def get_chaindb_class(cls) -> Type[ChainDatabaseAPI]:
        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: AtomicDatabaseAPI,
                     genesis_params: Dict[str, HeaderParams],
                     genesis_state: AccountState=None) -> 'BaseChain':
        genesis_vm_class = cls.get_vm_class_for_block_number(BlockNumber(0))

        pre_genesis_header = BlockHeader(difficulty=0, block_number=-1, gas_limit=0)
        chain_context = ChainContext(cls.chain_id)
        state = genesis_vm_class.build_state(base_db, pre_genesis_header, chain_context)

        if genesis_state is None:
            genesis_state = {}

        # mutation
        apply_state_dict(state, genesis_state)
        state.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', state.state_root)
        elif genesis_params['state_root'] != state.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 "
                f"genesis state root.  Got {state.state_root!r}.  "
                f"Expected {genesis_params['state_root']!r}"
            )

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

    @classmethod
    def from_genesis_header(cls,
                            base_db: AtomicDatabaseAPI,
                            genesis_header: BlockHeaderAPI) -> 'BaseChain':
        chaindb = cls.get_chaindb_class()(base_db)
        chaindb.persist_header(genesis_header)
        return cls(base_db)

    #
    # VM API
    #
    def get_vm(self, at_header: BlockHeaderAPI = None) -> VirtualMachineAPI:
        header = self.ensure_header(at_header)
        vm_class = self.get_vm_class_for_block_number(header.block_number)
        chain_context = ChainContext(self.chain_id)

        return vm_class(
            header=header,
            chaindb=self.chaindb,
            chain_context=chain_context,
            consensus_context=self.consensus_context
        )

    #
    # Header API
    #
    def create_header_from_parent(self,
                                  parent_header: BlockHeaderAPI,
                                  **header_params: HeaderParams) -> BlockHeaderAPI:
        return self.get_vm_class_for_block_number(
            block_number=BlockNumber(parent_header.block_number + 1),
        ).create_header_from_parent(parent_header, **header_params)

    def get_block_header_by_hash(self, block_hash: Hash32) -> BlockHeaderAPI:
        validate_word(block_hash, title="Block Hash")
        return self.chaindb.get_block_header_by_hash(block_hash)

    def get_canonical_block_header_by_number(self, block_number: BlockNumber) -> BlockHeaderAPI:
        return self.chaindb.get_canonical_block_header_by_number(block_number)

    def get_canonical_head(self) -> BlockHeaderAPI:
        return self.chaindb.get_canonical_head()

    def get_score(self, block_hash: Hash32) -> int:
        return self.headerdb.get_score(block_hash)

    def ensure_header(self, header: BlockHeaderAPI = None) -> BlockHeaderAPI:
        """
        Return ``header`` if it is not ``None``, otherwise return the header
        of the canonical head.
        """
        if header is None:
            head = self.get_canonical_head()
            return self.create_header_from_parent(head)
        else:
            return header

    #
    # Block API
    #
    def get_ancestors(self, limit: int, header: BlockHeaderAPI) -> Tuple[BlockAPI, ...]:
        ancestor_count = min(header.block_number, limit)

        # We construct a temporary block object
        vm_class = self.get_vm_class_for_block_number(header.block_number)
        block_class = vm_class.get_block_class()
        block = block_class(header=header, uncles=[], transactions=[])

        ancestor_generator = iterate(compose(
            self.get_block_by_hash,
            operator.attrgetter('parent_hash'),
            operator.attrgetter('header'),
        ), block)
        # we peel off the first element from the iterator which will be the
        # temporary block object we constructed.
        next(ancestor_generator)

        return tuple(take(ancestor_count, ancestor_generator))

    def get_block(self) -> BlockAPI:
        return self.get_vm().get_block()

    def get_block_by_hash(self, block_hash: Hash32) -> BlockAPI:
        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: BlockHeaderAPI) -> BlockAPI:
        vm = self.get_vm(block_header)
        return vm.get_block()

    def get_canonical_block_by_number(self, block_number: BlockNumber) -> BlockAPI:
        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:
        return self.chaindb.get_canonical_block_hash(block_number)

    def build_block_with_transactions(
            self,
            transactions: Sequence[SignedTransactionAPI],
            parent_header: BlockHeaderAPI = None
    ) -> Tuple[BlockAPI, Tuple[ReceiptAPI, ...], Tuple[ComputationAPI, ...]]:
        base_header = self.ensure_header(parent_header)
        vm = self.get_vm(base_header)

        new_header, receipts, computations = vm.apply_all_transactions(transactions, base_header)
        new_block = vm.set_block_transactions(vm.get_block(), new_header, transactions, receipts)

        return new_block, receipts, computations

    #
    # Transaction API
    #
    def get_canonical_transaction_index(self, transaction_hash: Hash32) -> Tuple[BlockNumber, int]:
        return self.chaindb.get_transaction_index(transaction_hash)

    def get_canonical_transaction(self, transaction_hash: Hash32) -> SignedTransactionAPI:
        (block_num, index) = self.chaindb.get_transaction_index(transaction_hash)

        transaction = self.get_canonical_transaction_by_index(block_num, index)

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

    def get_canonical_transaction_by_index(self,
                                           block_number: BlockNumber,
                                           index: int) -> SignedTransactionAPI:

        VM_class = self.get_vm_class_for_block_number(block_number)

        return self.chaindb.get_transaction_by_index(
            block_number,
            index,
            VM_class.get_transaction_class(),
        )

    def create_transaction(self, *args: Any, **kwargs: Any) -> SignedTransactionAPI:
        return self.get_vm().create_transaction(*args, **kwargs)

    def create_unsigned_transaction(self,
                                    *,
                                    nonce: int,
                                    gas_price: int,
                                    gas: int,
                                    to: Address,
                                    value: int,
                                    data: bytes) -> UnsignedTransactionAPI:
        return self.get_vm().create_unsigned_transaction(
            nonce=nonce,
            gas_price=gas_price,
            gas=gas,
            to=to,
            value=value,
            data=data,
        )

    def get_transaction_receipt(self, transaction_hash: Hash32) -> ReceiptAPI:
        transaction_block_number, transaction_index = self.chaindb.get_transaction_index(
            transaction_hash,
        )
        return self.get_transaction_receipt_by_index(transaction_block_number, transaction_index)

    def get_transaction_receipt_by_index(self,
                                         block_number: BlockNumber,
                                         index: int) -> ReceiptAPI:

        receipt = self.chaindb.get_receipt_by_index(
            block_number=block_number,
            receipt_index=index,
        )

        return receipt

    #
    # Execution API
    #
    def get_transaction_result(
            self,
            transaction: SignedTransactionAPI,
            at_header: BlockHeaderAPI) -> bytes:

        with self.get_vm(at_header).state_in_temp_block() as state:
            computation = state.costless_execute_transaction(transaction)

        computation.raise_if_error()
        return computation.output

    def estimate_gas(
            self,
            transaction: SignedTransactionAPI,
            at_header: BlockHeaderAPI = None) -> int:
        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: BlockAPI,
                     perform_validation: bool=True
                     ) -> BlockImportResult:

        try:
            parent_header = self.get_block_header_by_hash(block.header.parent_hash)
        except HeaderNotFound:
            raise ValidationError(
                f"Attempt to import block #{block.number}.  "
                f"Cannot import block {block.hash!r} before importing "
                f"its parent block at {block.header.parent_hash!r}"
            )

        base_header_for_import = self.create_header_from_parent(parent_header)
        # Make a copy of the empty header, adding in the expected amount of gas used. This
        #   allows for richer logging in the VM.
        annotated_header = base_header_for_import.copy(gas_used=block.header.gas_used)
        block_result = self.get_vm(annotated_header).import_block(block)
        imported_block = block_result.block

        # Validate the imported block.
        if perform_validation:
            try:
                validate_imported_block_unchanged(imported_block, block)
            except ValidationError:
                self.logger.warning("Proposed %s doesn't follow EVM rules, rejecting...", block)
                raise
            self.validate_block(imported_block)

        (
            new_canonical_hashes,
            old_canonical_hashes,
        ) = self.chaindb.persist_block(imported_block)

        self.logger.debug(
            'IMPORTED_BLOCK: number %s | hash %s',
            imported_block.number,
            encode_hex(imported_block.hash),
        )

        new_canonical_blocks = tuple(
            self.get_block_by_hash(header_hash)
            for header_hash
            in new_canonical_hashes
        )
        old_canonical_blocks = tuple(
            self.get_block_by_hash(header_hash)
            for header_hash
            in old_canonical_hashes
        )

        return BlockImportResult(
            imported_block=imported_block,
            new_canonical_blocks=new_canonical_blocks,
            old_canonical_blocks=old_canonical_blocks,
            meta_witness=block_result.meta_witness,
        )

    #
    # Validation API
    #
    def validate_receipt(self, receipt: ReceiptAPI, at_header: BlockHeaderAPI) -> None:
        VM_class = self.get_vm_class(at_header)
        VM_class.validate_receipt(receipt)

    def validate_block(self, block: BlockAPI) -> None:
        if block.is_genesis:
            raise ValidationError("Cannot validate genesis block this way")
        vm = self.get_vm(block.header)
        parent_header = self.get_block_header_by_hash(block.header.parent_hash)
        vm.validate_header(block.header, parent_header)
        vm.validate_seal(block.header)
        vm.validate_seal_extension(block.header, ())
        self.validate_uncles(block)
        self.validate_gaslimit(block.header)

    def validate_seal(self, header: BlockHeaderAPI) -> None:
        vm = self.get_vm(header)
        vm.validate_seal(header)

    def validate_gaslimit(self, header: BlockHeaderAPI) -> None:
        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(
                f"The gas limit on block {encode_hex(header.hash)} "
                f"is too low: {header.gas_limit}. "
                f"It must be at least {low_bound}"
            )
        elif header.gas_limit > high_bound:
            raise ValidationError(
                f"The gas limit on block {encode_hex(header.hash)} "
                f"is too high: {header.gas_limit}. "
                f"It must be at most {high_bound}"
            )

    def validate_uncles(self, block: BlockAPI) -> None:
        has_uncles = len(block.uncles) > 0
        should_have_uncles = block.header.uncles_hash != EMPTY_UNCLE_HASH

        if not has_uncles and not should_have_uncles:
            # optimization to avoid loading ancestors from DB, since the block has no uncles
            return
        elif has_uncles and not should_have_uncles:
            raise ValidationError("Block has uncles but header suggests uncles should be empty")
        elif should_have_uncles and not has_uncles:
            raise ValidationError("Header suggests block should have uncles but block has none")

        # 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"
                f" - {' - '.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(
                    f"Duplicate uncle: {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(
                    f"Uncle {encode_hex(uncle.hash)} cannot be an ancestor "
                    f"of {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(
                    f"Uncle's parent {encode_hex(uncle.parent_hash)} "
                    f"is not an ancestor of {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(
                    f"Uncle ancestor not found: {uncle.parent_hash!r}"
                )

            uncle_vm_class = self.get_vm_class_for_block_number(uncle.block_number)
            uncle_vm_class.validate_uncle(block, uncle, uncle_parent)
Example #18
0
def headerdb(base_db):
    return HeaderDB(base_db)