예제 #1
0
    async def apply_state_transitions(self, new_state: CoinSpend, block_height: uint32):
        """
        Updates the Pool state (including DB) with new singleton spends. The block spends can contain many spends
        that we are not interested in, and can contain many ephemeral spends. They must all be in the same block.
        The DB must be committed after calling this method. All validation should be done here.
        """
        tip: Tuple[uint32, CoinSpend] = await self.get_tip()
        tip_spend = tip[1]

        tip_coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_spend(tip_spend)
        assert tip_coin is not None
        spent_coin_name: bytes32 = tip_coin.name()
        if spent_coin_name != new_state.coin.name():
            self.log.warning(f"Failed to apply state transition. tip: {tip_coin} new_state: {new_state} ")
            return

        await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, new_state, block_height)
        tip_spend = (await self.get_tip())[1]
        self.log.info(f"New PoolWallet singleton tip_coin: {tip_spend}")

        # If we have reached the target state, resets it to None. Loops back to get current state
        for _, added_spend in reversed(self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id)):
            latest_state: Optional[PoolState] = solution_to_pool_state(added_spend)
            if latest_state is not None:
                if self.target_state == latest_state:
                    self.target_state = None
                    self.next_transaction_fee = uint64(0)
                break

        await self.update_pool_config(False)
예제 #2
0
    async def apply_state_transitions(self, block_spends: List[CoinSpend], block_height: uint32):
        """
        Updates the Pool state (including DB) with new singleton spends. The block spends can contain many spends
        that we are not interested in, and can contain many ephemeral spends. They must all be in the same block.
        The DB must be committed after calling this method. All validation should be done here.
        """
        coin_name_to_spend: Dict[bytes32, CoinSpend] = {cs.coin.name(): cs for cs in block_spends}

        tip: Tuple[uint32, CoinSpend] = await self.get_tip()
        tip_height = tip[0]
        tip_spend = tip[1]
        assert block_height >= tip_height  # We should not have a spend with a lesser block height

        while True:
            tip_coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_spend(tip_spend)
            assert tip_coin is not None
            spent_coin_name: bytes32 = tip_coin.name()
            if spent_coin_name not in coin_name_to_spend:
                break
            spend: CoinSpend = coin_name_to_spend[spent_coin_name]
            await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, spend, block_height)
            tip_spend = (await self.get_tip())[1]
            self.log.info(f"New PoolWallet singleton tip_coin: {tip_spend}")
            coin_name_to_spend.pop(spent_coin_name)

            # If we have reached the target state, resets it to None. Loops back to get current state
            for _, added_spend in reversed(self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id)):
                latest_state: Optional[PoolState] = solution_to_pool_state(added_spend)
                if latest_state is not None:
                    if self.target_state == latest_state:
                        self.target_state = None
                        self.next_transaction_fee = uint64(0)
                    break
        await self.update_pool_config(False)
예제 #3
0
 def get_next_interesting_coin_ids(spend: CoinSpend) -> List[bytes32]:
     # CoinSpend of one of the coins that we cared about. This coin was spent in a block, but might be in a reorg
     # If we return a value, it is a coin ID that we are also interested in (to support two transitions per block)
     coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_spend(spend)
     if coin is not None:
         return [coin.name()]
     return []
예제 #4
0
    async def new_peak(self, peak: BlockRecord) -> None:
        # This gets called from the WalletStateManager whenever there is a new peak

        pool_wallet_info: PoolWalletInfo = await self.get_current_state()
        tip_height, tip_spend = await self.get_tip()

        if self.target_state is None:
            return
        if self.target_state == pool_wallet_info.current.state:
            self.target_state = None
            raise ValueError("Internal error")

        if (
            self.target_state.state in [FARMING_TO_POOL, SELF_POOLING]
            and pool_wallet_info.current.state == LEAVING_POOL
        ):
            leave_height = tip_height + pool_wallet_info.current.relative_lock_height

            curr: BlockRecord = peak
            while not curr.is_transaction_block:
                curr = self.wallet_state_manager.blockchain.block_record(curr.prev_hash)

            self.log.info(f"Last transaction block height: {curr.height} OK to leave at height {leave_height}")

            # Add some buffer (+2) to reduce chances of a reorg
            if curr.height > leave_height + 2:
                unconfirmed: List[
                    TransactionRecord
                ] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.wallet_id)
                next_tip: Optional[Coin] = get_most_recent_singleton_coin_from_coin_spend(tip_spend)
                assert next_tip is not None

                if any([rem.name() == next_tip.name() for tx_rec in unconfirmed for rem in tx_rec.removals]):
                    self.log.info("Already submitted second transaction, will not resubmit.")
                    return

                self.log.info(f"Attempting to leave from\n{pool_wallet_info.current}\nto\n{self.target_state}")
                assert self.target_state.version == POOL_PROTOCOL_VERSION
                assert pool_wallet_info.current.state == LEAVING_POOL
                assert self.target_state.target_puzzle_hash is not None

                if self.target_state.state == SELF_POOLING:
                    assert self.target_state.relative_lock_height == 0
                    assert self.target_state.pool_url is None
                elif self.target_state.state == FARMING_TO_POOL:
                    assert self.target_state.relative_lock_height >= self.MINIMUM_RELATIVE_LOCK_HEIGHT
                    assert self.target_state.pool_url is not None

                tx_record = await self.generate_travel_transaction(self.next_transaction_fee)
                await self.wallet_state_manager.add_pending_transaction(tx_record)
예제 #5
0
    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 = CoinSpend(
            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_pool_state(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[CoinSpend] = 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: CoinSpend = list(
            filter(
                lambda e: e.coin.amount == START_AMOUNT,
                coin_sols,
            ))[0]
        coin_sols: List[CoinSpend] = 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: CoinSpend = 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 = CoinSpend(
            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_spend(
            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_pool_state(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_spend(
            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[CoinSpend] = 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: CoinSpend = list(
            filter(
                lambda e: e.coin.amount == START_AMOUNT,
                coin_sols,
            ))[0]
        singleton: Coin = get_most_recent_singleton_coin_from_coin_spend(
            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_pool_state(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[CoinSpend] = 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,
        )
예제 #7
0
    async def collect_pool_rewards_loop(self):
        """
        Iterates through the blockchain, looking for pool rewards, and claims them, creating a transaction to the
        pool's puzzle_hash.
        """

        while True:
            try:
                if not self.blockchain_state["sync"]["synced"]:
                    await asyncio.sleep(60)
                    continue

                scan_phs: List[bytes32] = list(
                    self.scan_p2_singleton_puzzle_hashes)
                peak_height = self.blockchain_state["peak"].height

                # Only get puzzle hashes with a certain number of confirmations or more, to avoid reorg issues
                coin_records: List[
                    CoinRecord] = await self.node_rpc_client.get_coin_records_by_puzzle_hashes(
                        scan_phs,
                        include_spent_coins=False,
                        start_height=self.scan_start_height,
                    )
                self.log.info(
                    f"Scanning for block rewards from {self.scan_start_height} to {peak_height}. "
                    f"Found: {len(coin_records)}")
                ph_to_amounts: Dict[bytes32, int] = {}
                ph_to_coins: Dict[bytes32, List[CoinRecord]] = {}
                not_buried_amounts = 0
                for cr in coin_records:
                    if not cr.coinbase:
                        self.log.info(
                            f"Non coinbase coin: {cr.coin}, ignoring")
                        continue

                    if cr.confirmed_block_index > peak_height - self.confirmation_security_threshold:
                        not_buried_amounts += cr.coin.amount
                        continue
                    if cr.coin.puzzle_hash not in ph_to_amounts:
                        ph_to_amounts[cr.coin.puzzle_hash] = 0
                        ph_to_coins[cr.coin.puzzle_hash] = []
                    ph_to_amounts[cr.coin.puzzle_hash] += cr.coin.amount
                    ph_to_coins[cr.coin.puzzle_hash].append(cr)

                # For each p2sph, get the FarmerRecords
                farmer_records = await self.store.get_farmer_records_for_p2_singleton_phs(
                    set(ph for ph in ph_to_amounts.keys()))

                # For each singleton, create, submit, and save a claim transaction
                claimable_amounts = 0
                not_claimable_amounts = 0
                for rec in farmer_records:
                    if rec.is_pool_member:
                        claimable_amounts += ph_to_amounts[
                            rec.p2_singleton_puzzle_hash]
                    else:
                        not_claimable_amounts += ph_to_amounts[
                            rec.p2_singleton_puzzle_hash]

                if len(coin_records) > 0:
                    self.log.info(
                        f"Claimable amount: {claimable_amounts / (10 ** 12)}")
                    self.log.info(
                        f"Not claimable amount: {not_claimable_amounts / (10 ** 12)}"
                    )
                    self.log.info(
                        f"Not buried amounts: {not_buried_amounts / (10 ** 12)}"
                    )

                for rec in farmer_records:
                    if rec.is_pool_member:
                        singleton_tip: Optional[
                            Coin] = get_most_recent_singleton_coin_from_coin_spend(
                                rec.singleton_tip)
                        if singleton_tip is None:
                            continue

                        singleton_coin_record: Optional[
                            CoinRecord] = await self.node_rpc_client.get_coin_record_by_name(
                                singleton_tip.name())
                        if singleton_coin_record is None:
                            continue
                        if singleton_coin_record.spent:
                            self.log.warning(
                                f"Singleton coin {singleton_coin_record.coin.name()} is spent, will not "
                                f"claim rewards")
                            continue

                        spend_bundle = await create_absorb_transaction(
                            self.node_rpc_client,
                            rec,
                            self.blockchain_state["peak"].height,
                            ph_to_coins[rec.p2_singleton_puzzle_hash],
                            self.constants.GENESIS_CHALLENGE,
                            self.claim_fee,
                            self.wallet_rpc_client,
                            self.default_target_puzzle_hash,
                        )

                        if spend_bundle is None:
                            continue

                        push_tx_response: Dict = await self.node_rpc_client.push_tx(
                            spend_bundle)
                        if push_tx_response["status"] == "SUCCESS":
                            # TODO(pool): save transaction in records
                            self.log.info(
                                f"Submitted transaction successfully: {spend_bundle.name().hex()}"
                            )
                        else:
                            self.log.error(
                                f"Error submitting transaction: {push_tx_response}"
                            )
                await asyncio.sleep(self.collect_pool_rewards_interval)
            except asyncio.CancelledError:
                self.log.info("Cancelled collect_pool_rewards_loop, closing")
                return
            except Exception as e:
                error_stack = traceback.format_exc()
                self.log.error(
                    f"Unexpected error in collect_pool_rewards_loop: {e} {error_stack}"
                )
                await asyncio.sleep(self.collect_pool_rewards_interval)
예제 #8
0
async def get_singleton_state(
    node_rpc_client: FullNodeRpcClient,
    launcher_id: bytes32,
    farmer_record: Optional[FarmerRecord],
    peak_height: uint32,
    confirmation_security_threshold: int,
    genesis_challenge: bytes32,
) -> Optional[Tuple[CoinSpend, PoolState, PoolState]]:
    try:
        if farmer_record is None:
            launcher_coin: Optional[
                CoinRecord] = await node_rpc_client.get_coin_record_by_name(
                    launcher_id)
            if launcher_coin is None:
                log.warning(f"Can not find genesis coin {launcher_id}")
                return None
            if not launcher_coin.spent:
                log.warning(f"Genesis coin {launcher_id} not spent")
                return None

            last_spend: Optional[CoinSpend] = await get_coin_spend(
                node_rpc_client, launcher_coin)
            delay_time, delay_puzzle_hash = get_delayed_puz_info_from_launcher_spend(
                last_spend)
            saved_state = solution_to_pool_state(last_spend)
            assert last_spend is not None and saved_state is not None
        else:
            last_spend = farmer_record.singleton_tip
            saved_state = farmer_record.singleton_tip_state
            delay_time = farmer_record.delay_time
            delay_puzzle_hash = farmer_record.delay_puzzle_hash

        saved_spend = last_spend
        last_not_none_state: PoolState = saved_state
        assert last_spend is not None

        last_coin_record: Optional[
            CoinRecord] = await node_rpc_client.get_coin_record_by_name(
                last_spend.coin.name())
        assert last_coin_record is not None

        while True:
            # Get next coin solution
            next_coin: Optional[
                Coin] = get_most_recent_singleton_coin_from_coin_spend(
                    last_spend)
            if next_coin is None:
                # This means the singleton is invalid
                return None
            next_coin_record: Optional[
                CoinRecord] = await node_rpc_client.get_coin_record_by_name(
                    next_coin.name())
            assert next_coin_record is not None

            if not next_coin_record.spent:
                if not validate_puzzle_hash(
                        launcher_id,
                        delay_puzzle_hash,
                        delay_time,
                        last_not_none_state,
                        next_coin_record.coin.puzzle_hash,
                        genesis_challenge,
                ):
                    log.warning(
                        f"Invalid singleton puzzle_hash for {launcher_id}")
                    return None
                break

            last_spend: Optional[CoinSpend] = await get_coin_spend(
                node_rpc_client, next_coin_record)
            assert last_spend is not None

            pool_state: Optional[PoolState] = solution_to_pool_state(
                last_spend)

            if pool_state is not None:
                last_not_none_state = pool_state
            if peak_height - confirmation_security_threshold >= next_coin_record.spent_block_index:
                # There is a state transition, and it is sufficiently buried
                saved_spend = last_spend
                saved_state = last_not_none_state

        return saved_spend, saved_state, last_not_none_state
    except Exception as e:
        log.error(f"Error getting singleton: {e}")
        return None