def create_or_update_from_tx_hashes(
            self, tx_hashes: List[Union[str, bytes]]) -> List['EthereumTx']:
        # Search first in database
        ethereum_txs_dict = OrderedDict.fromkeys(
            [HexBytes(tx_hash).hex() for tx_hash in tx_hashes])
        db_ethereum_txs = self.filter(tx_hash__in=tx_hashes).exclude(
            block=None)
        for db_ethereum_tx in db_ethereum_txs:
            ethereum_txs_dict[db_ethereum_tx.tx_hash] = db_ethereum_tx

        # Retrieve from the node the txs missing from database
        tx_hashes_not_in_db = [
            tx_hash for tx_hash, ethereum_tx in ethereum_txs_dict.items()
            if not ethereum_tx
        ]
        if not tx_hashes_not_in_db:
            return list(ethereum_txs_dict.values())

        ethereum_client = EthereumClientProvider()

        tx_receipts = []
        for tx_hash, tx_receipt in zip(
                tx_hashes_not_in_db,
                ethereum_client.get_transaction_receipts(tx_hashes_not_in_db)):
            if not tx_receipt:
                tx_receipt = ethereum_client.get_transaction_receipt(
                    tx_hash)  # Retry fetching

            if not tx_receipt:
                raise TransactionNotFoundException(
                    f'Cannot find tx with tx-hash={HexBytes(tx_hash).hex()}')
            elif tx_receipt.get('blockNumber') is None:
                raise TransactionWithoutBlockException(
                    f'Cannot find block for tx with tx-hash={HexBytes(tx_hash).hex()}'
                )
            else:
                tx_receipts.append(tx_receipt)

        txs = ethereum_client.get_transactions(tx_hashes_not_in_db)
        block_numbers = []
        for tx_hash, tx in zip(tx_hashes_not_in_db, txs):
            if not tx:
                raise TransactionNotFoundException(
                    f'Cannot find tx with tx-hash={HexBytes(tx_hash).hex()}')
            elif tx.get('blockNumber') is None:
                raise TransactionWithoutBlockException(
                    f'Cannot find block for tx with tx-hash={HexBytes(tx_hash).hex()}'
                )
            block_numbers.append(tx['blockNumber'])

        blocks = ethereum_client.get_blocks(block_numbers)

        current_block_number = ethereum_client.current_block_number
        for tx, tx_receipt, block in zip(txs, tx_receipts, blocks):
            ethereum_block = EthereumBlock.objects.get_or_create_from_block(
                block, current_block_number=current_block_number)
            try:
                ethereum_tx = self.get(tx_hash=tx['hash'])
                # For txs stored before being mined
                if ethereum_tx.block is None:
                    ethereum_tx.block = ethereum_block
                    ethereum_tx.gas_used = tx_receipt['gasUsed']
                    ethereum_tx.status = tx_receipt.get('status')
                    ethereum_tx.transaction_index = tx_receipt[
                        'transactionIndex']
                    ethereum_tx.save(update_fields=[
                        'block', 'gas_used', 'status', 'transaction_index'
                    ])
                ethereum_txs_dict[HexBytes(
                    ethereum_tx.tx_hash).hex()] = ethereum_tx
            except self.model.DoesNotExist:
                ethereum_tx = self.create_from_tx_dict(
                    tx, tx_receipt=tx_receipt, ethereum_block=ethereum_block)
                ethereum_txs_dict[HexBytes(
                    ethereum_tx.tx_hash).hex()] = ethereum_tx
        return list(ethereum_txs_dict.values())
class IndexService:
    def __init__(self, ethereum_client: EthereumClient, eth_reorg_blocks: int):
        self.ethereum_client = ethereum_client
        self.eth_reorg_blocks = eth_reorg_blocks

    def block_get_or_create_from_block_number(self, block_number: int):
        try:
            return EthereumBlock.get(number=block_number)
        except EthereumBlock.DoesNotExist:
            current_block_number = self.ethereum_client.current_block_number  # For reorgs
            block = self.ethereum_client.get_block(block_number)
            confirmed = (current_block_number - block['number']) >= self.eth_reorg_blocks
            return EthereumBlock.objects.create_from_block(block, cofirmed=confirmed)

    def tx_create_or_update_from_tx_hash(self, tx_hash: str) -> 'EthereumTx':
        try:
            ethereum_tx = EthereumTx.objects.get(tx_hash=tx_hash)
            # For txs stored before being mined
            if ethereum_tx.block is None:
                tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash)
                ethereum_block = self.block_get_or_create_from_block_number(tx_receipt['blockNumber'])
                ethereum_tx.update_with_block_and_receipt(ethereum_block, tx_receipt)
            return ethereum_tx
        except EthereumTx.DoesNotExist:
            tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash)
            ethereum_block = self.block_get_or_create_from_block_number(tx_receipt['blockNumber'])
            tx = self.ethereum_client.get_transaction(tx_hash)
            return EthereumTx.objects.create_from_tx_dict(tx, tx_receipt=tx_receipt, ethereum_block=ethereum_block)

    def txs_create_or_update_from_tx_hashes(self, tx_hashes: List[Union[str, bytes]]) -> List['EthereumTx']:
        # Search first in database
        ethereum_txs_dict = OrderedDict.fromkeys([HexBytes(tx_hash).hex() for tx_hash in tx_hashes])
        db_ethereum_txs = EthereumTx.objects.filter(tx_hash__in=tx_hashes).exclude(block=None)
        for db_ethereum_tx in db_ethereum_txs:
            ethereum_txs_dict[db_ethereum_tx.tx_hash] = db_ethereum_tx

        # Retrieve from the node the txs missing from database
        tx_hashes_not_in_db = [tx_hash for tx_hash, ethereum_tx in ethereum_txs_dict.items() if not ethereum_tx]
        if not tx_hashes_not_in_db:
            return list(ethereum_txs_dict.values())

        self.ethereum_client = EthereumClientProvider()

        # Get receipts for hashes not in db
        tx_receipts = []
        for tx_hash, tx_receipt in zip(tx_hashes_not_in_db,
                                       self.ethereum_client.get_transaction_receipts(tx_hashes_not_in_db)):
            tx_receipt = tx_receipt or self.ethereum_client.get_transaction_receipt(tx_hash)  # Retry fetching if failed
            if not tx_receipt:
                raise TransactionNotFoundException(f'Cannot find tx-receipt with tx-hash={HexBytes(tx_hash).hex()}')
            elif tx_receipt.get('blockNumber') is None:
                raise TransactionWithoutBlockException(f'Cannot find blockNumber for tx-receipt with '
                                                       f'tx-hash={HexBytes(tx_hash).hex()}')
            else:
                tx_receipts.append(tx_receipt)

        # Get transactions for hashes not in db
        txs = self.ethereum_client.get_transactions(tx_hashes_not_in_db)
        block_numbers = set()
        for tx_hash, tx in zip(tx_hashes_not_in_db, txs):
            tx = tx or self.ethereum_client.get_transaction(tx_hash)  # Retry fetching if failed
            if not tx:
                raise TransactionNotFoundException(f'Cannot find tx with tx-hash={HexBytes(tx_hash).hex()}')
            elif tx.get('blockNumber') is None:
                raise TransactionWithoutBlockException(f'Cannot find blockNumber for tx with '
                                                       f'tx-hash={HexBytes(tx_hash).hex()}')
            block_numbers.add(tx['blockNumber'])

        blocks = self.ethereum_client.get_blocks(block_numbers)
        block_dict = {}
        for block_number, block in zip(block_numbers, blocks):
            block = block or self.ethereum_client.get_block(block_number)  # Retry fetching if failed
            if not block:
                raise BlockNotFoundException(f'Block with number={block_number} was not found')
            assert block_number == block['number']
            block_dict[block['number']] = block

        # Create new transactions or update them if they have no receipt
        current_block_number = self.ethereum_client.current_block_number
        for tx, tx_receipt in zip(txs, tx_receipts):
            block = block_dict.get(tx['blockNumber'])
            confirmed = (current_block_number - block['number']) >= self.eth_reorg_blocks
            ethereum_block: EthereumBlock = EthereumBlock.objects.get_or_create_from_block(block, confirmed=confirmed)
            if HexBytes(ethereum_block.block_hash) != block['hash']:
                raise EthereumBlockHashMismatch(f'Stored block={ethereum_block.number} '
                                                f'with hash={ethereum_block.block_hash} '
                                                f'is not marching retrieved hash={block["hash"].hex()}')
            try:
                ethereum_tx = EthereumTx.objects.get(tx_hash=tx['hash'])
                # For txs stored before being mined
                ethereum_tx.update_with_block_and_receipt(ethereum_block, tx_receipt)
                ethereum_txs_dict[ethereum_tx.tx_hash] = ethereum_tx
            except EthereumTx.DoesNotExist:
                ethereum_tx = EthereumTx.objects.create_from_tx_dict(tx,
                                                                     tx_receipt=tx_receipt,
                                                                     ethereum_block=ethereum_block)
                ethereum_txs_dict[HexBytes(ethereum_tx.tx_hash).hex()] = ethereum_tx
        return list(ethereum_txs_dict.values())

    @transaction.atomic
    def reindex_addresses(self, addresses: List[str]) -> NoReturn:
        """
        Given a list of safe addresses it will delete all `SafeStatus`, conflicting `MultisigTxs` and will mark
        every `InternalTxDecoded` not processed to be processed again
        :param addresses: List of checksummed addresses or queryset
        :return: Number of `SafeStatus` deleted
        """
        if not addresses:
            return

        SafeStatus.objects.filter(address__in=addresses).delete()
        MultisigTransaction.objects.exclude(
            ethereum_tx=None
        ).filter(
            safe__in=addresses
        ).delete()  # Remove not indexed transactions
        InternalTxDecoded.objects.filter(internal_tx___from__in=addresses).update(processed=False)

    @transaction.atomic
    def reindex_all(self) -> NoReturn:
        MultisigConfirmation.objects.filter(signature=None).delete()  # Remove onchain confirmations
        MultisigTransaction.objects.exclude(ethereum_tx=None).delete()  # Remove not indexed transactions
        SafeStatus.objects.all().delete()
        InternalTxDecoded.objects.update(processed=False)
Exemple #3
0
class IndexService:
    def __init__(
        self,
        ethereum_client: EthereumClient,
        eth_reorg_blocks: int,
        eth_l2_network: bool,
        alert_out_of_sync_events_threshold: float,
    ):
        self.ethereum_client = ethereum_client
        self.eth_reorg_blocks = eth_reorg_blocks
        self.eth_l2_network = eth_l2_network
        self.alert_out_of_sync_events_threshold = alert_out_of_sync_events_threshold

    def block_get_or_create_from_block_hash(self, block_hash: int):
        try:
            return EthereumBlock.objects.get(block_hash=block_hash)
        except EthereumBlock.DoesNotExist:
            current_block_number = (self.ethereum_client.current_block_number
                                    )  # For reorgs
            block = self.ethereum_client.get_block(block_hash)
            confirmed = (current_block_number -
                         block["number"]) >= self.eth_reorg_blocks
            return EthereumBlock.objects.get_or_create_from_block(
                block, confirmed=confirmed)

    def is_service_synced(self) -> bool:
        """
        :return: `True` if master copies and ERC20/721 are synced, `False` otherwise
        """

        # Use number of reorg blocks to consider as not synced
        reference_block_number = (self.ethereum_client.current_block_number -
                                  self.eth_reorg_blocks)
        synced = True
        for safe_master_copy in SafeMasterCopy.objects.relevant().filter(
                tx_block_number__lt=reference_block_number):
            logger.error("Master Copy %s is out of sync",
                         safe_master_copy.address)
            synced = False

        out_of_sync_contracts = SafeContract.objects.filter(
            erc20_block_number__lt=reference_block_number).count()
        if out_of_sync_contracts > 0:
            total_number_of_contracts = SafeContract.objects.all().count()
            proportion_out_of_sync = out_of_sync_contracts / total_number_of_contracts
            # Ignore less than 10% of contracts out of sync
            if proportion_out_of_sync >= self.alert_out_of_sync_events_threshold:
                logger.error(
                    "%d Safe Contracts have ERC20/721 out of sync",
                    out_of_sync_contracts,
                )
                synced = False

        return synced

    def tx_create_or_update_from_tx_hash(self, tx_hash: str) -> "EthereumTx":
        try:
            ethereum_tx = EthereumTx.objects.get(tx_hash=tx_hash)
            # For txs stored before being mined
            if ethereum_tx.block is None:
                tx_receipt = self.ethereum_client.get_transaction_receipt(
                    tx_hash)
                ethereum_block = self.block_get_or_create_from_block_hash(
                    tx_receipt["blockHash"])
                ethereum_tx.update_with_block_and_receipt(
                    ethereum_block, tx_receipt)
            return ethereum_tx
        except EthereumTx.DoesNotExist:
            tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash)
            ethereum_block = self.block_get_or_create_from_block_hash(
                tx_receipt["blockHash"])
            tx = self.ethereum_client.get_transaction(tx_hash)
            return EthereumTx.objects.create_from_tx_dict(
                tx, tx_receipt=tx_receipt, ethereum_block=ethereum_block)

    def txs_create_or_update_from_tx_hashes(
            self, tx_hashes: Collection[Union[str,
                                              bytes]]) -> List["EthereumTx"]:
        # Search first in database
        ethereum_txs_dict = OrderedDict.fromkeys(
            [HexBytes(tx_hash).hex() for tx_hash in tx_hashes])
        db_ethereum_txs = EthereumTx.objects.filter(
            tx_hash__in=tx_hashes).exclude(block=None)
        for db_ethereum_tx in db_ethereum_txs:
            ethereum_txs_dict[db_ethereum_tx.tx_hash] = db_ethereum_tx

        # Retrieve from the node the txs missing from database
        tx_hashes_not_in_db = [
            tx_hash for tx_hash, ethereum_tx in ethereum_txs_dict.items()
            if not ethereum_tx
        ]
        if not tx_hashes_not_in_db:
            return list(ethereum_txs_dict.values())

        self.ethereum_client = EthereumClientProvider()

        # Get receipts for hashes not in db
        tx_receipts = []
        for tx_hash, tx_receipt in zip(
                tx_hashes_not_in_db,
                self.ethereum_client.get_transaction_receipts(
                    tx_hashes_not_in_db),
        ):
            tx_receipt = tx_receipt or self.ethereum_client.get_transaction_receipt(
                tx_hash)  # Retry fetching if failed
            if not tx_receipt:
                raise TransactionNotFoundException(
                    f"Cannot find tx-receipt with tx-hash={HexBytes(tx_hash).hex()}"
                )

            if tx_receipt.get("blockHash") is None:
                raise TransactionWithoutBlockException(
                    f"Cannot find blockHash for tx-receipt with "
                    f"tx-hash={HexBytes(tx_hash).hex()}")

            tx_receipts.append(tx_receipt)

        # Get transactions for hashes not in db
        fetched_txs = self.ethereum_client.get_transactions(
            tx_hashes_not_in_db)
        block_hashes = set()
        txs = []
        for tx_hash, tx in zip(tx_hashes_not_in_db, fetched_txs):
            tx = tx or self.ethereum_client.get_transaction(
                tx_hash)  # Retry fetching if failed
            if not tx:
                raise TransactionNotFoundException(
                    f"Cannot find tx with tx-hash={HexBytes(tx_hash).hex()}")

            if tx.get("blockHash") is None:
                raise TransactionWithoutBlockException(
                    f"Cannot find blockHash for tx with "
                    f"tx-hash={HexBytes(tx_hash).hex()}")

            block_hashes.add(tx["blockHash"].hex())
            txs.append(tx)

        blocks = self.ethereum_client.get_blocks(block_hashes)
        block_dict = {}
        for block_hash, block in zip(block_hashes, blocks):
            block = block or self.ethereum_client.get_block(
                block_hash)  # Retry fetching if failed
            if not block:
                raise BlockNotFoundException(
                    f"Block with hash={block_hash} was not found")
            assert block_hash == block["hash"].hex()
            block_dict[block["hash"]] = block

        # Create new transactions or update them if they have no receipt
        current_block_number = self.ethereum_client.current_block_number
        for tx, tx_receipt in zip(txs, tx_receipts):
            block = block_dict[tx["blockHash"]]
            confirmed = (current_block_number -
                         block["number"]) >= self.eth_reorg_blocks
            ethereum_block: EthereumBlock = (
                EthereumBlock.objects.get_or_create_from_block(
                    block, confirmed=confirmed))
            try:
                with transaction.atomic():
                    ethereum_tx = EthereumTx.objects.create_from_tx_dict(
                        tx,
                        tx_receipt=tx_receipt,
                        ethereum_block=ethereum_block)
                ethereum_txs_dict[HexBytes(
                    ethereum_tx.tx_hash).hex()] = ethereum_tx
            except IntegrityError:  # Tx exists
                ethereum_tx = EthereumTx.objects.get(tx_hash=tx["hash"])
                # For txs stored before being mined
                ethereum_tx.update_with_block_and_receipt(
                    ethereum_block, tx_receipt)
                ethereum_txs_dict[ethereum_tx.tx_hash] = ethereum_tx
        return list(ethereum_txs_dict.values())

    @transaction.atomic
    def _reprocess(self, addresses: List[str]):
        """
        Trigger processing of traces again. If addresses is empty, everything is reprocessed

        :param addresses:
        :return:
        """
        queryset = MultisigConfirmation.objects.filter(signature=None)
        if not addresses:
            logger.info("Remove onchain confirmations")
            queryset.delete()

        logger.info("Remove transactions automatically indexed")
        queryset = MultisigTransaction.objects.exclude(
            ethereum_tx=None).filter(Q(origin=None) | Q(origin=""))
        if addresses:
            queryset = queryset.filter(safe__in=addresses)
        queryset.delete()

        logger.info("Remove module transactions")
        queryset = ModuleTransaction.objects.all()
        if addresses:
            queryset = queryset.filter(safe__in=addresses)
        queryset.delete()

        logger.info("Remove Safe statuses")

        queryset = SafeStatus.objects.all()
        if addresses:
            queryset = queryset.filter(address__in=addresses)
        queryset.delete()

        logger.info("Mark all internal txs decoded as not processed")
        queryset = InternalTxDecoded.objects.all()
        if addresses:
            queryset = queryset.filter(internal_tx___from__in=addresses)
        queryset.update(processed=False)

    def reprocess_addresses(self, addresses: List[str]):
        """
        Given a list of safe addresses it will delete all `SafeStatus`, conflicting `MultisigTxs` and will mark
        every `InternalTxDecoded` not processed to be processed again

        :param addresses: List of checksummed addresses or queryset
        :return: Number of `SafeStatus` deleted
        """
        if not addresses:
            return None

        return self._reprocess(addresses)

    def reprocess_all(self):
        return self._reprocess(None)

    def reindex_master_copies(
        self,
        from_block_number: int,
        to_block_number: Optional[int] = None,
        block_process_limit: int = 100,
        addresses: Optional[ChecksumAddress] = None,
    ):
        """
        Reindexes master copies in parallel with the current running indexer, so service will have no missing txs
        while reindexing

        :param from_block_number: Block number to start indexing from
        :param to_block_number: Block number to stop indexing on
        :param block_process_limit: Number of blocks to process each time
        :param addresses: Master Copy or Safes(for L2 event processing) addresses. If not provided,
            all master copies will be used
        """
        assert (not to_block_number) or to_block_number > from_block_number

        from ..indexers import (
            EthereumIndexer,
            InternalTxIndexerProvider,
            SafeEventsIndexerProvider,
        )

        indexer_provider = (SafeEventsIndexerProvider if self.eth_l2_network
                            else InternalTxIndexerProvider)
        indexer: EthereumIndexer = indexer_provider()
        ethereum_client = EthereumClientProvider()

        if addresses:
            indexer.IGNORE_ADDRESSES_ON_LOG_FILTER = (
                False  # Just process addresses provided
            )
        else:
            addresses = list(
                indexer.database_queryset.values_list("address", flat=True))

        if not addresses:
            logger.warning("No addresses to process")
        else:
            logger.info("Start reindexing Safe Master Copy addresses %s",
                        addresses)
            current_block_number = ethereum_client.current_block_number
            stop_block_number = (min(current_block_number, to_block_number)
                                 if to_block_number else current_block_number)
            block_number = from_block_number
            while block_number < stop_block_number:
                elements = indexer.find_relevant_elements(
                    addresses, block_number,
                    block_number + block_process_limit)
                indexer.process_elements(elements)
                block_number += block_process_limit
                logger.info(
                    "Current block number %d, found %d traces/events",
                    block_number,
                    len(elements),
                )

            logger.info("End reindexing addresses %s", addresses)