async def update_farmer(self, request: PutFarmerRequest) -> Dict: farmer_record: Optional[ FarmerRecord] = await self.store.get_farmer_record( request.payload.launcher_id) if farmer_record is None: return error_dict( PoolErrorCode.FARMER_NOT_KNOWN, f"Farmer with launcher_id {request.payload.launcher_id} not known." ) singleton_state_tuple: Optional[ Tuple[CoinSolution, PoolState]] = await self.get_and_validate_singleton_state( request.payload.launcher_id) last_spend, last_state = singleton_state_tuple if singleton_state_tuple is None: return error_dict(PoolErrorCode.INVALID_SINGLETON, f"Invalid singleton, or not a pool member") if not AugSchemeMPL.verify(last_state.owner_pubkey, request.payload.get_hash(), request.signature): return error_dict(PoolErrorCode.INVALID_SIGNATURE, f"Invalid signature") farmer_dict = farmer_record.to_json_dict() response_dict = {} if request.payload.authentication_public_key is not None: is_new_value = farmer_record.authentication_public_key != request.payload.authentication_public_key response_dict["authentication_public_key"] = is_new_value if is_new_value: farmer_dict[ "authentication_public_key"] = request.payload.authentication_public_key if request.payload.payout_instructions is not None: is_new_value = ( farmer_record.payout_instructions != request.payload.payout_instructions and request.payload.payout_instructions is not None and len(hexstr_to_bytes( request.payload.payout_instructions)) == 32) response_dict["payout_instructions"] = is_new_value if is_new_value: farmer_dict[ "payout_instructions"] = request.payload.payout_instructions if request.payload.suggested_difficulty is not None: is_new_value = ( farmer_record.suggested_difficulty != request.payload.suggested_difficulty and request.payload.suggested_difficulty is not None and request.payload.suggested_difficulty >= self.min_difficulty) response_dict["suggested_difficulty"] = is_new_value if is_new_value: farmer_dict[ "suggested_difficulty"] = request.payload.suggested_difficulty self.log.info(f"Updated farmer: {response_dict}") await self.store.add_farmer_record( FarmerRecord.from_json_dict(farmer_dict)) return PutFarmerResponse.from_json_dict(response_dict).from_json_dict()
async def process_partial( self, partial: PostPartialRequest, farmer_record: FarmerRecord, time_received_partial: uint64, ) -> Dict: # Validate signatures message: bytes32 = partial.payload.get_hash() pk1: G1Element = partial.payload.proof_of_space.plot_public_key pk2: G1Element = farmer_record.authentication_public_key valid_sig = AugSchemeMPL.aggregate_verify([pk1, pk2], [message, message], partial.aggregate_signature) if not valid_sig: return error_dict( PoolErrorCode.INVALID_SIGNATURE, f"The aggregate signature is invalid {partial.aggregate_signature}", ) # TODO (chia-dev): Check DB p2_singleton_puzzle_hash and compare # if partial.payload.proof_of_space.pool_contract_puzzle_hash != p2_singleton_puzzle_hash: # return error_dict( # PoolErrorCode.INVALID_P2_SINGLETON_PUZZLE_HASH, # f"Invalid plot pool contract puzzle hash {partial.payload.proof_of_space.pool_contract_puzzle_hash}" # ) async def get_signage_point_or_eos(): if partial.payload.end_of_sub_slot: return await self.node_rpc_client.get_recent_signage_point_or_eos( None, partial.payload.sp_hash) else: return await self.node_rpc_client.get_recent_signage_point_or_eos( partial.payload.sp_hash, None) response = await get_signage_point_or_eos() if response is None: # Try again after 10 seconds in case we just didn't yet receive the signage point await asyncio.sleep(10) response = await get_signage_point_or_eos() if response is None or response["reverted"]: return error_dict( PoolErrorCode.NOT_FOUND, f"Did not find signage point or EOS {partial.payload.sp_hash}, {response}" ) node_time_received_sp = response["time_received"] signage_point: Optional[SignagePoint] = response.get( "signage_point", None) end_of_sub_slot: Optional[EndOfSubSlotBundle] = response.get( "eos", None) if time_received_partial - node_time_received_sp > self.partial_time_limit: return error_dict( PoolErrorCode.TOO_LATE, f"Received partial in {time_received_partial - node_time_received_sp}. " f"Make sure your proof of space lookups are fast, and network connectivity is good." f"Response must happen in less than {self.partial_time_limit} seconds. NAS or network" f" farming can be an issue", ) # Validate the proof if signage_point is not None: challenge_hash: bytes32 = signage_point.cc_vdf.challenge else: challenge_hash = end_of_sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf.get_hash( ) quality_string: Optional[ bytes32] = partial.payload.proof_of_space.verify_and_get_quality_string( self.constants, challenge_hash, partial.payload.sp_hash) if quality_string is None: return error_dict( PoolErrorCode.INVALID_PROOF, f"Invalid proof of space {partial.payload.sp_hash}") current_difficulty = farmer_record.difficulty required_iters: uint64 = calculate_iterations_quality( self.constants.DIFFICULTY_CONSTANT_FACTOR, quality_string, partial.payload.proof_of_space.size, current_difficulty, partial.payload.sp_hash, ) if required_iters >= self.iters_limit: return error_dict( PoolErrorCode.PROOF_NOT_GOOD_ENOUGH, f"Proof of space has required iters {required_iters}, too high for difficulty " f"{current_difficulty}", ) await self.pending_point_partials.put( (partial, time_received_partial, current_difficulty)) async with self.store.lock: # Obtains the new record in case we just updated difficulty farmer_record: Optional[ FarmerRecord] = await self.store.get_farmer_record( partial.payload.launcher_id) if farmer_record is not None: current_difficulty = farmer_record.difficulty # Decide whether to update the difficulty recent_partials = await self.store.get_recent_partials( partial.payload.launcher_id, self.number_of_partials_target) # Only update the difficulty if we meet certain conditions new_difficulty: uint64 = get_new_difficulty( recent_partials, int(self.number_of_partials_target), int(self.time_target), current_difficulty, time_received_partial, self.min_difficulty, ) if current_difficulty != new_difficulty: await self.store.update_difficulty( partial.payload.launcher_id, new_difficulty) return PostPartialResponse(current_difficulty).to_json_dict()
async def add_farmer(self, request: PostFarmerRequest) -> Dict: async with self.store.lock: farmer_record: Optional[ FarmerRecord] = await self.store.get_farmer_record( request.payload.launcher_id) if farmer_record is not None: return error_dict( PoolErrorCode.FARMER_ALREADY_KNOWN, f"Farmer with launcher_id {request.payload.launcher_id} already known.", ) singleton_state_tuple: Optional[Tuple[ CoinSolution, PoolState]] = await self.get_and_validate_singleton_state( request.payload.launcher_id) if singleton_state_tuple is None: return error_dict(PoolErrorCode.INVALID_SINGLETON, f"Invalid singleton, or not a pool member") last_spend, last_state = singleton_state_tuple if (request.payload.suggested_difficulty is None or request.payload.suggested_difficulty < self.min_difficulty): difficulty: uint64 = self.default_difficulty else: difficulty = request.payload.suggested_difficulty if len(hexstr_to_bytes(request.payload.payout_instructions)) != 32: return error_dict( PoolErrorCode.INVALID_PAYOUT_INSTRUCTIONS, f"Payout instructions must be an xch address for this pool.", ) if not AugSchemeMPL.verify(last_state.owner_pubkey, request.payload.get_hash(), request.signature): return error_dict(PoolErrorCode.INVALID_SIGNATURE, f"Invalid signature") launcher_coin: Optional[ CoinRecord] = await self.node_rpc_client.get_coin_record_by_name( request.payload.launcher_id) assert launcher_coin is not None and launcher_coin.spent launcher_solution: Optional[CoinSolution] = await get_coin_spend( self.node_rpc_client, launcher_coin) delay_time, delay_puzzle_hash = get_delayed_puz_info_from_launcher_spend( launcher_solution) if delay_time < 3600: return error_dict( PoolErrorCode.DELAY_TIME_TOO_SHORT, f"Delay time too short, must be at least 1 hour") p2_singleton_puzzle_hash = launcher_id_to_p2_puzzle_hash( request.payload.launcher_id, delay_time, delay_puzzle_hash) farmer_record = FarmerRecord( request.payload.launcher_id, p2_singleton_puzzle_hash, delay_time, delay_puzzle_hash, request.payload.authentication_public_key, last_spend, last_state, uint64(0), difficulty, request.payload.payout_instructions, True, ) self.scan_p2_singleton_puzzle_hashes.add(p2_singleton_puzzle_hash) await self.store.add_farmer_record(farmer_record) return PostFarmerResponse(self.welcome_message).to_json_dict()