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