async def get_current_state(self) -> PoolWalletInfo: history: List[Tuple[uint32, CoinSpend]] = await self.get_spend_history() all_spends: List[CoinSpend] = [cs for _, cs in history] # We must have at least the launcher spend assert len(all_spends) >= 1 launcher_coin: Coin = all_spends[0].coin delayed_seconds, delayed_puzhash = get_delayed_puz_info_from_launcher_spend( all_spends[0]) tip_singleton_coin: Optional[ Coin] = get_most_recent_singleton_coin_from_coin_spend( all_spends[-1]) launcher_id: bytes32 = launcher_coin.name() p2_singleton_puzzle_hash = launcher_id_to_p2_puzzle_hash( launcher_id, delayed_seconds, delayed_puzhash) assert tip_singleton_coin is not None curr_spend_i = len(all_spends) - 1 extra_data: Optional[PoolState] = None last_singleton_spend_height = uint32(0) while extra_data is None: full_spend: CoinSpend = all_spends[curr_spend_i] extra_data = solution_to_extra_data(full_spend) last_singleton_spend_height = uint32(history[curr_spend_i][0]) curr_spend_i -= 1 assert extra_data is not None current_inner = pool_state_to_inner_puzzle( extra_data, launcher_coin.name(), self.wallet_state_manager.constants.GENESIS_CHALLENGE, delayed_seconds, delayed_puzhash, ) return PoolWalletInfo( extra_data, self.target_state, launcher_coin, launcher_id, p2_singleton_puzzle_hash, current_inner, tip_singleton_coin.name(), last_singleton_spend_height, )
def test_pool_lifecycle(self): # START TESTS # Generate starting info key_lookup = KeyTool() sk: PrivateKey = PrivateKey.from_bytes( secret_exponent_for_index(1).to_bytes(32, "big"), ) pk: G1Element = G1Element.from_bytes( public_key_for_index(1, key_lookup)) starting_puzzle: Program = puzzle_for_pk(pk) starting_ph: bytes32 = starting_puzzle.get_tree_hash() # Get our starting standard coin created START_AMOUNT: uint64 = 1023 coin_db = CoinStore() time = CoinTimestamp(10000000, 1) coin_db.farm_coin(starting_ph, time, START_AMOUNT) starting_coin: Coin = next(coin_db.all_unspent_coins()) # LAUNCHING # Create the escaping inner puzzle GENESIS_CHALLENGE = bytes32.fromhex( "ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb") launcher_coin = singleton_top_layer.generate_launcher_coin( starting_coin, START_AMOUNT, ) DELAY_TIME = uint64(60800) DELAY_PH = starting_ph launcher_id = launcher_coin.name() relative_lock_height: uint32 = uint32(5000) # use a dummy pool state pool_state = PoolState( owner_pubkey=pk, pool_url="", relative_lock_height=relative_lock_height, state=3, # farming to pool target_puzzle_hash=starting_ph, version=1, ) # create a new dummy pool state for travelling target_pool_state = PoolState( owner_pubkey=pk, pool_url="", relative_lock_height=relative_lock_height, state=2, # Leaving pool target_puzzle_hash=starting_ph, version=1, ) # Standard format comment comment = Program.to([("p", bytes(pool_state)), ("t", DELAY_TIME), ("h", DELAY_PH)]) pool_wr_innerpuz: bytes32 = create_waiting_room_inner_puzzle( starting_ph, relative_lock_height, pk, launcher_id, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, ) pool_wr_inner_hash = pool_wr_innerpuz.get_tree_hash() pooling_innerpuz: Program = create_pooling_inner_puzzle( starting_ph, pool_wr_inner_hash, pk, launcher_id, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, ) # Driver tests assert is_pool_singleton_inner_puzzle(pooling_innerpuz) assert is_pool_singleton_inner_puzzle(pool_wr_innerpuz) assert get_pubkey_from_member_inner_puzzle(pooling_innerpuz) == pk # Generating launcher information conditions, launcher_coinsol = singleton_top_layer.launch_conditions_and_coinsol( starting_coin, pooling_innerpuz, comment, START_AMOUNT) # Creating solution for standard transaction delegated_puzzle: Program = puzzle_for_conditions(conditions) full_solution: Program = solution_for_conditions(conditions) starting_coinsol = CoinSolution( starting_coin, starting_puzzle, full_solution, ) # Create the spend bundle sig: G2Element = sign_delegated_puz(delegated_puzzle, starting_coin) spend_bundle = SpendBundle( [starting_coinsol, launcher_coinsol], sig, ) # Spend it! coin_db.update_coin_store_for_spend_bundle( spend_bundle, time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) # Test that we can retrieve the extra data assert get_delayed_puz_info_from_launcher_spend(launcher_coinsol) == ( DELAY_TIME, DELAY_PH) assert solution_to_extra_data(launcher_coinsol) == pool_state # TEST TRAVEL AFTER LAUNCH # fork the state fork_coin_db: CoinStore = copy.deepcopy(coin_db) post_launch_coinsol, _ = create_travel_spend( launcher_coinsol, launcher_coin, pool_state, target_pool_state, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, ) # Spend it! fork_coin_db.update_coin_store_for_spend_bundle( SpendBundle([post_launch_coinsol], G2Element()), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) # HONEST ABSORB time = CoinTimestamp(10000030, 2) # create the farming reward p2_singleton_puz: Program = create_p2_singleton_puzzle( SINGLETON_MOD_HASH, launcher_id, DELAY_TIME, DELAY_PH, ) p2_singleton_ph: bytes32 = p2_singleton_puz.get_tree_hash() assert uncurry_pool_waitingroom_inner_puzzle(pool_wr_innerpuz) == ( starting_ph, relative_lock_height, pk, p2_singleton_ph, ) assert launcher_id_to_p2_puzzle_hash(launcher_id, DELAY_TIME, DELAY_PH) == p2_singleton_ph assert get_seconds_and_delayed_puzhash_from_p2_singleton_puzzle( p2_singleton_puz) == (DELAY_TIME, DELAY_PH) coin_db.farm_coin(p2_singleton_ph, time, 1750000000000) coin_sols: List[CoinSolution] = create_absorb_spend( launcher_coinsol, pool_state, launcher_coin, 2, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, # height ) # Spend it! coin_db.update_coin_store_for_spend_bundle( SpendBundle(coin_sols, G2Element()), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) # ABSORB A NON EXISTENT REWARD (Negative test) last_coinsol: CoinSolution = list( filter( lambda e: e.coin.amount == START_AMOUNT, coin_sols, ))[0] coin_sols: List[CoinSolution] = create_absorb_spend( last_coinsol, pool_state, launcher_coin, 2, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, # height ) # filter for only the singleton solution singleton_coinsol: CoinSolution = list( filter( lambda e: e.coin.amount == START_AMOUNT, coin_sols, ))[0] # Spend it and hope it fails! try: coin_db.update_coin_store_for_spend_bundle( SpendBundle([singleton_coinsol], G2Element()), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) except BadSpendBundleError as e: assert str( e ) == "condition validation failure Err.ASSERT_ANNOUNCE_CONSUMED_FAILED" # SPEND A NON-REWARD P2_SINGLETON (Negative test) # create the dummy coin non_reward_p2_singleton = Coin( bytes32(32 * b"3"), p2_singleton_ph, uint64(1337), ) coin_db._add_coin_entry(non_reward_p2_singleton, time) # construct coin solution for the p2_singleton coin bad_coinsol = CoinSolution( non_reward_p2_singleton, p2_singleton_puz, Program.to([ pooling_innerpuz.get_tree_hash(), non_reward_p2_singleton.name(), ]), ) # Spend it and hope it fails! try: coin_db.update_coin_store_for_spend_bundle( SpendBundle([singleton_coinsol, bad_coinsol], G2Element()), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) except BadSpendBundleError as e: assert str( e ) == "condition validation failure Err.ASSERT_ANNOUNCE_CONSUMED_FAILED" # ENTER WAITING ROOM # find the singleton singleton = get_most_recent_singleton_coin_from_coin_solution( last_coinsol) # get the relevant coin solution travel_coinsol, _ = create_travel_spend( last_coinsol, launcher_coin, pool_state, target_pool_state, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, ) # Test that we can retrieve the extra data assert solution_to_extra_data(travel_coinsol) == target_pool_state # sign the serialized state data = Program.to(bytes(target_pool_state)).get_tree_hash() sig: G2Element = AugSchemeMPL.sign( sk, (data + singleton.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), ) # Spend it! coin_db.update_coin_store_for_spend_bundle( SpendBundle([travel_coinsol], sig), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) # ESCAPE TOO FAST (Negative test) # find the singleton singleton = get_most_recent_singleton_coin_from_coin_solution( travel_coinsol) # get the relevant coin solution return_coinsol, _ = create_travel_spend( travel_coinsol, launcher_coin, target_pool_state, pool_state, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, ) # sign the serialized target state sig = AugSchemeMPL.sign( sk, (data + singleton.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), ) # Spend it and hope it fails! try: coin_db.update_coin_store_for_spend_bundle( SpendBundle([return_coinsol], sig), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) except BadSpendBundleError as e: assert str( e ) == "condition validation failure Err.ASSERT_HEIGHT_RELATIVE_FAILED" # ABSORB WHILE IN WAITING ROOM time = CoinTimestamp(10000060, 3) # create the farming reward coin_db.farm_coin(p2_singleton_ph, time, 1750000000000) # generate relevant coin solutions coin_sols: List[CoinSolution] = create_absorb_spend( travel_coinsol, target_pool_state, launcher_coin, 3, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, # height ) # Spend it! coin_db.update_coin_store_for_spend_bundle( SpendBundle(coin_sols, G2Element()), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) # LEAVE THE WAITING ROOM time = CoinTimestamp(20000000, 10000) # find the singleton singleton_coinsol: CoinSolution = list( filter( lambda e: e.coin.amount == START_AMOUNT, coin_sols, ))[0] singleton: Coin = get_most_recent_singleton_coin_from_coin_solution( singleton_coinsol) # get the relevant coin solution return_coinsol, _ = create_travel_spend( singleton_coinsol, launcher_coin, target_pool_state, pool_state, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, ) # Test that we can retrieve the extra data assert solution_to_extra_data(return_coinsol) == pool_state # sign the serialized target state data = Program.to([ pooling_innerpuz.get_tree_hash(), START_AMOUNT, bytes(pool_state) ]).get_tree_hash() sig: G2Element = AugSchemeMPL.sign( sk, (data + singleton.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), ) # Spend it! coin_db.update_coin_store_for_spend_bundle( SpendBundle([return_coinsol], sig), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) # ABSORB ONCE MORE FOR GOOD MEASURE time = CoinTimestamp(20000000, 10005) # create the farming reward coin_db.farm_coin(p2_singleton_ph, time, 1750000000000) coin_sols: List[CoinSolution] = create_absorb_spend( return_coinsol, pool_state, launcher_coin, 10005, GENESIS_CHALLENGE, DELAY_TIME, DELAY_PH, # height ) # Spend it! coin_db.update_coin_store_for_spend_bundle( SpendBundle(coin_sols, G2Element()), time, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, )
async def create_new_pool_wallet_transaction( wallet_state_manager: Any, main_wallet: Wallet, initial_target_state: PoolState, fee: uint64 = uint64(0), p2_singleton_delay_time: Optional[uint64] = None, p2_singleton_delayed_ph: Optional[bytes32] = None, ) -> Tuple[TransactionRecord, bytes32, bytes32]: """ A "plot NFT", or pool wallet, represents the idea of a set of plots that all pay to the same pooling puzzle. This puzzle is a `chia singleton` that is parameterized with a public key controlled by the user's wallet (a `smart coin`). It contains an inner puzzle that can switch between paying block rewards to a pool, or to a user's own wallet. Call under the wallet state manger lock """ amount = 1 standard_wallet = main_wallet if p2_singleton_delayed_ph is None: p2_singleton_delayed_ph = await main_wallet.get_new_puzzlehash() if p2_singleton_delay_time is None: p2_singleton_delay_time = uint64(604800) unspent_records = await wallet_state_manager.coin_store.get_unspent_coins_for_wallet(standard_wallet.wallet_id) balance = await standard_wallet.get_confirmed_balance(unspent_records) if balance < PoolWallet.MINIMUM_INITIAL_BALANCE: raise ValueError("Not enough balance in main wallet to create a managed plotting pool.") if balance < fee: raise ValueError("Not enough balance in main wallet to create a managed plotting pool with fee {fee}.") # Verify Parameters - raise if invalid PoolWallet._verify_initial_target_state(initial_target_state) spend_bundle, singleton_puzzle_hash, launcher_coin_id = await PoolWallet.generate_launcher_spend( standard_wallet, uint64(1), initial_target_state, wallet_state_manager.constants.GENESIS_CHALLENGE, p2_singleton_delay_time, p2_singleton_delayed_ph, ) if spend_bundle is None: raise ValueError("failed to generate ID for wallet") standard_wallet_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), to_puzzle_hash=singleton_puzzle_hash, amount=uint64(amount), fee_amount=fee, confirmed=False, sent=uint32(0), spend_bundle=spend_bundle, additions=spend_bundle.additions(), removals=spend_bundle.removals(), wallet_id=wallet_state_manager.main_wallet.id(), sent_to=[], memos=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), ) await standard_wallet.push_transaction(standard_wallet_record) p2_singleton_puzzle_hash: bytes32 = launcher_id_to_p2_puzzle_hash( launcher_coin_id, p2_singleton_delay_time, p2_singleton_delayed_ph ) return standard_wallet_record, p2_singleton_puzzle_hash, launcher_coin_id
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()