Ejemplo n.º 1
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)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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
Ejemplo n.º 4
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)
Ejemplo n.º 5
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)