Example #1
0
    async def __handle_new_bounty(self, guid, artifact_type, author, amount,
                                  uri, expiration, metadata, block_number,
                                  txhash, chain):
        """Scan and assert on a posted bounty

        Args:
            guid (str): The bounty to assert on
            artifact_type (ArtifactType): The type of artifacts in this bounty
            author (str): The bounty author
            amount (str): Amount of the bounty in base NCT units (10 ^ -18)
            uri (str): IPFS hash of the root artifact
            expiration (str): Block number of the bounty's expiration
            metadata (dict): Dictionary of metadata or None
            block_number (int): Block number the bounty was posted on
            txhash (str): Transaction hash which caused the event
            chain (str): Is this on the home or side chain?
        Returns:
            Response JSON parsed from polyswarmd containing placed assertions
        """
        # Skip bounties for types we don't support
        if artifact_type not in self.valid_artifact_types:
            return []

        async with self.bounties_pending_locks[chain]:
            bounties_pending = self.bounties_pending.get(chain, set())
            if guid in bounties_pending:
                logger.debug('Bounty %s already seen, not responding', guid)
                return []
            self.bounties_pending[chain] = bounties_pending | {guid}

        self.bounties_seen += 1
        if self.testing > 0:
            if self.bounties_seen > self.testing:
                logger.info(
                    'Received new bounty, but finished with testing mode')
                return []
            logger.info('Testing mode, %s bounties remaining',
                        self.testing - self.bounties_seen)

        expiration = int(expiration)
        assertion_reveal_window = await self.client.bounties.parameters[
            chain].get('assertion_reveal_window')
        arbiter_vote_window = await self.client.bounties.parameters[chain].get(
            'arbiter_vote_window')

        vote_start = expiration + assertion_reveal_window
        settle_start = expiration + assertion_reveal_window + arbiter_vote_window
        duration = settle_start - block_number

        await self.client.liveness_recorder.add_waiting_task(
            guid, block_number)
        results = await self.fetch_and_scan_all(guid, artifact_type, uri,
                                                duration, metadata, chain)
        votes = [result.verdict for result in results]

        bounty = await self.client.bounties.get_bounty(guid, chain)
        if bounty is None:
            logger.error('Unable to get retrieve new bounty')
            await self.client.liveness_recorder.remove_waiting_task(guid)
            return []

        bloom_parts = await self.client.bounties.get_bloom(guid, chain)
        bounty_bloom = 0
        for b in bloom_parts:
            bounty_bloom = bounty_bloom << 256 | int(b)

        calculated_bloom = await self.client.bounties.calculate_bloom(uri)
        valid_bloom = bounty and bounty_bloom == calculated_bloom

        vb = VoteOnBounty(guid, votes, valid_bloom)
        self.client.schedule(vote_start, vb, chain)

        sb = SettleBounty(guid)
        self.client.schedule(settle_start, sb, chain)

        return []
    async def __handle_new_bounty(self, guid, artifact_type, author, amount,
                                  uri, expiration, metadata, block_number,
                                  txhash, chain):
        """Scan and assert on a posted bounty

        Args:
            guid (str): The bounty to assert on
            artifact_type (ArtifactType): The type of artifacts in this bounty
            author (str): The bounty author
            amount (str): Amount of the bounty in base NCT units (10 ^ -18)
            uri (str): IPFS hash of the root artifact
            expiration (str): Block number of the bounty's expiration
            metadata (dict): Dictionary of metadata or None
            block_number (int): Block number the bounty was placed on
            txhash (str): Transaction hash which caused the event
            chain (str): Is this on the home or side chain?

        Returns:
            Response JSON parsed from polyswarmd containing placed assertions
        """
        # Skip bounties for types we don't support
        if artifact_type not in self.valid_artifact_types:
            logger.info('Bounty artifact type %s is not supported',
                        artifact_type)
            return []

        async with self.bounties_pending_locks[chain]:
            bounties_pending = self.bounties_pending.get(chain, set())
            if guid in bounties_pending:
                logger.debug('Bounty %s already seen, not responding', guid)
                return []
            self.bounties_pending[chain] = bounties_pending | {guid}

        self.bounties_seen += 1
        if self.testing > 0:
            if self.bounties_seen > self.testing:
                logger.warning(
                    'Received new bounty, but finished with testing mode')
                return []
            logger.info('Testing mode, %s  bounties remaining',
                        self.testing - self.bounties_seen)

        expiration = int(expiration)
        duration = expiration - block_number

        await self.client.liveness_recorder.add_waiting_task(
            guid, block_number)
        results = await self.fetch_and_scan_all(guid, artifact_type, uri,
                                                duration, metadata, chain)
        mask = [r.bit for r in results]
        verdicts = [r.verdict for r in results]
        confidences = [r.confidence for r in results]
        metadatas = [r.metadata for r in results]
        combined_metadata = ';'.join(metadatas)

        try:
            if all([
                    metadata and verdict.Verdict.validate(json.loads(metadata))
                    for metadata in metadatas
            ]):
                combined_metadata = json.dumps(
                    [json.loads(metadata) for metadata in metadatas])
        except json.JSONDecodeError:
            logger.exception('Error decoding assertion metadata %s', metadatas)

        if not any(mask):
            await self.client.liveness_recorder.remove_waiting_task(guid)
            return []

        assertion_fee = await self.client.bounties.parameters[chain].get(
            'assertion_fee')
        assertion_reveal_window = await self.client.bounties.parameters[
            chain].get('assertion_reveal_window')
        arbiter_vote_window = await self.client.bounties.parameters[chain].get(
            'arbiter_vote_window')

        bid = await self.bid(guid, mask, verdicts, confidences, metadatas,
                             chain)
        # Check that microengine has sufficient balance to handle the assertion
        balance = await self.client.balances.get_nct_balance(chain)
        if balance < assertion_fee + sum(bid):
            logger.critical(
                'Insufficient balance to post assertion for bounty on %s. Have %s NCT. Need %s NCT',
                chain,
                balance,
                assertion_fee + sum(bid),
                extra={'extra': guid})
            if self.testing > 0:
                exit(1)

            await self.client.liveness_recorder.remove_waiting_task(guid)
            return []

        logger.info('Responding to %s bounty %s', artifact_type.name.lower(),
                    guid)
        nonce, assertions = await self.client.bounties.post_assertion(
            guid, bid, mask, verdicts, chain)
        await self.client.liveness_recorder.remove_waiting_task(guid)
        for a in assertions:
            # Post metadata to IPFS and post ipfs_hash as metadata, if it exists
            ipfs_hash = await self.client.bounties.post_metadata(
                combined_metadata, chain)
            metadata = ipfs_hash if ipfs_hash is not None else combined_metadata
            ra = RevealAssertion(guid, a['index'], nonce, verdicts, metadata)
            self.client.schedule(expiration, ra, chain)

            sb = SettleBounty(guid)
            self.client.schedule(
                expiration + assertion_reveal_window + arbiter_vote_window, sb,
                chain)

        return assertions
    async def submit_bounty(self, bounty, chain):
        """Submit a bounty in a new task

        Args:
            bounty (QueuedBounty): Bounty to submit
            chain: Name of the chain to post to
        """
        async with self.client.liveness_recorder.waiting_task(
                bounty.ipfs_uri, self.last_block):
            bounty_fee = await self.client.bounties.parameters[chain].get(
                'bounty_fee')
            try:
                await self.client.balances.raise_for_low_balance(
                    bounty.amount + bounty_fee, chain)
            except LowBalanceError as e:
                await self.on_bounty_post_failed(bounty.artifact_type,
                                                 bounty.amount,
                                                 bounty.ipfs_uri,
                                                 bounty.duration,
                                                 chain,
                                                 metadata=bounty.metadata)
                self.bounty_queues[chain].task_done()
                self.bounty_semaphores[chain].release()
                if self.client.tx_error_fatal:
                    raise FatalError(
                        'Failed to post bounty due to low balance') from e
                else:
                    return

            assertion_reveal_window = await self.client.bounties.parameters[
                chain].get('assertion_reveal_window')
            arbiter_vote_window = await self.client.bounties.parameters[
                chain].get('arbiter_vote_window')
            metadata = None
            if bounty.metadata is not None:
                metadata = await self.client.bounties.post_metadata(
                    bounty.metadata, chain)

            await self.on_before_bounty_posted(bounty.artifact_type,
                                               bounty.amount, bounty.ipfs_uri,
                                               bounty.duration, chain)
            bounties = await self.client.bounties.post_bounty(
                bounty.artifact_type,
                bounty.amount,
                bounty.ipfs_uri,
                bounty.duration,
                chain,
                api_key=bounty.api_key,
                metadata=metadata)
        if not bounties:
            await self.on_bounty_post_failed(bounty.artifact_type,
                                             bounty.amount,
                                             bounty.ipfs_uri,
                                             bounty.duration,
                                             chain,
                                             metadata=bounty.metadata)
        else:
            async with self.bounties_posted_locks[chain]:
                bounties_posted = self.bounties_posted.get(chain, 0)
                logger.info('Submitted bounty %s',
                            bounties_posted,
                            extra={'extra': bounty})
                self.bounties_posted[chain] = bounties_posted + len(bounties)

            async with self.bounties_pending_locks[chain]:
                bounties_pending = self.bounties_pending.get(chain, set())
                self.bounties_pending[chain] = bounties_pending | {
                    b.get('guid')
                    for b in bounties if 'guid' in b
                }

        for b in bounties:
            guid = b.get('guid')
            expiration = int(b.get('expiration', 0))

            if guid is None or expiration == 0:
                logger.error(
                    'Processing invalid bounty, not scheduling settle')
                continue

            # Handle any additional steps in derived implementations
            await self.on_after_bounty_posted(guid,
                                              bounty.artifact_type,
                                              bounty.amount,
                                              bounty.ipfs_uri,
                                              expiration,
                                              chain,
                                              metadata=bounty.metadata)

            sb = SettleBounty(guid)
            self.client.schedule(
                expiration + assertion_reveal_window + arbiter_vote_window, sb,
                chain)

        self.bounty_queues[chain].task_done()
        self.bounty_semaphores[chain].release()
    async def submit_bounty(self, bounty, chain):
        """Submit a bounty in a new task

        Args:
            bounty (QueuedBounty): Bounty to submit
            chain: Name of the chain to post to
        """
        assertion_reveal_window = await self.client.bounties.parameters[
            chain].get('assertion_reveal_window')
        arbiter_vote_window = await self.client.bounties.parameters[chain].get(
            'arbiter_vote_window')
        bounty_fee = await self.client.bounties.parameters[chain].get(
            'bounty_fee')

        tries = 0
        while tries < MAX_TRIES:
            balance = await self.client.balances.get_nct_balance(chain)

            # If we don't have the balance, don't submit. Wait and try a few times, then skip
            if balance < bounty.amount + bounty_fee:
                # Skip to next bounty, so one ultra high value bounty doesn't DOS ambassador
                if self.client.tx_error_fatal and tries >= MAX_TRIES:
                    logger.error(
                        f'Failed {tries} attempts to post bounty due to low balance. Exiting'
                    )
                    exit(1)
                    return
                else:
                    tries += 1
                    logger.critical(
                        f'Insufficient balance to post bounty on {chain}. Have {balance} NCT. '
                        f'Need {bounty.amount + bounty_fee} NCT.',
                        extra={'extra': bounty})
                    await asyncio.sleep(tries * tries)
                    continue

            metadata = None
            if bounty.metadata is not None:
                ipfs_hash = await self.client.bounties.post_metadata(
                    bounty.metadata, chain)
                metadata = ipfs_hash if ipfs_hash is not None else None

            await self.on_before_bounty_posted(bounty.artifact_type,
                                               bounty.amount, bounty.ipfs_uri,
                                               bounty.duration, chain)
            bounties = await self.client.bounties.post_bounty(
                bounty.artifact_type,
                bounty.amount,
                bounty.ipfs_uri,
                bounty.duration,
                chain,
                api_key=bounty.api_key,
                metadata=metadata)
            if not bounties:
                await self.on_bounty_post_failed(bounty.artifact_type,
                                                 bounty.amount,
                                                 bounty.ipfs_uri,
                                                 bounty.duration,
                                                 chain,
                                                 metadata=bounty.metadata)
            else:
                async with self.bounties_posted_locks[chain]:
                    bounties_posted = self.bounties_posted.get(chain, 0)
                    logger.info(f'Submitted bounty {bounties_posted}',
                                extra={'extra': bounty})
                    self.bounties_posted[chain] = bounties_posted + len(
                        bounties)

                async with self.bounties_pending_locks[chain]:
                    bounties_pending = self.bounties_pending.get(chain, set())
                    self.bounties_pending[chain] = bounties_pending | {
                        b.get('guid')
                        for b in bounties if 'guid' in b
                    }

            for b in bounties:
                guid = b.get('guid')
                expiration = int(b.get('expiration', 0))

                if guid is None or expiration == 0:
                    logger.error(
                        'Processing invalid bounty, not scheduling settle')
                    continue

                # Handle any additional steps in derived implementations
                await self.on_after_bounty_posted(guid,
                                                  bounty.artifact_type,
                                                  bounty.amount,
                                                  bounty.ipfs_uri,
                                                  expiration,
                                                  chain,
                                                  metadata=bounty.metadata)

                sb = SettleBounty(guid)
                self.client.schedule(
                    expiration + assertion_reveal_window + arbiter_vote_window,
                    sb, chain)

            self.bounty_queues[chain].task_done()
            self.bounty_semaphores[chain].release()
            return

        logger.warning(
            'Failed %s attempts to post bounty due to low balance. Skipping',
            tries,
            extra={'extra': bounty})
        await self.on_bounty_post_failed(bounty.artifact_type,
                                         bounty.amount,
                                         bounty.ipfs_uri,
                                         bounty.duration,
                                         chain,
                                         metadata=bounty.metadata)
    async def __handle_new_bounty(self, guid, artifact_type, author, amount,
                                  uri, expiration, metadata, block_number,
                                  txhash, chain):
        """Scan and assert on a posted bounty

        Args:
            guid (str): The bounty to assert on
            artifact_type (ArtifactType): The type of artifacts in this bounty
            author (str): The bounty author
            amount (str): Amount of the bounty in base NCT units (10 ^ -18)
            uri (str): IPFS hash of the root artifact
            expiration (str): Block number of the bounty's expiration
            metadata (dict): Dictionary of metadata or None
            block_number (int): Block number the bounty was placed on
            txhash (str): Transaction hash which caused the event
            chain (str): Is this on the home or side chain?

        Returns:
            Response JSON parsed from polyswarmd containing placed assertions
        """
        # Skip bounties for types we don't support
        if artifact_type not in self.valid_artifact_types:
            logger.info('Bounty artifact type %s is not supported',
                        artifact_type)
            return []

        self.bounties_seen += 1
        if self.testing > 0:
            if self.bounties_seen > self.testing:
                logger.warning(
                    'Received new bounty, but finished with testing mode')
                return []
            logger.info('Testing mode, %s  bounties remaining',
                        self.testing - self.bounties_seen)

        expiration = int(expiration)
        duration = expiration - block_number

        async with self.client.liveness_recorder.waiting_task(
                guid, block_number):
            results = await self.fetch_and_scan_all(guid, artifact_type, uri,
                                                    duration, metadata, chain)
            mask = [r.bit for r in results]
            verdicts = [r.verdict for r in results]
            confidences = [r.confidence for r in results]
            metadatas = [r.metadata for r in results]
            combined_metadata = ';'.join(metadatas)

            try:
                if all([
                        metadata
                        and verdict.Verdict.validate(json.loads(metadata))
                        for metadata in metadatas
                ]):
                    combined_metadata = json.dumps(
                        [json.loads(metadata) for metadata in metadatas])
            except json.JSONDecodeError:
                logger.exception('Error decoding assertion metadata %s',
                                 metadatas)

            if not any(mask):
                return []

            assertion_fee = await self.client.bounties.parameters[chain].get(
                'assertion_fee')
            assertion_reveal_window = await self.client.bounties.parameters[
                chain].get('assertion_reveal_window')
            arbiter_vote_window = await self.client.bounties.parameters[
                chain].get('arbiter_vote_window')

            bid = await self.bid(guid, mask, verdicts, confidences, metadatas,
                                 chain)
            try:
                await self.client.balances.raise_for_low_balance(
                    assertion_fee + sum(bid), chain)
            except LowBalanceError as e:
                if self.client.tx_error_fatal:
                    raise FatalError(
                        'Failed to assert on bounty due to low balance') from e

                return []

            logger.info('Responding to %s bounty %s',
                        artifact_type.name.lower(), guid)
            try:
                nonce, assertions = await self.client.bounties.post_assertion(
                    guid,
                    bid,
                    mask,
                    verdicts,
                    chain,
                    metadata=combined_metadata)
            except InvalidMetadataError:
                logger.exception('Received invalid metadata')
                return []

        for a in assertions:
            ra = RevealAssertion(guid, a['index'], nonce, verdicts,
                                 combined_metadata)
            self.client.schedule(expiration, ra, chain)

            sb = SettleBounty(guid)
            self.client.schedule(
                expiration + assertion_reveal_window + arbiter_vote_window, sb,
                chain)

        return assertions