diff --git a/.github/workflows/build-test-macos-wallet-cc_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml similarity index 91% rename from .github/workflows/build-test-macos-wallet-cc_wallet.yml rename to .github/workflows/build-test-macos-wallet-cat_wallet.yml index 05acc7d13d4f..d0ba73de4773 100644 --- a/.github/workflows/build-test-macos-wallet-cc_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -1,7 +1,7 @@ # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # -name: MacOS wallet-cc_wallet Tests +name: MacOS wallet-cat_wallet Tests on: push: @@ -15,7 +15,7 @@ on: jobs: build: - name: MacOS wallet-cc_wallet Tests + name: MacOS wallet-cat_wallet Tests runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: @@ -92,10 +92,10 @@ jobs: sh install-timelord.sh ./vdf_bench square_asm 400000 - - name: Test wallet-cc_wallet code with pytest + - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cc_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-ubuntu-wallet-cc_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml similarity index 92% rename from .github/workflows/build-test-ubuntu-wallet-cc_wallet.yml rename to .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index 60081d41d570..a0db82389a13 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cc_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -1,7 +1,7 @@ # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # -name: Ubuntu wallet-cc_wallet Test +name: Ubuntu wallet-cat_wallet Test on: push: @@ -15,7 +15,7 @@ on: jobs: build: - name: Ubuntu wallet-cc_wallet Test + name: Ubuntu wallet-cat_wallet Test runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: @@ -97,10 +97,10 @@ jobs: sh install-timelord.sh ./vdf_bench square_asm 400000 - - name: Test wallet-cc_wallet code with pytest + - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cc_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 # diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 4f3e11300d96..1a83da9f4df8 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -119,7 +119,7 @@ async def delete_unconfirmed_transactions(args: dict, wallet_client: WalletRpcCl def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]: - if typ == WalletType.COLOURED_COIN: + if typ == WalletType.CAT: return "", units["colouredcoin"] if typ in [WalletType.STANDARD_WALLET, WalletType.POOLING_WALLET, WalletType.MULTI_SIG, WalletType.RATE_LIMITED]: return address_prefix, units["chia"] diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index f0fea4fd9532..07012d676753 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -848,22 +848,12 @@ def add_block_record(self, block_record: BlockRecord): self.__heights_in_cache[block_record.height] = set() self.__heights_in_cache[block_record.height].add(block_record.header_hash) - # TODO: address hint error and remove ignore - # error: Argument 1 of "persist_sub_epoch_challenge_segments" is incompatible with supertype - # "BlockchainInterface"; supertype defines the argument type as "uint32" [override] - # note: This violates the Liskov substitution principle - # note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides - async def persist_sub_epoch_challenge_segments( # type: ignore[override] + async def persist_sub_epoch_challenge_segments( self, ses_block_hash: bytes32, segments: List[SubEpochChallengeSegment] ): return await self.block_store.persist_sub_epoch_challenge_segments(ses_block_hash, segments) - # TODO: address hint error and remove ignore - # error: Argument 1 of "get_sub_epoch_challenge_segments" is incompatible with supertype - # "BlockchainInterface"; supertype defines the argument type as "uint32" [override] - # note: This violates the Liskov substitution principle - # note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides - async def get_sub_epoch_challenge_segments( # type: ignore[override] + async def get_sub_epoch_challenge_segments( self, ses_block_hash: bytes32, ) -> Optional[List[SubEpochChallengeSegment]]: diff --git a/chia/consensus/blockchain_interface.py b/chia/consensus/blockchain_interface.py index da7371773f25..8eadd746e4b4 100644 --- a/chia/consensus/blockchain_interface.py +++ b/chia/consensus/blockchain_interface.py @@ -71,13 +71,13 @@ def try_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]: return None async def persist_sub_epoch_challenge_segments( - self, sub_epoch_summary_height: uint32, segments: List[SubEpochChallengeSegment] + self, sub_epoch_summary_height: bytes32, segments: List[SubEpochChallengeSegment] ): pass async def get_sub_epoch_challenge_segments( self, - sub_epoch_summary_height: uint32, + sub_epoch_summary_hash: bytes32, ) -> Optional[List[SubEpochChallengeSegment]]: pass diff --git a/chia/consensus/full_block_to_block_record.py b/chia/consensus/full_block_to_block_record.py index 8013bde8fafe..278d48aa3dab 100644 --- a/chia/consensus/full_block_to_block_record.py +++ b/chia/consensus/full_block_to_block_record.py @@ -22,6 +22,7 @@ def block_to_block_record( required_iters: uint64, full_block: Optional[Union[FullBlock, HeaderBlock]], header_block: Optional[HeaderBlock], + sub_slot_iters: Optional[uint64] = None, ) -> BlockRecord: if full_block is None: @@ -32,9 +33,10 @@ def block_to_block_record( prev_b = blocks.try_block_record(block.prev_header_hash) if block.height > 0: assert prev_b is not None - sub_slot_iters, _ = get_next_sub_slot_iters_and_difficulty( - constants, len(block.finished_sub_slots) > 0, prev_b, blocks - ) + if sub_slot_iters is None: + sub_slot_iters, _ = get_next_sub_slot_iters_and_difficulty( + constants, len(block.finished_sub_slots) > 0, prev_b, blocks + ) overflow = is_overflow_block(constants, block.reward_chain_block.signage_point_index) deficit = calculate_deficit( constants, diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 95961e7981be..979720719a88 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -58,11 +58,8 @@ def validate_clvm_and_signature( return Err(result.error), b"", {} pks: List[G1Element] = [] - msgs: List[bytes32] = [] - # TODO: address hint error and remove ignore - # error: Incompatible types in assignment (expression has type "List[bytes]", variable has type - # "List[bytes32]") [assignment] - pks, msgs = pkm_pairs(result.npc_list, additional_data) # type: ignore[assignment] + msgs: List[bytes] = [] + pks, msgs = pkm_pairs(result.npc_list, additional_data) # Verify aggregated signature cache: LRUCache = LRUCache(10000) @@ -249,6 +246,7 @@ async def pre_validate_spendbundle( start_time = time.time() if new_spend_bytes is None: new_spend_bytes = bytes(new_spend) + err, cached_result_bytes, new_cache_entries = await asyncio.get_running_loop().run_in_executor( self.pool, validate_clvm_and_signature, @@ -257,6 +255,7 @@ async def pre_validate_spendbundle( self.constants.COST_PER_BYTE, self.constants.AGG_SIG_ME_ADDITIONAL_DATA, ) + if err is not None: raise ValidationError(err) for cache_entry_key, cached_entry_value in new_cache_entries.items(): diff --git a/chia/full_node/weight_proof.py b/chia/full_node/weight_proof.py index 7b18c646e67f..b0a2fa405413 100644 --- a/chia/full_node/weight_proof.py +++ b/chia/full_node/weight_proof.py @@ -2,6 +2,7 @@ import dataclasses import logging import math +import pathlib import random from concurrent.futures.process import ProcessPoolExecutor from typing import Dict, List, Optional, Tuple @@ -23,7 +24,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.slots import ChallengeChainSubSlot, RewardChainSubSlot from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary -from chia.types.blockchain_format.vdf import VDFInfo +from chia.types.blockchain_format.vdf import VDFInfo, VDFProof from chia.types.end_of_slot_bundle import EndOfSubSlotBundle from chia.types.header_block import HeaderBlock from chia.types.weight_proof import ( @@ -58,6 +59,7 @@ def __init__( self.constants = constants self.blockchain = blockchain self.lock = asyncio.Lock() + self._num_processes = 4 async def get_proof_of_weight(self, tip: bytes32) -> Optional[WeightProof]: @@ -107,10 +109,9 @@ async def _create_proof_of_weight(self, tip: bytes32) -> Optional[WeightProof]: return None summary_heights = self.blockchain.get_ses_heights() - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_block_record_from_db" of "BlockchainInterface" has incompatible type - # "Optional[bytes32]"; expected "bytes32" [arg-type] - prev_ses_block = await self.blockchain.get_block_record_from_db(self.blockchain.height_to_hash(uint32(0))) # type: ignore[arg-type] # noqa: E501 + zero_hash = self.blockchain.height_to_hash(uint32(0)) + assert zero_hash is not None + prev_ses_block = await self.blockchain.get_block_record_from_db(zero_hash) if prev_ses_block is None: return None sub_epoch_data = self.get_sub_epoch_data(tip_rec.height, summary_heights) @@ -140,10 +141,7 @@ async def _create_proof_of_weight(self, tip: bytes32) -> Optional[WeightProof]: if _sample_sub_epoch(prev_ses_block.weight, ses_block.weight, weight_to_check): # type: ignore sample_n += 1 - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_sub_epoch_challenge_segments" of "BlockchainInterface" has - # incompatible type "bytes32"; expected "uint32" [arg-type] - segments = await self.blockchain.get_sub_epoch_challenge_segments(ses_block.header_hash) # type: ignore[arg-type] # noqa: E501 + segments = await self.blockchain.get_sub_epoch_challenge_segments(ses_block.header_hash) if segments is None: segments = await self.__create_sub_epoch_segments(ses_block, prev_ses_block, uint32(sub_epoch_n)) if segments is None: @@ -151,11 +149,7 @@ async def _create_proof_of_weight(self, tip: bytes32) -> Optional[WeightProof]: f"failed while building segments for sub epoch {sub_epoch_n}, ses height {ses_height} " ) return None - # TODO: address hint error and remove ignore - # error: Argument 1 to "persist_sub_epoch_challenge_segments" of "BlockchainInterface" has - # incompatible type "bytes32"; expected "uint32" [arg-type] - await self.blockchain.persist_sub_epoch_challenge_segments(ses_block.header_hash, segments) # type: ignore[arg-type] # noqa: E501 - log.debug(f"sub epoch {sub_epoch_n} has {len(segments)} segments") + await self.blockchain.persist_sub_epoch_challenge_segments(ses_block.header_hash, segments) sub_epoch_segments.extend(segments) prev_ses_block = ses_block log.debug(f"sub_epochs: {len(sub_epoch_data)}") @@ -195,10 +189,9 @@ async def _get_recent_chain(self, tip_height: uint32) -> Optional[List[HeaderBlo if curr_height == 0: break # add to needed reward chain recent blocks - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type - # "bytes32" [index] - header_block = headers[self.blockchain.height_to_hash(curr_height)] # type: ignore[index] + header_hash = self.blockchain.height_to_hash(curr_height) + assert header_hash is not None + header_block = headers[header_hash] block_rec = blocks[header_block.header_hash] if header_block is None: log.error("creating recent chain failed") @@ -209,10 +202,9 @@ async def _get_recent_chain(self, tip_height: uint32) -> Optional[List[HeaderBlo curr_height = uint32(curr_height - 1) blocks_n += 1 - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type "bytes32" - # [index] - header_block = headers[self.blockchain.height_to_hash(curr_height)] # type: ignore[index] + header_hash = self.blockchain.height_to_hash(curr_height) + assert header_hash is not None + header_block = headers[header_hash] recent_chain.insert(0, header_block) log.info( @@ -309,10 +301,9 @@ async def __create_sub_epoch_segments( first = False else: height = height + uint32(1) # type: ignore - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type - # "bytes32" [index] - curr = header_blocks[self.blockchain.height_to_hash(height)] # type: ignore[index] + header_hash = self.blockchain.height_to_hash(height) + assert header_hash is not None + curr = header_blocks[header_hash] if curr is None: return None log.debug(f"next sub epoch starts at {height}") @@ -331,10 +322,9 @@ async def get_prev_two_slots_height(self, se_start: BlockRecord) -> uint32: if end - curr_rec.height == batch_size - 1: blocks = await self.blockchain.get_block_records_in_range(curr_rec.height - batch_size, curr_rec.height) end = curr_rec.height - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, BlockRecord]"; expected type - # "bytes32" [index] - curr_rec = blocks[self.blockchain.height_to_hash(uint32(curr_rec.height - 1))] # type: ignore[index] + header_hash = self.blockchain.height_to_hash(uint32(curr_rec.height - 1)) + assert header_hash is not None + curr_rec = blocks[header_hash] return curr_rec.height async def _create_challenge_segment( @@ -447,10 +437,9 @@ async def __first_sub_slot_vdfs( curr.total_iters, ) tmp_sub_slots_data.append(ssd) - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type - # "bytes32" [index] - curr = header_blocks[self.blockchain.height_to_hash(uint32(curr.height + 1))] # type: ignore[index] + header_hash = self.blockchain.height_to_hash(uint32(curr.height + 1)) + assert header_hash is not None + curr = header_blocks[header_hash] if len(tmp_sub_slots_data) > 0: sub_slots_data.extend(tmp_sub_slots_data) @@ -479,10 +468,9 @@ async def __slot_end_vdf( ) -> Tuple[Optional[List[SubSlotData]], uint32]: # gets all vdfs first sub slot after challenge block to last sub slot log.debug(f"slot end vdf start height {start_height}") - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type "bytes32" - # [index] - curr = header_blocks[self.blockchain.height_to_hash(start_height)] # type: ignore[index] + header_hash = self.blockchain.height_to_hash(start_height) + assert header_hash is not None + curr = header_blocks[header_hash] curr_header_hash = curr.header_hash sub_slots_data: List[SubSlotData] = [] tmp_sub_slots_data: List[SubSlotData] = [] @@ -500,11 +488,9 @@ async def __slot_end_vdf( sub_slots_data.append(handle_end_of_slot(sub_slot, eos_vdf_iters)) tmp_sub_slots_data = [] tmp_sub_slots_data.append(self.handle_block_vdfs(curr, blocks)) - - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type - # "bytes32" [index] - curr = header_blocks[self.blockchain.height_to_hash(uint32(curr.height + 1))] # type: ignore[index] + header_hash = self.blockchain.height_to_hash(uint32(curr.height + 1)) + assert header_hash is not None + curr = header_blocks[header_hash] curr_header_hash = curr.header_hash if len(tmp_sub_slots_data) > 0: @@ -619,30 +605,42 @@ async def validate_weight_proof(self, weight_proof: WeightProof) -> Tuple[bool, log.error("failed weight proof sub epoch sample validation") return False, uint32(0), [] - executor = ProcessPoolExecutor(1) + executor = ProcessPoolExecutor(4) constants, summary_bytes, wp_segment_bytes, wp_recent_chain_bytes = vars_to_bytes( self.constants, summaries, weight_proof ) - segment_validation_task = asyncio.get_running_loop().run_in_executor( - executor, _validate_sub_epoch_segments, constants, rng, wp_segment_bytes, summary_bytes - ) recent_blocks_validation_task = asyncio.get_running_loop().run_in_executor( executor, _validate_recent_blocks, constants, wp_recent_chain_bytes, summary_bytes ) - valid_segment_task = segment_validation_task + segments_validated, vdfs_to_validate = _validate_sub_epoch_segments( + constants, rng, wp_segment_bytes, summary_bytes + ) + if not segments_validated: + return False, uint32(0), [] + + vdf_chunks = chunks(vdfs_to_validate, self._num_processes) + vdf_tasks = [] + for chunk in vdf_chunks: + byte_chunks = [] + for vdf_proof, classgroup, vdf_info in chunk: + byte_chunks.append((bytes(vdf_proof), bytes(classgroup), bytes(vdf_info))) + + vdf_task = asyncio.get_running_loop().run_in_executor(executor, _validate_vdf_batch, constants, byte_chunks) + vdf_tasks.append(vdf_task) + + for vdf_task in vdf_tasks: + validated = await vdf_task + if not validated: + return False, uint32(0), [] + valid_recent_blocks_task = recent_blocks_validation_task valid_recent_blocks = await valid_recent_blocks_task if not valid_recent_blocks: log.error("failed validating weight proof recent blocks") return False, uint32(0), [] - valid_segments = await valid_segment_task - if not valid_segments: - log.error("failed validating weight proof sub epoch segments") - return False, uint32(0), [] - return True, self.get_fork_point(summaries), summaries def get_fork_point(self, received_summaries: List[SubEpochSummary]) -> uint32: @@ -837,6 +835,11 @@ def handle_end_of_slot( ) +def chunks(some_list, chunk_size): + chunk_size = max(1, chunk_size) + return (some_list[i : i + chunk_size] for i in range(0, len(some_list), chunk_size)) + + def compress_segments(full_segment_index, segments: List[SubEpochChallengeSegment]) -> List[SubEpochChallengeSegment]: compressed_segments = [] compressed_segments.append(segments[0]) @@ -961,6 +964,7 @@ def _validate_sub_epoch_segments( prev_ses: Optional[SubEpochSummary] = None segments_by_sub_epoch = map_segments_by_sub_epoch(sub_epoch_segments.challenge_segments) curr_ssi = constants.SUB_SLOT_ITERS_STARTING + vdfs_to_validate = [] for sub_epoch_n, segments in segments_by_sub_epoch.items(): prev_ssi = curr_ssi curr_difficulty, curr_ssi = _get_curr_diff_ssi(constants, sub_epoch_n, summaries) @@ -975,9 +979,10 @@ def _validate_sub_epoch_segments( log.error(f"failed reward_chain_hash validation sub_epoch {sub_epoch_n}") return False for idx, segment in enumerate(segments): - valid_segment, ip_iters, slot_iters, slots = _validate_segment( + valid_segment, ip_iters, slot_iters, slots, vdf_list = _validate_segment( constants, segment, curr_ssi, prev_ssi, curr_difficulty, prev_ses, idx == 0, sampled_seg_index == idx ) + vdfs_to_validate.extend(vdf_list) if not valid_segment: log.error(f"failed to validate sub_epoch {segment.sub_epoch_n} segment {idx} slots") return False @@ -986,7 +991,7 @@ def _validate_sub_epoch_segments( total_slot_iters += slot_iters total_slots += slots total_ip_iters += ip_iters - return True + return True, vdfs_to_validate def _validate_segment( @@ -998,37 +1003,40 @@ def _validate_segment( ses: Optional[SubEpochSummary], first_segment_in_se: bool, sampled: bool, -) -> Tuple[bool, int, int, int]: +) -> Tuple[bool, int, int, int, List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]]: ip_iters, slot_iters, slots = 0, 0, 0 after_challenge = False + to_validate = [] for idx, sub_slot_data in enumerate(segment.sub_slots): if sampled and sub_slot_data.is_challenge(): after_challenge = True required_iters = __validate_pospace(constants, segment, idx, curr_difficulty, ses, first_segment_in_se) if required_iters is None: - return False, uint64(0), uint64(0), uint64(0) + return False, uint64(0), uint64(0), uint64(0), [] assert sub_slot_data.signage_point_index is not None ip_iters = ip_iters + calculate_ip_iters( constants, curr_ssi, sub_slot_data.signage_point_index, required_iters ) - if not _validate_challenge_block_vdfs(constants, idx, segment.sub_slots, curr_ssi): - log.error(f"failed to validate challenge slot {idx} vdfs") - return False, uint64(0), uint64(0), uint64(0) + vdf_list = _get_challenge_block_vdfs(constants, idx, segment.sub_slots, curr_ssi) + to_validate.extend(vdf_list) elif sampled and after_challenge: - if not _validate_sub_slot_data(constants, idx, segment.sub_slots, curr_ssi): + validated, vdf_list = _validate_sub_slot_data(constants, idx, segment.sub_slots, curr_ssi) + if not validated: log.error(f"failed to validate sub slot data {idx} vdfs") - return False, uint64(0), uint64(0), uint64(0) + return False, uint64(0), uint64(0), uint64(0), [] + to_validate.extend(vdf_list) slot_iters = slot_iters + curr_ssi slots = slots + uint64(1) - return True, ip_iters, slot_iters, slots + return True, ip_iters, slot_iters, slots, to_validate -def _validate_challenge_block_vdfs( +def _get_challenge_block_vdfs( constants: ConsensusConstants, sub_slot_idx: int, sub_slots: List[SubSlotData], ssi: uint64, -) -> bool: +) -> List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]: + to_validate = [] sub_slot_data = sub_slots[sub_slot_idx] if sub_slot_data.cc_signage_point is not None and sub_slot_data.cc_sp_vdf_info: assert sub_slot_data.signage_point_index @@ -1039,9 +1047,8 @@ def _validate_challenge_block_vdfs( sp_input = sub_slot_data_vdf_input( constants, sub_slot_data, sub_slot_idx, sub_slots, is_overflow, prev_ssd.is_end_of_slot(), ssi ) - if not sub_slot_data.cc_signage_point.is_valid(constants, sp_input, sub_slot_data.cc_sp_vdf_info): - log.error(f"failed to validate challenge chain signage point 2 {sub_slot_data.cc_sp_vdf_info}") - return False + to_validate.append((sub_slot_data.cc_signage_point, sp_input, sub_slot_data.cc_sp_vdf_info)) + assert sub_slot_data.cc_infusion_point assert sub_slot_data.cc_ip_vdf_info ip_input = ClassgroupElement.get_default_element() @@ -1057,10 +1064,9 @@ def _validate_challenge_block_vdfs( cc_ip_vdf_info = VDFInfo( sub_slot_data.cc_ip_vdf_info.challenge, ip_vdf_iters, sub_slot_data.cc_ip_vdf_info.output ) - if not sub_slot_data.cc_infusion_point.is_valid(constants, ip_input, cc_ip_vdf_info): - log.error(f"failed to validate challenge chain infusion point {sub_slot_data.cc_ip_vdf_info}") - return False - return True + to_validate.append((sub_slot_data.cc_infusion_point, ip_input, cc_ip_vdf_info)) + + return to_validate def _validate_sub_slot_data( @@ -1068,10 +1074,12 @@ def _validate_sub_slot_data( sub_slot_idx: int, sub_slots: List[SubSlotData], ssi: uint64, -) -> bool: +) -> Tuple[bool, List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]]: + sub_slot_data = sub_slots[sub_slot_idx] assert sub_slot_idx > 0 prev_ssd = sub_slots[sub_slot_idx - 1] + to_validate = [] if sub_slot_data.is_end_of_slot(): if sub_slot_data.icc_slot_end is not None: input = ClassgroupElement.get_default_element() @@ -1079,9 +1087,7 @@ def _validate_sub_slot_data( assert prev_ssd.icc_ip_vdf_info input = prev_ssd.icc_ip_vdf_info.output assert sub_slot_data.icc_slot_end_info - if not sub_slot_data.icc_slot_end.is_valid(constants, input, sub_slot_data.icc_slot_end_info, None): - log.error(f"failed icc slot end validation {sub_slot_data.icc_slot_end_info} ") - return False + to_validate.append((sub_slot_data.icc_slot_end, input, sub_slot_data.icc_slot_end_info)) assert sub_slot_data.cc_slot_end_info assert sub_slot_data.cc_slot_end input = ClassgroupElement.get_default_element() @@ -1090,7 +1096,7 @@ def _validate_sub_slot_data( input = prev_ssd.cc_ip_vdf_info.output if not sub_slot_data.cc_slot_end.is_valid(constants, input, sub_slot_data.cc_slot_end_info): log.error(f"failed cc slot end validation {sub_slot_data.cc_slot_end_info}") - return False + return False, [] else: # find end of slot idx = sub_slot_idx @@ -1101,7 +1107,7 @@ def _validate_sub_slot_data( assert curr_slot.cc_slot_end if curr_slot.cc_slot_end.normalized_to_identity is True: log.debug(f"skip intermediate vdfs slot {sub_slot_idx}") - return True + return True, to_validate else: break idx += 1 @@ -1109,10 +1115,7 @@ def _validate_sub_slot_data( input = ClassgroupElement.get_default_element() if not prev_ssd.is_challenge() and prev_ssd.icc_ip_vdf_info is not None: input = prev_ssd.icc_ip_vdf_info.output - if not sub_slot_data.icc_infusion_point.is_valid(constants, input, sub_slot_data.icc_ip_vdf_info, None): - log.error(f"failed icc infusion point vdf validation {sub_slot_data.icc_slot_end_info} ") - return False - + to_validate.append((sub_slot_data.icc_infusion_point, input, sub_slot_data.icc_ip_vdf_info)) assert sub_slot_data.signage_point_index is not None if sub_slot_data.cc_signage_point: assert sub_slot_data.cc_sp_vdf_info @@ -1122,10 +1125,8 @@ def _validate_sub_slot_data( input = sub_slot_data_vdf_input( constants, sub_slot_data, sub_slot_idx, sub_slots, is_overflow, prev_ssd.is_end_of_slot(), ssi ) + to_validate.append((sub_slot_data.cc_signage_point, input, sub_slot_data.cc_sp_vdf_info)) - if not sub_slot_data.cc_signage_point.is_valid(constants, input, sub_slot_data.cc_sp_vdf_info): - log.error(f"failed cc signage point vdf validation {sub_slot_data.cc_sp_vdf_info}") - return False input = ClassgroupElement.get_default_element() assert sub_slot_data.cc_ip_vdf_info assert sub_slot_data.cc_infusion_point @@ -1139,10 +1140,9 @@ def _validate_sub_slot_data( cc_ip_vdf_info = VDFInfo( sub_slot_data.cc_ip_vdf_info.challenge, ip_vdf_iters, sub_slot_data.cc_ip_vdf_info.output ) - if not sub_slot_data.cc_infusion_point.is_valid(constants, input, cc_ip_vdf_info): - log.error(f"failed cc infusion point vdf validation {sub_slot_data.cc_slot_end_info}") - return False - return True + to_validate.append((sub_slot_data.cc_infusion_point, input, cc_ip_vdf_info)) + + return True, to_validate def sub_slot_data_vdf_input( @@ -1203,14 +1203,17 @@ def sub_slot_data_vdf_input( return cc_input -def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, summaries_bytes: List[bytes]) -> bool: - constants, summaries = bytes_to_vars(constants_dict, summaries_bytes) - recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes) +def validate_recent_blocks( + constants: ConsensusConstants, + recent_chain: RecentChainData, + summaries: List[SubEpochSummary], + shutdown_file_path: Optional[pathlib.Path] = None, +) -> Tuple[bool, List[bytes]]: sub_blocks = BlockCache({}) first_ses_idx = _get_ses_idx(recent_chain.recent_chain_data) ses_idx = len(summaries) - len(first_ses_idx) ssi: uint64 = constants.SUB_SLOT_ITERS_STARTING - diff: Optional[uint64] = constants.DIFFICULTY_STARTING + diff: uint64 = constants.DIFFICULTY_STARTING last_blocks_to_validate = 100 # todo remove cap after benchmarks for summary in summaries[:ses_idx]: if summary.new_sub_slot_iters is not None: @@ -1219,10 +1222,11 @@ def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, sum diff = summary.new_difficulty ses_blocks, sub_slots, transaction_blocks = 0, 0, 0 - challenge, prev_challenge = None, None + challenge, prev_challenge = recent_chain.recent_chain_data[0].reward_chain_block.pos_ss_cc_challenge_hash, None tip_height = recent_chain.recent_chain_data[-1].height prev_block_record = None deficit = uint8(0) + adjusted = False for idx, block in enumerate(recent_chain.recent_chain_data): required_iters = uint64(0) overflow = False @@ -1243,21 +1247,30 @@ def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, sum if (challenge is not None) and (prev_challenge is not None): overflow = is_overflow_block(constants, block.reward_chain_block.signage_point_index) + if not adjusted: + prev_block_record = dataclasses.replace( + prev_block_record, deficit=deficit % constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK + ) + assert prev_block_record is not None + sub_blocks.add_block_record(prev_block_record) + adjusted = True deficit = get_deficit(constants, deficit, prev_block_record, overflow, len(block.finished_sub_slots)) log.debug(f"wp, validate block {block.height}") if sub_slots > 2 and transaction_blocks > 11 and (tip_height - block.height < last_blocks_to_validate): - required_iters, error = validate_finished_header_block( + caluclated_required_iters, error = validate_finished_header_block( constants, sub_blocks, block, False, diff, ssi, ses_blocks > 2 ) if error is not None: log.error(f"block {block.header_hash} failed validation {error}") - return False + return False, [] + assert caluclated_required_iters is not None + required_iters = caluclated_required_iters else: required_iters = _validate_pospace_recent_chain( constants, block, challenge, diff, overflow, prev_challenge ) if required_iters is None: - return False + return False, [] curr_block_ses = None if not ses else summaries[ses_idx - 1] block_record = header_block_to_sub_block_record( @@ -1274,7 +1287,29 @@ def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, sum ses_blocks += 1 prev_block_record = block_record - return True + if shutdown_file_path is not None and not shutdown_file_path.is_file(): + log.info(f"cancelling block {block.header_hash} validation, shutdown requested") + return False, [] + + return True, [bytes(sub) for sub in sub_blocks._block_records.values()] + + +def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, summaries_bytes: List[bytes]) -> bool: + constants, summaries = bytes_to_vars(constants_dict, summaries_bytes) + recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes) + success, records = validate_recent_blocks(constants, recent_chain, summaries) + return success + + +def _validate_recent_blocks_and_get_records( + constants_dict: Dict, + recent_chain_bytes: bytes, + summaries_bytes: List[bytes], + shutdown_file_path: Optional[pathlib.Path] = None, +) -> Tuple[bool, List[bytes]]: + constants, summaries = bytes_to_vars(constants_dict, summaries_bytes) + recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes) + return validate_recent_blocks(constants, recent_chain, summaries, shutdown_file_path) def _validate_pospace_recent_chain( @@ -1473,7 +1508,7 @@ def _get_curr_diff_ssi(constants: ConsensusConstants, idx, summaries): return curr_difficulty, curr_ssi -def vars_to_bytes(constants, summaries, weight_proof): +def vars_to_bytes(constants: ConsensusConstants, summaries: List[SubEpochSummary], weight_proof: WeightProof): constants_dict = recurse_jsonify(dataclasses.asdict(constants)) wp_recent_chain_bytes = bytes(RecentChainData(weight_proof.recent_chain_data)) wp_segment_bytes = bytes(SubEpochSegments(weight_proof.sub_epoch_segments)) @@ -1524,13 +1559,13 @@ def _get_ses_idx(recent_reward_chain: List[HeaderBlock]) -> List[int]: def get_deficit( constants: ConsensusConstants, curr_deficit: uint8, - prev_block: BlockRecord, + prev_block: Optional[BlockRecord], overflow: bool, num_finished_sub_slots: int, ) -> uint8: if prev_block is None: if curr_deficit >= 1 and not (overflow and curr_deficit == constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK): - curr_deficit -= 1 + curr_deficit = uint8(curr_deficit - 1) return curr_deficit return calculate_deficit(constants, uint32(prev_block.height + 1), prev_block, overflow, num_finished_sub_slots) @@ -1617,3 +1652,22 @@ def validate_total_iters( total_iters = uint128(prev_b.total_iters - prev_b.cc_ip_vdf_info.number_of_iterations) total_iters = uint128(total_iters + sub_slot_data.cc_ip_vdf_info.number_of_iterations) return total_iters == sub_slot_data.total_iters + + +def _validate_vdf_batch( + constants_dict, vdf_list: List[Tuple[bytes, bytes, bytes]], shutdown_file_path: Optional[pathlib.Path] = None +): + constants: ConsensusConstants = dataclass_from_dict(ConsensusConstants, constants_dict) + + for vdf_proof_bytes, class_group_bytes, info in vdf_list: + vdf = VDFProof.from_bytes(vdf_proof_bytes) + class_group = ClassgroupElement.from_bytes(class_group_bytes) + vdf_info = VDFInfo.from_bytes(info) + if not vdf.is_valid(constants, class_group, vdf_info): + return False + + if shutdown_file_path is not None and not shutdown_file_path.is_file(): + log.info("cancelling VDF validation, shutdown requested") + return False + + return True diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 918b2db888df..0db6efef914c 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -4,7 +4,6 @@ from blspy import PrivateKey, G2Element, G1Element -from chia.consensus.block_record import BlockRecord from chia.pools.pool_config import PoolWalletConfig, load_pool_config, update_pool_config from chia.pools.pool_wallet_info import ( PoolWalletInfo, @@ -265,39 +264,35 @@ def get_next_interesting_coin_ids(spend: CoinSpend) -> List[bytes32]: return [coin.name()] return [] - async def apply_state_transitions(self, block_spends: List[CoinSpend], block_height: uint32): + 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. """ - 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: + 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 - 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) async def rewind(self, block_height: int) -> bool: @@ -313,11 +308,6 @@ async def rewind(self, block_height: int) -> bool: await self.wallet_state_manager.pool_store.rollback(block_height, self.wallet_id) if len(history) > 0 and history[0][0] > block_height: - # If we have no entries in the DB, we have no singleton, so we should not have a wallet either - # The PoolWallet object becomes invalid after this. - await self.wallet_state_manager.interested_store.remove_interested_puzzle_hash( - prev_state.p2_singleton_puzzle_hash, in_transaction=True - ) return True else: if await self.get_current_state() != prev_state: @@ -362,12 +352,8 @@ async def create( await self.update_pool_config(True) p2_puzzle_hash: bytes32 = (await self.get_current_state()).p2_singleton_puzzle_hash - await self.wallet_state_manager.interested_store.add_interested_puzzle_hash( - p2_puzzle_hash, self.wallet_id, True - ) - + await self.wallet_state_manager.add_interested_puzzle_hash(p2_puzzle_hash, self.wallet_id, False) await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, create_puzzle_hashes=False) - self.wallet_state_manager.set_new_peak_callback(self.wallet_id, self.new_peak) return self @staticmethod @@ -388,7 +374,6 @@ async def create_from_db( self.wallet_info = wallet_info self.target_state = None self.log = logging.getLogger(name if name else __name__) - self.wallet_state_manager.set_new_peak_callback(self.wallet_id, self.new_peak) return self @staticmethod @@ -452,6 +437,7 @@ async def create_new_pool_wallet_transaction( 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(), @@ -567,6 +553,7 @@ async def generate_travel_transaction(self, fee: uint64) -> TransactionRecord: wallet_id=self.id(), sent_to=[], trade_id=None, + memos=[], type=uint32(TransactionType.OUTGOING_TX.value), name=signed_spend_bundle.name(), ) @@ -585,7 +572,6 @@ async def generate_launcher_spend( Creates the initial singleton, which includes spending an origin coin, the launcher, and creating a singleton with the "pooling" inner state, which can be either self pooling or using a pool """ - coins: Set[Coin] = await standard_wallet.select_coins(amount) if coins is None: raise ValueError("Not enough coins to create pool wallet") @@ -627,9 +613,9 @@ async def generate_launcher_spend( puzzle_hash: bytes32 = full_pooling_puzzle.get_tree_hash() pool_state_bytes = Program.to([("p", bytes(initial_target_state)), ("t", delay_time), ("h", delay_ph)]) - announcement_set: Set[bytes32] = set() + announcement_set: Set[Announcement] = set() announcement_message = Program.to([puzzle_hash, amount, pool_state_bytes]).get_tree_hash() - announcement_set.add(Announcement(launcher_coin.name(), announcement_message).name()) + announcement_set.add(Announcement(launcher_coin.name(), announcement_message)) create_launcher_tx_record: Optional[TransactionRecord] = await standard_wallet.generate_signed_transaction( amount, @@ -803,6 +789,7 @@ async def claim_pool_rewards(self, fee: uint64) -> TransactionRecord: removals=spend_bundle.removals(), wallet_id=uint32(self.wallet_id), sent_to=[], + memos=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), @@ -810,7 +797,7 @@ async def claim_pool_rewards(self, fee: uint64) -> TransactionRecord: await self.wallet_state_manager.add_pending_transaction(absorb_transaction) return absorb_transaction - async def new_peak(self, peak: BlockRecord) -> None: + async def new_peak(self, peak_height: uint64) -> None: # This gets called from the WalletStateManager whenever there is a new peak pool_wallet_info: PoolWalletInfo = await self.get_current_state() @@ -828,14 +815,8 @@ async def new_peak(self, peak: BlockRecord) -> None: ): 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: + if peak_height > leave_height + 2: unconfirmed: List[ TransactionRecord ] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.wallet_id) diff --git a/chia/rpc/full_node_rpc_api.py b/chia/rpc/full_node_rpc_api.py index b37cbe51010c..207439149254 100644 --- a/chia/rpc/full_node_rpc_api.py +++ b/chia/rpc/full_node_rpc_api.py @@ -556,10 +556,8 @@ async def get_puzzle_and_solution(self, request: Dict) -> Optional[Dict]: raise ValueError(f"Invalid height {height}. coin record {coin_record}") header_hash = self.service.blockchain.height_to_hash(height) - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block" of "BlockStore" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - block: Optional[FullBlock] = await self.service.block_store.get_full_block(header_hash) # type: ignore[arg-type] # noqa: E501 + assert header_hash is not None + block: Optional[FullBlock] = await self.service.block_store.get_full_block(header_hash) if block is None or block.transactions_generator is None: raise ValueError("Invalid block or block generator") diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 0843bbac9eef..7b1d81a47f70 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1,10 +1,10 @@ import asyncio import logging -import time from pathlib import Path -from typing import Callable, Dict, List, Optional, Tuple, Set +from typing import Callable, Dict, List, Optional, Tuple, Set, Any from blspy import PrivateKey, G1Element +from clvm_tools import binutils from chia.consensus.block_rewards import calculate_base_farmer_reward from chia.pools.pool_wallet import PoolWallet @@ -12,6 +12,8 @@ from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import NodeType, make_msg from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.announcement import Announcement +from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash @@ -20,15 +22,15 @@ from chia.util.keychain import KeyringIsLocked, bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root from chia.util.ws_message import WsRpcMessage, create_payload_dict -from chia.wallet.cc_wallet.cc_wallet import CCWallet -from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk +from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS +from chia.wallet.cat_wallet.cat_wallet import CATWallet +from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk, master_sk_to_wallet_sk_unhardened from chia.wallet.rl_wallet.rl_wallet import RLWallet from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_pool_sk, master_sk_to_wallet_sk from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.trade_record import TradeRecord +from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.backup_utils import download_backup, get_backup_info, upload_backup -from chia.wallet.util.trade_utils import trade_record_to_dict from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType from chia.wallet.wallet_info import WalletInfo @@ -47,6 +49,7 @@ def __init__(self, wallet_node: WalletNode): assert wallet_node is not None self.service = wallet_node self.service_name = "chia_wallet" + self.balance_cache: Dict[int, Any] = {} def get_routes(self) -> Dict[str, Callable]: return { @@ -75,25 +78,27 @@ def get_routes(self) -> Dict[str, Callable]: "/get_wallet_balance": self.get_wallet_balance, "/get_transaction": self.get_transaction, "/get_transactions": self.get_transactions, + "/get_transaction_count": self.get_transaction_count, "/get_next_address": self.get_next_address, "/send_transaction": self.send_transaction, "/send_transaction_multi": self.send_transaction_multi, - "/create_backup": self.create_backup, - "/get_transaction_count": self.get_transaction_count, "/get_farmed_amount": self.get_farmed_amount, "/create_signed_transaction": self.create_signed_transaction, "/delete_unconfirmed_transactions": self.delete_unconfirmed_transactions, # Coloured coins and trading - "/cc_set_name": self.cc_set_name, - "/cc_get_name": self.cc_get_name, - "/cc_spend": self.cc_spend, - "/cc_get_colour": self.cc_get_colour, + "/cat_set_name": self.cat_set_name, + "/cat_asset_id_to_name": self.cat_asset_id_to_name, + "/cat_get_name": self.cat_get_name, + "/cat_spend": self.cat_spend, + "/cat_get_asset_id": self.cat_get_asset_id, "/create_offer_for_ids": self.create_offer_for_ids, - "/get_discrepancies_for_offer": self.get_discrepancies_for_offer, - "/respond_to_offer": self.respond_to_offer, - "/get_trade": self.get_trade, - "/get_all_trades": self.get_all_trades, - "/cancel_trade": self.cancel_trade, + "/get_offer_summary": self.get_offer_summary, + "/check_offer_validity": self.check_offer_validity, + "/take_offer": self.take_offer, + "/get_offer": self.get_offer, + "/get_all_offers": self.get_all_offers, + "/cancel_offer": self.cancel_offer, + "/get_cat_list": self.get_cat_list, # DID Wallet "/did_update_recovery_ids": self.did_update_recovery_ids, "/did_get_pubkey": self.did_get_pubkey, @@ -138,7 +143,9 @@ async def _stop_wallet(self): """ if self.service is not None: self.service._close() - await self.service._await_closed() + peers_close_task: Optional[asyncio.Task] = await self.service._await_closed() + if peers_close_task is not None: + await peers_close_task ########################################################################################## # Key management @@ -154,45 +161,9 @@ async def log_in(self, request): return {"fingerprint": fingerprint} await self._stop_wallet() - log_in_type = request["type"] - recovery_host = request["host"] - testing = False - - if "testing" in self.service.config and self.service.config["testing"] is True: - testing = True - if log_in_type == "skip": - started = await self.service._start(fingerprint=fingerprint, skip_backup_import=True) - elif log_in_type == "restore_backup": - file_path = Path(request["file_path"]) - started = await self.service._start(fingerprint=fingerprint, backup_file=file_path) - else: - started = await self.service._start(fingerprint) - + started = await self.service._start(fingerprint) if started is True: return {"fingerprint": fingerprint} - elif testing is True and self.service.backup_initialized is False: - response = {"success": False, "error": "not_initialized"} - return response - elif self.service.backup_initialized is False: - backup_info = None - backup_path = None - try: - private_key = await self.service.get_key_for_fingerprint(fingerprint) - last_recovery = await download_backup(recovery_host, private_key) - backup_path = path_from_root(self.service.root_path, "last_recovery") - if backup_path.exists(): - backup_path.unlink() - backup_path.write_text(last_recovery) - backup_info = get_backup_info(backup_path, private_key) - backup_info["backup_host"] = recovery_host - backup_info["downloaded"] = True - except Exception as e: - log.error(f"error {e}") - response = {"success": False, "error": "not_initialized"} - if backup_info is not None: - response["backup_info"] = backup_info - response["backup_path"] = f"{backup_path}" - return response return {"success": False, "error": "Unknown Error"} @@ -270,15 +241,7 @@ async def add_key(self, request): await self.service.keychain_proxy.check_keys(self.service.root_path) except Exception as e: log.error(f"Failed to check_keys after adding a new key: {e}") - request_type = request["type"] - if request_type == "new_wallet": - started = await self.service._start(fingerprint=fingerprint, new_wallet=True) - elif request_type == "skip": - started = await self.service._start(fingerprint=fingerprint, skip_backup_import=True) - elif request_type == "restore_backup": - file_path = Path(request["file_path"]) - started = await self.service._start(fingerprint=fingerprint, backup_file=file_path) - + started = await self.service._start(fingerprint=fingerprint) if started is True: return {"fingerprint": fingerprint} raise ValueError("Failed to start") @@ -322,12 +285,17 @@ async def _check_key_used_for_rewards( if found_farmer and found_pool: break - ph = encode_puzzle_hash(create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(i)).get_g1()), prefix) - - if ph == farmer_target: - found_farmer = True - if ph == pool_target: - found_pool = True + phs = [ + encode_puzzle_hash(create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(i)).get_g1()), prefix), + encode_puzzle_hash( + create_puzzlehash_for_pk(master_sk_to_wallet_sk_unhardened(sk, uint32(i)).get_g1()), prefix + ), + ] + for ph in phs: + if ph == farmer_target: + found_farmer = True + if ph == pool_target: + found_pool = True return found_farmer, found_pool @@ -347,19 +315,18 @@ async def check_delete_key(self, request): if self.service.logged_in_fingerprint != fingerprint: await self._stop_wallet() - await self.service._start(fingerprint=fingerprint, skip_backup_import=True) + await self.service._start(fingerprint=fingerprint) - async with self.service.wallet_state_manager.lock: - wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries() - for w in wallets: - wallet = self.service.wallet_state_manager.wallets[w.id] - unspent = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(w.id) - balance = await wallet.get_confirmed_balance(unspent) - pending_balance = await wallet.get_unconfirmed_balance(unspent) + wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries() + for w in wallets: + wallet = self.service.wallet_state_manager.wallets[w.id] + unspent = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(w.id) + balance = await wallet.get_confirmed_balance(unspent) + pending_balance = await wallet.get_unconfirmed_balance(unspent) - if (balance + pending_balance) > 0: - walletBalance = True - break + if (balance + pending_balance) > 0: + walletBalance = True + break return { "fingerprint": fingerprint, @@ -393,11 +360,8 @@ async def get_sync_status(self, request: Dict): async def get_height_info(self, request: Dict): assert self.service.wallet_state_manager is not None - peak = self.service.wallet_state_manager.peak - if peak is None: - return {"height": 0} - else: - return {"height": peak.height} + height = self.service.wallet_state_manager.blockchain.get_peak_height() + return {"height": height} async def get_network_info(self, request: Dict): assert self.service.wallet_state_manager is not None @@ -424,53 +388,37 @@ async def get_wallets(self, request: Dict): return {"wallets": wallets} - async def _create_backup_and_upload(self, host) -> None: - assert self.service.wallet_state_manager is not None - try: - if "testing" in self.service.config and self.service.config["testing"] is True: - return None - now = time.time() - file_name = f"backup_{now}" - path = path_from_root(self.service.root_path, file_name) - await self.service.wallet_state_manager.create_wallet_backup(path) - backup_text = path.read_text() - response = await upload_backup(host, backup_text) - success = response["success"] - if success is False: - log.error("Failed to upload backup to wallet backup service") - elif success is True: - log.info("Finished upload of the backup file") - except Exception as e: - log.error(f"Exception in upload backup. Error: {e}") - async def create_new_wallet(self, request: Dict): assert self.service.wallet_state_manager is not None wallet_state_manager = self.service.wallet_state_manager + + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced.") main_wallet = wallet_state_manager.main_wallet - host = request["host"] fee = uint64(request.get("fee", 0)) - if request["wallet_type"] == "cc_wallet": + if request["wallet_type"] == "cat_wallet": + name = request.get("name", "CAT Wallet") if request["mode"] == "new": async with self.service.wallet_state_manager.lock: - cc_wallet: CCWallet = await CCWallet.create_new_cc( - wallet_state_manager, main_wallet, uint64(request["amount"]) + cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_state_manager, + main_wallet, + {"identifier": "genesis_by_id"}, + uint64(request["amount"]), + name, ) - colour = cc_wallet.get_colour() - asyncio.create_task(self._create_backup_and_upload(host)) - return { - "type": cc_wallet.type(), - "colour": colour, - "wallet_id": cc_wallet.id(), - } + asset_id = cat_wallet.get_asset_id() + self.service.wallet_state_manager.state_changed("wallet_created") + return {"type": cat_wallet.type(), "asset_id": asset_id, "wallet_id": cat_wallet.id()} elif request["mode"] == "existing": async with self.service.wallet_state_manager.lock: - cc_wallet = await CCWallet.create_wallet_for_cc( - wallet_state_manager, main_wallet, request["colour"] + cat_wallet = await CATWallet.create_wallet_for_cat( + wallet_state_manager, main_wallet, request["asset_id"] ) - asyncio.create_task(self._create_backup_and_upload(host)) - return {"type": cc_wallet.type()} + self.service.wallet_state_manager.state_changed("wallet_created") + return {"type": cat_wallet.type(), "asset_id": request["asset_id"], "wallet_id": cat_wallet.id()} else: # undefined mode pass @@ -487,7 +435,6 @@ async def create_new_wallet(self, request: Dict): uint64(int(request["amount"])), uint64(int(request["fee"])) if "fee" in request else uint64(0), ) - asyncio.create_task(self._create_backup_and_upload(host)) assert rl_admin.rl_info.admin_pubkey is not None return { "success": success, @@ -501,7 +448,6 @@ async def create_new_wallet(self, request: Dict): log.info("Create rl user wallet") async with self.service.wallet_state_manager.lock: rl_user: RLWallet = await RLWallet.create_rl_user(wallet_state_manager) - asyncio.create_task(self._create_backup_and_upload(host)) assert rl_user.rl_info.user_pubkey is not None return { "id": rl_user.id(), @@ -623,28 +569,48 @@ async def get_wallet_balance(self, request: Dict) -> Dict: assert self.service.wallet_state_manager is not None wallet_id = uint32(int(request["wallet_id"])) wallet = self.service.wallet_state_manager.wallets[wallet_id] - async with self.service.wallet_state_manager.lock: - unspent_records = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(wallet_id) - balance = await wallet.get_confirmed_balance(unspent_records) - pending_balance = await wallet.get_unconfirmed_balance(unspent_records) - spendable_balance = await wallet.get_spendable_balance(unspent_records) - pending_change = await wallet.get_pending_change_balance() - max_send_amount = await wallet.get_max_send_amount(unspent_records) - - unconfirmed_removals: Dict[ - bytes32, Coin - ] = await wallet.wallet_state_manager.unconfirmed_removals_for_wallet(wallet_id) - - wallet_balance = { - "wallet_id": wallet_id, - "confirmed_wallet_balance": balance, - "unconfirmed_wallet_balance": pending_balance, - "spendable_balance": spendable_balance, - "pending_change": pending_change, - "max_send_amount": max_send_amount, - "unspent_coin_count": len(unspent_records), - "pending_coin_removal_count": len(unconfirmed_removals), - } + + # If syncing return the last available info or 0s + syncing = self.service.wallet_state_manager.sync_mode + if syncing: + if wallet_id in self.balance_cache: + wallet_balance = self.balance_cache[wallet_id] + else: + wallet_balance = { + "wallet_id": wallet_id, + "confirmed_wallet_balance": 0, + "unconfirmed_wallet_balance": 0, + "spendable_balance": 0, + "pending_change": 0, + "max_send_amount": 0, + "unspent_coin_count": 0, + "pending_coin_removal_count": 0, + } + else: + async with self.service.wallet_state_manager.lock: + unspent_records = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet( + wallet_id + ) + balance = await wallet.get_confirmed_balance(unspent_records) + pending_balance = await wallet.get_unconfirmed_balance(unspent_records) + spendable_balance = await wallet.get_spendable_balance(unspent_records) + pending_change = await wallet.get_pending_change_balance() + max_send_amount = await wallet.get_max_send_amount(unspent_records) + + unconfirmed_removals: Dict[ + bytes32, Coin + ] = await wallet.wallet_state_manager.unconfirmed_removals_for_wallet(wallet_id) + wallet_balance = { + "wallet_id": wallet_id, + "confirmed_wallet_balance": balance, + "unconfirmed_wallet_balance": pending_balance, + "spendable_balance": spendable_balance, + "pending_change": pending_change, + "max_send_amount": max_send_amount, + "unspent_coin_count": len(unspent_records), + "pending_coin_removal_count": len(unconfirmed_removals), + } + self.balance_cache[wallet_id] = wallet_balance return {"wallet_balance": wallet_balance} @@ -656,7 +622,7 @@ async def get_transaction(self, request: Dict) -> Dict: raise ValueError(f"Transaction 0x{transaction_id.hex()} not found") return { - "transaction": tr, + "transaction": tr.to_json_dict_convenience(self.service.config), "transaction_id": tr.name, } @@ -673,15 +639,18 @@ async def get_transactions(self, request: Dict) -> Dict: transactions = await self.service.wallet_state_manager.tx_store.get_transactions_between( wallet_id, start, end, sort_key=sort_key, reverse=reverse ) - formatted_transactions = [] - selected = self.service.config["selected_network"] - prefix = self.service.config["network_overrides"]["config"][selected]["address_prefix"] - for tx in transactions: - formatted = tx.to_json_dict() - formatted["to_address"] = encode_puzzle_hash(tx.to_puzzle_hash, prefix) - formatted_transactions.append(formatted) return { - "transactions": formatted_transactions, + "transactions": [tr.to_json_dict_convenience(self.service.config) for tr in transactions], + "wallet_id": wallet_id, + } + + async def get_transaction_count(self, request: Dict) -> Dict: + assert self.service.wallet_state_manager is not None + + wallet_id = int(request["wallet_id"]) + count = await self.service.wallet_state_manager.tx_store.get_transaction_count_for_wallet(wallet_id) + return { + "count": count, "wallet_id": wallet_id, } @@ -708,8 +677,8 @@ async def get_next_address(self, request: Dict) -> Dict: if wallet.type() == WalletType.STANDARD_WALLET: raw_puzzle_hash = await wallet.get_puzzle_hash(create_new) address = encode_puzzle_hash(raw_puzzle_hash, prefix) - elif wallet.type() == WalletType.COLOURED_COIN: - raw_puzzle_hash = await wallet.get_puzzle_hash(create_new) + elif wallet.type() == WalletType.CAT: + raw_puzzle_hash = await wallet.standard_wallet.get_puzzle_hash(create_new) address = encode_puzzle_hash(raw_puzzle_hash, prefix) else: raise ValueError(f"Wallet type {wallet.type()} cannot create puzzle hashes") @@ -728,25 +697,33 @@ async def send_transaction(self, request): wallet_id = int(request["wallet_id"]) wallet = self.service.wallet_state_manager.wallets[wallet_id] + if wallet.type() == WalletType.CAT: + raise ValueError("send_transaction does not work for CAT wallets") + if not isinstance(request["amount"], int) or not isinstance(request["fee"], int): raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) puzzle_hash: bytes32 = decode_puzzle_hash(request["address"]) + + memos: List[bytes] = [] + if "memos" in request: + memos = [mem.encode("utf-8") for mem in request["memos"]] + if "fee" in request: fee = uint64(request["fee"]) else: fee = uint64(0) async with self.service.wallet_state_manager.lock: - tx: TransactionRecord = await wallet.generate_signed_transaction(amount, puzzle_hash, fee) + tx: TransactionRecord = await wallet.generate_signed_transaction(amount, puzzle_hash, fee, memos=memos) await wallet.push_transaction(tx) # Transaction may not have been included in the mempool yet. Use get_transaction to check. return { - "transaction": tx, + "transaction": tx.to_json_dict_convenience(self.service.config), "transaction_id": tx.name, } - async def send_transaction_multi(self, request): + async def send_transaction_multi(self, request) -> Dict: assert self.service.wallet_state_manager is not None if await self.service.wallet_state_manager.synced() is False: @@ -756,21 +733,20 @@ async def send_transaction_multi(self, request): wallet = self.service.wallet_state_manager.wallets[wallet_id] async with self.service.wallet_state_manager.lock: - transaction: TransactionRecord = (await self.create_signed_transaction(request, hold_lock=False))[ - "signed_tx" - ] - await wallet.push_transaction(transaction) + transaction: Dict = (await self.create_signed_transaction(request, hold_lock=False))["signed_tx"] + tr: TransactionRecord = TransactionRecord.from_json_dict_convenience(transaction) + await wallet.push_transaction(tr) # Transaction may not have been included in the mempool yet. Use get_transaction to check. - return { - "transaction": transaction, - "transaction_id": transaction.name, - } + return {"transaction": transaction, "transaction_id": tr.name} async def delete_unconfirmed_transactions(self, request): wallet_id = uint32(request["wallet_id"]) if wallet_id not in self.service.wallet_state_manager.wallets: raise ValueError(f"Wallet id {wallet_id} does not exist") + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced.") + async with self.service.wallet_state_manager.lock: async with self.service.wallet_state_manager.tx_store.db_wrapper.lock: await self.service.wallet_state_manager.tx_store.db_wrapper.begin_transaction() @@ -782,41 +758,48 @@ async def delete_unconfirmed_transactions(self, request): await self.service.wallet_state_manager.tx_store.rebuild_tx_cache() return {} - async def get_transaction_count(self, request): - wallet_id = int(request["wallet_id"]) - count = await self.service.wallet_state_manager.tx_store.get_transaction_count_for_wallet(wallet_id) - return {"wallet_id": wallet_id, "count": count} - - async def create_backup(self, request): - assert self.service.wallet_state_manager is not None - file_path = Path(request["file_path"]) - await self.service.wallet_state_manager.create_wallet_backup(file_path) - return {} - ########################################################################################## # Coloured Coins and Trading ########################################################################################## - async def cc_set_name(self, request): + async def get_cat_list(self, request): + return {"cat_list": list(DEFAULT_CATS.values())} + + async def cat_set_name(self, request): assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) - wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] + wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id] await wallet.set_name(str(request["name"])) return {"wallet_id": wallet_id} - async def cc_get_name(self, request): + async def cat_get_name(self, request): assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) - wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] + wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id] name: str = await wallet.get_name() return {"wallet_id": wallet_id, "name": name} - async def cc_spend(self, request): + async def cat_asset_id_to_name(self, request): assert self.service.wallet_state_manager is not None + wallet = await self.service.wallet_state_manager.get_wallet_for_asset_id(request["asset_id"]) + if wallet is None: + raise ValueError("The asset ID specified does not belong to a wallet") + else: + return {"wallet_id": wallet.id(), "name": (await wallet.get_name())} + + async def cat_spend(self, request): + assert self.service.wallet_state_manager is not None + + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced.") wallet_id = int(request["wallet_id"]) - wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] + wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id] + puzzle_hash: bytes32 = decode_puzzle_hash(request["inner_address"]) + memos: List[bytes] = [] + if "memos" in request: + memos = [mem.encode("utf-8") for mem in request["memos"]] if not isinstance(request["amount"], int) or not isinstance(request["amount"], int): raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) @@ -825,129 +808,133 @@ async def cc_spend(self, request): else: fee = uint64(0) async with self.service.wallet_state_manager.lock: - tx: TransactionRecord = await wallet.generate_signed_transaction([amount], [puzzle_hash], fee) - await wallet.push_transaction(tx) + txs: TransactionRecord = await wallet.generate_signed_transaction( + [amount], [puzzle_hash], fee, memos=[memos] + ) + for tx in txs: + await wallet.standard_wallet.push_transaction(tx) return { - "transaction": tx, + "transaction": tx.to_json_dict_convenience(self.service.config), "transaction_id": tx.name, } - async def cc_get_colour(self, request): + async def cat_get_asset_id(self, request): assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) - wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] - colour: str = wallet.get_colour() - return {"colour": colour, "wallet_id": wallet_id} + wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id] + asset_id: str = wallet.get_asset_id() + return {"asset_id": asset_id, "wallet_id": wallet_id} + + async def get_offer_summary(self, request): + assert self.service.wallet_state_manager is not None + offer_hex: str = request["offer"] + offer = Offer.from_bytes(hexstr_to_bytes(offer_hex)) + offered, requested = offer.summary() + + return {"summary": {"offered": offered, "requested": requested}} async def create_offer_for_ids(self, request): assert self.service.wallet_state_manager is not None - offer = request["ids"] - file_name = request["filename"] + offer: Dict[str, int] = request["offer"] + fee: uint64 = uint64(request.get("fee", 0)) + validate_only: bool = request.get("validate_only", False) + + modified_offer = {} + for key in offer: + modified_offer[int(key)] = offer[key] + async with self.service.wallet_state_manager.lock: ( success, - spend_bundle, + trade_record, error, - ) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids(offer, file_name) + ) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids( + modified_offer, fee=fee, validate_only=validate_only + ) if success: - self.service.wallet_state_manager.trade_manager.write_offer_to_disk(Path(file_name), spend_bundle) - return {} + return { + "offer": trade_record.offer.hex(), + "trade_record": trade_record.to_json_dict_convenience(), + } raise ValueError(error) - async def get_discrepancies_for_offer(self, request): + async def check_offer_validity(self, request): assert self.service.wallet_state_manager is not None - file_name = request["filename"] - file_path = Path(file_name) - async with self.service.wallet_state_manager.lock: - ( - success, - discrepancies, - error, - ) = await self.service.wallet_state_manager.trade_manager.get_discrepancies_for_offer(file_path) + offer_hex: str = request["offer"] + offer = Offer.from_bytes(hexstr_to_bytes(offer_hex)) - if success: - return {"discrepancies": discrepancies} - raise ValueError(error) + return {"valid": (await self.service.wallet_state_manager.trade_manager.check_offer_validity(offer))} - async def respond_to_offer(self, request): + async def take_offer(self, request): assert self.service.wallet_state_manager is not None - file_path = Path(request["filename"]) + offer_hex = request["offer"] + offer = Offer.from_bytes(hexstr_to_bytes(offer_hex)) + fee: uint64 = uint64(request.get("fee", 0)) + async with self.service.wallet_state_manager.lock: ( success, trade_record, error, - ) = await self.service.wallet_state_manager.trade_manager.respond_to_offer(file_path) + ) = await self.service.wallet_state_manager.trade_manager.respond_to_offer(offer, fee=fee) if not success: raise ValueError(error) - return {} + return {"trade_record": trade_record.to_json_dict_convenience()} - async def get_trade(self, request: Dict): + async def get_offer(self, request: Dict): assert self.service.wallet_state_manager is not None trade_mgr = self.service.wallet_state_manager.trade_manager trade_id = bytes32.from_hexstr(request["trade_id"]) - trade: Optional[TradeRecord] = await trade_mgr.get_trade_by_id(trade_id) - if trade is None: + file_contents: bool = request.get("file_contents", False) + trade_record: Optional[TradeRecord] = await trade_mgr.get_trade_by_id(bytes32(trade_id)) + if trade_record is None: raise ValueError(f"No trade with trade id: {trade_id.hex()}") - result = trade_record_to_dict(trade) - return {"trade": result} + offer_to_return: bytes = trade_record.offer if trade_record.taken_offer is None else trade_record.taken_offer + offer_value: Optional[str] = offer_to_return.hex() if file_contents else None + return {"trade_record": trade_record.to_json_dict_convenience(), "offer": offer_value} - async def get_all_trades(self, request: Dict): + async def get_all_offers(self, request: Dict): assert self.service.wallet_state_manager is not None trade_mgr = self.service.wallet_state_manager.trade_manager - all_trades = await trade_mgr.get_all_trades() + start: int = request.get("start", 0) + end: int = request.get("end", 50) + sort_key: Optional[str] = request.get("sort_key", None) + reverse: bool = request.get("reverse", False) + file_contents: bool = request.get("file_contents", False) + + all_trades = await trade_mgr.trade_store.get_trades_between(start, end, sort_key=sort_key, reverse=reverse) result = [] + offer_values: Optional[List[str]] = [] if file_contents else None for trade in all_trades: - result.append(trade_record_to_dict(trade)) + result.append(trade.to_json_dict_convenience()) + if file_contents and offer_values is not None: + offer_to_return: bytes = trade.offer if trade.taken_offer is None else trade.taken_offer + offer_values.append(offer_to_return.hex()) - return {"trades": result} + return {"trade_records": result, "offers": offer_values} - async def cancel_trade(self, request: Dict): + async def cancel_offer(self, request: Dict): assert self.service.wallet_state_manager is not None wsm = self.service.wallet_state_manager secure = request["secure"] trade_id = bytes32.from_hexstr(request["trade_id"]) + fee: uint64 = uint64(request.get("fee", 0)) async with self.service.wallet_state_manager.lock: if secure: - await wsm.trade_manager.cancel_pending_offer_safely(trade_id) + await wsm.trade_manager.cancel_pending_offer_safely(bytes32(trade_id), fee=fee) else: - await wsm.trade_manager.cancel_pending_offer(trade_id) + await wsm.trade_manager.cancel_pending_offer(bytes32(trade_id)) return {} - async def get_backup_info(self, request: Dict): - file_path = Path(request["file_path"]) - sk = None - if "words" in request: - mnemonic = request["words"] - passphrase = "" - try: - assert self.service.keychain_proxy is not None # An offering to the mypy gods - sk = await self.service.keychain_proxy.add_private_key(" ".join(mnemonic), passphrase) - except KeyError as e: - return { - "success": False, - "error": f"The word '{e.args[0]}' is incorrect.'", - "word": e.args[0], - } - except Exception as e: - return {"success": False, "error": str(e)} - elif "fingerprint" in request: - sk, seed = await self._get_private_key(request["fingerprint"]) - - if sk is None: - raise ValueError("Unable to decrypt the backup file.") - backup_info = get_backup_info(file_path, sk) - return {"backup_info": backup_info} - ########################################################################################## # Distributed Identities ########################################################################################## @@ -1156,26 +1143,30 @@ async def get_farmed_amount(self, request): "last_height_farmed": last_height_farmed, } - async def create_signed_transaction(self, request, hold_lock=True): + async def create_signed_transaction(self, request, hold_lock=True) -> Dict: + assert self.service.wallet_state_manager is not None if "additions" not in request or len(request["additions"]) < 1: raise ValueError("Specify additions list") additions: List[Dict] = request["additions"] amount_0: uint64 = uint64(additions[0]["amount"]) assert amount_0 <= self.service.constants.MAX_COIN_AMOUNT - puzzle_hash_0 = hexstr_to_bytes(additions[0]["puzzle_hash"]) + puzzle_hash_0 = bytes32.from_hexstr(additions[0]["puzzle_hash"]) if len(puzzle_hash_0) != 32: - raise ValueError(f"Address must be 32 bytes. {puzzle_hash_0}") + raise ValueError(f"Address must be 32 bytes. {puzzle_hash_0.hex()}") + + memos_0 = None if "memos" not in additions[0] else [mem.encode("utf-8") for mem in additions[0]["memos"]] additional_outputs: List[AmountWithPuzzlehash] = [] for addition in additions[1:]: - receiver_ph = hexstr_to_bytes(addition["puzzle_hash"]) + receiver_ph = bytes32.from_hexstr(addition["puzzle_hash"]) if len(receiver_ph) != 32: - raise ValueError(f"Address must be 32 bytes. {receiver_ph}") + raise ValueError(f"Address must be 32 bytes. {receiver_ph.hex()}") amount = uint64(addition["amount"]) if amount > self.service.constants.MAX_COIN_AMOUNT: raise ValueError(f"Coin amount cannot exceed {self.service.constants.MAX_COIN_AMOUNT}") - additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount}) + memos = [] if "memos" not in addition else [mem.encode("utf-8") for mem in addition["memos"]] + additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount, "memos": memos}) fee = uint64(0) if "fee" in request: @@ -1185,36 +1176,62 @@ async def create_signed_transaction(self, request, hold_lock=True): if "coins" in request and len(request["coins"]) > 0: coins = set([Coin.from_json_dict(coin_json) for coin_json in request["coins"]]) - coin_announcements: Optional[Set[bytes32]] = None + coin_announcements: Optional[Set[Announcement]] = None if ( "coin_announcements" in request and request["coin_announcements"] is not None and len(request["coin_announcements"]) > 0 ): - coin_announcements = set([hexstr_to_bytes(announcement) for announcement in request["coin_announcements"]]) + coin_announcements = { + Announcement( + bytes32.from_hexstr(announcement["coin_id"]), + bytes(Program.to(binutils.assemble(announcement["message"]))), + hexstr_to_bytes(announcement["morph_bytes"]) if "morph_bytes" in announcement else None, + ) + for announcement in request["coin_announcements"] + } + + puzzle_announcements: Optional[Set[Announcement]] = None + if ( + "puzzle_announcements" in request + and request["puzzle_announcements"] is not None + and len(request["puzzle_announcements"]) > 0 + ): + puzzle_announcements = { + Announcement( + bytes32.from_hexstr(announcement["puzzle_hash"]), + bytes(Program.to(binutils.assemble(announcement["message"]))), + hexstr_to_bytes(announcement["morph_bytes"]) if "morph_bytes" in announcement else None, + ) + for announcement in request["puzzle_announcements"] + } if hold_lock: async with self.service.wallet_state_manager.lock: signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction( amount_0, - puzzle_hash_0, + bytes32(puzzle_hash_0), fee, coins=coins, ignore_max_send_amount=True, primaries=additional_outputs, - announcements_to_consume=coin_announcements, + memos=memos_0, + coin_announcements_to_consume=coin_announcements, + puzzle_announcements_to_consume=puzzle_announcements, ) else: signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction( amount_0, - puzzle_hash_0, + bytes32(puzzle_hash_0), fee, coins=coins, ignore_max_send_amount=True, primaries=additional_outputs, - announcements_to_consume=coin_announcements, + memos=memos_0, + coin_announcements_to_consume=coin_announcements, + puzzle_announcements_to_consume=puzzle_announcements, ) - return {"signed_tx": signed_tx} + return {"signed_tx": signed_tx.to_json_dict_convenience(self.service.config)} ########################################################################################## # Pool Wallet @@ -1228,14 +1245,16 @@ async def pw_join_pool(self, request) -> Dict: pool_wallet_info: PoolWalletInfo = await wallet.get_current_state() owner_pubkey = pool_wallet_info.current.owner_pubkey target_puzzlehash = None + + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced.") + if "target_puzzlehash" in request: target_puzzlehash = bytes32(hexstr_to_bytes(request["target_puzzlehash"])) - # TODO: address hint error and remove ignore - # error: Argument 2 to "create_pool_state" has incompatible type "Optional[bytes32]"; expected "bytes32" - # [arg-type] + assert target_puzzlehash is not None new_target_state: PoolState = create_pool_state( FARMING_TO_POOL, - target_puzzlehash, # type: ignore[arg-type] + target_puzzlehash, owner_pubkey, request["pool_url"], uint32(request["relative_lock_height"]), @@ -1254,6 +1273,9 @@ async def pw_self_pool(self, request) -> Dict: wallet_id = uint32(request["wallet_id"]) wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced.") + async with self.service.wallet_state_manager.lock: total_fee, tx = await wallet.self_pool(fee) # total_fee: uint64, tx: TransactionRecord return {"total_fee": total_fee, "transaction": tx} diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index c37e6398c230..240a13275376 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -3,10 +3,13 @@ from chia.pools.pool_wallet_info import PoolWalletInfo from chia.rpc.rpc_client import RpcClient +from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.bech32m import decode_puzzle_hash +from chia.util.byte_types import hexstr_to_bytes from chia.util.ints import uint32, uint64 +from chia.wallet.trade_record import TradeRecord +from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey @@ -104,7 +107,7 @@ async def get_transaction(self, wallet_id: str, transaction_id: bytes32) -> Tran "get_transaction", {"walled_id": wallet_id, "transaction_id": transaction_id.hex()}, ) - return TransactionRecord.from_json_dict(res["transaction"]) + return TransactionRecord.from_json_dict_convenience(res["transaction"]) async def get_transactions( self, @@ -128,32 +131,46 @@ async def get_transactions( "get_transactions", request, ) - reverted_tx: List[TransactionRecord] = [] - for modified_tx in res["transactions"]: - # Server returns address instead of ph, but TransactionRecord requires ph - modified_tx["to_puzzle_hash"] = decode_puzzle_hash(modified_tx["to_address"]).hex() - del modified_tx["to_address"] - reverted_tx.append(TransactionRecord.from_json_dict(modified_tx)) - return reverted_tx + return [TransactionRecord.from_json_dict_convenience(tx) for tx in res["transactions"]] + + async def get_transaction_count( + self, + wallet_id: str, + ) -> List[TransactionRecord]: + res = await self.fetch( + "get_transaction_count", + {"wallet_id": wallet_id}, + ) + return res["count"] async def get_next_address(self, wallet_id: str, new_address: bool) -> str: return (await self.fetch("get_next_address", {"wallet_id": wallet_id, "new_address": new_address}))["address"] async def send_transaction( - self, wallet_id: str, amount: uint64, address: str, fee: uint64 = uint64(0) + self, wallet_id: str, amount: uint64, address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None ) -> TransactionRecord: - - res = await self.fetch( - "send_transaction", - {"wallet_id": wallet_id, "amount": amount, "address": address, "fee": fee}, - ) - return TransactionRecord.from_json_dict(res["transaction"]) + if memos is None: + send_dict: Dict = {"wallet_id": wallet_id, "amount": amount, "address": address, "fee": fee} + else: + send_dict = { + "wallet_id": wallet_id, + "amount": amount, + "address": address, + "fee": fee, + "memos": memos, + } + res = await self.fetch("send_transaction", send_dict) + return TransactionRecord.from_json_dict_convenience(res["transaction"]) async def send_transaction_multi( self, wallet_id: str, additions: List[Dict], coins: List[Coin] = None, fee: uint64 = uint64(0) ) -> TransactionRecord: # Converts bytes to hex for puzzle hashes - additions_hex = [{"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()} for ad in additions] + additions_hex = [] + for ad in additions: + additions_hex.append({"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()}) + if "memos" in ad: + additions_hex[-1]["memos"] = ad["memos"] if coins is not None and len(coins) > 0: coins_json = [c.to_json_dict() for c in coins] response: Dict = await self.fetch( @@ -164,7 +181,8 @@ async def send_transaction_multi( response = await self.fetch( "send_transaction_multi", {"wallet_id": wallet_id, "additions": additions_hex, "fee": fee} ) - return TransactionRecord.from_json_dict(response["transaction"]) + + return TransactionRecord.from_json_dict_convenience(response["transaction"]) async def delete_unconfirmed_transactions(self, wallet_id: str) -> None: await self.fetch( @@ -184,35 +202,47 @@ async def create_signed_transaction( additions: List[Dict], coins: List[Coin] = None, fee: uint64 = uint64(0), - coin_announcements: List[bytes32] = None, + coin_announcements: Optional[List[Announcement]] = None, + puzzle_announcements: Optional[List[Announcement]] = None, ) -> TransactionRecord: # Converts bytes to hex for puzzle hashes - additions_hex = [{"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()} for ad in additions] - # Converts bytes to hex for coin announcements and does not if it is none. - coin_announcements_hex: Optional[List[str]] = None + additions_hex = [] + for ad in additions: + additions_hex.append({"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()}) + if "memos" in ad: + additions_hex[-1]["memos"] = ad["memos"] + + request: Dict[str, Any] = { + "additions": additions_hex, + "fee": fee, + } + if coin_announcements is not None and len(coin_announcements) > 0: - coin_announcements_hex = [announcement.hex() for announcement in coin_announcements] - if coins is not None and len(coins) > 0: - coins_json = [c.to_json_dict() for c in coins] - response: Dict = await self.fetch( - "create_signed_transaction", + request["coin_announcements"] = [ { - "additions": additions_hex, - "coins": coins_json, - "fee": fee, - "coin_announcements": coin_announcements_hex, - }, - ) - else: - response = await self.fetch( - "create_signed_transaction", + "coin_id": ann.origin_info.hex(), + "message": ann.message.hex(), + "morph_bytes": ann.morph_bytes.hex() if ann.morph_bytes is not None else b"".hex(), + } + for ann in coin_announcements + ] + + if puzzle_announcements is not None and len(puzzle_announcements) > 0: + request["puzzle_announcements"] = [ { - "additions": additions_hex, - "fee": fee, - "coin_announcements": coin_announcements_hex, - }, - ) - return TransactionRecord.from_json_dict(response["signed_tx"]) + "puzzle_hash": ann.origin_info.hex(), + "message": ann.message.hex(), + "morph_bytes": ann.morph_bytes.hex() if ann.morph_bytes is not None else b"".hex(), + } + for ann in puzzle_announcements + ] + + if coins is not None and len(coins) > 0: + coins_json = [c.to_json_dict() for c in coins] + request["coins"] = coins_json + + response: Dict = await self.fetch("create_signed_transaction", request) + return TransactionRecord.from_json_dict_convenience(response["signed_tx"]) async def create_new_did_wallet(self, amount): request: Dict[str, Any] = { @@ -318,3 +348,122 @@ async def pw_status(self, wallet_id: str) -> Tuple[PoolWalletInfo, List[Transact PoolWalletInfo.from_json_dict(json_dict["state"]), [TransactionRecord.from_json_dict(tr) for tr in json_dict["unconfirmed_transactions"]], ) + + # CATS + async def create_new_cat_and_wallet(self, amount: uint64) -> Dict: + request: Dict[str, Any] = { + "wallet_type": "cat_wallet", + "mode": "new", + "amount": amount, + "host": f"{self.hostname}:{self.port}", + } + return await self.fetch("create_new_wallet", request) + + async def create_wallet_for_existing_cat(self, asset_id: bytes) -> Dict: + request: Dict[str, Any] = { + "wallet_type": "cat_wallet", + "asset_id": asset_id.hex(), + "mode": "existing", + "host": f"{self.hostname}:{self.port}", + } + return await self.fetch("create_new_wallet", request) + + async def get_cat_asset_id(self, wallet_id: str) -> bytes: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + } + return bytes.fromhex((await self.fetch("cat_get_asset_id", request))["asset_id"]) + + async def cat_asset_id_to_name(self, asset_id: bytes32) -> Optional[Tuple[uint32, str]]: + request: Dict[str, Any] = { + "asset_id": asset_id.hex(), + } + try: + res = await self.fetch("cat_asset_id_to_name", request) + return uint32(int(res["wallet_id"])), res["name"] + except Exception: + return None + + async def get_cat_name(self, wallet_id: str) -> str: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + } + return (await self.fetch("cat_get_name", request))["name"] + + async def set_cat_name(self, wallet_id: str, name: str) -> None: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + "name": name, + } + await self.fetch("cat_set_name", request) + + async def cat_spend( + self, + wallet_id: str, + amount: uint64, + inner_address: str, + fee: uint64 = uint64(0), + memos: Optional[List[str]] = None, + ) -> TransactionRecord: + send_dict = { + "wallet_id": wallet_id, + "amount": amount, + "inner_address": inner_address, + "fee": fee, + "memos": memos if memos else [], + } + res = await self.fetch("cat_spend", send_dict) + return TransactionRecord.from_json_dict_convenience(res["transaction"]) + + # Offers + async def create_offer_for_ids( + self, offer_dict: Dict[uint32, int], fee=uint64(0), validate_only: bool = False + ) -> Tuple[Optional[Offer], TradeRecord]: + send_dict: Dict[str, int] = {} + for key in offer_dict: + send_dict[str(key)] = offer_dict[key] + + res = await self.fetch("create_offer_for_ids", {"offer": send_dict, "validate_only": validate_only, "fee": fee}) + offer: Optional[Offer] = None if validate_only else Offer.from_bytes(hexstr_to_bytes(res["offer"])) + return offer, TradeRecord.from_json_dict_convenience(res["trade_record"], res["offer"]) + + async def get_offer_summary(self, offer: Offer) -> Dict[str, Dict[str, int]]: + res = await self.fetch("get_offer_summary", {"offer": bytes(offer).hex()}) + return res["summary"] + + async def check_offer_validity(self, offer: Offer) -> bool: + res = await self.fetch("check_offer_validity", {"offer": bytes(offer).hex()}) + return res["valid"] + + async def take_offer(self, offer: Offer, fee=uint64(0)) -> TradeRecord: + res = await self.fetch("take_offer", {"offer": bytes(offer).hex(), "fee": fee}) + return TradeRecord.from_json_dict_convenience(res["trade_record"]) + + async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> TradeRecord: + res = await self.fetch("get_offer", {"trade_id": trade_id.hex(), "file_contents": file_contents}) + offer_str = res["offer"] if file_contents else "" + return TradeRecord.from_json_dict_convenience(res["trade_record"], offer_str) + + async def get_all_offers( + self, start: int = 0, end: int = 50, sort_key: str = None, reverse: bool = False, file_contents: bool = False + ) -> List[TradeRecord]: + res = await self.fetch( + "get_all_offers", + { + "start": start, + "end": end, + "sort_key": sort_key, + "reverse": reverse, + "file_contents": file_contents, + }, + ) + + records = [] + optional_offers = res["offers"] if file_contents else ([""] * len(res["trade_records"])) + for record, offer in zip(res["trade_records"], optional_offers): + records.append(TradeRecord.from_json_dict_convenience(record, offer)) + + return records + + async def cancel_offer(self, trade_id: bytes32, fee=uint64(0), secure: bool = True): + await self.fetch("cancel_offer", {"trade_id": trade_id.hex(), "secure": secure, "fee": fee}) diff --git a/chia/server/node_discovery.py b/chia/server/node_discovery.py index 3036402ab33a..344fd51b5365 100644 --- a/chia/server/node_discovery.py +++ b/chia/server/node_discovery.py @@ -707,7 +707,7 @@ def __init__( ) async def start(self) -> None: - self.initial_wait = 60 + self.initial_wait = 1 await self.migrate_address_manager_if_necessary() await self.initialize_address_manager() await self.start_tasks() diff --git a/chia/server/server.py b/chia/server/server.py index 868187a6d08c..b60b24be5454 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -803,13 +803,9 @@ def accept_inbound_connections(self, node_type: NodeType) -> bool: def is_trusted_peer(self, peer: WSChiaConnection, trusted_peers: Dict) -> bool: if trusted_peers is None: return False - for trusted_peer in trusted_peers: - cert = self.root_path / trusted_peers[trusted_peer] - pem_cert = x509.load_pem_x509_certificate(cert.read_bytes()) - cert_bytes = pem_cert.public_bytes(encoding=serialization.Encoding.DER) - der_cert = x509.load_der_x509_certificate(cert_bytes) - peer_id = bytes32(der_cert.fingerprint(hashes.SHA256())) - if peer_id == peer.peer_node_id: - self.log.debug(f"trusted node {peer.peer_node_id} {peer.peer_host}") - return True - return False + if not self.config["testing"] and peer.peer_host == "127.0.0.1": + return True + if peer.peer_node_id.hex() not in trusted_peers: + return False + + return True diff --git a/chia/server/ws_connection.py b/chia/server/ws_connection.py index a58e43ecda89..d1d00a0f1304 100644 --- a/chia/server/ws_connection.py +++ b/chia/server/ws_connection.py @@ -13,7 +13,6 @@ from chia.protocols.shared_protocol import Capability, Handshake from chia.server.outbound_message import Message, NodeType, make_msg from chia.server.rate_limits import RateLimiter -from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.peer_info import PeerInfo from chia.util.errors import Err, ProtocolError from chia.util.ints import uint8, uint16 @@ -89,9 +88,9 @@ def __init__( self.session = session self.close_callback = close_callback - self.pending_requests: Dict[bytes32, asyncio.Event] = {} - self.pending_timeouts: Dict[bytes32, asyncio.Task] = {} - self.request_results: Dict[bytes32, Message] = {} + self.pending_requests: Dict[uint16, asyncio.Event] = {} + self.pending_timeouts: Dict[uint16, asyncio.Task] = {} + self.request_results: Dict[uint16, Message] = {} self.closed = False self.connection_type: Optional[NodeType] = None if is_outbound: @@ -108,6 +107,7 @@ def __init__( # Used by the Chia Seeder. self.version = None + self.protocol_version = "" async def perform_handshake(self, network_id: str, protocol_version: str, server_port: int, local_type: NodeType): if self.is_outbound: @@ -142,7 +142,7 @@ async def perform_handshake(self, network_id: str, protocol_version: str, server raise ProtocolError(Err.INCOMPATIBLE_NETWORK_ID) self.version = inbound_handshake.software_version - + self.protocol_version = inbound_handshake.protocol_version self.peer_server_port = inbound_handshake.server_port self.connection_type = NodeType(inbound_handshake.node_type) @@ -342,11 +342,8 @@ async def send_request(self, message_no_id: Message, timeout: int) -> Optional[M ) message = Message(message_no_id.type, request_id, message_no_id.data) - - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[uint16]" for "Dict[bytes32, Event]"; expected type "bytes32" - # [index] - self.pending_requests[message.id] = event # type: ignore[index] + assert message.id is not None + self.pending_requests[message.id] = event await self.outgoing_queue.put(message) # If the timeout passes, we set the event @@ -361,34 +358,16 @@ async def time_out(req_id, req_timeout): raise timeout_task = asyncio.create_task(time_out(message.id, timeout)) - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[uint16]" for "Dict[bytes32, Task[Any]]"; expected type "bytes32" - # [index] - self.pending_timeouts[message.id] = timeout_task # type: ignore[index] + self.pending_timeouts[message.id] = timeout_task await event.wait() - # TODO: address hint error and remove ignore - # error: No overload variant of "pop" of "MutableMapping" matches argument type "Optional[uint16]" - # [call-overload] - # note: Possible overload variants: - # note: def pop(self, key: bytes32) -> Event - # note: def [_T] pop(self, key: bytes32, default: Union[Event, _T] = ...) -> Union[Event, _T] - self.pending_requests.pop(message.id) # type: ignore[call-overload] + self.pending_requests.pop(message.id) result: Optional[Message] = None if message.id in self.request_results: - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[uint16]" for "Dict[bytes32, Message]"; expected type "bytes32" - # [index] - result = self.request_results[message.id] # type: ignore[index] + result = self.request_results[message.id] assert result is not None self.log.debug(f"<- {ProtocolMessageTypes(result.type).name} from: {self.peer_host}:{self.peer_port}") - # TODO: address hint error and remove ignore - # error: No overload variant of "pop" of "MutableMapping" matches argument type "Optional[uint16]" - # [call-overload] - # note: Possible overload variants: - # note: def pop(self, key: bytes32) -> Message - # note: def [_T] pop(self, key: bytes32, default: Union[Message, _T] = ...) -> Union[Message, _T] - self.request_results.pop(result.id) # type: ignore[call-overload] + self.request_results.pop(message.id) return result diff --git a/chia/types/announcement.py b/chia/types/announcement.py index f12d23144673..d4324f0bdf22 100644 --- a/chia/types/announcement.py +++ b/chia/types/announcement.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.hash import std_hash @@ -8,9 +9,14 @@ class Announcement: origin_info: bytes32 message: bytes + morph_bytes: Optional[bytes] = None # CATs morph their announcements and other puzzles may choose to do so too def name(self) -> bytes32: - return std_hash(bytes(self.origin_info + self.message)) + if self.morph_bytes is not None: + message: bytes = std_hash(self.morph_bytes + self.message) + else: + message = self.message + return std_hash(bytes(self.origin_info + message)) def __str__(self): return self.name().decode("utf-8") diff --git a/chia/types/coin_spend.py b/chia/types/coin_spend.py index ae15a08fd3b1..f074e87379bb 100644 --- a/chia/types/coin_spend.py +++ b/chia/types/coin_spend.py @@ -1,8 +1,10 @@ from dataclasses import dataclass from typing import List +from blspy import G2Element from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import SerializedProgram, INFINITE_COST +from chia.types.condition_opcodes import ConditionOpcode from chia.util.chain_utils import additions_for_solution, fee_for_solution from chia.util.streamable import Streamable, streamable @@ -25,3 +27,26 @@ def additions(self) -> List[Coin]: def reserved_fee(self) -> int: return fee_for_solution(self.puzzle_reveal, self.solution, INFINITE_COST) + + def hints(self) -> List[bytes]: + # import above was causing circular import issue + from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions + from chia.consensus.default_constants import DEFAULT_CONSTANTS + from chia.types.spend_bundle import SpendBundle + from chia.full_node.bundle_tools import simple_solution_generator + + bundle = SpendBundle([self], G2Element()) + generator = simple_solution_generator(bundle) + + npc_result = get_name_puzzle_conditions( + generator, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=False + ) + h_list = [] + for npc in npc_result.npc_list: + for opcode, conditions in npc.conditions: + if opcode == ConditionOpcode.CREATE_COIN: + for condition in conditions: + if len(condition.vars) > 2 and condition.vars[2] != b"": + h_list.append(condition.vars[2]) + + return h_list diff --git a/chia/types/spend_bundle.py b/chia/types/spend_bundle.py index f8e9977cce2f..9dacec241dc3 100644 --- a/chia/types/spend_bundle.py +++ b/chia/types/spend_bundle.py @@ -2,17 +2,20 @@ import warnings from dataclasses import dataclass -from typing import List +from typing import List, Dict from blspy import AugSchemeMPL, G2Element +from clvm.casts import int_from_bytes from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.streamable import Streamable, dataclass_from_dict, recurse_jsonify, streamable from chia.wallet.util.debug_spend_bundle import debug_spend_bundle +from .blockchain_format.program import Program from .coin_spend import CoinSpend +from .condition_opcodes import ConditionOpcode @dataclass(frozen=True) @@ -77,6 +80,27 @@ def not_ephemeral_additions(self): return result + def get_memos(self) -> Dict[bytes32, List[bytes]]: + """ + Retrieves the memos for additions in this spend_bundle, which are formatted as a list in the 3rd parameter of + CREATE_COIN. If there are no memos, the addition coin_id is not included. If they are not formatted as a list + of bytes, they are not included. This is expensive to call, it should not be used in full node code. + """ + memos: Dict[bytes32, List[bytes]] = {} + for coin_spend in self.coin_spends: + result = Program.from_bytes(bytes(coin_spend.puzzle_reveal)).run( + Program.from_bytes(bytes(coin_spend.solution)) + ) + for condition in result.as_python(): + if condition[0] == ConditionOpcode.CREATE_COIN and len(condition) >= 4: + # If only 3 elements (opcode + 2 args), there is no memo, this is ph, amount + coin_added = Coin(coin_spend.coin.name(), bytes32(condition[1]), int_from_bytes(condition[2])) + if type(condition[3]) != list: + # If it's not a list, it's not the correct format + continue + memos[coin_added.name()] = condition[3] + return memos + # Note that `coin_spends` used to have the bad name `coin_solutions`. # Some API still expects this name. For now, we accept both names. # diff --git a/chia/util/block_cache.py b/chia/util/block_cache.py index 24d1c1fb22d2..17d4c74a486f 100644 --- a/chia/util/block_cache.py +++ b/chia/util/block_cache.py @@ -28,7 +28,7 @@ def __init__( self._headers = headers self._height_to_hash = height_to_hash self._sub_epoch_summaries = sub_epoch_summaries - self._sub_epoch_segments: Dict[uint32, SubEpochSegments] = {} + self._sub_epoch_segments: Dict[bytes32, SubEpochSegments] = {} self.log = logging.getLogger(__name__) def block_record(self, header_hash: bytes32) -> BlockRecord: @@ -83,15 +83,15 @@ async def get_header_blocks_in_range( return self._headers async def persist_sub_epoch_challenge_segments( - self, sub_epoch_summary_height: uint32, segments: List[SubEpochChallengeSegment] + self, sub_epoch_summary_hash: bytes32, segments: List[SubEpochChallengeSegment] ): - self._sub_epoch_segments[sub_epoch_summary_height] = SubEpochSegments(segments) + self._sub_epoch_segments[sub_epoch_summary_hash] = SubEpochSegments(segments) async def get_sub_epoch_challenge_segments( self, - sub_epoch_summary_height: uint32, + sub_epoch_summary_hash: bytes32, ) -> Optional[List[SubEpochChallengeSegment]]: - segments = self._sub_epoch_segments.get(sub_epoch_summary_height) + segments = self._sub_epoch_segments.get(sub_epoch_summary_hash) if segments is None: return None return segments.challenge_segments diff --git a/chia/util/condition_tools.py b/chia/util/condition_tools.py index 66896da3a185..287edc3657e4 100644 --- a/chia/util/condition_tools.py +++ b/chia/util/condition_tools.py @@ -114,9 +114,7 @@ def created_outputs_for_conditions_dict( for cvp in conditions_dict.get(ConditionOpcode.CREATE_COIN, []): puzzle_hash, amount_bin = cvp.vars[0], cvp.vars[1] amount = int_from_bytes(amount_bin) - # TODO: address hint error and remove ignore - # error: Argument 2 to "Coin" has incompatible type "bytes"; expected "bytes32" [arg-type] - coin = Coin(input_coin_name, puzzle_hash, uint64(amount)) # type: ignore[arg-type] + coin = Coin(input_coin_name, bytes32(puzzle_hash), uint64(amount)) output_coins.append(coin) return output_coins diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index a8c6fd48319f..e7c3e35d8b34 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -73,6 +73,7 @@ network_overrides: &network_overrides default_full_node_port: 8444 testnet0: address_prefix: "txch" + default_full_node_port: 58444 testnet1: address_prefix: "txch" testnet2: diff --git a/chia/util/merkle_set.py b/chia/util/merkle_set.py index ada19e9a2b4e..7f16fa1dd7da 100644 --- a/chia/util/merkle_set.py +++ b/chia/util/merkle_set.py @@ -353,7 +353,7 @@ def confirm_included(root: Node, val: bytes, proof: bytes32) -> bool: return confirm_not_included_already_hashed(root, sha256(val).digest(), proof) -def confirm_included_already_hashed(root: Node, val: bytes, proof: bytes32) -> bool: +def confirm_included_already_hashed(root: Node, val: bytes, proof: bytes) -> bool: return _confirm(root, val, proof, True) @@ -361,11 +361,11 @@ def confirm_not_included(root: Node, val: bytes, proof: bytes32) -> bool: return confirm_not_included_already_hashed(root, sha256(val).digest(), proof) -def confirm_not_included_already_hashed(root: Node, val: bytes, proof: bytes32) -> bool: +def confirm_not_included_already_hashed(root: Node, val: bytes, proof: bytes) -> bool: return _confirm(root, val, proof, False) -def _confirm(root: Node, val: bytes, proof: bytes32, expected: bool) -> bool: +def _confirm(root: Node, val: bytes, proof: bytes, expected: bool) -> bool: try: p = deserialize_proof(proof) if p.get_root() != root: @@ -376,7 +376,7 @@ def _confirm(root: Node, val: bytes, proof: bytes32, expected: bool) -> bool: return False -def deserialize_proof(proof: bytes32) -> MerkleSet: +def deserialize_proof(proof: bytes) -> MerkleSet: try: r, pos = _deserialize(proof, 0, []) if pos != len(proof): @@ -386,7 +386,7 @@ def deserialize_proof(proof: bytes32) -> MerkleSet: raise SetError() -def _deserialize(proof: bytes32, pos: int, bits: List[int]) -> Tuple[Node, int]: +def _deserialize(proof: bytes, pos: int, bits: List[int]) -> Tuple[Node, int]: t = proof[pos : pos + 1] # flake8: noqa if t == EMPTY: return _empty, pos + 1 diff --git a/chia/wallet/cc_wallet/__init__.py b/chia/wallet/cat_wallet/__init__.py similarity index 100% rename from chia/wallet/cc_wallet/__init__.py rename to chia/wallet/cat_wallet/__init__.py diff --git a/chia/wallet/cat_wallet/cat_constants.py b/chia/wallet/cat_wallet/cat_constants.py new file mode 100644 index 000000000000..e0d9439833a2 --- /dev/null +++ b/chia/wallet/cat_wallet/cat_constants.py @@ -0,0 +1,30 @@ +SPACEBUCKS = { + "asset_id": "78ad32a8c9ea70f27d73e9306fc467bab2a6b15b30289791e37ab6e8612212b1", + "name": "Spacebucks", + "symbol": "SBX", +} + +MARMOT = { + "asset_id": "8ebf855de6eb146db5602f0456d2f0cbe750d57f821b6f91a8592ee9f1d4cf31", + "name": "Marmot", + "symbol": "MRMT", +} + +DUCK_SAUCE = { + "asset_id": "6d95dae356e32a71db5ddcb42224754a02524c615c5fc35f568c2af04774e589", + "name": "Duck Sauce", + "symbol": "DSC", +} + +CHIA_HOLIDAY_TOKEN = { + "asset_id": "509deafe3cd8bbfbb9ccce1d930e3d7b57b40c964fa33379b18d628175eb7a8f", + "name": "Chia Holiday 2021 Token", + "symbol": "CH21", +} + +DEFAULT_CATS = { + SPACEBUCKS["asset_id"]: SPACEBUCKS, + MARMOT["asset_id"]: MARMOT, + DUCK_SAUCE["asset_id"]: DUCK_SAUCE, + CHIA_HOLIDAY_TOKEN["asset_id"]: CHIA_HOLIDAY_TOKEN, +} diff --git a/chia/wallet/cc_wallet/cc_info.py b/chia/wallet/cat_wallet/cat_info.py similarity index 51% rename from chia/wallet/cc_wallet/cc_info.py rename to chia/wallet/cat_wallet/cat_info.py index d74884d05528..6fa33af89528 100644 --- a/chia/wallet/cc_wallet/cc_info.py +++ b/chia/wallet/cat_wallet/cat_info.py @@ -3,11 +3,13 @@ from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.wallet.lineage_proof import LineageProof from chia.util.streamable import Streamable, streamable @dataclass(frozen=True) @streamable -class CCInfo(Streamable): - my_genesis_checker: Optional[Program] # this is the program - lineage_proofs: List[Tuple[bytes32, Optional[Program]]] # {coin.name(): lineage_proof} +class CATInfo(Streamable): + limitations_program_hash: bytes32 + my_tail: Optional[Program] # this is the program + lineage_proofs: List[Tuple[bytes32, Optional[LineageProof]]] # {coin.name(): lineage_proof} diff --git a/chia/wallet/cat_wallet/cat_utils.py b/chia/wallet/cat_wallet/cat_utils.py new file mode 100644 index 000000000000..4b777d33c14b --- /dev/null +++ b/chia/wallet/cat_wallet/cat_utils.py @@ -0,0 +1,135 @@ +import dataclasses +from typing import List, Tuple, Iterator + +from blspy import G2Element + +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program, INFINITE_COST +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.condition_opcodes import ConditionOpcode +from chia.types.spend_bundle import CoinSpend, SpendBundle +from chia.util.condition_tools import conditions_dict_for_solution +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.puzzles.cat_loader import CAT_MOD + +NULL_SIGNATURE = G2Element() + +ANYONE_CAN_SPEND_PUZZLE = Program.to(1) # simply return the conditions + + +# information needed to spend a cc +@dataclasses.dataclass +class SpendableCAT: + coin: Coin + limitations_program_hash: bytes32 + inner_puzzle: Program + inner_solution: Program + limitations_solution: Program = Program.to([]) + lineage_proof: LineageProof = LineageProof() + extra_delta: int = 0 + limitations_program_reveal: Program = Program.to([]) + + +def match_cat_puzzle(puzzle: Program) -> Tuple[bool, Iterator[Program]]: + """ + Given a puzzle test if it's a CAT and, if it is, return the curried arguments + """ + mod, curried_args = puzzle.uncurry() + if mod == CAT_MOD: + return True, curried_args.as_iter() + else: + return False, iter(()) + + +def construct_cat_puzzle(mod_code: Program, limitations_program_hash: bytes32, inner_puzzle: Program) -> Program: + """ + Given an inner puzzle hash and tail hash calculate a puzzle program for a specific cc. + """ + return mod_code.curry(mod_code.get_tree_hash(), limitations_program_hash, inner_puzzle) + + +def subtotals_for_deltas(deltas) -> List[int]: + """ + Given a list of deltas corresponding to input coins, create the "subtotals" list + needed in solutions spending those coins. + """ + + subtotals = [] + subtotal = 0 + + for delta in deltas: + subtotals.append(subtotal) + subtotal += delta + + # tweak the subtotals so the smallest value is 0 + subtotal_offset = min(subtotals) + subtotals = [_ - subtotal_offset for _ in subtotals] + return subtotals + + +def next_info_for_spendable_cat(spendable_cat: SpendableCAT) -> Program: + c = spendable_cat.coin + list = [c.parent_coin_info, spendable_cat.inner_puzzle.get_tree_hash(), c.amount] + return Program.to(list) + + +# This should probably return UnsignedSpendBundle if that type ever exists +def unsigned_spend_bundle_for_spendable_cats(mod_code: Program, spendable_cat_list: List[SpendableCAT]) -> SpendBundle: + """ + Given a list of `SpendableCAT` objects, create a `SpendBundle` that spends all those coins. + Note that no signing is done here, so it falls on the caller to sign the resultant bundle. + """ + + N = len(spendable_cat_list) + + # figure out what the deltas are by running the inner puzzles & solutions + deltas = [] + for spend_info in spendable_cat_list: + error, conditions, cost = conditions_dict_for_solution( + spend_info.inner_puzzle, spend_info.inner_solution, INFINITE_COST + ) + total = spend_info.extra_delta * -1 + if conditions: + for _ in conditions.get(ConditionOpcode.CREATE_COIN, []): + if _.vars[1] != b"\x8f": # -113 in bytes + total += Program.to(_.vars[1]).as_int() + deltas.append(spend_info.coin.amount - total) + + if sum(deltas) != 0: + raise ValueError("input and output amounts don't match") + + subtotals = subtotals_for_deltas(deltas) + + infos_for_next = [] + infos_for_me = [] + ids = [] + for _ in spendable_cat_list: + infos_for_next.append(next_info_for_spendable_cat(_)) + infos_for_me.append(Program.to(_.coin.as_list())) + ids.append(_.coin.name()) + + coin_spends = [] + for index in range(N): + spend_info = spendable_cat_list[index] + + puzzle_reveal = construct_cat_puzzle(mod_code, spend_info.limitations_program_hash, spend_info.inner_puzzle) + + prev_index = (index - 1) % N + next_index = (index + 1) % N + prev_id = ids[prev_index] + my_info = infos_for_me[index] + next_info = infos_for_next[next_index] + + solution = [ + spend_info.inner_solution, + spend_info.lineage_proof.to_program(), + prev_id, + my_info, + next_info, + subtotals[index], + spend_info.extra_delta, + ] + coin_spend = CoinSpend(spend_info.coin, puzzle_reveal, Program.to(solution)) + coin_spends.append(coin_spend) + + return SpendBundle(coin_spends, NULL_SIGNATURE) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py new file mode 100644 index 000000000000..ca3f0fb52580 --- /dev/null +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -0,0 +1,776 @@ +from __future__ import annotations + +import dataclasses +import logging +import time +from secrets import token_bytes +from typing import Any, Dict, List, Optional, Set, Tuple + +from blspy import AugSchemeMPL, G2Element + +from chia.consensus.cost_calculator import calculate_cost_of_program, NPCResult +from chia.full_node.bundle_tools import simple_solution_generator +from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions +from chia.protocols.wallet_protocol import PuzzleSolutionResponse, CoinState +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.announcement import Announcement +from chia.types.generator_types import BlockGenerator +from chia.types.spend_bundle import SpendBundle +from chia.types.condition_opcodes import ConditionOpcode +from chia.util.byte_types import hexstr_to_bytes +from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict +from chia.util.ints import uint8, uint32, uint64, uint128 +from chia.util.json_util import dict_to_json_str +from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS +from chia.wallet.cat_wallet.cat_info import CATInfo +from chia.wallet.cat_wallet.cat_utils import ( + CAT_MOD, + SpendableCAT, + construct_cat_puzzle, + unsigned_spend_bundle_for_spendable_cats, + match_cat_puzzle, +) +from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.payment import Payment +from chia.wallet.puzzles.genesis_checkers import ALL_LIMITATIONS_PROGRAMS +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + DEFAULT_HIDDEN_PUZZLE_HASH, + calculate_synthetic_secret_key, +) +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.wallet_types import WalletType, AmountWithPuzzlehash +from chia.wallet.wallet import Wallet +from chia.wallet.wallet_coin_record import WalletCoinRecord +from chia.wallet.wallet_info import WalletInfo + + +# This should probably not live in this file but it's for experimental right now + + +class CATWallet: + wallet_state_manager: Any + log: logging.Logger + wallet_info: WalletInfo + cat_info: CATInfo + standard_wallet: Wallet + cost_of_single_tx: Optional[int] + + @staticmethod + async def create_new_cat_wallet( + wallet_state_manager: Any, wallet: Wallet, cat_tail_info: Dict[str, Any], amount: uint64, name="CAT WALLET" + ): + self = CATWallet() + self.cost_of_single_tx = None + self.standard_wallet = wallet + self.log = logging.getLogger(__name__) + std_wallet_id = self.standard_wallet.wallet_id + bal = await wallet_state_manager.get_confirmed_balance_for_wallet_already_locked(std_wallet_id) + if amount > bal: + raise ValueError("Not enough balance") + self.wallet_state_manager = wallet_state_manager + + # We use 00 bytes because it's not optional. We must check this is overidden during issuance. + empty_bytes = bytes32(32 * b"\0") + self.cat_info = CATInfo(empty_bytes, None, []) + info_as_string = bytes(self.cat_info).hex() + self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) + if self.wallet_info is None: + raise ValueError("Internal Error") + + try: + chia_tx, spend_bundle = await ALL_LIMITATIONS_PROGRAMS[ + cat_tail_info["identifier"] + ].generate_issuance_bundle( + self, + cat_tail_info, + amount, + ) + assert self.cat_info.limitations_program_hash != empty_bytes + assert self.cat_info.lineage_proofs != [] + except Exception: + await wallet_state_manager.user_store.delete_wallet(self.id(), False) + raise + if spend_bundle is None: + await wallet_state_manager.user_store.delete_wallet(self.id()) + raise ValueError("Failed to create spend.") + + await self.wallet_state_manager.add_new_wallet(self, self.id()) + + # Change and actual CAT coin + non_ephemeral_coins: List[Coin] = spend_bundle.not_ephemeral_additions() + cc_coin = None + puzzle_store = self.wallet_state_manager.puzzle_store + for c in non_ephemeral_coins: + info = await puzzle_store.wallet_info_for_puzzle_hash(c.puzzle_hash) + if info is None: + raise ValueError("Internal Error") + id, wallet_type = info + if id == self.id(): + cc_coin = c + + if cc_coin is None: + raise ValueError("Internal Error, unable to generate new CAT coin") + cc_pid: bytes32 = cc_coin.parent_coin_info + + cc_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=cc_coin.puzzle_hash, + amount=uint64(cc_coin.amount), + fee_amount=uint64(0), + confirmed=False, + sent=uint32(10), + spend_bundle=None, + additions=[cc_coin], + removals=list(filter(lambda rem: rem.name() == cc_pid, spend_bundle.removals())), + wallet_id=self.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=bytes32(token_bytes()), + memos=[], + ) + chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle) + await self.standard_wallet.push_transaction(chia_tx) + await self.standard_wallet.push_transaction(cc_record) + return self + + @staticmethod + async def create_wallet_for_cat( + wallet_state_manager: Any, wallet: Wallet, limitations_program_hash_hex: str, name="CAT WALLET" + ) -> CATWallet: + self = CATWallet() + self.cost_of_single_tx = None + self.standard_wallet = wallet + self.log = logging.getLogger(__name__) + + for id, wallet in wallet_state_manager.wallets.items(): + if wallet.type() == CATWallet.type(): + if wallet.get_asset_id() == limitations_program_hash_hex: # type: ignore + self.log.warning("Not creating wallet for already existing CAT wallet") + raise ValueError("Wallet already exists") + + self.wallet_state_manager = wallet_state_manager + if limitations_program_hash_hex in DEFAULT_CATS: + cat_info = DEFAULT_CATS[limitations_program_hash_hex] + name = cat_info["name"] + + limitations_program_hash = bytes32(hexstr_to_bytes(limitations_program_hash_hex)) + self.cat_info = CATInfo(limitations_program_hash, None, []) + info_as_string = bytes(self.cat_info).hex() + self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) + if self.wallet_info is None: + raise Exception("wallet_info is None") + + await self.wallet_state_manager.add_new_wallet(self, self.id()) + return self + + @staticmethod + async def create( + wallet_state_manager: Any, + wallet: Wallet, + wallet_info: WalletInfo, + ) -> CATWallet: + self = CATWallet() + + self.log = logging.getLogger(__name__) + + self.cost_of_single_tx = None + self.wallet_state_manager = wallet_state_manager + self.wallet_info = wallet_info + self.standard_wallet = wallet + self.cat_info = CATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data)) + return self + + @classmethod + def type(cls) -> uint8: + return uint8(WalletType.CAT) + + def id(self) -> uint32: + return self.wallet_info.id + + async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint64: + if record_list is None: + record_list = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.id()) + + amount: uint64 = uint64(0) + for record in record_list: + lineage = await self.get_lineage_proof_for_coin(record.coin) + if lineage is not None: + amount = uint64(amount + record.coin.amount) + + self.log.info(f"Confirmed balance for cc wallet {self.id()} is {amount}") + return uint64(amount) + + async def get_unconfirmed_balance(self, unspent_records=None) -> uint128: + return await self.wallet_state_manager.get_unconfirmed_balance(self.id(), unspent_records) + + async def get_max_send_amount(self, records=None): + spendable: List[WalletCoinRecord] = list(await self.get_cat_spendable_coins()) + if len(spendable) == 0: + return 0 + spendable.sort(reverse=True, key=lambda record: record.coin.amount) + if self.cost_of_single_tx is None: + coin = spendable[0].coin + txs = await self.generate_signed_transaction( + [coin.amount], [coin.puzzle_hash], coins={coin}, ignore_max_send_amount=True + ) + program: BlockGenerator = simple_solution_generator(txs[0].spend_bundle) + # npc contains names of the coins removed, puzzle_hashes and their spend conditions + result: NPCResult = get_name_puzzle_conditions( + program, + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + cost_per_byte=self.wallet_state_manager.constants.COST_PER_BYTE, + mempool_mode=True, + ) + cost_result: uint64 = calculate_cost_of_program( + program.program, result, self.wallet_state_manager.constants.COST_PER_BYTE + ) + self.cost_of_single_tx = cost_result + self.log.info(f"Cost of a single tx for CAT wallet: {self.cost_of_single_tx}") + + max_cost = self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 2 # avoid full block TXs + current_cost = 0 + total_amount = 0 + total_coin_count = 0 + + for record in spendable: + current_cost += self.cost_of_single_tx + total_amount += record.coin.amount + total_coin_count += 1 + if current_cost + self.cost_of_single_tx > max_cost: + break + + return total_amount + + async def get_name(self): + return self.wallet_info.name + + async def set_name(self, new_name: str): + new_info = dataclasses.replace(self.wallet_info, name=new_name) + self.wallet_info = new_info + await self.wallet_state_manager.user_store.update_wallet(self.wallet_info, False) + + def get_asset_id(self) -> str: + return bytes(self.cat_info.limitations_program_hash).hex() + + async def set_tail_program(self, tail_program: str): + assert Program.fromhex(tail_program).get_tree_hash() == self.cat_info.limitations_program_hash + await self.save_info( + CATInfo( + self.cat_info.limitations_program_hash, Program.fromhex(tail_program), self.cat_info.lineage_proofs + ), + False, + ) + + async def coin_added(self, coin: Coin, height: uint32): + """Notification from wallet state manager that wallet has been received.""" + self.log.info(f"CC wallet has been notified that {coin} was added") + search_for_parent: bool = True + + inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) + lineage_proof = LineageProof(coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount) + await self.add_lineage(coin.name(), lineage_proof, True) + + for name, lineage_proofs in self.cat_info.lineage_proofs: + if coin.parent_coin_info == name: + search_for_parent = False + break + + if search_for_parent: + data: Dict[str, Any] = { + "data": { + "action_data": { + "api_name": "request_puzzle_solution", + "height": height, + "coin_name": coin.parent_coin_info, + "received_coin": coin.name(), + } + } + } + + data_str = dict_to_json_str(data) + await self.wallet_state_manager.create_action( + name="request_puzzle_solution", + wallet_id=self.id(), + wallet_type=self.type(), + callback="puzzle_solution_received", + done=False, + data=data_str, + in_transaction=True, + ) + + async def puzzle_solution_received(self, response: PuzzleSolutionResponse, action_id: int): + coin_name = response.coin_name + puzzle: Program = response.puzzle + matched, curried_args = match_cat_puzzle(puzzle) + if matched: + mod_hash, genesis_coin_checker_hash, inner_puzzle = curried_args + self.log.info(f"parent: {coin_name} inner_puzzle for parent is {inner_puzzle}") + parent_coin = None + coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin_name) + if coin_record is None: + coin_states: Optional[List[CoinState]] = await self.wallet_state_manager.get_coin_state([coin_name]) + if coin_states is not None: + parent_coin = coin_states[0].coin + if coin_record is not None: + parent_coin = coin_record.coin + if parent_coin is None: + raise ValueError("Error in finding parent") + await self.add_lineage( + coin_name, LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount) + ) + await self.wallet_state_manager.action_store.action_done(action_id) + else: + # The parent is not a CAT which means we need to scrub all of its children from our DB + child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(coin_name) + if len(child_coin_records) > 0: + for record in child_coin_records: + if record.wallet_id == self.id(): + await self.wallet_state_manager.coin_store.delete_coin_record(record.coin.name()) + await self.remove_lineage(record.coin.name()) + # We also need to make sure there's no record of the transaction + await self.wallet_state_manager.tx_store.delete_transaction_record(record.coin.name()) + + async def get_new_inner_hash(self) -> bytes32: + puzzle = await self.get_new_inner_puzzle() + return puzzle.get_tree_hash() + + async def get_new_inner_puzzle(self) -> Program: + return await self.standard_wallet.get_new_puzzle() + + async def get_new_puzzlehash(self) -> bytes32: + return await self.standard_wallet.get_new_puzzlehash() + + def puzzle_for_pk(self, pubkey) -> Program: + inner_puzzle = self.standard_wallet.puzzle_for_pk(bytes(pubkey)) + cc_puzzle: Program = construct_cat_puzzle(CAT_MOD, self.cat_info.limitations_program_hash, inner_puzzle) + return cc_puzzle + + async def get_new_cat_puzzle_hash(self): + return (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash + + async def get_spendable_balance(self, records=None) -> uint64: + coins = await self.get_cat_spendable_coins(records) + amount = 0 + for record in coins: + amount += record.coin.amount + + return uint64(amount) + + async def get_pending_change_balance(self) -> uint64: + unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id()) + addition_amount = 0 + for record in unconfirmed_tx: + if not record.is_in_mempool(): + continue + our_spend = False + for coin in record.removals: + if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()): + our_spend = True + break + + if our_spend is not True: + continue + + for coin in record.additions: + if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()): + addition_amount += coin.amount + + return uint64(addition_amount) + + async def get_cat_spendable_coins(self, records=None) -> List[WalletCoinRecord]: + result: List[WalletCoinRecord] = [] + + record_list: Set[WalletCoinRecord] = await self.wallet_state_manager.get_spendable_coins_for_wallet( + self.id(), records + ) + + for record in record_list: + lineage = await self.get_lineage_proof_for_coin(record.coin) + if lineage is not None and not lineage.is_none(): + result.append(record) + + return result + + async def select_coins(self, amount: uint64) -> Set[Coin]: + """ + Returns a set of coins that can be used for generating a new transaction. + Note: Must be called under wallet state manager lock + """ + + spendable_am = await self.get_confirmed_balance() + + if amount > spendable_am: + error_msg = f"Can't select amount higher than our spendable balance {amount}, spendable {spendable_am}" + self.log.warning(error_msg) + raise ValueError(error_msg) + + self.log.info(f"About to select coins for amount {amount}") + spendable: List[WalletCoinRecord] = await self.get_cat_spendable_coins() + + sum = 0 + used_coins: Set = set() + + # Use older coins first + spendable.sort(key=lambda r: r.confirmed_block_height) + + # Try to use coins from the store, if there isn't enough of "unused" + # coins use change coins that are not confirmed yet + unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet( + self.id() + ) + for coinrecord in spendable: + if sum >= amount and len(used_coins) > 0: + break + if coinrecord.coin.name() in unconfirmed_removals: + continue + sum += coinrecord.coin.amount + used_coins.add(coinrecord.coin) + self.log.info(f"Selected coin: {coinrecord.coin.name()} at height {coinrecord.confirmed_block_height}!") + + # This happens when we couldn't use one of the coins because it's already used + # but unconfirmed, and we are waiting for the change. (unconfirmed_additions) + if sum < amount: + raise ValueError( + "Can't make this transaction at the moment. Waiting for the change from the previous transaction." + ) + + self.log.info(f"Successfully selected coins: {used_coins}") + return used_coins + + async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: + sigs: List[G2Element] = [] + for spend in spend_bundle.coin_spends: + matched, puzzle_args = match_cat_puzzle(spend.puzzle_reveal.to_program()) + if matched: + _, _, inner_puzzle = puzzle_args + puzzle_hash = inner_puzzle.get_tree_hash() + pubkey, private = await self.wallet_state_manager.get_keys(puzzle_hash) + synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) + error, conditions, cost = conditions_dict_for_solution( + spend.puzzle_reveal.to_program(), + spend.solution.to_program(), + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + ) + if conditions is not None: + synthetic_pk = synthetic_secret_key.get_g1() + for pk, msg in pkm_pairs_for_conditions_dict( + conditions, spend.coin.name(), self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ): + try: + assert bytes(synthetic_pk) == pk + sigs.append(AugSchemeMPL.sign(synthetic_secret_key, msg)) + except AssertionError: + raise ValueError("This spend bundle cannot be signed by the CAT wallet") + + agg_sig = AugSchemeMPL.aggregate(sigs) + return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) + + async def inner_puzzle_for_cc_puzhash(self, cc_hash: bytes32) -> Program: + record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( + cc_hash + ) + inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(bytes(record.pubkey)) + return inner_puzzle + + async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: + record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( + puzzle_hash + ) + if record is None: + return puzzle_hash + else: + return (await self.inner_puzzle_for_cc_puzhash(puzzle_hash)).get_tree_hash() + + async def get_lineage_proof_for_coin(self, coin) -> Optional[LineageProof]: + for name, proof in self.cat_info.lineage_proofs: + if name == coin.parent_coin_info: + return proof + return None + + async def create_tandem_xch_tx( + self, + fee: uint64, + amount_to_claim: uint64, + announcement_to_assert: Optional[Announcement] = None, + ) -> Tuple[TransactionRecord, Optional[Announcement]]: + """ + This function creates a non-CAT transaction to pay fees, contribute funds for issuance, and absorb melt value. + It is meant to be called in `generate_unsigned_spendbundle` and as such should be called under the + wallet_state_manager lock + """ + announcement = None + if fee > amount_to_claim: + chia_coins = await self.standard_wallet.select_coins(fee) + origin_id = list(chia_coins)[0].name() + chia_tx = await self.standard_wallet.generate_signed_transaction( + uint64(0), + (await self.standard_wallet.get_new_puzzlehash()), + fee=uint64(fee - amount_to_claim), + coins=chia_coins, + origin_id=origin_id, # We specify this so that we know the coin that is making the announcement + negative_change_allowed=False, + coin_announcements_to_consume={announcement_to_assert} if announcement_to_assert is not None else None, + ) + assert chia_tx.spend_bundle is not None + + message = None + for spend in chia_tx.spend_bundle.coin_spends: + if spend.coin.name() == origin_id: + conditions = spend.puzzle_reveal.to_program().run(spend.solution.to_program()).as_python() + for condition in conditions: + if condition[0] == ConditionOpcode.CREATE_COIN_ANNOUNCEMENT: + message = condition[1] + + assert message is not None + announcement = Announcement(origin_id, message) + else: + chia_coins = await self.standard_wallet.select_coins(fee) + selected_amount = sum([c.amount for c in chia_coins]) + chia_tx = await self.standard_wallet.generate_signed_transaction( + uint64(selected_amount + amount_to_claim - fee), + (await self.standard_wallet.get_new_puzzlehash()), + coins=chia_coins, + negative_change_allowed=True, + coin_announcements_to_consume={announcement_to_assert} if announcement_to_assert is not None else None, + ) + assert chia_tx.spend_bundle is not None + + return chia_tx, announcement + + async def generate_unsigned_spendbundle( + self, + payments: List[Payment], + fee: uint64 = uint64(0), + cat_discrepancy: Optional[Tuple[int, Program]] = None, # (extra_delta, limitations_solution) + coins: Set[Coin] = None, + coin_announcements_to_consume: Optional[Set[Announcement]] = None, + puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, + ) -> Tuple[SpendBundle, Optional[TransactionRecord]]: + if coin_announcements_to_consume is not None: + coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume} + else: + coin_announcements_bytes = None + + if puzzle_announcements_to_consume is not None: + puzzle_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in puzzle_announcements_to_consume} + else: + puzzle_announcements_bytes = None + + if cat_discrepancy is not None: + extra_delta, limitations_solution = cat_discrepancy + else: + extra_delta, limitations_solution = 0, Program.to([]) + payment_amount: int = sum([p.amount for p in payments]) + starting_amount: int = payment_amount - extra_delta + + if coins is None: + cat_coins = await self.select_coins(uint64(starting_amount)) + else: + cat_coins = coins + + selected_cat_amount = sum([c.amount for c in cat_coins]) + assert selected_cat_amount >= starting_amount + + # Figure out if we need to absorb/melt some XCH as part of this + regular_chia_to_claim: int = 0 + if payment_amount > starting_amount: + fee = uint64(fee + payment_amount - starting_amount) + elif payment_amount < starting_amount: + regular_chia_to_claim = payment_amount + + need_chia_transaction = (fee > 0 or regular_chia_to_claim > 0) and (fee - regular_chia_to_claim != 0) + + # Calculate standard puzzle solutions + change = selected_cat_amount - starting_amount + primaries: List[AmountWithPuzzlehash] = [] + for payment in payments: + primaries.append({"puzzlehash": payment.puzzle_hash, "amount": payment.amount, "memos": payment.memos}) + + if change > 0: + changepuzzlehash = await self.get_new_inner_hash() + primaries.append({"puzzlehash": changepuzzlehash, "amount": uint64(change), "memos": []}) + + limitations_program_reveal = Program.to([]) + if self.cat_info.my_tail is None: + assert cat_discrepancy is None + elif cat_discrepancy is not None: + limitations_program_reveal = self.cat_info.my_tail + + # Loop through the coins we've selected and gather the information we need to spend them + spendable_cc_list = [] + chia_tx = None + first = True + for coin in cat_coins: + if first: + first = False + if need_chia_transaction: + if fee > regular_chia_to_claim: + announcement = Announcement(coin.name(), b"$", b"\xca") + chia_tx, _ = await self.create_tandem_xch_tx( + fee, uint64(regular_chia_to_claim), announcement_to_assert=announcement + ) + innersol = self.standard_wallet.make_solution( + primaries=primaries, + coin_announcements={announcement.message}, + coin_announcements_to_assert=coin_announcements_bytes, + puzzle_announcements_to_assert=puzzle_announcements_bytes, + ) + elif regular_chia_to_claim > fee: + chia_tx, _ = await self.create_tandem_xch_tx(fee, uint64(regular_chia_to_claim)) + innersol = self.standard_wallet.make_solution( + primaries=primaries, coin_announcements_to_assert={announcement.name()} + ) + else: + innersol = self.standard_wallet.make_solution( + primaries=primaries, + coin_announcements_to_assert=coin_announcements_bytes, + puzzle_announcements_to_assert=puzzle_announcements_bytes, + ) + else: + innersol = self.standard_wallet.make_solution(primaries=[]) + inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) + lineage_proof = await self.get_lineage_proof_for_coin(coin) + assert lineage_proof is not None + new_spendable_cc = SpendableCAT( + coin, + self.cat_info.limitations_program_hash, + inner_puzzle, + innersol, + limitations_solution=limitations_solution, + extra_delta=extra_delta, + lineage_proof=lineage_proof, + limitations_program_reveal=limitations_program_reveal, + ) + spendable_cc_list.append(new_spendable_cc) + + cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cc_list) + chia_spend_bundle = SpendBundle([], G2Element()) + if chia_tx is not None and chia_tx.spend_bundle is not None: + chia_spend_bundle = chia_tx.spend_bundle + + return ( + SpendBundle.aggregate( + [ + cat_spend_bundle, + chia_spend_bundle, + ] + ), + chia_tx, + ) + + async def generate_signed_transaction( + self, + amounts: List[uint64], + puzzle_hashes: List[bytes32], + fee: uint64 = uint64(0), + coins: Set[Coin] = None, + ignore_max_send_amount: bool = False, + memos: Optional[List[List[bytes]]] = None, + coin_announcements_to_consume: Optional[Set[Announcement]] = None, + puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, + ) -> List[TransactionRecord]: + if memos is None: + memos = [[] for _ in range(len(puzzle_hashes))] + + if not (len(memos) == len(puzzle_hashes) == len(amounts)): + raise ValueError("Memos, puzzle_hashes, and amounts must have the same length") + + payments = [] + for amount, puzhash, memo_list in zip(amounts, puzzle_hashes, memos): + memos_with_hint: List[bytes] = [puzhash] + memos_with_hint.extend(memo_list) + payments.append(Payment(puzhash, amount, memos_with_hint)) + + payment_sum = sum([p.amount for p in payments]) + if not ignore_max_send_amount: + max_send = await self.get_max_send_amount() + if payment_sum > max_send: + raise ValueError(f"Can't send more than {max_send} in a single transaction") + + unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle( + payments, + fee, + coins=coins, + coin_announcements_to_consume=coin_announcements_to_consume, + puzzle_announcements_to_consume=puzzle_announcements_to_consume, + ) + spend_bundle = await self.sign(unsigned_spend_bundle) + + # TODO add support for array in stored records + tx_list = [ + TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=puzzle_hashes[0], + amount=uint64(payment_sum), + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=spend_bundle.additions(), + removals=spend_bundle.removals(), + wallet_id=self.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=spend_bundle.name(), + memos=list(spend_bundle.get_memos().items()), + ) + ] + + if chia_tx is not None: + tx_list.append( + TransactionRecord( + confirmed_at_height=chia_tx.confirmed_at_height, + created_at_time=chia_tx.created_at_time, + to_puzzle_hash=chia_tx.to_puzzle_hash, + amount=chia_tx.amount, + fee_amount=chia_tx.fee_amount, + confirmed=chia_tx.confirmed, + sent=chia_tx.sent, + spend_bundle=None, + additions=chia_tx.additions, + removals=chia_tx.removals, + wallet_id=chia_tx.wallet_id, + sent_to=chia_tx.sent_to, + trade_id=chia_tx.trade_id, + type=chia_tx.type, + name=chia_tx.name, + memos=[], + ) + ) + + return tx_list + + async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof], in_transaction=False): + """ + Lineage proofs are stored as a list of parent coins and the lineage proof you will need if they are the + parent of the coin you are trying to spend. 'If I'm your parent, here's the info you need to spend yourself' + """ + self.log.info(f"Adding parent {name}: {lineage}") + current_list = self.cat_info.lineage_proofs.copy() + if (name, lineage) not in current_list: + current_list.append((name, lineage)) + cat_info: CATInfo = CATInfo(self.cat_info.limitations_program_hash, self.cat_info.my_tail, current_list) + await self.save_info(cat_info, in_transaction) + + async def remove_lineage(self, name: bytes32, in_transaction=False): + self.log.info(f"Removing parent {name} (probably had a non-CAT parent)") + current_list = self.cat_info.lineage_proofs.copy() + current_list = list(filter(lambda tup: tup[0] != name, current_list)) + cat_info: CATInfo = CATInfo(self.cat_info.limitations_program_hash, self.cat_info.my_tail, current_list) + await self.save_info(cat_info, in_transaction) + + async def save_info(self, cat_info: CATInfo, in_transaction): + self.cat_info = cat_info + current_info = self.wallet_info + data_str = bytes(cat_info).hex() + wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) + self.wallet_info = wallet_info + await self.wallet_state_manager.user_store.update_wallet(wallet_info, in_transaction) diff --git a/chia/wallet/cc_wallet/cc_utils.py b/chia/wallet/cc_wallet/cc_utils.py deleted file mode 100644 index 269d2d0e9bcd..000000000000 --- a/chia/wallet/cc_wallet/cc_utils.py +++ /dev/null @@ -1,255 +0,0 @@ -import dataclasses -from typing import List, Optional, Tuple - -from blspy import AugSchemeMPL, G2Element - -from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.program import Program, INFINITE_COST -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.condition_opcodes import ConditionOpcode -from chia.types.spend_bundle import CoinSpend, SpendBundle -from chia.util.condition_tools import conditions_dict_for_solution -from chia.util.ints import uint64 -from chia.wallet.puzzles.cc_loader import CC_MOD, LOCK_INNER_PUZZLE -from chia.wallet.puzzles.genesis_by_coin_id_with_0 import ( - genesis_coin_id_for_genesis_coin_checker, - lineage_proof_for_coin, - lineage_proof_for_genesis, - lineage_proof_for_zero, -) - -NULL_SIGNATURE = G2Element() - -ANYONE_CAN_SPEND_PUZZLE = Program.to(1) # simply return the conditions - -# information needed to spend a cc -# if we ever support more genesis conditions, like a re-issuable coin, -# we may need also to save the `genesis_coin_mod` or its hash - - -@dataclasses.dataclass -class SpendableCC: - coin: Coin - genesis_coin_id: bytes32 - inner_puzzle: Program - lineage_proof: Program - - -def cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, inner_puzzle) -> Program: - """ - Given an inner puzzle, generate a puzzle program for a specific cc. - """ - return mod_code.curry(mod_code.get_tree_hash(), genesis_coin_checker, inner_puzzle) - # return mod_code.curry([mod_code.get_tree_hash(), genesis_coin_checker, inner_puzzle]) - - -def cc_puzzle_hash_for_inner_puzzle_hash(mod_code, genesis_coin_checker, inner_puzzle_hash) -> bytes32: - """ - Given an inner puzzle hash, calculate a puzzle program hash for a specific cc. - """ - gcc_hash = genesis_coin_checker.get_tree_hash() - return mod_code.curry(mod_code.get_tree_hash(), gcc_hash, inner_puzzle_hash).get_tree_hash( - gcc_hash, inner_puzzle_hash - ) - - -def lineage_proof_for_cc_parent(parent_coin: Coin, parent_inner_puzzle_hash: bytes32) -> Program: - return Program.to( - ( - 1, - [parent_coin.parent_coin_info, parent_inner_puzzle_hash, parent_coin.amount], - ) - ) - - -def subtotals_for_deltas(deltas) -> List[int]: - """ - Given a list of deltas corresponding to input coins, create the "subtotals" list - needed in solutions spending those coins. - """ - - subtotals = [] - subtotal = 0 - - for delta in deltas: - subtotals.append(subtotal) - subtotal += delta - - # tweak the subtotals so the smallest value is 0 - subtotal_offset = min(subtotals) - subtotals = [_ - subtotal_offset for _ in subtotals] - return subtotals - - -def coin_spend_for_lock_coin( - prev_coin: Coin, - subtotal: int, - coin: Coin, -) -> CoinSpend: - puzzle_reveal = LOCK_INNER_PUZZLE.curry(prev_coin.as_list(), subtotal) - coin = Coin(coin.name(), puzzle_reveal.get_tree_hash(), uint64(0)) - coin_spend = CoinSpend(coin, puzzle_reveal, Program.to(0)) - return coin_spend - - -def bundle_for_spendable_cc_list(spendable_cc: SpendableCC) -> Program: - pair = (spendable_cc.coin.as_list(), spendable_cc.lineage_proof) - return Program.to(pair) - - -def spend_bundle_for_spendable_ccs( - mod_code: Program, - genesis_coin_checker: Program, - spendable_cc_list: List[SpendableCC], - inner_solutions: List[Program], - sigs: Optional[List[G2Element]] = [], -) -> SpendBundle: - """ - Given a list of `SpendableCC` objects and inner solutions for those objects, create a `SpendBundle` - that spends all those coins. Note that it the signature is not calculated it, so the caller is responsible - for fixing it. - """ - - N = len(spendable_cc_list) - - if len(inner_solutions) != N: - raise ValueError("spendable_cc_list and inner_solutions are different lengths") - - input_coins = [_.coin for _ in spendable_cc_list] - - # figure out what the output amounts are by running the inner puzzles & solutions - output_amounts = [] - for cc_spend_info, inner_solution in zip(spendable_cc_list, inner_solutions): - error, conditions, cost = conditions_dict_for_solution( - cc_spend_info.inner_puzzle, inner_solution, INFINITE_COST - ) - total = 0 - if conditions: - for _ in conditions.get(ConditionOpcode.CREATE_COIN, []): - total += Program.to(_.vars[1]).as_int() - output_amounts.append(total) - - coin_spends = [] - - deltas = [input_coins[_].amount - output_amounts[_] for _ in range(N)] - subtotals = subtotals_for_deltas(deltas) - - if sum(deltas) != 0: - raise ValueError("input and output amounts don't match") - - bundles = [bundle_for_spendable_cc_list(_) for _ in spendable_cc_list] - - for index in range(N): - cc_spend_info = spendable_cc_list[index] - - puzzle_reveal = cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, cc_spend_info.inner_puzzle) - - prev_index = (index - 1) % N - next_index = (index + 1) % N - prev_bundle = bundles[prev_index] - my_bundle = bundles[index] - next_bundle = bundles[next_index] - - solution = [ - inner_solutions[index], - prev_bundle, - my_bundle, - next_bundle, - subtotals[index], - ] - coin_spend = CoinSpend(input_coins[index], puzzle_reveal, Program.to(solution)) - coin_spends.append(coin_spend) - - if sigs is None or sigs == []: - return SpendBundle(coin_spends, NULL_SIGNATURE) - else: - return SpendBundle(coin_spends, AugSchemeMPL.aggregate(sigs)) - - -def is_cc_mod(inner_f: Program): - """ - You may want to generalize this if different `CC_MOD` templates are supported. - """ - return inner_f == CC_MOD - - -def check_is_cc_puzzle(puzzle: Program): - r = puzzle.uncurry() - if r is None: - return False - inner_f, args = r - return is_cc_mod(inner_f) - - -def uncurry_cc(puzzle: Program) -> Optional[Tuple[Program, Program, Program]]: - """ - Take a puzzle and return `None` if it's not a `CC_MOD` cc, or - a triple of `mod_hash, genesis_coin_checker, inner_puzzle` if it is. - """ - r = puzzle.uncurry() - if r is None: - return r - inner_f, args = r - if not is_cc_mod(inner_f): - return None - - mod_hash, genesis_coin_checker, inner_puzzle = list(args.as_iter()) - return mod_hash, genesis_coin_checker, inner_puzzle - - -def get_lineage_proof_from_coin_and_puz(parent_coin, parent_puzzle): - r = uncurry_cc(parent_puzzle) - if r: - mod_hash, genesis_checker, inner_puzzle = r - lineage_proof = lineage_proof_for_cc_parent(parent_coin, inner_puzzle.get_tree_hash()) - else: - if parent_coin.amount == 0: - lineage_proof = lineage_proof_for_zero(parent_coin) - else: - lineage_proof = lineage_proof_for_genesis(parent_coin) - return lineage_proof - - -def spendable_cc_list_from_coin_spend(coin_spend: CoinSpend, hash_to_puzzle_f) -> List[SpendableCC]: - - """ - Given a `CoinSpend`, extract out a list of `SpendableCC` objects. - - Since `SpendableCC` needs to track the inner puzzles and a `Coin` only includes - puzzle hash, we also need a `hash_to_puzzle_f` function that turns puzzle hashes into - the corresponding puzzles. This is generally either a `dict` or some kind of DB - (if it's large or persistent). - """ - - spendable_cc_list = [] - - coin = coin_spend.coin - puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal)) - r = uncurry_cc(puzzle) - if r: - mod_hash, genesis_coin_checker, inner_puzzle = r - lineage_proof = lineage_proof_for_cc_parent(coin, inner_puzzle.get_tree_hash()) - else: - lineage_proof = lineage_proof_for_coin(coin) - - for new_coin in coin_spend.additions(): - puzzle = hash_to_puzzle_f(new_coin.puzzle_hash) - if puzzle is None: - # we don't recognize this puzzle hash, skip it - continue - r = uncurry_cc(puzzle) - if r is None: - # this isn't a cc puzzle - continue - - mod_hash, genesis_coin_checker, inner_puzzle = r - - genesis_coin_id = genesis_coin_id_for_genesis_coin_checker(genesis_coin_checker) - - # TODO: address hint error and remove ignore - # error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32" - # [arg-type] - cc_spend_info = SpendableCC(new_coin, genesis_coin_id, inner_puzzle, lineage_proof) # type: ignore[arg-type] - spendable_cc_list.append(cc_spend_info) - - return spendable_cc_list diff --git a/chia/wallet/cc_wallet/cc_wallet.py b/chia/wallet/cc_wallet/cc_wallet.py deleted file mode 100644 index 6d555766ffa3..000000000000 --- a/chia/wallet/cc_wallet/cc_wallet.py +++ /dev/null @@ -1,765 +0,0 @@ -from __future__ import annotations - -import logging -import time -from dataclasses import replace -from secrets import token_bytes -from typing import Any, Dict, List, Optional, Set - -from blspy import AugSchemeMPL, G2Element - -from chia.consensus.cost_calculator import calculate_cost_of_program, NPCResult -from chia.full_node.bundle_tools import simple_solution_generator -from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions -from chia.protocols.wallet_protocol import PuzzleSolutionResponse -from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.program import Program -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.coin_spend import CoinSpend -from chia.types.generator_types import BlockGenerator -from chia.types.spend_bundle import SpendBundle -from chia.util.byte_types import hexstr_to_bytes -from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict -from chia.util.ints import uint8, uint32, uint64, uint128 -from chia.util.json_util import dict_to_json_str -from chia.wallet.block_record import HeaderBlockRecord -from chia.wallet.cc_wallet.cc_info import CCInfo -from chia.wallet.cc_wallet.cc_utils import ( - CC_MOD, - SpendableCC, - cc_puzzle_for_inner_puzzle, - cc_puzzle_hash_for_inner_puzzle_hash, - get_lineage_proof_from_coin_and_puz, - spend_bundle_for_spendable_ccs, - uncurry_cc, -) -from chia.wallet.derivation_record import DerivationRecord -from chia.wallet.puzzles.genesis_by_coin_id_with_0 import ( - create_genesis_or_zero_coin_checker, - genesis_coin_id_for_genesis_coin_checker, - lineage_proof_for_genesis, -) -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( - DEFAULT_HIDDEN_PUZZLE_HASH, - calculate_synthetic_secret_key, -) -from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.transaction_type import TransactionType -from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType -from chia.wallet.wallet import Wallet -from chia.wallet.wallet_coin_record import WalletCoinRecord -from chia.wallet.wallet_info import WalletInfo - - -class CCWallet: - wallet_state_manager: Any - log: logging.Logger - wallet_info: WalletInfo - cc_coin_record: WalletCoinRecord - cc_info: CCInfo - standard_wallet: Wallet - base_puzzle_program: Optional[bytes] - base_inner_puzzle_hash: Optional[bytes32] - cost_of_single_tx: Optional[int] - - @staticmethod - async def create_new_cc( - wallet_state_manager: Any, - wallet: Wallet, - amount: uint64, - ): - self = CCWallet() - self.cost_of_single_tx = None - self.base_puzzle_program = None - self.base_inner_puzzle_hash = None - self.standard_wallet = wallet - self.log = logging.getLogger(__name__) - std_wallet_id = self.standard_wallet.wallet_id - bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id, None) - if amount > bal: - raise ValueError("Not enough balance") - self.wallet_state_manager = wallet_state_manager - - self.cc_info = CCInfo(None, []) - info_as_string = bytes(self.cc_info).hex() - self.wallet_info = await wallet_state_manager.user_store.create_wallet( - "CC Wallet", WalletType.COLOURED_COIN, info_as_string - ) - if self.wallet_info is None: - raise ValueError("Internal Error") - - try: - spend_bundle = await self.generate_new_coloured_coin(amount) - except Exception: - await wallet_state_manager.user_store.delete_wallet(self.id()) - raise - if spend_bundle is None: - await wallet_state_manager.user_store.delete_wallet(self.id()) - raise ValueError("Failed to create spend.") - - await self.wallet_state_manager.add_new_wallet(self, self.id()) - - # Change and actual coloured coin - non_ephemeral_spends: List[Coin] = spend_bundle.not_ephemeral_additions() - cc_coin = None - puzzle_store = self.wallet_state_manager.puzzle_store - - for c in non_ephemeral_spends: - info = await puzzle_store.wallet_info_for_puzzle_hash(c.puzzle_hash) - if info is None: - raise ValueError("Internal Error") - id, wallet_type = info - if id == self.id(): - cc_coin = c - - if cc_coin is None: - raise ValueError("Internal Error, unable to generate new coloured coin") - - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] - regular_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=cc_coin.puzzle_hash, - amount=uint64(cc_coin.amount), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(0), - spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), - wallet_id=self.wallet_state_manager.main_wallet.id(), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), - name=token_bytes(), # type: ignore[arg-type] - ) - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] - cc_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=cc_coin.puzzle_hash, - amount=uint64(cc_coin.amount), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(10), - spend_bundle=None, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), - wallet_id=self.id(), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.INCOMING_TX.value), - name=token_bytes(), # type: ignore[arg-type] - ) - await self.standard_wallet.push_transaction(regular_record) - await self.standard_wallet.push_transaction(cc_record) - return self - - @staticmethod - async def create_wallet_for_cc( - wallet_state_manager: Any, - wallet: Wallet, - genesis_checker_hex: str, - ) -> CCWallet: - self = CCWallet() - self.cost_of_single_tx = None - self.base_puzzle_program = None - self.base_inner_puzzle_hash = None - self.standard_wallet = wallet - self.log = logging.getLogger(__name__) - - self.wallet_state_manager = wallet_state_manager - - self.cc_info = CCInfo(Program.from_bytes(bytes.fromhex(genesis_checker_hex)), []) - info_as_string = bytes(self.cc_info).hex() - self.wallet_info = await wallet_state_manager.user_store.create_wallet( - "CC Wallet", WalletType.COLOURED_COIN, info_as_string - ) - if self.wallet_info is None: - raise Exception("wallet_info is None") - - await self.wallet_state_manager.add_new_wallet(self, self.id()) - return self - - @staticmethod - async def create( - wallet_state_manager: Any, - wallet: Wallet, - wallet_info: WalletInfo, - ) -> CCWallet: - self = CCWallet() - - self.log = logging.getLogger(__name__) - - self.cost_of_single_tx = None - self.wallet_state_manager = wallet_state_manager - self.wallet_info = wallet_info - self.standard_wallet = wallet - self.cc_info = CCInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data)) - self.base_puzzle_program = None - self.base_inner_puzzle_hash = None - return self - - @classmethod - def type(cls) -> uint8: - return uint8(WalletType.COLOURED_COIN) - - def id(self) -> uint32: - return self.wallet_info.id - - async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint64: - if record_list is None: - record_list = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.id()) - - amount: uint64 = uint64(0) - for record in record_list: - lineage = await self.get_lineage_proof_for_coin(record.coin) - if lineage is not None: - amount = uint64(amount + record.coin.amount) - - self.log.info(f"Confirmed balance for cc wallet {self.id()} is {amount}") - return uint64(amount) - - async def get_unconfirmed_balance(self, unspent_records=None) -> uint128: - confirmed = await self.get_confirmed_balance(unspent_records) - unconfirmed_tx: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( - self.id() - ) - addition_amount = 0 - removal_amount = 0 - - for record in unconfirmed_tx: - if TransactionType(record.type) is TransactionType.INCOMING_TX: - addition_amount += record.amount - else: - removal_amount += record.amount - - result = confirmed - removal_amount + addition_amount - - self.log.info(f"Unconfirmed balance for cc wallet {self.id()} is {result}") - return uint128(result) - - async def get_max_send_amount(self, records=None): - spendable: List[WalletCoinRecord] = list( - await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id(), records) - ) - if len(spendable) == 0: - return 0 - spendable.sort(reverse=True, key=lambda record: record.coin.amount) - if self.cost_of_single_tx is None: - coin = spendable[0].coin - tx = await self.generate_signed_transaction( - [coin.amount], [coin.puzzle_hash], coins={coin}, ignore_max_send_amount=True - ) - program: BlockGenerator = simple_solution_generator(tx.spend_bundle) - # npc contains names of the coins removed, puzzle_hashes and their spend conditions - result: NPCResult = get_name_puzzle_conditions( - program, - self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - cost_per_byte=self.wallet_state_manager.constants.COST_PER_BYTE, - mempool_mode=True, - ) - cost_result: uint64 = calculate_cost_of_program( - program.program, result, self.wallet_state_manager.constants.COST_PER_BYTE - ) - self.cost_of_single_tx = cost_result - self.log.info(f"Cost of a single tx for standard wallet: {self.cost_of_single_tx}") - - max_cost = self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 2 # avoid full block TXs - current_cost = 0 - total_amount = 0 - total_coin_count = 0 - - for record in spendable: - current_cost += self.cost_of_single_tx - total_amount += record.coin.amount - total_coin_count += 1 - if current_cost + self.cost_of_single_tx > max_cost: - break - - return total_amount - - async def get_name(self): - return self.wallet_info.name - - async def set_name(self, new_name: str): - new_info = replace(self.wallet_info, name=new_name) - self.wallet_info = new_info - await self.wallet_state_manager.user_store.update_wallet(self.wallet_info, False) - - def get_colour(self) -> str: - assert self.cc_info.my_genesis_checker is not None - return bytes(self.cc_info.my_genesis_checker).hex() - - async def coin_added(self, coin: Coin, height: uint32): - """Notification from wallet state manager that wallet has been received.""" - self.log.info(f"CC wallet has been notified that {coin} was added") - - search_for_parent: bool = True - - inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) - lineage_proof = Program.to((1, [coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount])) - await self.add_lineage(coin.name(), lineage_proof, True) - - for name, lineage_proofs in self.cc_info.lineage_proofs: - if coin.parent_coin_info == name: - search_for_parent = False - break - - if search_for_parent: - data: Dict[str, Any] = { - "data": { - "action_data": { - "api_name": "request_puzzle_solution", - "height": height, - "coin_name": coin.parent_coin_info, - "received_coin": coin.name(), - } - } - } - - data_str = dict_to_json_str(data) - await self.wallet_state_manager.create_action( - name="request_puzzle_solution", - wallet_id=self.id(), - wallet_type=self.type(), - callback="puzzle_solution_received", - done=False, - data=data_str, - in_transaction=True, - ) - - async def puzzle_solution_received(self, response: PuzzleSolutionResponse, action_id: int): - coin_name = response.coin_name - height = response.height - puzzle: Program = response.puzzle - r = uncurry_cc(puzzle) - header_hash = self.wallet_state_manager.blockchain.height_to_hash(height) - block: Optional[ - HeaderBlockRecord - ] = await self.wallet_state_manager.blockchain.block_store.get_header_block_record(header_hash) - if block is None: - return None - - removals = block.removals - - if r is not None: - mod_hash, genesis_coin_checker, inner_puzzle = r - self.log.info(f"parent: {coin_name} inner_puzzle for parent is {inner_puzzle}") - parent_coin = None - for coin in removals: - if coin.name() == coin_name: - parent_coin = coin - if parent_coin is None: - raise ValueError("Error in finding parent") - lineage_proof = get_lineage_proof_from_coin_and_puz(parent_coin, puzzle) - await self.add_lineage(coin_name, lineage_proof) - await self.wallet_state_manager.action_store.action_done(action_id) - - async def get_new_inner_hash(self) -> bytes32: - return await self.standard_wallet.get_new_puzzlehash() - - async def get_new_inner_puzzle(self) -> Program: - return await self.standard_wallet.get_new_puzzle() - - async def get_puzzle_hash(self, new: bool): - return await self.standard_wallet.get_puzzle_hash(new) - - async def get_new_puzzlehash(self) -> bytes32: - return await self.standard_wallet.get_new_puzzlehash() - - def puzzle_for_pk(self, pubkey) -> Program: - inner_puzzle = self.standard_wallet.puzzle_for_pk(bytes(pubkey)) - cc_puzzle: Program = cc_puzzle_for_inner_puzzle(CC_MOD, self.cc_info.my_genesis_checker, inner_puzzle) - self.base_puzzle_program = bytes(cc_puzzle) - self.base_inner_puzzle_hash = inner_puzzle.get_tree_hash() - return cc_puzzle - - async def get_new_cc_puzzle_hash(self): - return (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash - - # Create a new coin of value 0 with a given colour - async def generate_zero_val_coin(self, send=True, exclude: List[Coin] = None) -> SpendBundle: - if self.cc_info.my_genesis_checker is None: - raise ValueError("My genesis checker is None") - if exclude is None: - exclude = [] - coins = await self.standard_wallet.select_coins(0, exclude) - - assert coins != set() - - origin = coins.copy().pop() - origin_id = origin.name() - - cc_inner = await self.get_new_inner_hash() - cc_puzzle_hash: bytes32 = cc_puzzle_hash_for_inner_puzzle_hash( - CC_MOD, self.cc_info.my_genesis_checker, cc_inner - ) - - tx: TransactionRecord = await self.standard_wallet.generate_signed_transaction( - uint64(0), cc_puzzle_hash, uint64(0), origin_id, coins - ) - assert tx.spend_bundle is not None - full_spend: SpendBundle = tx.spend_bundle - self.log.info(f"Generate zero val coin: cc_puzzle_hash is {cc_puzzle_hash}") - - # generate eve coin so we can add future lineage_proofs even if we don't eve spend - eve_coin = Coin(origin_id, cc_puzzle_hash, uint64(0)) - - await self.add_lineage( - eve_coin.name(), - Program.to( - ( - 1, - [eve_coin.parent_coin_info, cc_inner, eve_coin.amount], - ) - ), - ) - await self.add_lineage(eve_coin.parent_coin_info, Program.to((0, [origin.as_list(), 1]))) - - if send: - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] - regular_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=cc_puzzle_hash, - amount=uint64(0), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(10), - spend_bundle=full_spend, - additions=full_spend.additions(), - removals=full_spend.removals(), - wallet_id=uint32(1), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.INCOMING_TX.value), - name=token_bytes(), # type: ignore[arg-type] - ) - cc_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=cc_puzzle_hash, - amount=uint64(0), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(0), - spend_bundle=full_spend, - additions=full_spend.additions(), - removals=full_spend.removals(), - wallet_id=self.id(), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.INCOMING_TX.value), - name=full_spend.name(), - ) - await self.wallet_state_manager.add_transaction(regular_record) - await self.wallet_state_manager.add_pending_transaction(cc_record) - - return full_spend - - async def get_spendable_balance(self, records=None) -> uint64: - coins = await self.get_cc_spendable_coins(records) - amount = 0 - for record in coins: - amount += record.coin.amount - - return uint64(amount) - - async def get_pending_change_balance(self) -> uint64: - unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id()) - addition_amount = 0 - for record in unconfirmed_tx: - if not record.is_in_mempool(): - continue - our_spend = False - for coin in record.removals: - # Don't count eve spend as change - if coin.parent_coin_info.hex() == self.get_colour(): - continue - if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()): - our_spend = True - break - - if our_spend is not True: - continue - - for coin in record.additions: - if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()): - addition_amount += coin.amount - - return uint64(addition_amount) - - async def get_cc_spendable_coins(self, records=None) -> List[WalletCoinRecord]: - result: List[WalletCoinRecord] = [] - - record_list: Set[WalletCoinRecord] = await self.wallet_state_manager.get_spendable_coins_for_wallet( - self.id(), records - ) - - for record in record_list: - lineage = await self.get_lineage_proof_for_coin(record.coin) - if lineage is not None: - result.append(record) - - return result - - async def select_coins(self, amount: uint64) -> Set[Coin]: - """ - Returns a set of coins that can be used for generating a new transaction. - Note: Must be called under wallet state manager lock - """ - - spendable_am = await self.get_confirmed_balance() - - if amount > spendable_am: - error_msg = f"Can't select amount higher than our spendable balance {amount}, spendable {spendable_am}" - self.log.warning(error_msg) - raise ValueError(error_msg) - - self.log.info(f"About to select coins for amount {amount}") - spendable: List[WalletCoinRecord] = await self.get_cc_spendable_coins() - - sum = 0 - used_coins: Set = set() - - # Use older coins first - spendable.sort(key=lambda r: r.confirmed_block_height) - - # Try to use coins from the store, if there isn't enough of "unused" - # coins use change coins that are not confirmed yet - unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet( - self.id() - ) - for coinrecord in spendable: - if sum >= amount and len(used_coins) > 0: - break - if coinrecord.coin.name() in unconfirmed_removals: - continue - sum += coinrecord.coin.amount - used_coins.add(coinrecord.coin) - self.log.info(f"Selected coin: {coinrecord.coin.name()} at height {coinrecord.confirmed_block_height}!") - - # This happens when we couldn't use one of the coins because it's already used - # but unconfirmed, and we are waiting for the change. (unconfirmed_additions) - if sum < amount: - raise ValueError( - "Can't make this transaction at the moment. Waiting for the change from the previous transaction." - ) - - self.log.info(f"Successfully selected coins: {used_coins}") - return used_coins - - async def get_sigs(self, innerpuz: Program, innersol: Program, coin_name: bytes32) -> List[G2Element]: - puzzle_hash = innerpuz.get_tree_hash() - pubkey, private = await self.wallet_state_manager.get_keys(puzzle_hash) - synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) - sigs: List[G2Element] = [] - error, conditions, cost = conditions_dict_for_solution( - innerpuz, innersol, self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM - ) - if conditions is not None: - for _, msg in pkm_pairs_for_conditions_dict( - conditions, coin_name, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - signature = AugSchemeMPL.sign(synthetic_secret_key, msg) - sigs.append(signature) - return sigs - - async def inner_puzzle_for_cc_puzhash(self, cc_hash: bytes32) -> Program: - record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( - cc_hash - ) - inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(bytes(record.pubkey)) - return inner_puzzle - - async def get_lineage_proof_for_coin(self, coin) -> Optional[Program]: - for name, proof in self.cc_info.lineage_proofs: - if name == coin.parent_coin_info: - return proof - return None - - async def generate_signed_transaction( - self, - amounts: List[uint64], - puzzle_hashes: List[bytes32], - fee: uint64 = uint64(0), - origin_id: bytes32 = None, - coins: Set[Coin] = None, - ignore_max_send_amount: bool = False, - ) -> TransactionRecord: - # Get coins and calculate amount of change required - outgoing_amount = uint64(sum(amounts)) - total_outgoing = outgoing_amount + fee - - if not ignore_max_send_amount: - max_send = await self.get_max_send_amount() - if total_outgoing > max_send: - raise ValueError(f"Can't send more than {max_send} in a single transaction") - - if coins is None: - selected_coins: Set[Coin] = await self.select_coins(uint64(total_outgoing)) - else: - selected_coins = coins - - total_amount = sum([x.amount for x in selected_coins]) - change = total_amount - total_outgoing - primaries: List[AmountWithPuzzlehash] = [] - for amount, puzzle_hash in zip(amounts, puzzle_hashes): - primaries.append({"puzzlehash": puzzle_hash, "amount": amount}) - - if change > 0: - changepuzzlehash = await self.get_new_inner_hash() - primaries.append({"puzzlehash": changepuzzlehash, "amount": uint64(change)}) - - coin = list(selected_coins)[0] - inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) - - if self.cc_info.my_genesis_checker is None: - raise ValueError("My genesis checker is None") - - genesis_id = genesis_coin_id_for_genesis_coin_checker(self.cc_info.my_genesis_checker) - - spendable_cc_list = [] - innersol_list = [] - sigs: List[G2Element] = [] - first = True - for coin in selected_coins: - coin_inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) - if first: - first = False - if fee > 0: - innersol = self.standard_wallet.make_solution(primaries=primaries, fee=fee) - else: - innersol = self.standard_wallet.make_solution(primaries=primaries) - else: - innersol = self.standard_wallet.make_solution() - innersol_list.append(innersol) - lineage_proof = await self.get_lineage_proof_for_coin(coin) - assert lineage_proof is not None - # TODO: address hint error and remove ignore - # error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32" - # [arg-type] - spendable_cc_list.append(SpendableCC(coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501 - sigs = sigs + await self.get_sigs(coin_inner_puzzle, innersol, coin.name()) - - spend_bundle = spend_bundle_for_spendable_ccs( - CC_MOD, - self.cc_info.my_genesis_checker, - spendable_cc_list, - innersol_list, - sigs, - ) - # TODO add support for array in stored records - return TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=puzzle_hashes[0], - amount=uint64(outgoing_amount), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(0), - spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), - wallet_id=self.id(), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), - name=spend_bundle.name(), - ) - - async def add_lineage(self, name: bytes32, lineage: Optional[Program], in_transaction=False): - self.log.info(f"Adding parent {name}: {lineage}") - current_list = self.cc_info.lineage_proofs.copy() - current_list.append((name, lineage)) - cc_info: CCInfo = CCInfo(self.cc_info.my_genesis_checker, current_list) - await self.save_info(cc_info, in_transaction) - - async def save_info(self, cc_info: CCInfo, in_transaction): - self.cc_info = cc_info - current_info = self.wallet_info - data_str = bytes(cc_info).hex() - wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) - self.wallet_info = wallet_info - await self.wallet_state_manager.user_store.update_wallet(wallet_info, in_transaction) - - async def generate_new_coloured_coin(self, amount: uint64) -> SpendBundle: - coins = await self.standard_wallet.select_coins(amount) - - origin = coins.copy().pop() - origin_id = origin.name() - - cc_inner_hash = await self.get_new_inner_hash() - await self.add_lineage(origin_id, Program.to((0, [origin.as_list(), 0]))) - genesis_coin_checker = create_genesis_or_zero_coin_checker(origin_id) - - minted_cc_puzzle_hash = cc_puzzle_hash_for_inner_puzzle_hash(CC_MOD, genesis_coin_checker, cc_inner_hash) - - tx_record: TransactionRecord = await self.standard_wallet.generate_signed_transaction( - amount, minted_cc_puzzle_hash, uint64(0), origin_id, coins - ) - assert tx_record.spend_bundle is not None - - lineage_proof: Optional[Program] = lineage_proof_for_genesis(origin) - lineage_proofs = [(origin_id, lineage_proof)] - cc_info: CCInfo = CCInfo(genesis_coin_checker, lineage_proofs) - await self.save_info(cc_info, False) - return tx_record.spend_bundle - - async def create_spend_bundle_relative_amount(self, cc_amount, zero_coin: Coin = None) -> Optional[SpendBundle]: - # If we're losing value then get coloured coins with at least that much value - # If we're gaining value then our amount doesn't matter - if cc_amount < 0: - cc_spends = await self.select_coins(abs(cc_amount)) - else: - if zero_coin is None: - return None - cc_spends = set() - cc_spends.add(zero_coin) - - if cc_spends is None: - return None - - # Calculate output amount given relative difference and sum of actual values - spend_value = sum([coin.amount for coin in cc_spends]) - cc_amount = spend_value + cc_amount - - # Loop through coins and create solution for innerpuzzle - list_of_solutions = [] - output_created = None - sigs: List[G2Element] = [] - for coin in cc_spends: - if output_created is None: - newinnerpuzhash = await self.get_new_inner_hash() - innersol = self.standard_wallet.make_solution( - primaries=[{"puzzlehash": newinnerpuzhash, "amount": cc_amount}] - ) - output_created = coin - else: - innersol = self.standard_wallet.make_solution(consumed=[output_created.name()]) - innerpuz: Program = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) - sigs = sigs + await self.get_sigs(innerpuz, innersol, coin.name()) - lineage_proof = await self.get_lineage_proof_for_coin(coin) - puzzle_reveal = cc_puzzle_for_inner_puzzle(CC_MOD, self.cc_info.my_genesis_checker, innerpuz) - # Use coin info to create solution and add coin and solution to list of CoinSpends - solution = [ - innersol, - coin.as_list(), - lineage_proof, - None, - None, - None, - None, - None, - ] - list_of_solutions.append(CoinSpend(coin, puzzle_reveal, Program.to(solution))) - - aggsig = AugSchemeMPL.aggregate(sigs) - return SpendBundle(list_of_solutions, aggsig) diff --git a/chia/wallet/derivation_record.py b/chia/wallet/derivation_record.py index 697ec795f047..7856bb45d454 100644 --- a/chia/wallet/derivation_record.py +++ b/chia/wallet/derivation_record.py @@ -19,3 +19,4 @@ class DerivationRecord: pubkey: G1Element wallet_type: WalletType wallet_id: uint32 + hardened: bool diff --git a/chia/wallet/derive_keys.py b/chia/wallet/derive_keys.py index 52747be0c1a1..2b362238cac2 100644 --- a/chia/wallet/derive_keys.py +++ b/chia/wallet/derive_keys.py @@ -17,6 +17,12 @@ def _derive_path(sk: PrivateKey, path: List[int]) -> PrivateKey: return sk +def _derive_path_unhardened(sk: PrivateKey, path: List[int]) -> PrivateKey: + for index in path: + sk = AugSchemeMPL.derive_child_sk_unhardened(sk, index) + return sk + + def master_sk_to_farmer_sk(master: PrivateKey) -> PrivateKey: return _derive_path(master, [12381, 8444, 0, 0]) @@ -25,8 +31,22 @@ def master_sk_to_pool_sk(master: PrivateKey) -> PrivateKey: return _derive_path(master, [12381, 8444, 1, 0]) +def master_sk_to_wallet_sk_intermediate(master: PrivateKey) -> PrivateKey: + return _derive_path(master, [12381, 8444, 2]) + + def master_sk_to_wallet_sk(master: PrivateKey, index: uint32) -> PrivateKey: - return _derive_path(master, [12381, 8444, 2, index]) + intermediate = master_sk_to_wallet_sk_intermediate(master) + return _derive_path(intermediate, [index]) + + +def master_sk_to_wallet_sk_unhardened_intermediate(master: PrivateKey) -> PrivateKey: + return _derive_path_unhardened(master, [12381, 8444, 2]) + + +def master_sk_to_wallet_sk_unhardened(master: PrivateKey, index: uint32) -> PrivateKey: + intermediate = master_sk_to_wallet_sk_unhardened_intermediate(master) + return _derive_path_unhardened(intermediate, [index]) def master_sk_to_local_sk(master: PrivateKey) -> PrivateKey: diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 717ae8fed339..cdf3a50473b6 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -2,13 +2,11 @@ import time import json -from typing import Dict, Optional, List, Any, Set, Tuple, Union - +from typing import Dict, Optional, List, Any, Set, Tuple from blspy import AugSchemeMPL, G1Element from secrets import token_bytes from chia.protocols import wallet_protocol -from chia.protocols.wallet_protocol import RespondAdditions, RejectAdditionsRequest -from chia.server.outbound_message import NodeType +from chia.protocols.wallet_protocol import CoinState from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program @@ -27,7 +25,7 @@ from chia.wallet.wallet_info import WalletInfo from chia.wallet.derivation_record import DerivationRecord from chia.wallet.did_wallet import did_wallet_puzzles -from chia.wallet.derive_keys import master_sk_to_wallet_sk +from chia.wallet.derive_keys import master_sk_to_wallet_sk_unhardened class DIDWallet: @@ -97,9 +95,6 @@ async def create_new_did_wallet( self.did_info.current_inner, self.did_info.origin_coin.name() ).get_tree_hash() - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -115,11 +110,9 @@ async def create_new_did_wallet( sent_to=[], trade_id=None, type=uint32(TransactionType.INCOMING_TX.value), - name=token_bytes(), # type: ignore[arg-type] + name=bytes32(token_bytes()), + memos=[], ) - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] regular_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -135,7 +128,8 @@ async def create_new_did_wallet( sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=token_bytes(), # type: ignore[arg-type] + name=bytes32(token_bytes()), + memos=list(spend_bundle.get_memos().items()), ) await self.standard_wallet.push_transaction(regular_record) await self.standard_wallet.push_transaction(did_record) @@ -200,7 +194,7 @@ async def get_confirmed_balance(self, record_list=None) -> uint64: amount: uint64 = uint64(0) for record in record_list: - parent = await self.get_parent_for_coin(record.coin) + parent = self.get_parent_for_coin(record.coin) if parent is not None: amount = uint64(amount + record.coin.amount) @@ -279,7 +273,7 @@ async def select_coins(self, amount, exclude: List[Coin] = None) -> Optional[Set # This will be used in the recovery case where we don't have the parent info already async def coin_added(self, coin: Coin, _: uint32): """Notification from wallet state manager that wallet has been received.""" - self.log.info("DID wallet has been notified that coin was added") + self.log.info(f"DID wallet has been notified that coin was added: {coin.name()}:{coin}") inner_puzzle = await self.inner_puzzle_for_did_puzzle(coin.puzzle_hash) if self.did_info.temp_coin is not None: self.wallet_state_manager.state_changed("did_coin_added", self.wallet_info.id) @@ -303,6 +297,27 @@ async def coin_added(self, coin: Coin, _: uint32): ) await self.add_parent(coin.name(), future_parent, True) + parent = self.get_parent_for_coin(coin) + if parent is None: + parent_state: CoinState = ( + await self.wallet_state_manager.wallet_node.get_coin_state([coin.parent_coin_info]) + )[0] + node = self.wallet_state_manager.wallet_node.get_full_node_peer() + assert parent_state.spent_height is not None + puzzle_solution_request = wallet_protocol.RequestPuzzleSolution( + coin.parent_coin_info, parent_state.spent_height + ) + response = await node.request_puzzle_solution(puzzle_solution_request) + req_puz_sol = response.response + assert req_puz_sol.puzzle is not None + parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle) + assert parent_innerpuz is not None + parent_info = LineageProof( + parent_state.coin.parent_coin_info, + parent_innerpuz.get_tree_hash(), + parent_state.coin.amount, + ) + await self.add_parent(coin.parent_coin_info, parent_info, False) def create_backup(self, filename: str): assert self.did_info.current_inner is not None @@ -355,82 +370,61 @@ async def load_backup(self, filename: str): await self.save_info(did_info, False) await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id) - full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.name()) - full_puzzle_hash = full_puz.get_tree_hash() - ( - sub_height, - header_hash, - ) = await self.wallet_state_manager.search_blockrecords_for_puzzlehash(full_puzzle_hash) - assert sub_height is not None - assert header_hash is not None - full_nodes = self.wallet_state_manager.server.connection_by_type[NodeType.FULL_NODE] - additions: Union[RespondAdditions, RejectAdditionsRequest, None] = None - for id, node in full_nodes.items(): - request = wallet_protocol.RequestAdditions(sub_height, header_hash, None) - additions = await node.request_additions(request) - if additions is not None: - break - if isinstance(additions, RejectAdditionsRequest): - continue - - assert additions is not None - assert isinstance(additions, RespondAdditions) + # full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.name()) # All additions in this block here: new_puzhash = await self.get_new_inner_hash() new_pubkey = bytes( (await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey ) - - all_parents: Set[bytes32] = set() - for puzzle_list_coin in additions.coins: - puzzle_hash, coins = puzzle_list_coin - for coin in coins: - all_parents.add(coin.parent_coin_info) parent_info = None - for puzzle_list_coin in additions.coins: - puzzle_hash, coins = puzzle_list_coin - if puzzle_hash == full_puzzle_hash: - # our coin - for coin in coins: - future_parent = LineageProof( - coin.parent_coin_info, - innerpuz.get_tree_hash(), - coin.amount, - ) - await self.add_parent(coin.name(), future_parent, False) - if coin.name() not in all_parents: - did_info = DIDInfo( - origin, - backup_ids, - num_of_backup_ids_needed, - self.did_info.parent_info, - innerpuz, - coin, - new_puzhash, - new_pubkey, - False, - ) - await self.save_info(did_info, False) - removal_request = wallet_protocol.RequestRemovals(sub_height, header_hash, None) - removals_response = await node.request_removals(removal_request) - for coin_tuple in removals_response.coins: - if coin_tuple[0] == coin.parent_coin_info: - puzzle_solution_request = wallet_protocol.RequestPuzzleSolution( - coin.parent_coin_info, sub_height - ) - response = await node.request_puzzle_solution(puzzle_solution_request) - req_puz_sol = response.response - assert req_puz_sol.puzzle is not None - parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle) - assert parent_innerpuz is not None - parent_info = LineageProof( - coin_tuple[1].parent_coin_info, - parent_innerpuz.get_tree_hash(), - coin_tuple[1].amount, - ) - await self.add_parent(coin.parent_coin_info, parent_info, False) - break + node = self.wallet_state_manager.wallet_node.get_full_node_peer() + children = await self.wallet_state_manager.wallet_node.fetch_children(node, origin.name()) + while True: + if len(children) == 0: + break + + children_state: CoinState = children[0] + coin = children_state.coin + name = coin.name() + children = await self.wallet_state_manager.wallet_node.fetch_children(node, name) + future_parent = LineageProof( + coin.parent_coin_info, + innerpuz.get_tree_hash(), + coin.amount, + ) + await self.add_parent(coin.name(), future_parent, False) + if children_state.spent_height != children_state.created_height: + did_info = DIDInfo( + origin, + backup_ids, + num_of_backup_ids_needed, + self.did_info.parent_info, + innerpuz, + coin, + new_puzhash, + new_pubkey, + False, + ) + await self.save_info(did_info, False) + assert children_state.created_height + puzzle_solution_request = wallet_protocol.RequestPuzzleSolution( + coin.parent_coin_info, children_state.created_height + ) + parent_state: CoinState = ( + await self.wallet_state_manager.wallet_node.get_coin_state([coin.parent_coin_info]) + )[0] + response = await node.request_puzzle_solution(puzzle_solution_request) + req_puz_sol = response.response + assert req_puz_sol.puzzle is not None + parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle) + assert parent_innerpuz is not None + parent_info = LineageProof( + parent_state.coin.parent_coin_info, + parent_innerpuz.get_tree_hash(), + parent_state.coin.amount, + ) + await self.add_parent(coin.parent_coin_info, parent_info, False) assert parent_info is not None return None except Exception as e: @@ -474,7 +468,7 @@ async def create_update_spend(self): innerpuz, self.did_info.origin_coin.name(), ) - parent_info = await self.get_parent_for_coin(coin) + parent_info = self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( [ @@ -496,7 +490,7 @@ async def create_update_spend(self): ) pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) signature = AugSchemeMPL.sign(private, message) # assert signature.validate([signature.PkMessagePair(pubkey, message)]) sigs = [signature] @@ -519,6 +513,7 @@ async def create_update_spend(self): trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=token_bytes(), + memos=list(spend_bundle.get_memos().items()), ) await self.standard_wallet.push_transaction(did_record) return spend_bundle @@ -541,7 +536,7 @@ async def create_message_spend(self, messages: List[Tuple[int, bytes]], new_inne innerpuz, self.did_info.origin_coin.name(), ) - parent_info = await self.get_parent_for_coin(coin) + parent_info = self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( [ @@ -564,16 +559,13 @@ async def create_message_spend(self, messages: List[Tuple[int, bytes]], new_inne ) pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) signature = AugSchemeMPL.sign(private, message) # assert signature.validate([signature.PkMessagePair(pubkey, message)]) sigs = [signature] aggsig = AugSchemeMPL.aggregate(sigs) spend_bundle = SpendBundle(list_of_solutions, aggsig) - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -589,7 +581,8 @@ async def create_message_spend(self, messages: List[Tuple[int, bytes]], new_inne sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=token_bytes(), # type: ignore[arg-type] + name=bytes32(token_bytes()), + memos=list(spend_bundle.get_memos().items()), ) await self.standard_wallet.push_transaction(did_record) return spend_bundle @@ -611,7 +604,7 @@ async def create_exit_spend(self, puzhash: bytes32): innerpuz, self.did_info.origin_coin.name(), ) - parent_info = await self.get_parent_for_coin(coin) + parent_info = self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( [ @@ -633,16 +626,13 @@ async def create_exit_spend(self, puzhash: bytes32): ) pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) signature = AugSchemeMPL.sign(private, message) # assert signature.validate([signature.PkMessagePair(pubkey, message)]) sigs = [signature] aggsig = AugSchemeMPL.aggregate(sigs) spend_bundle = SpendBundle(list_of_solutions, aggsig) - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -658,7 +648,8 @@ async def create_exit_spend(self, puzhash: bytes32): sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=token_bytes(), # type: ignore[arg-type] + name=bytes32(token_bytes()), + memos=list(spend_bundle.get_memos().items()), ) await self.standard_wallet.push_transaction(did_record) return spend_bundle @@ -685,7 +676,7 @@ async def create_attestment( innerpuz, self.did_info.origin_coin.name(), ) - parent_info = await self.get_parent_for_coin(coin) + parent_info = self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( @@ -707,13 +698,10 @@ async def create_attestment( message = to_sign + coin.name() + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) signature = AugSchemeMPL.sign(private, message) # assert signature.validate([signature.PkMessagePair(pubkey, message)]) spend_bundle = SpendBundle(list_of_solutions, signature) - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -729,7 +717,8 @@ async def create_attestment( sent_to=[], trade_id=None, type=uint32(TransactionType.INCOMING_TX.value), - name=token_bytes(), # type: ignore[arg-type] + name=bytes32(token_bytes()), + memos=list(spend_bundle.get_memos().items()), ) await self.standard_wallet.push_transaction(did_record) if filename is not None: @@ -824,7 +813,7 @@ async def recovery_spend( innerpuz, self.did_info.origin_coin.name(), ) - parent_info = await self.get_parent_for_coin(coin) + parent_info = self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( [ @@ -842,7 +831,7 @@ async def recovery_spend( index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) if index is None: raise ValueError("Unknown pubkey.") - private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) message = bytes(puzhash) sigs = [AugSchemeMPL.sign(private, message)] for _ in spend_bundle.coin_spends: @@ -854,9 +843,6 @@ async def recovery_spend( else: spend_bundle = spend_bundle.aggregate([spend_bundle, SpendBundle(list_of_solutions, aggsig)]) - # TODO: address hint error and remove ignore - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -872,7 +858,8 @@ async def recovery_spend( sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=token_bytes(), # type: ignore[arg-type] + name=bytes32(token_bytes()), + memos=list(spend_bundle.get_memos().items()), ) await self.standard_wallet.push_transaction(did_record) new_did_info = DIDInfo( @@ -923,7 +910,7 @@ async def inner_puzzle_for_did_puzzle(self, did_hash: bytes32) -> Program: ) return inner_puzzle - async def get_parent_for_coin(self, coin) -> Optional[LineageProof]: + def get_parent_for_coin(self, coin) -> Optional[LineageProof]: parent_info = None for name, ccparent in self.did_info.parent_info: if name == coin.parent_coin_info: @@ -949,9 +936,9 @@ async def generate_new_decentralised_id(self, amount: uint64) -> Optional[SpendB did_full_puz = did_wallet_puzzles.create_fullpuz(did_inner, launcher_coin.name()) did_puzzle_hash = did_full_puz.get_tree_hash() - announcement_set: Set[bytes32] = set() + announcement_set: Set[Announcement] = set() announcement_message = Program.to([did_puzzle_hash, amount, bytes(0x80)]).get_tree_hash() - announcement_set.add(Announcement(launcher_coin.name(), announcement_message).name()) + announcement_set.add(Announcement(launcher_coin.name(), announcement_message)) tx_record: Optional[TransactionRecord] = await self.standard_wallet.generate_signed_transaction( amount, genesis_launcher_puz.get_tree_hash(), uint64(0), origin.name(), coins, None, False, announcement_set @@ -1015,8 +1002,9 @@ async def generate_eve_spend(self, coin: Coin, full_puzzle: Program, innerpuz: P + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA ) pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) - index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + record: Optional[DerivationRecord] = await self.wallet_state_manager.puzzle_store.record_for_pubkey(pubkey) + assert record is not None + private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, record.index) signature = AugSchemeMPL.sign(private, message) sigs = [signature] aggsig = AugSchemeMPL.aggregate(sigs) diff --git a/chia/wallet/did_wallet/did_wallet_puzzles.py b/chia/wallet/did_wallet/did_wallet_puzzles.py index 72fae9258422..f5c0fa04bf7c 100644 --- a/chia/wallet/did_wallet/did_wallet_puzzles.py +++ b/chia/wallet/did_wallet/did_wallet_puzzles.py @@ -41,7 +41,7 @@ def get_pubkey_from_innerpuz(innerpuz: Program) -> G1Element: def is_did_innerpuz(inner_f: Program): """ - You may want to generalize this if different `CC_MOD` templates are supported. + You may want to generalize this if different `CAT_MOD` templates are supported. """ return inner_f == DID_INNERPUZ_MOD @@ -52,7 +52,7 @@ def is_did_core(inner_f: Program): def uncurry_innerpuz(puzzle: Program) -> Optional[Tuple[Program, Program]]: """ - Take a puzzle and return `None` if it's not a `CC_MOD` cc, or + Take a puzzle and return `None` if it's not a `CAT_MOD` cc, or a triple of `mod_hash, genesis_coin_checker, inner_puzzle` if it is. """ r = puzzle.uncurry() diff --git a/chia/wallet/key_val_store.py b/chia/wallet/key_val_store.py index 7c332e8658e7..3a2a509429d7 100644 --- a/chia/wallet/key_val_store.py +++ b/chia/wallet/key_val_store.py @@ -2,7 +2,6 @@ import aiosqlite -from chia.util.byte_types import hexstr_to_bytes from chia.util.db_wrapper import DBWrapper from chia.util.streamable import Streamable @@ -21,7 +20,7 @@ async def create(cls, db_wrapper: DBWrapper): self.db_wrapper = db_wrapper self.db_connection = db_wrapper.db await self.db_connection.execute( - ("CREATE TABLE IF NOT EXISTS key_val_store(" " key text PRIMARY KEY," " value text)") + "CREATE TABLE IF NOT EXISTS key_val_store(" " key text PRIMARY KEY," " value blob)" ) await self.db_connection.execute("CREATE INDEX IF NOT EXISTS name on key_val_store(key)") @@ -34,7 +33,7 @@ async def _clear_database(self): await cursor.close() await self.db_connection.commit() - async def get_object(self, key: str, type: Any) -> Any: + async def get_object(self, key: str, object_type: Any) -> Any: """ Return bytes representation of stored object """ @@ -46,7 +45,7 @@ async def get_object(self, key: str, type: Any) -> Any: if row is None: return None - return type.from_bytes(hexstr_to_bytes(row[1])) + return object_type.from_bytes(row[1]) async def set_object(self, key: str, obj: Streamable): """ @@ -55,7 +54,12 @@ async def set_object(self, key: str, obj: Streamable): async with self.db_wrapper.lock: cursor = await self.db_connection.execute( "INSERT OR REPLACE INTO key_val_store VALUES(?, ?)", - (key, bytes(obj).hex()), + (key, bytes(obj)), ) await cursor.close() await self.db_connection.commit() + + async def remove_object(self, key: str): + cursor = await self.db_connection.execute("DELETE FROM key_val_store where key=?", (key,)) + await cursor.close() + await self.db_connection.commit() diff --git a/chia/wallet/lineage_proof.py b/chia/wallet/lineage_proof.py index 0c3f04030be5..177f2d127434 100644 --- a/chia/wallet/lineage_proof.py +++ b/chia/wallet/lineage_proof.py @@ -1,7 +1,8 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, Any, List from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.program import Program from chia.util.ints import uint64 from chia.util.streamable import Streamable, streamable @@ -9,6 +10,19 @@ @dataclass(frozen=True) @streamable class LineageProof(Streamable): - parent_name: bytes32 - inner_puzzle_hash: Optional[bytes32] - amount: uint64 + parent_name: Optional[bytes32] = None + inner_puzzle_hash: Optional[bytes32] = None + amount: Optional[uint64] = None + + def to_program(self) -> Program: + final_list: List[Any] = [] + if self.parent_name is not None: + final_list.append(self.parent_name) + if self.inner_puzzle_hash is not None: + final_list.append(self.inner_puzzle_hash) + if self.amount is not None: + final_list.append(self.amount) + return Program.to(final_list) + + def is_none(self) -> bool: + return all([self.parent_name is None, self.inner_puzzle_hash is None, self.amount is None]) diff --git a/chia/wallet/payment.py b/chia/wallet/payment.py new file mode 100644 index 000000000000..3293376a87dc --- /dev/null +++ b/chia/wallet/payment.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass + +from typing import List + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.program import Program +from chia.util.ints import uint64 + + +# This class is supposed to correspond to a CREATE_COIN condition +@dataclass(frozen=True) +class Payment: + puzzle_hash: bytes32 + amount: uint64 + memos: List[bytes] + + def as_condition_args(self) -> List: + return [self.puzzle_hash, self.amount, self.memos] + + def as_condition(self) -> Program: + return Program.to([51, *self.as_condition_args()]) + + def name(self) -> bytes32: + return self.as_condition().get_tree_hash() + + @classmethod + def from_condition(cls, condition: Program) -> "Payment": + python_condition: List = condition.as_python() + puzzle_hash, amount = python_condition[1:3] + memos: List[bytes] = [] + if len(python_condition) > 3: + memos = python_condition[3] + return cls(bytes32(puzzle_hash), uint64(int.from_bytes(amount, "big")), memos) diff --git a/chia/wallet/puzzles/cat.clvm b/chia/wallet/puzzles/cat.clvm new file mode 100644 index 000000000000..e16f2123894c --- /dev/null +++ b/chia/wallet/puzzles/cat.clvm @@ -0,0 +1,417 @@ +; Coins locked with this puzzle are spendable cats. +; +; Choose a list of n inputs (n>=1), I_1, ... I_n with amounts A_1, ... A_n. +; +; We put them in a ring, so "previous" and "next" have intuitive k-1 and k+1 semantics, +; wrapping so {n} and 0 are the same, ie. all indices are mod n. +; +; Each coin creates 0 or more coins with total output value O_k. +; Let D_k = the "debt" O_k - A_k contribution of coin I_k, ie. how much debt this input accumulates. +; Some coins may spend more than they contribute and some may spend less, ie. D_k need +; not be zero. That's okay. It's enough for the total of all D_k in the ring to be 0. +; +; A coin can calculate its own D_k since it can verify A_k (it's hashed into the coin id) +; and it can sum up `CREATE_COIN` conditions for O_k. +; +; Defines a "subtotal of debts" S_k for each coin as follows: +; +; S_1 = 0 +; S_k = S_{k-1} + D_{k-1} +; +; Here's the main trick that shows the ring sums to 0. +; You can prove by induction that S_{k+1} = D_1 + D_2 + ... + D_k. +; But it's a ring, so S_{n+1} is also S_1, which is 0. So D_1 + D_2 + ... + D_k = 0. +; So the total debts must be 0, ie. no coins are created or destroyed. +; +; Each coin's solution includes I_{k-1}, I_k, and I_{k+1} along with proofs that I_{k}, and I_{k+1} are CATs of the same type. +; Each coin's solution includes S_{k-1}. It calculates D_k = O_k - A_k, and then S_k = S_{k-1} + D_{k-1} +; +; Announcements are used to ensure that each S_k follows the pattern is valid. +; Announcements automatically commit to their own coin id. +; Coin I_k creates an announcement that further commits to I_{k-1} and S_{k-1}. +; +; Coin I_k gets a proof that I_{k+1} is a cat, so it knows it must also create an announcement +; when spent. It checks that I_{k+1} creates an announcement committing to I_k and S_k. +; +; So S_{k+1} is correct iff S_k is correct. +; +; Coins also receive proofs that their neighbours are CATs, ensuring the announcements aren't forgeries. +; Inner puzzles and the CAT layer prepend `CREATE_COIN_ANNOUNCEMENT` with different prefixes to avoid forgeries. +; Ring announcements use 0xcb, and inner puzzles are given 0xca +; +; In summary, I_k generates a coin_announcement Y_k ("Y" for "yell") as follows: +; +; Y_k: hash of I_k (automatically), I_{k-1}, S_k +; +; Each coin creates an assert_coin_announcement to ensure that the next coin's announcement is as expected: +; Y_{k+1} : hash of I_{k+1}, I_k, S_{k+1} +; +; TLDR: +; I_k : coins +; A_k : amount coin k contributes +; O_k : amount coin k spend +; D_k : difference/delta that coin k incurs (A - O) +; S_k : subtotal of debts D_1 + D_2 ... + D_k +; Y_k : announcements created by coin k commiting to I_{k-1}, I_k, S_k +; +; All conditions go through a "transformer" that looks for CREATE_COIN conditions +; generated by the inner solution, and wraps the puzzle hash ensuring the output is a cat. +; +; Three output conditions are prepended to the list of conditions for each I_k: +; (ASSERT_MY_ID I_k) to ensure that the passed in value for I_k is correct +; (CREATE_COIN_ANNOUNCEMENT I_{k-1} S_k) to create this coin's announcement +; (ASSERT_COIN_ANNOUNCEMENT hashed_announcement(Y_{k+1})) to ensure the next coin really is next and +; the relative values of S_k and S_{k+1} are correct +; +; This is all we need to do to ensure cats exactly balance in the inputs and outputs. +; +; Proof: +; Consider n, k, I_k values, O_k values, S_k and A_k as above. +; For the (CREATE_COIN_ANNOUNCEMENT Y_{k+1}) (created by the next coin) +; and (ASSERT_COIN_ANNOUNCEMENT hashed(Y_{k+1})) to match, +; we see that I_k can ensure that is has the correct value for S_{k+1}. +; +; By induction, we see that S_{m+1} = sum(i, 1, m) [O_i - A_i] = sum(i, 1, m) O_i - sum(i, 1, m) A_i +; So S_{n+1} = sum(i, 1, n) O_i - sum(i, 1, n) A_i. But S_{n+1} is actually S_1 = 0, +; so thus sum(i, 1, n) O_i = sum (i, 1, n) A_i, ie. output total equals input total. + +;; GLOSSARY: +;; MOD_HASH: this code's sha256 tree hash +;; TAIL_PROGRAM_HASH: the program that determines if a coin can mint new cats, burn cats, and check if its lineage is valid if its parent is not a CAT +;; INNER_PUZZLE: an independent puzzle protecting the coins. Solutions to this puzzle are expected to generate `AGG_SIG` conditions and possibly `CREATE_COIN` conditions. +;; ---- items above are curried into the puzzle hash ---- +;; inner_puzzle_solution: the solution to the inner puzzle +;; prev_coin_id: the id for the previous coin +;; tail_program_reveal: reveal of TAIL_PROGRAM_HASH required to run the program if desired +;; tail_solution: optional solution passed into tail_program +;; lineage_proof: optional proof that our coin's parent is a CAT +;; this_coin_info: (parent_id puzzle_hash amount) +;; next_coin_proof: (parent_id inner_puzzle_hash amount) +;; prev_subtotal: the subtotal between prev-coin and this-coin +;; extra_delta: an amount that is added to our delta and checked by the TAIL program +;; + +(mod ( + MOD_HASH ;; curried into puzzle + TAIL_PROGRAM_HASH ;; curried into puzzle + INNER_PUZZLE ;; curried into puzzle + inner_puzzle_solution ;; if invalid, INNER_PUZZLE will fail + lineage_proof ;; This is the parent's coin info, used to check if the parent was a CAT. Optional if using tail_program. + prev_coin_id ;; used in this coin's announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong + this_coin_info ;; verified with ASSERT_MY_COIN_ID + next_coin_proof ;; used to generate ASSERT_COIN_ANNOUNCEMENT + prev_subtotal ;; included in announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong + extra_delta ;; this is the "legal discrepancy" between your real delta and what you're announcing your delta is + ) + + ;;;;; start library code + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + (include cat_truths.clib) + + (defconstant ANNOUNCEMENT_MORPH_BYTE 0xca) + (defconstant RING_MORPH_BYTE 0xcb) + + (defmacro assert items + (if (r items) + (list if (f items) (c assert (r items)) (q . (x))) + (f items) + ) + ) + + (defmacro and ARGS + (if ARGS + (qq (if (unquote (f ARGS)) + (unquote (c and (r ARGS))) + () + )) + 1) + ) + + ; takes a lisp tree and returns the hash of it + (defun sha256tree1 (TREE) + (if (l TREE) + (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) + (sha256 ONE TREE))) + + ; take two lists and merge them into one + (defun merge_list (list_a list_b) + (if list_a + (c (f list_a) (merge_list (r list_a) list_b)) + list_b + ) + ) + + ; cat_mod_struct = (MOD_HASH MOD_HASH_hash GENESIS_COIN_CHECKER GENESIS_COIN_CHECKER_hash) + + (defun-inline mod_hash_from_cat_mod_struct (cat_mod_struct) (f cat_mod_struct)) + (defun-inline mod_hash_hash_from_cat_mod_struct (cat_mod_struct) (f (r cat_mod_struct))) + (defun-inline tail_program_hash_from_cat_mod_struct (cat_mod_struct) (f (r (r cat_mod_struct)))) + + ;;;;; end library code + + ;; return the puzzle hash for a cat with the given `GENESIS_COIN_CHECKER_hash` & `INNER_PUZZLE` + (defun-inline cat_puzzle_hash (cat_mod_struct inner_puzzle_hash) + (puzzle-hash-of-curried-function (mod_hash_from_cat_mod_struct cat_mod_struct) + inner_puzzle_hash + (sha256 ONE (tail_program_hash_from_cat_mod_struct cat_mod_struct)) + (mod_hash_hash_from_cat_mod_struct cat_mod_struct) + ) + ) + + ;; tweak `CREATE_COIN` condition by wrapping the puzzle hash, forcing it to be a cat + ;; prepend `CREATE_COIN_ANNOUNCEMENT` with 0xca as bytes so it cannot be used to cheat the coin ring + + (defun-inline morph_condition (condition cat_mod_struct) + (if (= (f condition) CREATE_COIN) + (c CREATE_COIN + (c (cat_puzzle_hash cat_mod_struct (f (r condition))) + (r (r condition))) + ) + (if (= (f condition) CREATE_COIN_ANNOUNCEMENT) + (c CREATE_COIN_ANNOUNCEMENT + (c (sha256 ANNOUNCEMENT_MORPH_BYTE (f (r condition))) + (r (r condition)) + ) + ) + condition + ) + ) + ) + + ;; given a coin's parent, inner_puzzle and amount, and the cat_mod_struct, calculate the id of the coin + (defun-inline coin_id_for_proof (coin cat_mod_struct) + (sha256 (f coin) (cat_puzzle_hash cat_mod_struct (f (r coin))) (f (r (r coin)))) + ) + + ;; utility to fetch coin amount from coin + (defun-inline input_amount_for_coin (coin) + (f (r (r coin))) + ) + + ;; calculate the hash of an announcement + ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles + (defun-inline calculate_annoucement_id (this_coin_id this_subtotal next_coin_id cat_mod_struct) + (sha256 next_coin_id (sha256 RING_MORPH_BYTE this_coin_id this_subtotal)) + ) + + ;; create the `ASSERT_COIN_ANNOUNCEMENT` condition that ensures the next coin's announcement is correct + (defun-inline create_assert_next_announcement_condition (this_coin_id this_subtotal next_coin_id cat_mod_struct) + (list ASSERT_COIN_ANNOUNCEMENT + (calculate_annoucement_id this_coin_id + this_subtotal + next_coin_id + cat_mod_struct + ) + ) + ) + + ;; here we commit to I_{k-1} and S_k + ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles + (defun-inline create_announcement_condition (prev_coin_id prev_subtotal) + (list CREATE_COIN_ANNOUNCEMENT + (sha256 RING_MORPH_BYTE prev_coin_id prev_subtotal) + ) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;; + + ;; this function takes a condition and returns an integer indicating + ;; the value of all output coins created with CREATE_COIN. If it's not + ;; a CREATE_COIN condition, it returns 0. + + (defun-inline output_value_for_condition (condition) + (if (= (f condition) CREATE_COIN) + (f (r (r condition))) + 0 + ) + ) + + ;; add two conditions to the list of morphed conditions: + ;; CREATE_COIN_ANNOUNCEMENT for my announcement + ;; ASSERT_COIN_ANNOUNCEMENT for the next coin's announcement + (defun-inline generate_final_output_conditions + ( + prev_subtotal + this_subtotal + morphed_conditions + prev_coin_id + this_coin_id + next_coin_id + cat_mod_struct + ) + (c (create_announcement_condition prev_coin_id prev_subtotal) + (c (create_assert_next_announcement_condition this_coin_id this_subtotal next_coin_id cat_mod_struct) + morphed_conditions) + ) + ) + + + ;; This next section of code loops through all of the conditions to do three things: + ;; 1) Look for a "magic" value of -113 and, if one exists, filter it, and take note of the tail reveal and solution + ;; 2) Morph any CREATE_COIN or CREATE_COIN_ANNOUNCEMENT conditions + ;; 3) Sum the total output amount of all of the CREATE_COINs that are output by the inner puzzle + ;; + ;; After everything return a struct in the format (morphed_conditions . (output_sum . tail_reveal_and_solution)) + ;; If multiple magic conditions are specified, the later one will take precedence + + (defun-inline condition_tail_reveal (condition) (f (r (r (r condition))))) + (defun-inline condition_tail_solution (condition) (f (r (r (r (r condition)))))) + + (defun cons_onto_first_and_add_to_second (morphed_condition output_value struct) + (c (c morphed_condition (f struct)) (c (+ output_value (f (r struct))) (r (r struct)))) + ) + + (defun find_and_strip_tail_info (inner_conditions cat_mod_struct tail_reveal_and_solution) + (if inner_conditions + (if (= (output_value_for_condition (f inner_conditions)) -113) ; Checks this is a CREATE_COIN of value -113 + (find_and_strip_tail_info + (r inner_conditions) + cat_mod_struct + (c (condition_tail_reveal (f inner_conditions)) (condition_tail_solution (f inner_conditions))) + ) + (cons_onto_first_and_add_to_second + (morph_condition (f inner_conditions) cat_mod_struct) + (output_value_for_condition (f inner_conditions)) + (find_and_strip_tail_info + (r inner_conditions) + cat_mod_struct + tail_reveal_and_solution + ) + ) + ) + (c () (c 0 tail_reveal_and_solution)) + ) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;; lineage checking + + ;; return true iff parent of `this_coin_info` is provably a cat + ;; A 'lineage proof' consists of (parent_parent_id parent_INNER_puzzle_hash parent_amount) + ;; We use this information to construct a coin who's puzzle has been wrapped in this MOD and verify that, + ;; once wrapped, it matches our parent coin's ID. + (defun-inline is_parent_cat ( + cat_mod_struct + parent_id + lineage_proof + ) + (= parent_id + (sha256 (f lineage_proof) + (cat_puzzle_hash cat_mod_struct (f (r lineage_proof))) + (f (r (r lineage_proof))) + ) + ) + ) + + (defun check_lineage_or_run_tail_program + ( + this_coin_info + tail_reveal_and_solution + parent_is_cat ; flag which says whether or not the parent CAT check ran and passed + lineage_proof + Truths + extra_delta + inner_conditions + ) + (if tail_reveal_and_solution + (assert (= (sha256tree1 (f tail_reveal_and_solution)) (cat_tail_program_hash_truth Truths)) + (merge_list + (a (f tail_reveal_and_solution) + (list + Truths + parent_is_cat + lineage_proof ; Lineage proof is only guaranteed to be true if parent_is_cat + extra_delta + inner_conditions + (r tail_reveal_and_solution) + ) + ) + inner_conditions + ) + ) + (assert parent_is_cat (not extra_delta) + inner_conditions + ) + ) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (defun stager_two ( + Truths + (inner_conditions . (output_sum . tail_reveal_and_solution)) + lineage_proof + prev_coin_id + this_coin_info + next_coin_id + prev_subtotal + extra_delta + ) + (check_lineage_or_run_tail_program + this_coin_info + tail_reveal_and_solution + (if lineage_proof (is_parent_cat (cat_struct_truth Truths) (my_parent_cat_truth Truths) lineage_proof) ()) + lineage_proof + Truths + extra_delta + (generate_final_output_conditions + prev_subtotal + ; the expression on the next line calculates `this_subtotal` by adding the delta to `prev_subtotal` + (+ prev_subtotal (- (input_amount_for_coin this_coin_info) output_sum) extra_delta) + inner_conditions + prev_coin_id + (my_id_cat_truth Truths) + next_coin_id + (cat_struct_truth Truths) + ) + ) + ) + + ; CAT TRUTHS struct is: ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount))) + ; create truths - this_coin_info verified true because we calculated my ID from it! + ; lineage proof is verified later by cat parent check or tail_program + + (defun stager ( + cat_mod_struct + inner_conditions + lineage_proof + inner_puzzle_hash + my_id + prev_coin_id + this_coin_info + next_coin_proof + prev_subtotal + extra_delta + ) + (c (list ASSERT_MY_COIN_ID my_id) (stager_two + (cat_truth_data_to_truth_struct + inner_puzzle_hash + cat_mod_struct + my_id + this_coin_info + ) + (find_and_strip_tail_info inner_conditions cat_mod_struct ()) + lineage_proof + prev_coin_id + this_coin_info + (coin_id_for_proof next_coin_proof cat_mod_struct) + prev_subtotal + extra_delta + )) + ) + + (stager + ;; calculate cat_mod_struct, inner_puzzle_hash, coin_id + (list MOD_HASH (sha256 ONE MOD_HASH) TAIL_PROGRAM_HASH) + (a INNER_PUZZLE inner_puzzle_solution) + lineage_proof + (sha256tree1 INNER_PUZZLE) + (sha256 (f this_coin_info) (f (r this_coin_info)) (f (r (r this_coin_info)))) + prev_coin_id ; ID + this_coin_info ; (parent_id puzzle_hash amount) + next_coin_proof ; (parent_id innerpuzhash amount) + prev_subtotal + extra_delta + ) +) diff --git a/chia/wallet/puzzles/cat.clvm.hex b/chia/wallet/puzzles/cat.clvm.hex new file mode 100644 index 000000000000..613d2b96bb7a --- /dev/null +++ b/chia/wallet/puzzles/cat.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080 diff --git a/chia/wallet/puzzles/cat.clvm.hex.sha256tree b/chia/wallet/puzzles/cat.clvm.hex.sha256tree new file mode 100644 index 000000000000..abaa1816595c --- /dev/null +++ b/chia/wallet/puzzles/cat.clvm.hex.sha256tree @@ -0,0 +1 @@ +72dec062874cd4d3aab892a0906688a1ae412b0109982e1797a170add88bdcdc diff --git a/chia/wallet/puzzles/cc_loader.py b/chia/wallet/puzzles/cat_loader.py similarity index 68% rename from chia/wallet/puzzles/cc_loader.py rename to chia/wallet/puzzles/cat_loader.py index 5cfbc4f095c3..448fea212e8c 100644 --- a/chia/wallet/puzzles/cc_loader.py +++ b/chia/wallet/puzzles/cat_loader.py @@ -1,4 +1,4 @@ from chia.wallet.puzzles.load_clvm import load_clvm -CC_MOD = load_clvm("cc.clvm", package_or_requirement=__name__) +CAT_MOD = load_clvm("cat.clvm", package_or_requirement=__name__) LOCK_INNER_PUZZLE = load_clvm("lock.inner.puzzle.clvm", package_or_requirement=__name__) diff --git a/chia/wallet/puzzles/cat_truths.clib b/chia/wallet/puzzles/cat_truths.clib new file mode 100644 index 000000000000..563ec65f3a56 --- /dev/null +++ b/chia/wallet/puzzles/cat_truths.clib @@ -0,0 +1,31 @@ +( + (defun-inline cat_truth_data_to_truth_struct (innerpuzhash cat_struct my_id this_coin_info) + (c + (c + innerpuzhash + cat_struct + ) + (c + my_id + this_coin_info + ) + ) + ) + + ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount))) + + (defun-inline my_inner_puzzle_hash_cat_truth (Truths) (f (f Truths))) + (defun-inline cat_struct_truth (Truths) (r (f Truths))) + (defun-inline my_id_cat_truth (Truths) (f (r Truths))) + (defun-inline my_coin_info_truth (Truths) (r (r Truths))) + (defun-inline my_amount_cat_truth (Truths) (f (r (r (my_coin_info_truth Truths))))) + (defun-inline my_full_puzzle_hash_cat_truth (Truths) (f (r (my_coin_info_truth Truths)))) + (defun-inline my_parent_cat_truth (Truths) (f (my_coin_info_truth Truths))) + + + ; CAT mod_struct is: (MOD_HASH MOD_HASH_hash TAIL_PROGRAM TAIL_PROGRAM_hash) + + (defun-inline cat_mod_hash_truth (Truths) (f (cat_struct_truth Truths))) + (defun-inline cat_mod_hash_hash_truth (Truths) (f (r (cat_struct_truth Truths)))) + (defun-inline cat_tail_program_hash_truth (Truths) (f (r (r (cat_struct_truth Truths))))) +) diff --git a/chia/wallet/puzzles/cc.clvm b/chia/wallet/puzzles/cc.clvm deleted file mode 100644 index f335f70ea708..000000000000 --- a/chia/wallet/puzzles/cc.clvm +++ /dev/null @@ -1,377 +0,0 @@ -; Coins locked with this puzzle are spendable ccs. -; -; Choose a list of n inputs (n>=1), I_1, ... I_n with amounts A_1, ... A_n. -; -; We put them in a ring, so "previous" and "next" have intuitive k-1 and k+1 semantics, -; wrapping so {n} and 0 are the same, ie. all indices are mod n. -; -; Each coin creates 0 or more coins with total output value O_k. -; Let D_k = the "debt" O_k - A_k contribution of coin I_k, ie. how much debt this input accumulates. -; Some coins may spend more than they contribute and some may spend less, ie. D_k need -; not be zero. That's okay. It's enough for the total of all D_k in the ring to be 0. -; -; A coin can calculate its own D_k since it can verify A_k (it's hashed into the coin id) -; and it can sum up `CREATE_COIN` conditions for O_k. -; -; Defines a "subtotal of debts" S_k for each coin as follows: -; -; S_1 = 0 -; S_k = S_{k-1} + D_{k-1} -; -; Here's the main trick that shows the ring sums to 0. -; You can prove by induction that S_{k+1} = D_1 + D_2 + ... + D_k. -; But it's a ring, so S_{n+1} is also S_1, which is 0. So D_1 + D_2 + ... + D_k = 0. -; So the total debts must be 0, ie. no coins are created or destroyed. -; -; Each coin's solution includes I_{k-1}, I_k, and I_{k+1} along with proofs that each is a CC. -; Each coin's solution includes S_{k-1}. It calculates D_k = O_k - A_k, and then S_k = S_{k-1} + D_{k-1} -; -; Announcements are used to ensure that each S_k follows the pattern is valid. -; Announcements automatically commit to their own coin id. -; Coin I_k creates an announcement that further commits to I_{k-1} and S_{k-1}. -; -; Coin I_k gets a proof that I_{k+1} is a CC, so it knows it must also create an announcement -; when spent. It checks that I_{k+1} creates an announcement committing to I_k and S_k. -; -; So S_{k+1} is correct iff S_k is correct. -; -; Coins also receive proofs that their neighbors are ccs, ensuring the announcements aren't forgeries, as -; inner puzzles are not allowed to use `CREATE_COIN_ANNOUNCEMENT`. -; -; In summary, I_k generates an announcement Y_k (for "yell") as follows: -; -; Y_k: hash of I_k (automatically), I_{k-1}, S_k -; -; Each coin ensures that the next coin's announcement is as expected: -; Y_{k+1} : hash of I_{k+1}, I_k, S_{k+1} -; -; TLDR: -; I_k : coins -; A_k : amount coin k contributes -; O_k : amount coin k spend -; D_k : difference/delta that coin k incurs (A - O) -; S_k : subtotal of debts D_1 + D_2 ... + D_k -; Y_k : announcements created by coin k commiting to I_{k-1}, I_k, S_k -; -; All conditions go through a "transformer" that looks for CREATE_COIN conditions -; generated by the inner solution, and wraps the puzzle hash ensuring the output is a cc. -; -; Three output conditions are prepended to the list of conditions for each I_k: -; (ASSERT_MY_ID I_k) to ensure that the passed in value for I_k is correct -; (CREATE_COIN_ANNOUNCEMENT I_{k-1} S_k) to create this coin's announcement -; (ASSERT_COIN_ANNOUNCEMENT hashed_announcement(Y_{k+1})) to ensure the next coin really is next and -; the relative values of S_k and S_{k+1} are correct -; -; This is all we need to do to ensure ccs exactly balance in the inputs and outputs. -; -; Proof: -; Consider n, k, I_k values, O_k values, S_k and A_k as above. -; For the (CREATE_COIN_ANNOUNCEMENT Y_{k+1}) (created by the next coin) -; and (ASSERT_COIN_ANNOUNCEMENT hashed(Y_{k+1})) to match, -; we see that I_k can ensure that is has the correct value for S_{k+1}. -; -; By induction, we see that S_{m+1} = sum(i, 1, m) [O_i - A_i] = sum(i, 1, m) O_i - sum(i, 1, m) A_i -; So S_{n+1} = sum(i, 1, n) O_i - sum(i, 1, n) A_i. But S_{n+1} is actually S_1 = 0, -; so thus sum(i, 1, n) O_i = sum (i, 1, n) A_i, ie. output total equals input total. -; -; QUESTION: do we want a secondary puzzle that allows for coins to be spent? This could be good for -; bleaching coins (sendable to any address), or reclaiming them by a central authority. -; - -;; GLOSSARY: -;; mod-hash: this code's sha256 tree hash -;; genesis-coin-checker: the function that determines if a coin can mint new ccs -;; inner-puzzle: an independent puzzle protecting the coins. Solutions to this puzzle are expected to -;; generate `AGG_SIG` conditions and possibly `CREATE_COIN` conditions. -;; ---- items above are curried into the puzzle hash ---- -;; inner-puzzle-solution: the solution to the inner puzzle -;; prev-coin-bundle: the bundle for previous coin -;; this-coin-bundle: the bundle for this coin -;; next-coin-bundle: the bundle for next coin -;; prev-subtotal: the subtotal between prev-coin and this-coin -;; -;; coin-info: `(parent_id puzzle_hash amount)`. This defines the coin id used with ASSERT_MY_COIN_ID -;; coin-bundle: the cons box `(coin-info . lineage_proof)` -;; -;; and automatically hashed in to the announcement generated with CREATE_COIN_ANNOUNCEMENT. -;; - -(mod (mod-hash ;; curried into puzzle - genesis-coin-checker ;; curried into puzzle - inner-puzzle ;; curried into puzzle - inner-puzzle-solution ;; if invalid, inner-puzzle will fail - prev-coin-bundle ;; used in this coin's announcement, prev-coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong - this-coin-bundle ;; verified with ASSERT_MY_COIN_ID - next-coin-bundle ;; used to generate ASSERT_COIN_ANNOUNCEMENT - prev-subtotal ;; included in announcement, prev-coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong - ) - - ;;;;; start library code - - (include condition_codes.clvm) - - (defmacro assert items - (if (r items) - (list if (f items) (c assert (r items)) (q . (x))) - (f items) - ) - ) - - ;; utility function used by `curry_args` - (defun fix_curry_args (items core) - (if items - (qq (c (q . (unquote (f items))) (unquote (fix_curry_args (r items) core)))) - core - ) - ) - - ; (curry_args sum (list 50 60)) => returns a function that is like (sum 50 60 ...) - (defun curry_args (func list_of_args) (qq (a (q . (unquote func)) (unquote (fix_curry_args list_of_args (q . 1)))))) - - ;; (curry sum 50 60) => returns a function that is like (sum 50 60 ...) - (defun curry (func . args) (curry_args func args)) - - (defun is-in-list (atom items) - ;; returns 1 iff `atom` is in the list of `items` - (if items - (if (= atom (f items)) - 1 - (is-in-list atom (r items)) - ) - 0 - ) - ) - - ;; hash a tree with escape values representing already-hashed subtrees - ;; This optimization can be useful if you know the puzzle hash of a sub-expression. - ;; You probably actually want to use `curry_and_hash` though. - (defun sha256tree_esc_list - (TREE LITERALS) - (if (l TREE) - (sha256 2 (sha256tree_esc_list (f TREE) LITERALS) (sha256tree_esc_list (r TREE) LITERALS)) - (if (is-in-list TREE LITERALS) - TREE - (sha256 1 TREE) - ) - ) - ) - - ;; hash a tree with escape values representing already-hashed subtrees - ;; This optimization can be useful if you know the tree hash of a sub-expression. - (defun sha256tree_esc - (TREE . LITERAL) - (sha256tree_esc_list TREE LITERAL) - ) - - ; takes a lisp tree and returns the hash of it - (defun sha256tree1 (TREE) - (if (l TREE) - (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) - (sha256 1 TREE))) - - ;;;;; end library code - - ;; return the puzzle hash for a cc with the given `genesis-coin-checker-hash` & `inner-puzzle` - (defun cc-puzzle-hash ((mod-hash mod-hash-hash genesis-coin-checker genesis-coin-checker-hash) inner-puzzle-hash) - (sha256tree_esc (curry mod-hash mod-hash-hash genesis-coin-checker-hash inner-puzzle-hash) - mod-hash - mod-hash-hash - genesis-coin-checker-hash - inner-puzzle-hash) - ) - - ;; tweak `CREATE_COIN` condition by wrapping the puzzle hash, forcing it to be a cc - ;; prohibit CREATE_COIN_ANNOUNCEMENT - (defun-inline morph-condition (condition lineage-proof-parameters) - (if (= (f condition) CREATE_COIN) - (list CREATE_COIN - (cc-puzzle-hash lineage-proof-parameters (f (r condition))) - (f (r (r condition))) - ) - (if (= (f condition) CREATE_COIN_ANNOUNCEMENT) - (x) - condition - ) - ) - ) - - ;; tweak all `CREATE_COIN` conditions, enforcing created coins to be ccs - (defun morph-conditions (conditions lineage-proof-parameters) - (if conditions - (c - (morph-condition (f conditions) lineage-proof-parameters) - (morph-conditions (r conditions) lineage-proof-parameters) - ) - () - ) - ) - - ;; given a coin triplet, return the id of the coin - (defun coin-id-for-coin ((parent-id puzzle-hash amount)) - (sha256 parent-id puzzle-hash amount) - ) - - ;; utility to fetch coin amount from coin - (defun-inline input-amount-for-coin (coin) - (f (r (r coin))) - ) - - ;; calculate the hash of an announcement - (defun-inline calculate-annoucement-id (this-coin-info this-subtotal next-coin-info) - ; NOTE: the next line containts a bug, as sha256tree1 ignores `this-subtotal` - (sha256 (coin-id-for-coin next-coin-info) (sha256tree1 (list this-coin-info this-subtotal))) - ) - - ;; create the `ASSERT_COIN_ANNOUNCEMENT` condition that ensures the next coin's announcement is correct - (defun-inline create-assert-next-announcement-condition (this-coin-info this-subtotal next-coin-info) - (list ASSERT_COIN_ANNOUNCEMENT - (calculate-annoucement-id this-coin-info - this-subtotal - next-coin-info - ) - ) - ) - - ;; here we commit to I_{k-1} and S_k - (defun-inline create-announcement-condition (prev-coin-info prev-subtotal) - (list CREATE_COIN_ANNOUNCEMENT - (sha256tree1 (list prev-coin-info prev-subtotal)) - ) - ) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;; - - ;; this function takes a condition and returns an integer indicating - ;; the value of all output coins created with CREATE_COIN. If it's not - ;; a CREATE_COIN condition, it returns 0. - - (defun-inline output-value-for-condition (condition) - (if (= (f condition) CREATE_COIN) - (f (r (r condition))) - 0 - ) - ) - - ;; this function takes a list of conditions and returns an integer indicating - ;; the value of all output coins created with CREATE_COIN - (defun output-totals (conditions) - (if conditions - (+ (output-value-for-condition (f conditions)) (output-totals (r conditions))) - 0 - ) - ) - - ;; ensure `this-coin-info` is correct by creating the `ASSERT_MY_COIN_ID` condition - (defun-inline create-assert-my-id (this-coin-info) - (list ASSERT_MY_COIN_ID (coin-id-for-coin this-coin-info)) - ) - - ;; add three conditions to the list of morphed conditions: - ;; ASSERT_MY_COIN_ID for `this-coin-info` - ;; CREATE_COIN_ANNOUNCEMENT for my announcement - ;; ASSERT_COIN_ANNOUNCEMENT for the next coin's announcement - (defun-inline generate-final-output-conditions - ( - prev-subtotal - this-subtotal - morphed-conditions - prev-coin-info - this-coin-info - next-coin-info - ) - (c (create-assert-my-id this-coin-info) - (c (create-announcement-condition prev-coin-info prev-subtotal) - (c (create-assert-next-announcement-condition this-coin-info this-subtotal next-coin-info) - morphed-conditions) - ) - ) - ) - - (defun-inline coin-info-for-coin-bundle (coin-bundle) - (f coin-bundle) - ) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;; lineage checking - - ;; return true iff parent of `this-coin-info` is provably a cc - (defun is-parent-cc ( - lineage-proof-parameters - this-coin-info - (parent-parent-coin-id parent-inner-puzzle-hash parent-amount) - ) - (= (f this-coin-info) - (sha256 parent-parent-coin-id - (cc-puzzle-hash lineage-proof-parameters parent-inner-puzzle-hash) - parent-amount - ) - ) - ) - - ;; return true iff the lineage proof is valid - ;; lineage-proof is of one of two forms: - ;; (1 . (parent-parent-coin-id parent-inner-puzzle-hash parent-amount)) - ;; (0 . some-opaque-proof-passed-to-genesis-coin-checker) - ;; so the `f` value determines what kind of proof it is, and the `r` value is the proof - - (defun genesis-coin-checker-for-lpp ((mod_hash mod_hash_hash genesis-coin-checker genesis-coin-checker-hash)) - genesis-coin-checker - ) - - (defun-inline is-lineage-proof-valid ( - lineage-proof-parameters coin-info lineage-proof) - (if - (f lineage-proof) - (is-parent-cc lineage-proof-parameters coin-info (r lineage-proof)) - (a (genesis-coin-checker-for-lpp lineage-proof-parameters) - (list lineage-proof-parameters coin-info (r lineage-proof))) - ) - ) - - (defun is-bundle-valid ((coin . lineage-proof) lineage-proof-parameters) - (is-lineage-proof-valid lineage-proof-parameters coin lineage-proof) - ) - - - - ;;;;;;;;;;;;;;;;;;;;;;;;;;; - - (defun main ( - lineage-proof-parameters - inner-conditions - prev-coin-bundle - this-coin-bundle - next-coin-bundle - prev-subtotal - ) - (assert - ; ensure prev is a cc (is this really necessary?) - (is-bundle-valid prev-coin-bundle lineage-proof-parameters) - - ; ensure this is a cc (to ensure parent wasn't counterfeit) - (is-bundle-valid this-coin-bundle lineage-proof-parameters) - - ; ensure next is a cc (to ensure its announcements can be trusted) - (is-bundle-valid next-coin-bundle lineage-proof-parameters) - - (generate-final-output-conditions - prev-subtotal - ; the expression on the next line calculates `this-subtotal` by adding the delta to `prev-subtotal` - (+ prev-subtotal (- (input-amount-for-coin (coin-info-for-coin-bundle this-coin-bundle)) (output-totals inner-conditions))) - (morph-conditions inner-conditions lineage-proof-parameters) - (coin-info-for-coin-bundle prev-coin-bundle) - (coin-info-for-coin-bundle this-coin-bundle) - (coin-info-for-coin-bundle next-coin-bundle) - ) - ) - ) - - (main - ;; cache some stuff: output conditions, and lineage-proof-parameters - (list mod-hash (sha256tree1 mod-hash) genesis-coin-checker (sha256tree1 genesis-coin-checker)) - (a inner-puzzle inner-puzzle-solution) - prev-coin-bundle - this-coin-bundle - next-coin-bundle - prev-subtotal - ) -) diff --git a/chia/wallet/puzzles/cc.clvm.hex b/chia/wallet/puzzles/cc.clvm.hex deleted file mode 100644 index 02189e5c3b61..000000000000 --- a/chia/wallet/puzzles/cc.clvm.hex +++ /dev/null @@ -1 +0,0 @@ -ff02ffff01ff02ff7affff04ff02ffff04ffff04ff05ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ffff04ff0bffff04ffff02ff2effff04ff02ffff04ff0bff80808080ff8080808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffff808080808080808080ffff04ffff01ffffffff3d46ff333cffffff02ff5effff04ff02ffff04ffff02ff2cffff04ff02ffff04ff09ffff04ff15ffff04ff5dffff04ff0bff80808080808080ffff04ff09ffff04ff15ffff04ff5dffff04ff0bff8080808080808080ff0bff09ff15ff2d80ffff02ff5cffff04ff02ffff04ff05ffff04ff07ff8080808080ffff04ffff0102ffff04ffff04ffff0101ff0580ffff04ffff02ff7cffff04ff02ffff04ff0bffff01ff0180808080ff80808080ff02ffff03ff05ffff01ff04ffff0104ffff04ffff04ffff0101ff0980ffff04ffff02ff7cffff04ff02ffff04ff0dffff04ff0bff8080808080ff80808080ffff010b80ff0180ffffff2dff02ffff03ff15ffff01ff02ff5affff04ff02ffff04ff0bffff04ff09ffff04ff1dff808080808080ffff01ff02ffff02ff22ffff04ff02ffff04ff0bff80808080ffff04ff0bffff04ff09ffff04ff1dff808080808080ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ff05ff1380ffff01ff0101ffff01ff02ff2affff04ff02ffff04ff05ffff04ff1bff808080808080ff0180ff8080ff0180ffff09ff13ffff0bff27ffff02ff24ffff04ff02ffff04ff05ffff04ff57ff8080808080ff81b78080ff02ffff03ffff02ff32ffff04ff02ffff04ff17ffff04ff05ff8080808080ffff01ff02ffff03ffff02ff32ffff04ff02ffff04ff2fffff04ff05ff8080808080ffff01ff02ffff03ffff02ff32ffff04ff02ffff04ff5fffff04ff05ff8080808080ffff01ff04ffff04ff30ffff04ffff02ff34ffff04ff02ffff04ff4fff80808080ff808080ffff04ffff04ff38ffff04ffff02ff2effff04ff02ffff04ffff04ff27ffff04ff81bfff808080ff80808080ff808080ffff04ffff04ff20ffff04ffff0bffff02ff34ffff04ff02ffff04ff819fff80808080ffff02ff2effff04ff02ffff04ffff04ff4fffff04ffff10ff81bfffff11ff8202cfffff02ff36ffff04ff02ffff04ff0bff808080808080ff808080ff8080808080ff808080ffff02ff26ffff04ff02ffff04ff0bffff04ff05ff8080808080808080ffff01ff088080ff0180ffff01ff088080ff0180ffff01ff088080ff0180ffffff02ffff03ff05ffff01ff04ffff02ffff03ffff09ff11ff2880ffff01ff04ff28ffff04ffff02ff24ffff04ff02ffff04ff0bffff04ff29ff8080808080ffff04ff59ff80808080ffff01ff02ffff03ffff09ff11ff3880ffff01ff0880ffff010980ff018080ff0180ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ff8080ff0180ff02ffff03ff05ffff01ff10ffff02ffff03ffff09ff11ff2880ffff0159ff8080ff0180ffff02ff36ffff04ff02ffff04ff0dff8080808080ff8080ff0180ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ff7effff04ff02ffff04ff05ffff04ff07ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ffff04ff0bff8080808080ffff02ff7effff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff02ffff03ffff02ff2affff04ff02ffff04ff05ffff04ff0bff8080808080ffff0105ffff01ff0bffff0101ff058080ff018080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/cc.clvm.hex.sha256tree b/chia/wallet/puzzles/cc.clvm.hex.sha256tree deleted file mode 100644 index d464e3eb7e1d..000000000000 --- a/chia/wallet/puzzles/cc.clvm.hex.sha256tree +++ /dev/null @@ -1 +0,0 @@ -d4596fa7aa6eaa267ebce8d527546827de083d58fb4e14f4137c2448f7252e5c diff --git a/chia/wallet/puzzles/condition_codes.clvm.hex b/chia/wallet/puzzles/condition_codes.clvm.hex new file mode 100644 index 000000000000..ec0643a65a2d --- /dev/null +++ b/chia/wallet/puzzles/condition_codes.clvm.hex @@ -0,0 +1 @@ +can't compile ("defconstant" "AGG_SIG_UNSAFE" 49), unknown operator diff --git a/chia/wallet/puzzles/create-lock-puzzlehash.clvm.hex b/chia/wallet/puzzles/create-lock-puzzlehash.clvm.hex new file mode 100644 index 000000000000..06c984069fe1 --- /dev/null +++ b/chia/wallet/puzzles/create-lock-puzzlehash.clvm.hex @@ -0,0 +1 @@ +can't compile ("my-id"), unknown operator diff --git a/chia/wallet/puzzles/delegated_genesis_checker.clvm b/chia/wallet/puzzles/delegated_genesis_checker.clvm new file mode 100644 index 000000000000..57cc677bd499 --- /dev/null +++ b/chia/wallet/puzzles/delegated_genesis_checker.clvm @@ -0,0 +1,25 @@ +; This is a "limitations_program" for use with cat.clvm. +(mod ( + PUBKEY + Truths + parent_is_cat + lineage_proof + delta + inner_conditions + ( + delegated_puzzle + delegated_solution + ) + ) + + (include condition_codes.clvm) + + (defun sha256tree1 (TREE) + (if (l TREE) + (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) + (sha256 1 TREE))) + + (c (list AGG_SIG_UNSAFE PUBKEY (sha256tree1 delegated_puzzle)) + (a delegated_puzzle (c Truths (c parent_is_cat (c lineage_proof (c delta (c inner_conditions delegated_solution)))))) + ) +) \ No newline at end of file diff --git a/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex b/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex new file mode 100644 index 000000000000..d131b36b4d49 --- /dev/null +++ b/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff82027fff80808080ff80808080ffff02ff82027fffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff82057f80808080808080ffff04ffff01ff31ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex.sha256tree b/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex.sha256tree new file mode 100644 index 000000000000..f1d6d7408d04 --- /dev/null +++ b/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex.sha256tree @@ -0,0 +1 @@ +999c3696e167f8a79d938adc11feba3a3dcb39ccff69a426d570706e7b8ec399 diff --git a/chia/wallet/puzzles/delegated_tail.clvm b/chia/wallet/puzzles/delegated_tail.clvm new file mode 100644 index 000000000000..57cc677bd499 --- /dev/null +++ b/chia/wallet/puzzles/delegated_tail.clvm @@ -0,0 +1,25 @@ +; This is a "limitations_program" for use with cat.clvm. +(mod ( + PUBKEY + Truths + parent_is_cat + lineage_proof + delta + inner_conditions + ( + delegated_puzzle + delegated_solution + ) + ) + + (include condition_codes.clvm) + + (defun sha256tree1 (TREE) + (if (l TREE) + (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) + (sha256 1 TREE))) + + (c (list AGG_SIG_UNSAFE PUBKEY (sha256tree1 delegated_puzzle)) + (a delegated_puzzle (c Truths (c parent_is_cat (c lineage_proof (c delta (c inner_conditions delegated_solution)))))) + ) +) \ No newline at end of file diff --git a/chia/wallet/puzzles/delegated_tail.clvm.hex b/chia/wallet/puzzles/delegated_tail.clvm.hex new file mode 100644 index 000000000000..d131b36b4d49 --- /dev/null +++ b/chia/wallet/puzzles/delegated_tail.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff82027fff80808080ff80808080ffff02ff82027fffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff82057f80808080808080ffff04ffff01ff31ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/delegated_tail.clvm.hex.sha256tree b/chia/wallet/puzzles/delegated_tail.clvm.hex.sha256tree new file mode 100644 index 000000000000..f1d6d7408d04 --- /dev/null +++ b/chia/wallet/puzzles/delegated_tail.clvm.hex.sha256tree @@ -0,0 +1 @@ +999c3696e167f8a79d938adc11feba3a3dcb39ccff69a426d570706e7b8ec399 diff --git a/chia/wallet/puzzles/everything_with_signature.clvm b/chia/wallet/puzzles/everything_with_signature.clvm new file mode 100644 index 000000000000..f467a46de516 --- /dev/null +++ b/chia/wallet/puzzles/everything_with_signature.clvm @@ -0,0 +1,15 @@ +; This is a "limitations_program" for use with cat.clvm. +(mod ( + PUBKEY + Truths + parent_is_cat + lineage_proof + delta + inner_conditions + _ + ) + + (include condition_codes.clvm) + + (list (list AGG_SIG_ME PUBKEY delta)) ; Careful with a delta of zero, the bytecode is 80 not 00 +) \ No newline at end of file diff --git a/chia/wallet/puzzles/everything_with_signature.clvm.hex b/chia/wallet/puzzles/everything_with_signature.clvm.hex new file mode 100644 index 000000000000..0a12a681ffb4 --- /dev/null +++ b/chia/wallet/puzzles/everything_with_signature.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff02ffff04ff05ffff04ff5fff80808080ff8080ffff04ffff0132ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/everything_with_signature.clvm.hex.sha256tree b/chia/wallet/puzzles/everything_with_signature.clvm.hex.sha256tree new file mode 100644 index 000000000000..375a544097ce --- /dev/null +++ b/chia/wallet/puzzles/everything_with_signature.clvm.hex.sha256tree @@ -0,0 +1 @@ +1720d13250a7c16988eaf530331cefa9dd57a76b2c82236bec8bbbff91499b89 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm index 07aafb7e48ea..c136bb070333 100644 --- a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm +++ b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm @@ -1,39 +1,26 @@ ; This is a "genesis checker" for use with cc.clvm. ; -; This checker allows new ccs to be created if they have a particular -; coin id as parent; or created by anyone if their value is 0. - +; This checker allows new CATs to be created if they have a particular coin id as parent +; +; The genesis_id is curried in, making this lineage_check program unique and giving the CAT it's uniqueness (mod ( - genesis-id - lineage-proof-parameters - my-coin-info - (parent-coin zero-parent-inner-puzzle-hash) - ) - - ;; boolean or macro - ;; This lets you write something like (if (or COND1 COND2 COND3) (do-something) (do-something-else)) - (defmacro or ARGS - (if ARGS - (qq (if (unquote (f ARGS)) - 1 - (unquote (c or (r ARGS))) - )) - 0) + GENESIS_ID + Truths + parent_is_cat + lineage_proof + delta + inner_conditions + _ ) - (defun-inline main ( - genesis-id - my-coin-info - ) + (include cat_truths.clib) - (or - (= (f (r (r my-coin-info))) 0) - (= (f my-coin-info) genesis-id) - ) + (if delta + (x) + (if (= (my_parent_cat_truth Truths) GENESIS_ID) + () + (x) + ) ) - (main - genesis-id - my-coin-info - ) -) \ No newline at end of file +) diff --git a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex index 80911af9c52f..3f287e448218 100644 --- a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex +++ b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex @@ -1 +1 @@ -ff02ffff03ffff09ff5bff8080ffff01ff0101ffff01ff02ffff03ffff09ff13ff0280ffff01ff0101ff8080ff018080ff0180 \ No newline at end of file +ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ff2dff0280ff80ffff01ff088080ff018080ff0180 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree index d65060957144..f240ff941767 100644 --- a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree +++ b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree @@ -1 +1 @@ -258008f81f21c270f4b58488b108a46a35e5df43ca5b0313ac83e900a5e44a5f +493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm index 3f4fe7c5f121..720465075d0a 100644 --- a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm +++ b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm @@ -1,55 +1,24 @@ -; This is a "genesis checker" for use with cc.clvm. +; This is a "limitations_program" for use with cat.clvm. ; -; This checker allows new ccs to be created if their parent has a particular -; puzzle hash; or created by anyone if their value is 0. - +; This checker allows new CATs to be created if their parent has a particular puzzle hash (mod ( - genesis-puzzle-hash - lineage-proof-parameters - my-coin-info - (parent-coin zero-parent-inner-puzzle-hash) - ) - - ;; boolean and macro - ;; This lets you write something like (if (and COND1 COND2 COND3) (do-something) (do-something-else)) - (defmacro and ARGS - (if ARGS - (qq (if (unquote (f ARGS)) - (unquote (c and (r ARGS))) - () - )) - 1) - ) - - ;; boolean or macro - ;; This lets you write something like (if (or COND1 COND2 COND3) (do-something) (do-something-else)) - (defmacro or ARGS - (if ARGS - (qq (if (unquote (f ARGS)) - 1 - (unquote (c or (r ARGS))) - )) - 0) + GENESIS_PUZZLE_HASH + Truths + parent_is_cat + lineage_proof + delta + inner_conditions + (parent_parent_id parent_amount) ) - (defun-inline main ( - genesis-puzzle-hash - my-coin-info - parent-coin - ) + (include cat_truths.clib) - (or - (= (f (r (r my-coin-info))) 0) - (and - (= (sha256 (f parent-coin) (f (r parent-coin)) (f (r (r parent-coin)))) (f my-coin-info)) - (= (f (r parent-coin)) genesis-puzzle-hash) + ; Returns nil since we don't need to add any conditions + (if delta + (x) + (if (= (sha256 parent_parent_id GENESIS_PUZZLE_HASH parent_amount) (my_parent_cat_truth Truths)) + () + (x) ) - ) - ) - - (main - genesis-puzzle-hash - my-coin-info - parent-coin ) -) \ No newline at end of file +) diff --git a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex index 1c7bb81b3ff1..2d367721d1cb 100644 --- a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex +++ b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex @@ -1 +1 @@ -ff02ffff03ffff09ff5bff8080ffff01ff0101ffff01ff02ffff03ffff02ffff03ffff09ffff0bff47ff81a7ff82016780ff1380ffff01ff02ffff03ffff09ff81a7ff0280ffff01ff0101ff8080ff0180ff8080ff0180ffff01ff0101ff8080ff018080ff0180 \ No newline at end of file +ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ffff0bff82013fff02ff8202bf80ff2d80ff80ffff01ff088080ff018080ff0180 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree index 2a85e38ef1e5..69cdc4bce6c8 100644 --- a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree +++ b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree @@ -1 +1 @@ -795964e0324fbc08e8383d67659194a70455956ad1ebd2329ccf20008da00936 +de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis_by_coin_id.clvm b/chia/wallet/puzzles/genesis_by_coin_id.clvm new file mode 100644 index 000000000000..78769f87d48c --- /dev/null +++ b/chia/wallet/puzzles/genesis_by_coin_id.clvm @@ -0,0 +1,26 @@ +; This is a TAIL for use with cat.clvm. +; +; This checker allows new CATs to be created if they have a particular coin id as parent +; +; The genesis_id is curried in, making this lineage_check program unique and giving the CAT it's uniqueness +(mod ( + GENESIS_ID + Truths + parent_is_cat + lineage_proof + delta + inner_conditions + _ + ) + + (include cat_truths.clib) + + (if delta + (x) + (if (= (my_parent_cat_truth Truths) GENESIS_ID) + () + (x) + ) + ) + +) diff --git a/chia/wallet/puzzles/genesis_by_coin_id.clvm.hex b/chia/wallet/puzzles/genesis_by_coin_id.clvm.hex new file mode 100644 index 000000000000..3f287e448218 --- /dev/null +++ b/chia/wallet/puzzles/genesis_by_coin_id.clvm.hex @@ -0,0 +1 @@ +ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ff2dff0280ff80ffff01ff088080ff018080ff0180 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis_by_coin_id.clvm.hex.sha256tree b/chia/wallet/puzzles/genesis_by_coin_id.clvm.hex.sha256tree new file mode 100644 index 000000000000..f240ff941767 --- /dev/null +++ b/chia/wallet/puzzles/genesis_by_coin_id.clvm.hex.sha256tree @@ -0,0 +1 @@ +493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis_by_coin_id_with_0.py b/chia/wallet/puzzles/genesis_by_coin_id_with_0.py deleted file mode 100644 index 3b473d64cca6..000000000000 --- a/chia/wallet/puzzles/genesis_by_coin_id_with_0.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Optional - -from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.program import Program -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.wallet.puzzles.load_clvm import load_clvm - -MOD = load_clvm("genesis-by-coin-id-with-0.clvm", package_or_requirement=__name__) - - -def create_genesis_or_zero_coin_checker(genesis_coin_id: bytes32) -> Program: - """ - Given a specific genesis coin id, create a `genesis_coin_mod` that allows - both that coin id to issue a cc, or anyone to create a cc with amount 0. - """ - genesis_coin_mod = MOD - return genesis_coin_mod.curry(genesis_coin_id) - - -def genesis_coin_id_for_genesis_coin_checker( - genesis_coin_checker: Program, -) -> Optional[bytes32]: - """ - Given a `genesis_coin_checker` program, pull out the genesis coin id. - """ - r = genesis_coin_checker.uncurry() - if r is None: - return r - f, args = r - if f != MOD: - return None - return args.first().as_atom() - - -def lineage_proof_for_genesis(parent_coin: Coin) -> Program: - return Program.to((0, [parent_coin.as_list(), 0])) - - -def lineage_proof_for_zero(parent_coin: Coin) -> Program: - return Program.to((0, [parent_coin.as_list(), 1])) - - -def lineage_proof_for_coin(parent_coin: Coin) -> Program: - if parent_coin.amount == 0: - return lineage_proof_for_zero(parent_coin) - return lineage_proof_for_genesis(parent_coin) diff --git a/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm b/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm new file mode 100644 index 000000000000..720465075d0a --- /dev/null +++ b/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm @@ -0,0 +1,24 @@ +; This is a "limitations_program" for use with cat.clvm. +; +; This checker allows new CATs to be created if their parent has a particular puzzle hash +(mod ( + GENESIS_PUZZLE_HASH + Truths + parent_is_cat + lineage_proof + delta + inner_conditions + (parent_parent_id parent_amount) + ) + + (include cat_truths.clib) + + ; Returns nil since we don't need to add any conditions + (if delta + (x) + (if (= (sha256 parent_parent_id GENESIS_PUZZLE_HASH parent_amount) (my_parent_cat_truth Truths)) + () + (x) + ) + ) +) diff --git a/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex b/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex new file mode 100644 index 000000000000..2d367721d1cb --- /dev/null +++ b/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex @@ -0,0 +1 @@ +ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ffff0bff82013fff02ff8202bf80ff2d80ff80ffff01ff088080ff018080ff0180 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex.sha256tree b/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex.sha256tree new file mode 100644 index 000000000000..69cdc4bce6c8 --- /dev/null +++ b/chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex.sha256tree @@ -0,0 +1 @@ +de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis_by_puzzle_hash_with_0.py b/chia/wallet/puzzles/genesis_by_puzzle_hash_with_0.py deleted file mode 100644 index 53829902421b..000000000000 --- a/chia/wallet/puzzles/genesis_by_puzzle_hash_with_0.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Optional - -from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.program import Program -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.wallet.puzzles.load_clvm import load_clvm - -MOD = load_clvm("genesis-by-puzzle-hash-with-0.clvm", package_or_requirement=__name__) - - -def create_genesis_puzzle_or_zero_coin_checker(genesis_puzzle_hash: bytes32) -> Program: - """ - Given a specific genesis coin id, create a `genesis_coin_mod` that allows - both that coin id to issue a cc, or anyone to create a cc with amount 0. - """ - genesis_coin_mod = MOD - return genesis_coin_mod.curry(genesis_puzzle_hash) - - -def genesis_puzzle_hash_for_genesis_coin_checker( - genesis_coin_checker: Program, -) -> Optional[bytes32]: - """ - Given a `genesis_coin_checker` program, pull out the genesis puzzle hash. - """ - r = genesis_coin_checker.uncurry() - if r is None: - return r - f, args = r - if f != MOD: - return None - return args.first().as_atom() - - -def lineage_proof_for_genesis_puzzle(parent_coin: Coin) -> Program: - return Program.to((0, [parent_coin.as_list(), 0])) - - -def lineage_proof_for_zero(parent_coin: Coin) -> Program: - return Program.to((0, [parent_coin.as_list(), 1])) - - -def lineage_proof_for_coin(parent_coin: Coin) -> Program: - if parent_coin.amount == 0: - return lineage_proof_for_zero(parent_coin) - return lineage_proof_for_genesis_puzzle(parent_coin) diff --git a/chia/wallet/puzzles/genesis_checkers.py b/chia/wallet/puzzles/genesis_checkers.py new file mode 100644 index 000000000000..7ba72af62a53 --- /dev/null +++ b/chia/wallet/puzzles/genesis_checkers.py @@ -0,0 +1,208 @@ +from typing import Tuple, Dict, List, Optional, Any + +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint64 +from chia.util.byte_types import hexstr_to_bytes +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.cat_wallet.cat_utils import ( + CAT_MOD, + construct_cat_puzzle, + unsigned_spend_bundle_for_spendable_cats, + SpendableCAT, +) +from chia.wallet.cat_wallet.cat_info import CATInfo +from chia.wallet.transaction_record import TransactionRecord + +GENESIS_BY_ID_MOD = load_clvm("genesis-by-coin-id-with-0.clvm") +GENESIS_BY_PUZHASH_MOD = load_clvm("genesis-by-puzzle-hash-with-0.clvm") +EVERYTHING_WITH_SIG_MOD = load_clvm("everything_with_signature.clvm") +DELEGATED_LIMITATIONS_MOD = load_clvm("delegated_genesis_checker.clvm") + + +class LimitationsProgram: + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + raise NotImplementedError("Need to implement 'match' on limitations programs") + + @staticmethod + def construct(args: List[Program]) -> Program: + raise NotImplementedError("Need to implement 'construct' on limitations programs") + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + raise NotImplementedError("Need to implement 'solve' on limitations programs") + + @classmethod + async def generate_issuance_bundle( + cls, wallet, cat_tail_info: Dict, amount: uint64 + ) -> Tuple[TransactionRecord, SpendBundle]: + raise NotImplementedError("Need to implement 'generate_issuance_bundle' on limitations programs") + + +class GenesisById(LimitationsProgram): + """ + This TAIL allows for coins to be issued only by a specific "genesis" coin ID. + There can therefore only be one issuance. There is no minting or melting allowed. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == GENESIS_BY_ID_MOD: + genesis_id = curried_args.first() + return True, [genesis_id] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return GENESIS_BY_ID_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + return Program.to([]) + + @classmethod + async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tuple[TransactionRecord, SpendBundle]: + coins = await wallet.standard_wallet.select_coins(amount) + + origin = coins.copy().pop() + origin_id = origin.name() + + cc_inner: Program = await wallet.get_new_inner_puzzle() + await wallet.add_lineage(origin_id, LineageProof()) + genesis_coin_checker: Program = cls.construct([Program.to(origin_id)]) + + minted_cc_puzzle_hash: bytes32 = construct_cat_puzzle( + CAT_MOD, genesis_coin_checker.get_tree_hash(), cc_inner + ).get_tree_hash() + + tx_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction( + amount, minted_cc_puzzle_hash, uint64(0), origin_id, coins + ) + assert tx_record.spend_bundle is not None + + inner_solution = wallet.standard_wallet.add_condition_to_solution( + Program.to([51, 0, -113, genesis_coin_checker, []]), + wallet.standard_wallet.make_solution( + primaries=[{"puzzlehash": cc_inner.get_tree_hash(), "amount": amount}], + ), + ) + eve_spend = unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + [ + SpendableCAT( + list(filter(lambda a: a.amount == amount, tx_record.additions))[0], + genesis_coin_checker.get_tree_hash(), + cc_inner, + inner_solution, + limitations_program_reveal=genesis_coin_checker, + ) + ], + ) + signed_eve_spend = await wallet.sign(eve_spend) + + if wallet.cat_info.my_tail is None: + await wallet.save_info( + CATInfo(genesis_coin_checker.get_tree_hash(), genesis_coin_checker, wallet.cat_info.lineage_proofs), + False, + ) + + return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend]) + + +class GenesisByPuzhash(LimitationsProgram): + """ + This TAIL allows for issuance of a certain coin only by a specific puzzle hash. + There is no minting or melting allowed. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == GENESIS_BY_PUZHASH_MOD: + genesis_puzhash = curried_args.first() + return True, [genesis_puzhash] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return GENESIS_BY_PUZHASH_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + pid = hexstr_to_bytes(solution_dict["parent_coin_info"]) + return Program.to([pid, solution_dict["amount"]]) + + +class EverythingWithSig(LimitationsProgram): + """ + This TAIL allows for issuance, minting, and melting as long as you provide a signature with the spend. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == EVERYTHING_WITH_SIG_MOD: + pubkey = curried_args.first() + return True, [pubkey] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return EVERYTHING_WITH_SIG_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + return Program.to([]) + + +class DelegatedLimitations(LimitationsProgram): + """ + This TAIL allows for another TAIL to be used, as long as a signature of that TAIL's puzzlehash is included. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == DELEGATED_LIMITATIONS_MOD: + pubkey = curried_args.first() + return True, [pubkey] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return DELEGATED_LIMITATIONS_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + signed_program = ALL_LIMITATIONS_PROGRAMS[solution_dict["signed_program"]["identifier"]] + inner_program_args = [Program.fromhex(item) for item in solution_dict["signed_program"]["args"]] + inner_solution_dict = solution_dict["program_arguments"] + return Program.to( + [ + signed_program.construct(inner_program_args), + signed_program.solve(inner_program_args, inner_solution_dict), + ] + ) + + +# This should probably be much more elegant than just a dictionary with strings as identifiers +# Right now this is small and experimental so it can stay like this +ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = { + "genesis_by_id": GenesisById, + "genesis_by_puzhash": GenesisByPuzhash, + "everything_with_signature": EverythingWithSig, + "delegated_limitations": DelegatedLimitations, +} + + +def match_limitations_program(limitations_program: Program) -> Tuple[Optional[LimitationsProgram], List[Program]]: + uncurried_mod, curried_args = limitations_program.uncurry() + for key, lp in ALL_LIMITATIONS_PROGRAMS.items(): + matched, args = lp.match(uncurried_mod, curried_args) + if matched: + return lp, args + return None, [] diff --git a/chia/wallet/puzzles/prefarm/make_prefarm_ph.py b/chia/wallet/puzzles/prefarm/make_prefarm_ph.py index a0c476955e45..5d5ebf75cb9a 100644 --- a/chia/wallet/puzzles/prefarm/make_prefarm_ph.py +++ b/chia/wallet/puzzles/prefarm/make_prefarm_ph.py @@ -7,6 +7,7 @@ from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.condition_tools import parse_sexp_to_conditions from chia.util.ints import uint32 +from chia.types.blockchain_format.sized_bytes import bytes32 address1 = "txch15gx26ndmacfaqlq8m0yajeggzceu7cvmaz4df0hahkukes695rss6lej7h" # Gene wallet (m/12381/8444/2/42): address2 = "txch1c2cguswhvmdyz9hr3q6hak2h6p9dw4rz82g4707k2xy2sarv705qcce4pn" # Mariano address (m/12381/8444/2/0) @@ -45,11 +46,8 @@ def make_puzzle(amount: int) -> int: for cvp in result_human: assert len(cvp.vars) == 2 total_chia += int_from_bytes(cvp.vars[1]) - # TODO: address hint error and remove ignore - # error: Argument 1 to "encode_puzzle_hash" has incompatible type "bytes"; expected "bytes32" - # [arg-type] print( - f"{ConditionOpcode(cvp.opcode).name}: {encode_puzzle_hash(cvp.vars[0], prefix)}," # type: ignore[arg-type] # noqa E501 + f"{ConditionOpcode(cvp.opcode).name}: {encode_puzzle_hash(bytes32(cvp.vars[0]), prefix)}," f" amount: {int_from_bytes(cvp.vars[1])}" ) return total_chia diff --git a/chia/wallet/puzzles/puzzle_utils.py b/chia/wallet/puzzles/puzzle_utils.py index 961b9aefbf8e..1d8b72b4b2d4 100644 --- a/chia/wallet/puzzles/puzzle_utils.py +++ b/chia/wallet/puzzles/puzzle_utils.py @@ -1,7 +1,11 @@ +from typing import Optional, List + from chia.util.condition_tools import ConditionOpcode -def make_create_coin_condition(puzzle_hash, amount): +def make_create_coin_condition(puzzle_hash, amount, memos: Optional[List[bytes]]) -> List: + if memos is not None: + return [ConditionOpcode.CREATE_COIN, puzzle_hash, amount, memos] return [ConditionOpcode.CREATE_COIN, puzzle_hash, amount] diff --git a/chia/wallet/puzzles/settlement_payments.clvm b/chia/wallet/puzzles/settlement_payments.clvm new file mode 100644 index 000000000000..8de19339a0d6 --- /dev/null +++ b/chia/wallet/puzzles/settlement_payments.clvm @@ -0,0 +1,45 @@ +(mod notarized_payments + ;; `notarized_payments` is a list of notarized coin payments + ;; a notarized coin payment is `(nonce . ((puzzle_hash amount ...) (puzzle_hash amount ...) ...))` + ;; Each notarized coin payment creates some `(CREATE_COIN puzzle_hash amount ...)` payments + ;; and a `(CREATE_PUZZLE_ANNOUNCEMENT (sha256tree notarized_coin_payment))` announcement + ;; The idea is the other side of this trade requires observing the announcement from a + ;; `settlement_payments` puzzle hash as a condition of one or more coin spends. + + (include condition_codes.clvm) + + (defun sha256tree (TREE) + (if (l TREE) + (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) + (sha256 1 TREE) + ) + ) + + (defun create_coins_for_payment (payment_params so_far) + (if payment_params + (c (c CREATE_COIN (f payment_params)) (create_coins_for_payment (r payment_params) so_far)) + so_far + ) + ) + + (defun-inline create_announcement_for_payment (notarized_payment) + (list CREATE_PUZZLE_ANNOUNCEMENT + (sha256tree notarized_payment)) + ) + + (defun-inline augment_condition_list (notarized_payment so_far) + (c + (create_announcement_for_payment notarized_payment) + (create_coins_for_payment (r notarized_payment) so_far) + ) + ) + + (defun construct_condition_list (notarized_payments) + (if notarized_payments + (augment_condition_list (f notarized_payments) (construct_condition_list (r notarized_payments))) + () + ) + ) + + (construct_condition_list notarized_payments) +) \ No newline at end of file diff --git a/chia/wallet/puzzles/settlement_payments.clvm.hex b/chia/wallet/puzzles/settlement_payments.clvm.hex new file mode 100644 index 000000000000..c3319eaf4c60 --- /dev/null +++ b/chia/wallet/puzzles/settlement_payments.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 diff --git a/chia/wallet/puzzles/settlement_payments.clvm.hex.sha256tree b/chia/wallet/puzzles/settlement_payments.clvm.hex.sha256tree new file mode 100644 index 000000000000..a1394c5e679b --- /dev/null +++ b/chia/wallet/puzzles/settlement_payments.clvm.hex.sha256tree @@ -0,0 +1 @@ +bae24162efbd568f89bc7a340798a6118df0189eb9e3f8697bcea27af99f8f79 diff --git a/chia/wallet/puzzles/singleton_top_layer.clvm b/chia/wallet/puzzles/singleton_top_layer.clvm index db762a4c9a1b..19204e8e0ed7 100644 --- a/chia/wallet/puzzles/singleton_top_layer.clvm +++ b/chia/wallet/puzzles/singleton_top_layer.clvm @@ -30,13 +30,13 @@ (defmacro assert items (if (r items) (list if (f items) (c assert (r items)) (q . (x))) - (f items) - ) + (f items) ) + ) - (defun-inline mod_hash_for_singleton_struct (SINGLETON_STRUCT) (f SINGLETON_STRUCT)) - (defun-inline launcher_id_for_singleton_struct (SINGLETON_STRUCT) (f (r SINGLETON_STRUCT))) - (defun-inline launcher_puzzle_hash_for_singleton_struct (SINGLETON_STRUCT) (r (r SINGLETON_STRUCT))) + (defun-inline mod_hash_for_singleton_struct (SINGLETON_STRUCT) (f SINGLETON_STRUCT)) + (defun-inline launcher_id_for_singleton_struct (SINGLETON_STRUCT) (f (r SINGLETON_STRUCT))) + (defun-inline launcher_puzzle_hash_for_singleton_struct (SINGLETON_STRUCT) (r (r SINGLETON_STRUCT))) ;; return the full puzzlehash for a singleton with the innerpuzzle curried in ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc diff --git a/chia/wallet/puzzles/singleton_top_layer.py b/chia/wallet/puzzles/singleton_top_layer.py index 3cb0effe55e0..ab626582093b 100644 --- a/chia/wallet/puzzles/singleton_top_layer.py +++ b/chia/wallet/puzzles/singleton_top_layer.py @@ -31,6 +31,16 @@ def adapt_inner_to_singleton(inner_puzzle: Program) -> Program: return Program.to([2, (1, inner_puzzle), [6, 1]]) +def adapt_inner_puzzle_hash_to_singleton(inner_puzzle_hash: bytes32) -> bytes32: + puzzle = adapt_inner_to_singleton(Program.to(inner_puzzle_hash)) + return puzzle.get_tree_hash(inner_puzzle_hash) + + +def remove_singleton_truth_wrapper(puzzle: Program) -> Program: + inner_puzzle = puzzle.rest().first().rest() + return inner_puzzle + + # Take standard coin and amount -> launch conditions & launcher coin solution def launch_conditions_and_coinsol( coin: Coin, diff --git a/chia/wallet/puzzles/tails.py b/chia/wallet/puzzles/tails.py new file mode 100644 index 000000000000..f6321e67b8ef --- /dev/null +++ b/chia/wallet/puzzles/tails.py @@ -0,0 +1,206 @@ +from typing import Tuple, Dict, List, Optional, Any + +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint64 +from chia.util.byte_types import hexstr_to_bytes +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.cat_wallet.cat_utils import ( + CAT_MOD, + construct_cat_puzzle, + unsigned_spend_bundle_for_spendable_cats, + SpendableCAT, +) +from chia.wallet.cat_wallet.cat_info import CATInfo +from chia.wallet.transaction_record import TransactionRecord + +GENESIS_BY_ID_MOD = load_clvm("genesis_by_coin_id.clvm") +GENESIS_BY_PUZHASH_MOD = load_clvm("genesis_by_puzzle_hash.clvm") +EVERYTHING_WITH_SIG_MOD = load_clvm("everything_with_signature.clvm") +DELEGATED_LIMITATIONS_MOD = load_clvm("delegated_tail.clvm") + + +class LimitationsProgram: + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + raise NotImplementedError("Need to implement 'match' on limitations programs") + + @staticmethod + def construct(args: List[Program]) -> Program: + raise NotImplementedError("Need to implement 'construct' on limitations programs") + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + raise NotImplementedError("Need to implement 'solve' on limitations programs") + + @classmethod + async def generate_issuance_bundle( + cls, wallet, cat_tail_info: Dict, amount: uint64 + ) -> Tuple[TransactionRecord, SpendBundle]: + raise NotImplementedError("Need to implement 'generate_issuance_bundle' on limitations programs") + + +class GenesisById(LimitationsProgram): + """ + This TAIL allows for coins to be issued only by a specific "genesis" coin ID. + There can therefore only be one issuance. There is no minting or melting allowed. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == GENESIS_BY_ID_MOD: + genesis_id = curried_args.first() + return True, [genesis_id] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return GENESIS_BY_ID_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + return Program.to([]) + + @classmethod + async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tuple[TransactionRecord, SpendBundle]: + coins = await wallet.standard_wallet.select_coins(amount) + + origin = coins.copy().pop() + origin_id = origin.name() + + cat_inner: Program = await wallet.get_new_inner_puzzle() + await wallet.add_lineage(origin_id, LineageProof()) + tail: Program = cls.construct([Program.to(origin_id)]) + + minted_cat_puzzle_hash: bytes32 = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), cat_inner).get_tree_hash() + + tx_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction( + amount, minted_cat_puzzle_hash, uint64(0), origin_id, coins + ) + assert tx_record.spend_bundle is not None + + inner_solution = wallet.standard_wallet.add_condition_to_solution( + Program.to([51, 0, -113, tail, []]), + wallet.standard_wallet.make_solution( + primaries=[{"puzzlehash": cat_inner.get_tree_hash(), "amount": amount}], + ), + ) + eve_spend = unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + [ + SpendableCAT( + list(filter(lambda a: a.amount == amount, tx_record.additions))[0], + tail.get_tree_hash(), + cat_inner, + inner_solution, + limitations_program_reveal=tail, + ) + ], + ) + signed_eve_spend = await wallet.sign(eve_spend) + + if wallet.cat_info.my_tail is None: + await wallet.save_info( + CATInfo(tail.get_tree_hash(), tail, wallet.cat_info.lineage_proofs), + False, + ) + + return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend]) + + +class GenesisByPuzhash(LimitationsProgram): + """ + This TAIL allows for issuance of a certain coin only by a specific puzzle hash. + There is no minting or melting allowed. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == GENESIS_BY_PUZHASH_MOD: + genesis_puzhash = curried_args.first() + return True, [genesis_puzhash] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return GENESIS_BY_PUZHASH_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + pid = hexstr_to_bytes(solution_dict["parent_coin_info"]) + return Program.to([pid, solution_dict["amount"]]) + + +class EverythingWithSig(LimitationsProgram): + """ + This TAIL allows for issuance, minting, and melting as long as you provide a signature with the spend. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == EVERYTHING_WITH_SIG_MOD: + pubkey = curried_args.first() + return True, [pubkey] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return EVERYTHING_WITH_SIG_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + return Program.to([]) + + +class DelegatedLimitations(LimitationsProgram): + """ + This TAIL allows for another TAIL to be used, as long as a signature of that TAIL's puzzlehash is included. + """ + + @staticmethod + def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: + if uncurried_mod == DELEGATED_LIMITATIONS_MOD: + pubkey = curried_args.first() + return True, [pubkey] + else: + return False, [] + + @staticmethod + def construct(args: List[Program]) -> Program: + return DELEGATED_LIMITATIONS_MOD.curry(args[0]) + + @staticmethod + def solve(args: List[Program], solution_dict: Dict) -> Program: + signed_program = ALL_LIMITATIONS_PROGRAMS[solution_dict["signed_program"]["identifier"]] + inner_program_args = [Program.fromhex(item) for item in solution_dict["signed_program"]["args"]] + inner_solution_dict = solution_dict["program_arguments"] + return Program.to( + [ + signed_program.construct(inner_program_args), + signed_program.solve(inner_program_args, inner_solution_dict), + ] + ) + + +# This should probably be much more elegant than just a dictionary with strings as identifiers +# Right now this is small and experimental so it can stay like this +ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = { + "genesis_by_id": GenesisById, + "genesis_by_puzhash": GenesisByPuzhash, + "everything_with_signature": EverythingWithSig, + "delegated_limitations": DelegatedLimitations, +} + + +def match_limitations_program(limitations_program: Program) -> Tuple[Optional[LimitationsProgram], List[Program]]: + uncurried_mod, curried_args = limitations_program.uncurry() + for key, lp in ALL_LIMITATIONS_PROGRAMS.items(): + matched, args = lp.match(uncurried_mod, curried_args) + if matched: + return lp, args + return None, [] diff --git a/chia/wallet/puzzles/test_cc.py b/chia/wallet/puzzles/test_cc.py deleted file mode 100644 index e676a02d1564..000000000000 --- a/chia/wallet/puzzles/test_cc.py +++ /dev/null @@ -1,242 +0,0 @@ -# this is used to iterate on `cc.clvm` to ensure that it's producing the sort -# of output that we expect - -from typing import Dict, List, Optional, Tuple - -from blspy import G2Element - -from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.program import Program -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.condition_opcodes import ConditionOpcode -from chia.types.spend_bundle import CoinSpend, SpendBundle -from chia.util.ints import uint64 -from chia.wallet.cc_wallet.cc_utils import ( - CC_MOD, - cc_puzzle_for_inner_puzzle, - cc_puzzle_hash_for_inner_puzzle_hash, - spend_bundle_for_spendable_ccs, - spendable_cc_list_from_coin_spend, -) -from chia.wallet.puzzles.genesis_by_coin_id_with_0 import create_genesis_or_zero_coin_checker -from chia.wallet.puzzles.genesis_by_puzzle_hash_with_0 import create_genesis_puzzle_or_zero_coin_checker - -CONDITIONS = dict((k, bytes(v)[0]) for k, v in ConditionOpcode.__members__.items()) # pylint: disable=E1101 - -NULL_SIGNATURE = G2Element() - -ANYONE_CAN_SPEND_PUZZLE = Program.to(1) # simply return the conditions - -PUZZLE_TABLE: Dict[bytes32, Program] = dict((_.get_tree_hash(), _) for _ in [ANYONE_CAN_SPEND_PUZZLE]) - - -def hash_to_puzzle_f(puzzle_hash: bytes32) -> Optional[Program]: - return PUZZLE_TABLE.get(puzzle_hash) - - -def add_puzzles_to_puzzle_preimage_db(puzzles: List[Program]) -> None: - for _ in puzzles: - PUZZLE_TABLE[_.get_tree_hash()] = _ - - -def int_as_bytes32(v: int) -> bytes32: - return bytes32(v.to_bytes(32, byteorder="big")) - - -def generate_farmed_coin( - block_index: int, - puzzle_hash: bytes32, - amount: int, -) -> Coin: - """ - Generate a (fake) coin which can be used as a starting point for a chain - of coin tests. - """ - return Coin(int_as_bytes32(block_index), puzzle_hash, uint64(amount)) - - -def issue_cc_from_farmed_coin( - mod_code: Program, - coin_checker_for_farmed_coin, - block_id: int, - inner_puzzle_hash: bytes32, - amount: int, -) -> Tuple[Program, SpendBundle]: - """ - This is an example of how to issue a cc. - """ - # get a farmed coin - - farmed_puzzle = ANYONE_CAN_SPEND_PUZZLE - farmed_puzzle_hash = farmed_puzzle.get_tree_hash() - - # mint a cc - - farmed_coin = generate_farmed_coin(block_id, farmed_puzzle_hash, amount=uint64(amount)) - genesis_coin_checker = coin_checker_for_farmed_coin(farmed_coin) - - minted_cc_puzzle_hash = cc_puzzle_hash_for_inner_puzzle_hash(mod_code, genesis_coin_checker, inner_puzzle_hash) - - output_conditions = [[ConditionOpcode.CREATE_COIN, minted_cc_puzzle_hash, farmed_coin.amount]] - - # for this very simple puzzle, the solution is simply the output conditions - # this is just a coincidence... for more complicated puzzles, you'll likely have to do some real work - - solution = Program.to(output_conditions) - coin_spend = CoinSpend(farmed_coin, farmed_puzzle, solution) - spend_bundle = SpendBundle([coin_spend], NULL_SIGNATURE) - return genesis_coin_checker, spend_bundle - - -def solution_for_pay_to_any(puzzle_hash_amount_pairs: List[Tuple[bytes32, int]]) -> Program: - output_conditions = [ - [ConditionOpcode.CREATE_COIN, puzzle_hash, amount] for puzzle_hash, amount in puzzle_hash_amount_pairs - ] - return Program.to(output_conditions) - - -def test_spend_through_n(mod_code, coin_checker_for_farmed_coin, n): - """ - Test to spend ccs from a farmed coin to a cc genesis coin, then to N outputs, - then joining back down to two outputs. - """ - - ################################ - - # spend from a farmed coin to a cc genesis coin - - # get a farmed coin - - eve_inner_puzzle = ANYONE_CAN_SPEND_PUZZLE - eve_inner_puzzle_hash = eve_inner_puzzle.get_tree_hash() - - # generate output values [0x100, 0x200, ...] - - output_values = [0x100 + 0x100 * _ for _ in range(n)] - total_minted = sum(output_values) - - genesis_coin_checker, spend_bundle = issue_cc_from_farmed_coin( - mod_code, coin_checker_for_farmed_coin, 1, eve_inner_puzzle_hash, total_minted - ) - - # hack the wrapped puzzles into the PUZZLE_TABLE DB - - puzzles_for_db = [cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, eve_inner_puzzle)] - add_puzzles_to_puzzle_preimage_db(puzzles_for_db) - spend_bundle.debug() - - ################################ - - # collect up the spendable coins - - spendable_cc_list = [] - for coin_spend in spend_bundle.coin_spends: - spendable_cc_list.extend(spendable_cc_list_from_coin_spend(coin_spend, hash_to_puzzle_f)) - - # now spend the genesis coin cc to N outputs - - output_conditions = solution_for_pay_to_any([(eve_inner_puzzle_hash, _) for _ in output_values]) - inner_puzzle_solution = Program.to(output_conditions) - - spend_bundle = spend_bundle_for_spendable_ccs( - mod_code, - genesis_coin_checker, - spendable_cc_list, - [inner_puzzle_solution], - ) - - spend_bundle.debug() - - ################################ - - # collect up the spendable coins - - spendable_cc_list = [] - for coin_spend in spend_bundle.coin_spends: - spendable_cc_list.extend(spendable_cc_list_from_coin_spend(coin_spend, hash_to_puzzle_f)) - - # now spend N inputs to two outputs - - output_amounts = ([0] * (n - 2)) + [0x1, total_minted - 1] - - inner_solutions = [ - solution_for_pay_to_any([(eve_inner_puzzle_hash, amount)] if amount else []) for amount in output_amounts - ] - - spend_bundle = spend_bundle_for_spendable_ccs( - mod_code, - genesis_coin_checker, - spendable_cc_list, - inner_solutions, - ) - - spend_bundle.debug() - - -def test_spend_zero_coin(mod_code: Program, coin_checker_for_farmed_coin): - """ - Test to spend ccs from a farmed coin to a cc genesis coin, then to N outputs, - then joining back down to two outputs. - """ - - eve_inner_puzzle = ANYONE_CAN_SPEND_PUZZLE - eve_inner_puzzle_hash = eve_inner_puzzle.get_tree_hash() - - total_minted = 0x111 - - genesis_coin_checker, spend_bundle = issue_cc_from_farmed_coin( - mod_code, coin_checker_for_farmed_coin, 1, eve_inner_puzzle_hash, total_minted - ) - - puzzles_for_db = [cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, eve_inner_puzzle)] - add_puzzles_to_puzzle_preimage_db(puzzles_for_db) - - eve_cc_list = [] - for _ in spend_bundle.coin_spends: - eve_cc_list.extend(spendable_cc_list_from_coin_spend(_, hash_to_puzzle_f)) - assert len(eve_cc_list) == 1 - eve_cc_spendable = eve_cc_list[0] - - # farm regular chia - - farmed_coin = generate_farmed_coin(2, eve_inner_puzzle_hash, amount=500) - - # create a zero cc from this farmed coin - - wrapped_cc_puzzle_hash = cc_puzzle_hash_for_inner_puzzle_hash(mod_code, genesis_coin_checker, eve_inner_puzzle_hash) - - solution = solution_for_pay_to_any([(wrapped_cc_puzzle_hash, 0)]) - coin_spend = CoinSpend(farmed_coin, ANYONE_CAN_SPEND_PUZZLE, solution) - spendable_cc_list = spendable_cc_list_from_coin_spend(coin_spend, hash_to_puzzle_f) - assert len(spendable_cc_list) == 1 - zero_cc_spendable = spendable_cc_list[0] - - # we have our zero coin - # now try to spend it - - spendable_cc_list = [eve_cc_spendable, zero_cc_spendable] - inner_solutions = [ - solution_for_pay_to_any([]), - solution_for_pay_to_any([(wrapped_cc_puzzle_hash, eve_cc_spendable.coin.amount)]), - ] - spend_bundle = spend_bundle_for_spendable_ccs(mod_code, genesis_coin_checker, spendable_cc_list, inner_solutions) - spend_bundle.debug() - - -def main(): - mod_code = CC_MOD - - def coin_checker_for_farmed_coin_by_coin_id(coin: Coin): - return create_genesis_or_zero_coin_checker(coin.name()) - - test_spend_through_n(mod_code, coin_checker_for_farmed_coin_by_coin_id, 12) - test_spend_zero_coin(mod_code, coin_checker_for_farmed_coin_by_coin_id) - - def coin_checker_for_farmed_coin_by_puzzle_hash(coin: Coin): - return create_genesis_puzzle_or_zero_coin_checker(coin.puzzle_hash) - - test_spend_through_n(mod_code, coin_checker_for_farmed_coin_by_puzzle_hash, 10) - - -if __name__ == "__main__": - main() diff --git a/chia/wallet/rl_wallet/rl_wallet.py b/chia/wallet/rl_wallet/rl_wallet.py index 144e93dd5eeb..8a3fdcbba3c6 100644 --- a/chia/wallet/rl_wallet/rl_wallet.py +++ b/chia/wallet/rl_wallet/rl_wallet.py @@ -85,6 +85,7 @@ async def create_rl_admin( pubkey, WalletType.RATE_LIMITED, wallet_info.id, + False, ) ] ) @@ -121,11 +122,7 @@ async def create_rl_user( await wallet_state_manager.puzzle_store.add_derivation_paths( [ DerivationRecord( - unused, - bytes32(token_bytes(32)), - pubkey, - WalletType.RATE_LIMITED, - wallet_info.id, + unused, bytes32(token_bytes(32)), pubkey, WalletType.RATE_LIMITED, wallet_info.id, False ) ] ) @@ -193,6 +190,7 @@ async def admin_create_coin( G1Element.from_bytes(self.rl_info.admin_pubkey), WalletType.RATE_LIMITED, self.id(), + False, ) await self.wallet_state_manager.puzzle_store.add_derivation_paths([record]) @@ -235,8 +233,8 @@ async def set_user_info( assert self.rl_info.user_pubkey is not None origin = Coin( - bytes32.from_hexstr(origin_parent_id), - bytes32.from_hexstr(origin_puzzle_hash), + bytes32(hexstr_to_bytes(origin_parent_id)), + bytes32(hexstr_to_bytes(origin_puzzle_hash)), origin_amount, ) rl_puzzle = rl_puzzle_for_pk( @@ -274,6 +272,7 @@ async def set_user_info( user_pubkey, WalletType.RATE_LIMITED, self.id(), + False, ) aggregation_puzzlehash = self.rl_get_aggregation_puzzlehash(new_rl_info.rl_puzzle_hash) @@ -283,6 +282,7 @@ async def set_user_info( user_pubkey, WalletType.RATE_LIMITED, self.id(), + False, ) await self.wallet_state_manager.puzzle_store.add_derivation_paths([record, record2]) self.wallet_state_manager.set_coin_with_puzzlehash_created_callback( @@ -303,13 +303,11 @@ async def aggregate_this_coin(self, coin: Coin): rl_coin = await self._get_rl_coin() puzzle_hash = rl_coin.puzzle_hash if rl_coin is not None else None - # TODO: address hint error and remove ignore - # error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] + assert puzzle_hash is not None tx_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), - to_puzzle_hash=puzzle_hash, # type: ignore[arg-type] + to_puzzle_hash=puzzle_hash, amount=uint64(0), fee_amount=uint64(0), confirmed=False, @@ -322,6 +320,7 @@ async def aggregate_this_coin(self, coin: Coin): trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), + memos=list(spend_bundle.get_memos().items()), ) asyncio.create_task(self.push_transaction(tx_record)) @@ -520,7 +519,9 @@ async def rl_generate_unsigned_transaction(self, to_puzzlehash, amount, fee) -> spends.append(CoinSpend(coin, puzzle, solution)) return spends - async def generate_signed_transaction(self, amount, to_puzzle_hash, fee: uint64 = uint64(0)) -> TransactionRecord: + async def generate_signed_transaction( + self, amount, to_puzzle_hash, fee: uint64 = uint64(0), memo: Optional[List[bytes]] = None + ) -> TransactionRecord: self.rl_coin_record = await self._get_rl_coin_record() if not self.rl_coin_record: raise ValueError("No unspent coin (zero balance)") @@ -545,6 +546,7 @@ async def generate_signed_transaction(self, amount, to_puzzle_hash, fee: uint64 trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), + memos=list(spend_bundle.get_memos().items()), ) async def rl_sign_transaction(self, spends: List[CoinSpend]) -> SpendBundle: @@ -621,6 +623,7 @@ async def clawback_rl_coin_transaction(self, fee) -> TransactionRecord: trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), + memos=list(spend_bundle.get_memos().items()), ) # This is for using the AC locked coin and aggregating it into wallet - must happen in same block as RL Mode 2 diff --git a/chia/wallet/settings/user_settings.py b/chia/wallet/settings/user_settings.py index e8363b1d335c..b83acec18ab8 100644 --- a/chia/wallet/settings/user_settings.py +++ b/chia/wallet/settings/user_settings.py @@ -39,36 +39,3 @@ async def setting_updated(self, setting: Any): name = setting.__class__.__name__ await self.basic_store.set_object(name, setting) self.settings[name] = setting - - async def user_skipped_backup_import(self): - new = BackupInitialized( - user_initialized=True, - user_skipped=True, - backup_info_imported=False, - new_wallet=False, - ) - await self.setting_updated(new) - return new - - async def user_imported_backup(self): - new = BackupInitialized( - user_initialized=True, - user_skipped=False, - backup_info_imported=True, - new_wallet=False, - ) - await self.setting_updated(new) - return new - - async def user_created_new_wallet(self): - new = BackupInitialized( - user_initialized=True, - user_skipped=False, - backup_info_imported=False, - new_wallet=True, - ) - await self.setting_updated(new) - return new - - def get_backup_settings(self) -> BackupInitialized: - return self.settings[BackupInitialized.__name__] diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index ed75182e2551..d0f1d5ad9563 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -1,34 +1,23 @@ import logging import time import traceback -from pathlib import Path -from secrets import token_bytes -from typing import Any, Dict, List, Optional, Tuple - -from blspy import AugSchemeMPL +from typing import Any, Dict, List, Optional, Tuple, Union, Set +from chia.protocols.wallet_protocol import CoinState from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle -from chia.types.coin_spend import CoinSpend -from chia.util.byte_types import hexstr_to_bytes from chia.util.db_wrapper import DBWrapper from chia.util.hash import std_hash from chia.util.ints import uint32, uint64 -from chia.wallet.cc_wallet import cc_utils -from chia.wallet.cc_wallet.cc_utils import CC_MOD, SpendableCC, spend_bundle_for_spendable_ccs, uncurry_cc -from chia.wallet.cc_wallet.cc_wallet import CCWallet -from chia.wallet.puzzles.genesis_by_coin_id_with_0 import genesis_coin_id_for_genesis_coin_checker +from chia.wallet.cat_wallet.cat_wallet import CATWallet +from chia.wallet.payment import Payment from chia.wallet.trade_record import TradeRecord +from chia.wallet.trading.offer import Offer, NotarizedPayment from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.trading.trade_store import TradeStore from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.trade_utils import ( - get_discrepancies_for_spend_bundle, - get_output_amount_for_puzzle_and_solution, - get_output_discrepancy_for_puzzle_and_solution, -) from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet import Wallet @@ -62,7 +51,7 @@ async def get_offers_with_status(self, status: TradeStatus) -> List[TradeRecord] async def get_coins_of_interest( self, - ) -> Tuple[Dict[bytes32, Coin], Dict[bytes32, Coin]]: + ) -> Dict[bytes32, Coin]: """ Returns list of coins we want to check if they are included in filter, These will include coins that belong to us and coins that that on other side of treade @@ -74,81 +63,69 @@ async def get_coins_of_interest( all_pending.extend(pending_accept) all_pending.extend(pending_confirm) all_pending.extend(pending_cancel) - removals = {} - additions = {} + interested_dict = {} for trade in all_pending: - for coin in trade.removals: - removals[coin.name()] = coin - for coin in trade.additions: - additions[coin.name()] = coin + for coin in trade.coins_of_interest: + interested_dict[coin.name()] = coin - return removals, additions + return interested_dict async def get_trade_by_coin(self, coin: Coin) -> Optional[TradeRecord]: all_trades = await self.get_all_trades() for trade in all_trades: - if trade.status == TradeStatus.CANCELED.value: + if trade.status == TradeStatus.CANCELLED.value: continue - if coin in trade.removals: - return trade - if coin in trade.additions: + if coin in trade.coins_of_interest: return trade return None - async def coins_of_interest_farmed(self, removals: List[Coin], additions: List[Coin], height: uint32): + async def coins_of_interest_farmed(self, coin_state: CoinState): """ If both our coins and other coins in trade got removed that means that trade was successfully executed If coins from other side of trade got farmed without ours, that means that trade failed because either someone else completed trade or other side of trade canceled the trade by doing a spend. If our coins got farmed but coins from other side didn't, we successfully canceled trade by spending inputs. """ - removal_dict = {} - addition_dict = {} - checked: Dict[bytes32, Coin] = {} - for coin in removals: - removal_dict[coin.name()] = coin - for coin in additions: - addition_dict[coin.name()] = coin - - all_coins = [] - all_coins.extend(removals) - all_coins.extend(additions) - - for coin in all_coins: - if coin.name() in checked: - continue - trade = await self.get_trade_by_coin(coin) - if trade is None: - self.log.error(f"Coin: {Coin}, not in any trade") - continue - - # Check if all coins that are part of the trade got farmed - # If coin is missing, trade failed - failed = False - for removed_coin in trade.removals: - if removed_coin.name() not in removal_dict: - self.log.error(f"{removed_coin} from trade not removed") - failed = True - checked[removed_coin.name()] = removed_coin - for added_coin in trade.additions: - if added_coin.name() not in addition_dict: - self.log.error(f"{added_coin} from trade not added") - failed = True - checked[coin.name()] = coin - - if failed is False: - # Mark this trade as successful - await self.trade_store.set_status(trade.trade_id, TradeStatus.CONFIRMED, True, height) - self.log.info(f"Trade with id: {trade.trade_id} confirmed at height: {height}") - else: - # Either we canceled this trade or this trade failed - if trade.status == TradeStatus.PENDING_CANCEL.value: - await self.trade_store.set_status(trade.trade_id, TradeStatus.CANCELED, True) - self.log.info(f"Trade with id: {trade.trade_id} canceled at height: {height}") - elif trade.status == TradeStatus.PENDING_CONFIRM.value: - await self.trade_store.set_status(trade.trade_id, TradeStatus.FAILED, True) - self.log.warning(f"Trade with id: {trade.trade_id} failed at height: {height}") + trade = await self.get_trade_by_coin(coin_state.coin) + if trade is None: + self.log.error(f"Coin: {coin_state.coin}, not in any trade") + return + if coin_state.spent_height is None: + self.log.error(f"Coin: {coin_state.coin}, has not been spent so trade can remain valid") + + # Then let's filter the offer into coins that WE offered + offer = Offer.from_bytes(trade.offer) + primary_coin_ids = [c.name() for c in offer.get_primary_coins()] + our_coin_records: List[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_multiple_coin_records( + primary_coin_ids + ) + our_primary_coins: List[bytes32] = [cr.coin.name() for cr in our_coin_records] + all_settlement_payments: List[Coin] = [c for coins in offer.get_offered_coins().values() for c in coins] + our_settlement_payments: List[Coin] = list( + filter(lambda c: offer.get_root_removal(c).name() in our_primary_coins, all_settlement_payments) + ) + our_settlement_ids: List[bytes32] = [c.name() for c in our_settlement_payments] + + # And get all relevant coin states + coin_states = await self.wallet_state_manager.get_coin_state(our_settlement_ids) + assert coin_states is not None + coin_state_names: List[bytes32] = [cs.coin.name() for cs in coin_states] + + # If any of our settlement_payments were spent, this offer was a success! + if set(our_settlement_ids) & set(coin_state_names): + height = coin_states[0].spent_height + await self.trade_store.set_status(trade.trade_id, TradeStatus.CONFIRMED, True, height) + self.log.info(f"Trade with id: {trade.trade_id} confirmed at height: {height}") + else: + # In any other scenario this trade failed + await self.wallet_state_manager.delete_trade_transactions(trade.trade_id) + if trade.status == TradeStatus.PENDING_CANCEL.value: + await self.trade_store.set_status(trade.trade_id, TradeStatus.CANCELLED, True) + self.log.info(f"Trade with id: {trade.trade_id} canceled") + elif trade.status == TradeStatus.PENDING_CONFIRM.value: + await self.trade_store.set_status(trade.trade_id, TradeStatus.FAILED, True) + self.log.warning(f"Trade with id: {trade.trade_id} failed") async def get_locked_coins(self, wallet_id: int = None) -> Dict[bytes32, WalletCoinRecord]: """Returns a dictionary of confirmed coins that are locked by a trade.""" @@ -159,18 +136,16 @@ async def get_locked_coins(self, wallet_id: int = None) -> Dict[bytes32, WalletC all_pending.extend(pending_accept) all_pending.extend(pending_confirm) all_pending.extend(pending_cancel) - if len(all_pending) == 0: - return {} - result = {} + coins_of_interest = [] for trade_offer in all_pending: - if trade_offer.tx_spend_bundle is None: - locked = await self.get_locked_coins_in_spend_bundle(trade_offer.spend_bundle) - else: - locked = await self.get_locked_coins_in_spend_bundle(trade_offer.tx_spend_bundle) - for name, record in locked.items(): - if wallet_id is None or record.wallet_id == wallet_id: - result[name] = record + coins_of_interest.extend([c.name() for c in Offer.from_bytes(trade_offer.offer).get_involved_coins()]) + + result = {} + coin_records = await self.wallet_state_manager.coin_store.get_multiple_coin_records(coins_of_interest) + for record in coin_records: + if wallet_id is None or record.wallet_id == wallet_id: + result[record.name()] = record return result @@ -182,496 +157,341 @@ async def get_trade_by_id(self, trade_id: bytes32) -> Optional[TradeRecord]: record = await self.trade_store.get_trade_record(trade_id) return record - async def get_locked_coins_in_spend_bundle(self, bundle: SpendBundle) -> Dict[bytes32, WalletCoinRecord]: - """Returns a list of coin records that are used in this SpendBundle""" - result = {} - removals = bundle.removals() - for coin in removals: - coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin.name()) - if coin_record is None: - continue - result[coin_record.name()] = coin_record - return result - async def cancel_pending_offer(self, trade_id: bytes32): - await self.trade_store.set_status(trade_id, TradeStatus.CANCELED, False) + await self.trade_store.set_status(trade_id, TradeStatus.CANCELLED, False) - async def cancel_pending_offer_safely(self, trade_id: bytes32): + async def cancel_pending_offer_safely( + self, trade_id: bytes32, fee: uint64 = uint64(0) + ) -> Optional[List[TransactionRecord]]: """This will create a transaction that includes coins that were offered""" self.log.info(f"Secure-Cancel pending offer with id trade_id {trade_id.hex()}") trade = await self.trade_store.get_trade_record(trade_id) if trade is None: return None - all_coins = trade.removals - - for coin in all_coins: + all_txs: List[TransactionRecord] = [] + fee_to_pay: uint64 = fee + for coin in Offer.from_bytes(trade.offer).get_primary_coins(): wallet = await self.wallet_state_manager.get_wallet_for_coin(coin.name()) if wallet is None: continue new_ph = await wallet.get_new_puzzlehash() - if wallet.type() == WalletType.COLOURED_COIN.value: - tx = await wallet.generate_signed_transaction( - [coin.amount], [new_ph], 0, coins={coin}, ignore_max_send_amount=True + # This should probably not switch on whether or not we're spending a CAT but it has to for now + if wallet.type() == WalletType.CAT: + txs = await wallet.generate_signed_transaction( + [coin.amount], [new_ph], fee=fee_to_pay, coins={coin}, ignore_max_send_amount=True ) + all_txs.extend(txs) else: + if fee_to_pay > coin.amount: + selected_coins: Set[Coin] = await wallet.select_coins( + uint64(fee_to_pay - coin.amount), + exclude=[coin], + ) + selected_coins.add(coin) + else: + selected_coins = {coin} tx = await wallet.generate_signed_transaction( - coin.amount, new_ph, 0, coins={coin}, ignore_max_send_amount=True + uint64(sum([c.amount for c in selected_coins]) - fee_to_pay), + new_ph, + fee=fee_to_pay, + coins=selected_coins, + ignore_max_send_amount=True, ) + all_txs.append(tx) + fee_to_pay = uint64(0) + + for tx in all_txs: await self.wallet_state_manager.add_pending_transaction(tx_record=tx) await self.trade_store.set_status(trade_id, TradeStatus.PENDING_CANCEL, False) - return None + + return all_txs async def save_trade(self, trade: TradeRecord): await self.trade_store.add_trade_record(trade, False) async def create_offer_for_ids( - self, offer: Dict[int, int], file_name: str + self, offer: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0), validate_only: bool = False ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: - success, trade_offer, error = await self._create_offer_for_ids(offer) + success, created_offer, error = await self._create_offer_for_ids(offer, fee=fee) + if not success or created_offer is None: + raise Exception(f"Error creating offer: {error}") - if success is True and trade_offer is not None: - self.write_offer_to_disk(Path(file_name), trade_offer) + now = uint64(int(time.time())) + trade_offer: TradeRecord = TradeRecord( + confirmed_at_index=uint32(0), + accepted_at_time=None, + created_at_time=now, + is_my_offer=True, + sent=uint32(0), + offer=bytes(created_offer), + taken_offer=None, + coins_of_interest=created_offer.get_involved_coins(), + trade_id=created_offer.name(), + status=uint32(TradeStatus.PENDING_ACCEPT.value), + sent_to=[], + ) + + if success is True and trade_offer is not None and not validate_only: await self.save_trade(trade_offer) return success, trade_offer, error - async def _create_offer_for_ids(self, offer: Dict[int, int]) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: + async def _create_offer_for_ids( + self, offer_dict: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0) + ) -> Tuple[bool, Optional[Offer], Optional[str]]: """ Offer is dictionary of wallet ids and amount """ - spend_bundle = None try: - for id in offer.keys(): - amount = offer[id] - wallet_id = uint32(int(id)) - wallet = self.wallet_state_manager.wallets[wallet_id] - if isinstance(wallet, CCWallet): + coins_to_offer: Dict[uint32, List[Coin]] = {} + requested_payments: Dict[Optional[bytes32], List[Payment]] = {} + for id, amount in offer_dict.items(): + if amount > 0: + if isinstance(id, int): + wallet_id = uint32(id) + wallet = self.wallet_state_manager.wallets[wallet_id] + p2_ph: bytes32 = await wallet.get_new_puzzlehash() + if wallet.type() == WalletType.STANDARD_WALLET: + key: Optional[bytes32] = None + memos: List[bytes] = [] + elif wallet.type() == WalletType.CAT: + key = bytes32(bytes.fromhex(wallet.get_asset_id())) + memos = [p2_ph] + else: + raise ValueError(f"Offers are not implemented for {wallet.type()}") + else: + p2_ph = await self.wallet_state_manager.main_wallet.get_new_puzzlehash() + key = id + memos = [p2_ph] + requested_payments[key] = [Payment(p2_ph, uint64(amount), memos)] + elif amount < 0: + assert isinstance(id, int) + wallet_id = uint32(id) + wallet = self.wallet_state_manager.wallets[wallet_id] balance = await wallet.get_confirmed_balance() - if balance < abs(amount) and amount < 0: + if balance < abs(amount): raise Exception(f"insufficient funds in wallet {wallet_id}") - if amount > 0: - if spend_bundle is None: - to_exclude: List[Coin] = [] - else: - to_exclude = spend_bundle.removals() - zero_spend_bundle: SpendBundle = await wallet.generate_zero_val_coin(False, to_exclude) + coins_to_offer[wallet_id] = await wallet.select_coins(uint64(abs(amount))) + elif amount == 0: + raise ValueError("You cannot offer nor request 0 amount of something") - if spend_bundle is None: - spend_bundle = zero_spend_bundle - else: - spend_bundle = SpendBundle.aggregate([spend_bundle, zero_spend_bundle]) - - additions = zero_spend_bundle.additions() - removals = zero_spend_bundle.removals() - zero_val_coin: Optional[Coin] = None - for add in additions: - if add not in removals and add.amount == 0: - zero_val_coin = add - new_spend_bundle = await wallet.create_spend_bundle_relative_amount(amount, zero_val_coin) - else: - new_spend_bundle = await wallet.create_spend_bundle_relative_amount(amount) - elif isinstance(wallet, Wallet): - if spend_bundle is None: - to_exclude = [] - else: - to_exclude = spend_bundle.removals() - new_spend_bundle = await wallet.create_spend_bundle_relative_chia(amount, to_exclude) - else: - return False, None, "unsupported wallet type" - if new_spend_bundle is None or new_spend_bundle.removals() == []: - raise Exception(f"Wallet {id} was unable to create offer.") - if spend_bundle is None: - spend_bundle = new_spend_bundle - else: - spend_bundle = SpendBundle.aggregate([spend_bundle, new_spend_bundle]) - - if spend_bundle is None: - return False, None, None - - now = uint64(int(time.time())) - trade_offer: TradeRecord = TradeRecord( - confirmed_at_index=uint32(0), - accepted_at_time=None, - created_at_time=now, - my_offer=True, - sent=uint32(0), - spend_bundle=spend_bundle, - tx_spend_bundle=None, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), - trade_id=std_hash(spend_bundle.name() + bytes(now)), - status=uint32(TradeStatus.PENDING_ACCEPT.value), - sent_to=[], + all_coins: List[Coin] = [c for coins in coins_to_offer.values() for c in coins] + notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments( + requested_payments, all_coins ) - return True, trade_offer, None + announcements_to_assert = Offer.calculate_announcements(notarized_payments) + + all_transactions: List[TransactionRecord] = [] + fee_left_to_pay: uint64 = fee + for wallet_id, selected_coins in coins_to_offer.items(): + wallet = self.wallet_state_manager.wallets[wallet_id] + # This should probably not switch on whether or not we're spending a CAT but it has to for now + + if wallet.type() == WalletType.CAT: + txs = await wallet.generate_signed_transaction( + [abs(offer_dict[int(wallet_id)])], + [Offer.ph()], + fee=fee_left_to_pay, + coins=set(selected_coins), + puzzle_announcements_to_consume=announcements_to_assert, + ) + all_transactions.extend(txs) + else: + tx = await wallet.generate_signed_transaction( + abs(offer_dict[int(wallet_id)]), + Offer.ph(), + fee=fee_left_to_pay, + coins=set(selected_coins), + puzzle_announcements_to_consume=announcements_to_assert, + ) + all_transactions.append(tx) + + fee_left_to_pay = uint64(0) + + transaction_bundles: List[Optional[SpendBundle]] = [tx.spend_bundle for tx in all_transactions] + total_spend_bundle = SpendBundle.aggregate(list(filter(lambda b: b is not None, transaction_bundles))) + offer = Offer(notarized_payments, total_spend_bundle) + return True, offer, None + except Exception as e: tb = traceback.format_exc() self.log.error(f"Error with creating trade offer: {type(e)}{tb}") return False, None, str(e) - def write_offer_to_disk(self, file_path: Path, offer: TradeRecord): - if offer is not None: - file_path.write_text(bytes(offer).hex()) - - async def get_discrepancies_for_offer(self, file_path: Path) -> Tuple[bool, Optional[Dict], Optional[Exception]]: - self.log.info(f"trade offer: {file_path}") - trade_offer_hex = file_path.read_text() - trade_offer = TradeRecord.from_bytes(bytes.fromhex(trade_offer_hex)) - return get_discrepancies_for_spend_bundle(trade_offer.spend_bundle) - - async def get_inner_puzzle_for_puzzle_hash(self, puzzle_hash) -> Program: - info = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(puzzle_hash) - assert info is not None - puzzle = self.wallet_state_manager.main_wallet.puzzle_for_pk(bytes(info.pubkey)) - return puzzle + async def maybe_create_wallets_for_offer(self, offer: Offer): - async def maybe_create_wallets_for_offer(self, file_path: Path) -> bool: - success, result, error = await self.get_discrepancies_for_offer(file_path) - if not success or result is None: - return False - - for key, value in result.items(): + for key in offer.arbitrage(): wsm = self.wallet_state_manager wallet: Wallet = wsm.main_wallet - if key == "chia": - continue - self.log.info(f"value is {key}") - exists = await wsm.get_wallet_for_colour(key) - if exists is not None: + if key is None: continue - - await CCWallet.create_wallet_for_cc(wsm, wallet, key) - - return True - - async def respond_to_offer(self, file_path: Path) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: - has_wallets = await self.maybe_create_wallets_for_offer(file_path) - if not has_wallets: - return False, None, "Unknown Error" - trade_offer = None - try: - trade_offer_hex = file_path.read_text() - trade_offer = TradeRecord.from_bytes(hexstr_to_bytes(trade_offer_hex)) - except Exception as e: - return False, None, f"Error: {e}" - if trade_offer is not None: - offer_spend_bundle: SpendBundle = trade_offer.spend_bundle - - coinsols: List[CoinSpend] = [] # [] of CoinSpends - cc_coinsol_outamounts: Dict[str, List[Tuple[CoinSpend, int]]] = dict() - aggsig = offer_spend_bundle.aggregated_signature - cc_discrepancies: Dict[str, int] = dict() - chia_discrepancy = None - wallets: Dict[str, Any] = dict() # colour to wallet dict - - for coinsol in offer_spend_bundle.coin_spends: - puzzle: Program = Program.from_bytes(bytes(coinsol.puzzle_reveal)) - solution: Program = Program.from_bytes(bytes(coinsol.solution)) - - # work out the deficits between coin amount and expected output for each - r = cc_utils.uncurry_cc(puzzle) - if r: - # Calculate output amounts - mod_hash, genesis_checker, inner_puzzle = r - colour = bytes(genesis_checker).hex() - if colour not in wallets: - wallets[colour] = await self.wallet_state_manager.get_wallet_for_colour(colour) - unspent = await self.wallet_state_manager.get_spendable_coins_for_wallet(wallets[colour].id()) - if coinsol.coin in [record.coin for record in unspent]: - return False, None, "can't respond to own offer" - - innersol = solution.first() - - total = get_output_amount_for_puzzle_and_solution(inner_puzzle, innersol) - if colour in cc_discrepancies: - cc_discrepancies[colour] += coinsol.coin.amount - total - else: - cc_discrepancies[colour] = coinsol.coin.amount - total - # Store coinsol and output amount for later - if colour in cc_coinsol_outamounts: - cc_coinsol_outamounts[colour].append((coinsol, total)) - else: - cc_coinsol_outamounts[colour] = [(coinsol, total)] - - else: - # standard chia coin - unspent = await self.wallet_state_manager.get_spendable_coins_for_wallet(1) - if coinsol.coin in [record.coin for record in unspent]: - return False, None, "can't respond to own offer" - if chia_discrepancy is None: - chia_discrepancy = get_output_discrepancy_for_puzzle_and_solution(coinsol.coin, puzzle, solution) - else: - chia_discrepancy += get_output_discrepancy_for_puzzle_and_solution(coinsol.coin, puzzle, solution) - coinsols.append(coinsol) - - chia_spend_bundle: Optional[SpendBundle] = None - if chia_discrepancy is not None: - chia_spend_bundle = await self.wallet_state_manager.main_wallet.create_spend_bundle_relative_chia( - chia_discrepancy, [] - ) - if chia_spend_bundle is not None: - for coinsol in coinsols: - chia_spend_bundle.coin_spends.append(coinsol) - - zero_spend_list: List[SpendBundle] = [] - spend_bundle = None - # create coloured coin - self.log.info(cc_discrepancies) - for colour in cc_discrepancies.keys(): - if cc_discrepancies[colour] < 0: - my_cc_spends = await wallets[colour].select_coins(abs(cc_discrepancies[colour])) + exists: Optional[Wallet] = await wsm.get_wallet_for_asset_id(key.hex()) + if exists is None: + self.log.info(f"Creating wallet for asset ID: {key}") + await CATWallet.create_wallet_for_cat(wsm, wallet, key.hex()) + + async def check_offer_validity(self, offer: Offer) -> bool: + all_removals: List[Coin] = offer.bundle.removals() + all_removal_names: List[bytes32] = [c.name() for c in all_removals] + non_ephemeral_removals: List[Coin] = list( + filter(lambda c: c.parent_coin_info not in all_removal_names, all_removals) + ) + coin_states = await self.wallet_state_manager.get_coin_state([c.name() for c in non_ephemeral_removals]) + assert coin_states is not None + return not any([cs.spent_height is not None for cs in coin_states]) + + async def respond_to_offer(self, offer: Offer, fee=uint64(0)) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: + take_offer_dict: Dict[Union[bytes32, int], int] = {} + arbitrage: Dict[Optional[bytes32], int] = offer.arbitrage() + for asset_id, amount in arbitrage.items(): + if asset_id is None: + wallet = self.wallet_state_manager.main_wallet + key: Union[bytes32, int] = int(wallet.id()) else: - if chia_spend_bundle is None: - to_exclude: List = [] + wallet = await self.wallet_state_manager.get_wallet_for_asset_id(asset_id.hex()) + if wallet is None and amount < 0: + return False, None, f"Do not have a CAT of asset ID: {asset_id} to fulfill offer" + elif wallet is None: + key = asset_id else: - to_exclude = chia_spend_bundle.removals() - my_cc_spends = await wallets[colour].select_coins(0) - if my_cc_spends is None or my_cc_spends == set(): - zero_spend_bundle: SpendBundle = await wallets[colour].generate_zero_val_coin(False, to_exclude) - if zero_spend_bundle is None: - return ( - False, - None, - "Unable to generate zero value coin. Confirm that you have chia available", + key = int(wallet.id()) + take_offer_dict[key] = amount + + # First we validate that all of the coins in this offer exist + valid: bool = await self.check_offer_validity(offer) + if not valid: + return False, None, "This offer is no longer valid" + + success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, fee=fee) + if not success or take_offer is None: + return False, None, error + + complete_offer = Offer.aggregate([offer, take_offer]) + assert complete_offer.is_valid() + final_spend_bundle: SpendBundle = complete_offer.to_valid_spend() + + await self.maybe_create_wallets_for_offer(complete_offer) + + # Now to deal with transaction history before pushing the spend + settlement_coins: List[Coin] = [c for coins in complete_offer.get_offered_coins().values() for c in coins] + settlement_coin_ids: List[bytes32] = [c.name() for c in settlement_coins] + additions: List[Coin] = final_spend_bundle.not_ephemeral_additions() + removals: List[Coin] = final_spend_bundle.removals() + all_fees = uint64(final_spend_bundle.fees()) + + txs = [] + + addition_dict: Dict[uint32, List[Coin]] = {} + for addition in additions: + wallet_info = await self.wallet_state_manager.get_wallet_id_for_puzzle_hash(addition.puzzle_hash) + if wallet_info is not None: + wallet_id, _ = wallet_info + if addition.parent_coin_info in settlement_coin_ids: + wallet = self.wallet_state_manager.wallets[wallet_id] + to_puzzle_hash = await wallet.convert_puzzle_hash(addition.puzzle_hash) + txs.append( + TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=to_puzzle_hash, + amount=addition.amount, + fee_amount=uint64(0), + confirmed=False, + sent=uint32(10), + spend_bundle=None, + additions=[addition], + removals=[], + wallet_id=wallet_id, + sent_to=[], + trade_id=complete_offer.name(), + type=uint32(TransactionType.INCOMING_TRADE.value), + name=std_hash(final_spend_bundle.name() + addition.name()), + memos=[], ) - zero_spend_list.append(zero_spend_bundle) - - additions = zero_spend_bundle.additions() - removals = zero_spend_bundle.removals() - my_cc_spends = set() - for add in additions: - if add not in removals and add.amount == 0: - my_cc_spends.add(add) - - if my_cc_spends == set() or my_cc_spends is None: - return False, None, "insufficient funds" - - # Create SpendableCC list and innersol_list with both my coins and the offered coins - # Firstly get the output coin - my_output_coin = my_cc_spends.pop() - spendable_cc_list = [] - innersol_list = [] - genesis_id = genesis_coin_id_for_genesis_coin_checker(Program.from_bytes(bytes.fromhex(colour))) - # Make the rest of the coins assert the output coin is consumed - for coloured_coin in my_cc_spends: - inner_solution = self.wallet_state_manager.main_wallet.make_solution(consumed=[my_output_coin.name()]) - inner_puzzle = await self.get_inner_puzzle_for_puzzle_hash(coloured_coin.puzzle_hash) - assert inner_puzzle is not None - - sigs = await wallets[colour].get_sigs(inner_puzzle, inner_solution, coloured_coin.name()) - sigs.append(aggsig) - aggsig = AugSchemeMPL.aggregate(sigs) - - lineage_proof = await wallets[colour].get_lineage_proof_for_coin(coloured_coin) - # TODO: address hint error and remove ignore - # error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32" - # [arg-type] - spendable_cc_list.append(SpendableCC(coloured_coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501 - innersol_list.append(inner_solution) - - # Create SpendableCC for each of the coloured coins received - for cc_coinsol_out in cc_coinsol_outamounts[colour]: - cc_coinsol = cc_coinsol_out[0] - puzzle = Program.from_bytes(bytes(cc_coinsol.puzzle_reveal)) - solution = Program.from_bytes(bytes(cc_coinsol.solution)) - - r = uncurry_cc(puzzle) - if r: - mod_hash, genesis_coin_checker, inner_puzzle = r - inner_solution = solution.first() - lineage_proof = solution.rest().rest().first() - # TODO: address hint error and remove ignore - # error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected - # "bytes32" [arg-type] - spendable_cc_list.append(SpendableCC(cc_coinsol.coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501 - innersol_list.append(inner_solution) - - # Finish the output coin SpendableCC with new information - newinnerpuzhash = await wallets[colour].get_new_inner_hash() - outputamount = sum([c.amount for c in my_cc_spends]) + cc_discrepancies[colour] + my_output_coin.amount - inner_solution = self.wallet_state_manager.main_wallet.make_solution( - primaries=[{"puzzlehash": newinnerpuzhash, "amount": outputamount}] - ) - inner_puzzle = await self.get_inner_puzzle_for_puzzle_hash(my_output_coin.puzzle_hash) - assert inner_puzzle is not None - - lineage_proof = await wallets[colour].get_lineage_proof_for_coin(my_output_coin) - # TODO: address hint error and remove ignore - # error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32" - # [arg-type] - spendable_cc_list.append(SpendableCC(my_output_coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501 - innersol_list.append(inner_solution) - - sigs = await wallets[colour].get_sigs(inner_puzzle, inner_solution, my_output_coin.name()) - sigs.append(aggsig) - aggsig = AugSchemeMPL.aggregate(sigs) - if spend_bundle is None: - spend_bundle = spend_bundle_for_spendable_ccs( - CC_MOD, - Program.from_bytes(bytes.fromhex(colour)), - spendable_cc_list, - innersol_list, - [aggsig], - ) - else: - new_spend_bundle = spend_bundle_for_spendable_ccs( - CC_MOD, - Program.from_bytes(bytes.fromhex(colour)), - spendable_cc_list, - innersol_list, - [aggsig], - ) - spend_bundle = SpendBundle.aggregate([spend_bundle, new_spend_bundle]) - # reset sigs and aggsig so that they aren't included next time around - sigs = [] - aggsig = AugSchemeMPL.aggregate(sigs) - my_tx_records = [] - if zero_spend_list is not None and spend_bundle is not None: - zero_spend_list.append(spend_bundle) - spend_bundle = SpendBundle.aggregate(zero_spend_list) - - if spend_bundle is None: - return False, None, "spend_bundle missing" - - # Add transaction history for this trade - now = uint64(int(time.time())) - if chia_spend_bundle is not None: - spend_bundle = SpendBundle.aggregate([spend_bundle, chia_spend_bundle]) - if chia_discrepancy < 0: - # TODO: address hint error and remove ignore - # error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected - # "bytes32" [arg-type] - tx_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=now, - to_puzzle_hash=token_bytes(), # type: ignore[arg-type] - amount=uint64(abs(chia_discrepancy)), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(10), - spend_bundle=chia_spend_bundle, - additions=chia_spend_bundle.additions(), - removals=chia_spend_bundle.removals(), - wallet_id=uint32(1), - sent_to=[], - trade_id=std_hash(spend_bundle.name() + bytes(now)), - type=uint32(TransactionType.OUTGOING_TRADE.value), - name=chia_spend_bundle.name(), - ) - else: - # TODO: address hint error and remove ignore - # error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected - # "bytes32" [arg-type] - tx_record = TransactionRecord( + ) + else: # This is change + addition_dict.setdefault(wallet_id, []) + addition_dict[wallet_id].append(addition) + + # While we want additions to show up as separate records, removals of the same wallet should show as one + removal_dict: Dict[uint32, List[Coin]] = {} + for removal in removals: + wallet_info = await self.wallet_state_manager.get_wallet_id_for_puzzle_hash(removal.puzzle_hash) + if wallet_info is not None: + wallet_id, _ = wallet_info + removal_dict.setdefault(wallet_id, []) + removal_dict[wallet_id].append(removal) + + for wid, grouped_removals in removal_dict.items(): + wallet = self.wallet_state_manager.wallets[wid] + to_puzzle_hash = bytes32([1] * 32) # We use all zeros to be clear not to send here + removal_tree_hash = Program.to([rem.as_list() for rem in grouped_removals]).get_tree_hash() + # We also need to calculate the sent amount + removed: int = sum(c.amount for c in grouped_removals) + change_coins: List[Coin] = addition_dict[wid] if wid in addition_dict else [] + change_amount: int = sum(c.amount for c in change_coins) + sent_amount: int = removed - change_amount + txs.append( + TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), - to_puzzle_hash=token_bytes(), # type: ignore[arg-type] - amount=uint64(abs(chia_discrepancy)), - fee_amount=uint64(0), + to_puzzle_hash=to_puzzle_hash, + amount=uint64(sent_amount), + fee_amount=all_fees, confirmed=False, sent=uint32(10), - spend_bundle=chia_spend_bundle, - additions=chia_spend_bundle.additions(), - removals=chia_spend_bundle.removals(), - wallet_id=uint32(1), - sent_to=[], - trade_id=std_hash(spend_bundle.name() + bytes(now)), - type=uint32(TransactionType.INCOMING_TRADE.value), - name=chia_spend_bundle.name(), - ) - my_tx_records.append(tx_record) - - for colour, amount in cc_discrepancies.items(): - wallet = wallets[colour] - if chia_discrepancy > 0: - # TODO: address hint error and remove ignore - # error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected - # "bytes32" [arg-type] - tx_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=token_bytes(), # type: ignore[arg-type] - amount=uint64(abs(amount)), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(10), - spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), + spend_bundle=None, + additions=change_coins, + removals=grouped_removals, wallet_id=wallet.id(), sent_to=[], - trade_id=std_hash(spend_bundle.name() + bytes(now)), + trade_id=complete_offer.name(), type=uint32(TransactionType.OUTGOING_TRADE.value), - name=spend_bundle.name(), - ) - else: - # TODO: address hint errors and remove ignores - # error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected - # "bytes32" [arg-type] - # error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32" - # [arg-type] - tx_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=token_bytes(), # type: ignore[arg-type] - amount=uint64(abs(amount)), - fee_amount=uint64(0), - confirmed=False, - sent=uint32(10), - spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), - wallet_id=wallet.id(), - sent_to=[], - trade_id=std_hash(spend_bundle.name() + bytes(now)), - type=uint32(TransactionType.INCOMING_TRADE.value), - name=token_bytes(), # type: ignore[arg-type] + name=std_hash(final_spend_bundle.name() + removal_tree_hash), + memos=[], ) - my_tx_records.append(tx_record) + ) + + trade_record: TradeRecord = TradeRecord( + confirmed_at_index=uint32(0), + accepted_at_time=uint64(int(time.time())), + created_at_time=uint64(int(time.time())), + is_my_offer=False, + sent=uint32(0), + offer=bytes(complete_offer), + taken_offer=bytes(offer), + coins_of_interest=complete_offer.get_involved_coins(), + trade_id=complete_offer.name(), + status=uint32(TradeStatus.PENDING_CONFIRM.value), + sent_to=[], + ) - # TODO: address hint error and remove ignore - # error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected - # "bytes32" [arg-type] - tx_record = TransactionRecord( + await self.save_trade(trade_record) + + # Dummy transaction for the sake of the wallet push + push_tx = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), - to_puzzle_hash=token_bytes(), # type: ignore[arg-type] + to_puzzle_hash=bytes32([1] * 32), amount=uint64(0), fee_amount=uint64(0), confirmed=False, sent=uint32(0), - spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), + spend_bundle=final_spend_bundle, + additions=final_spend_bundle.additions(), + removals=final_spend_bundle.removals(), wallet_id=uint32(0), sent_to=[], - trade_id=std_hash(spend_bundle.name() + bytes(now)), + trade_id=complete_offer.name(), type=uint32(TransactionType.OUTGOING_TRADE.value), - name=spend_bundle.name(), + name=final_spend_bundle.name(), + memos=list(final_spend_bundle.get_memos().items()), ) - - now = uint64(int(time.time())) - trade_record: TradeRecord = TradeRecord( - confirmed_at_index=uint32(0), - accepted_at_time=now, - created_at_time=now, - my_offer=False, - sent=uint32(0), - spend_bundle=offer_spend_bundle, - tx_spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), - trade_id=std_hash(spend_bundle.name() + bytes(now)), - status=uint32(TradeStatus.PENDING_CONFIRM.value), - sent_to=[], - ) - - await self.save_trade(trade_record) - await self.wallet_state_manager.add_pending_transaction(tx_record) - for tx in my_tx_records: + await self.wallet_state_manager.add_pending_transaction(push_tx) + for tx in txs: await self.wallet_state_manager.add_transaction(tx) return True, trade_record, None diff --git a/chia/wallet/trade_record.py b/chia/wallet/trade_record.py index 32fafe049bf1..5e0dd9656528 100644 --- a/chia/wallet/trade_record.py +++ b/chia/wallet/trade_record.py @@ -1,11 +1,12 @@ from dataclasses import dataclass -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Dict, Any from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint8, uint32, uint64 from chia.util.streamable import Streamable, streamable +from chia.wallet.trading.offer import Offer +from chia.wallet.trading.trade_status import TradeStatus @dataclass(frozen=True) @@ -18,12 +19,34 @@ class TradeRecord(Streamable): confirmed_at_index: uint32 accepted_at_time: Optional[uint64] created_at_time: uint64 - my_offer: bool + is_my_offer: bool sent: uint32 - spend_bundle: SpendBundle # This in not complete spendbundle - tx_spend_bundle: Optional[SpendBundle] # this is full trade - additions: List[Coin] - removals: List[Coin] + offer: bytes + taken_offer: Optional[bytes] + coins_of_interest: List[Coin] trade_id: bytes32 status: uint32 # TradeStatus, enum not streamable sent_to: List[Tuple[str, uint8, Optional[str]]] + + def to_json_dict_convenience(self) -> Dict[str, Any]: + formatted = self.to_json_dict() + formatted["status"] = TradeStatus(self.status).name + offer_to_summarize: bytes = self.offer if self.taken_offer is None else self.taken_offer + offer = Offer.from_bytes(offer_to_summarize) + offered, requested = offer.summary() + formatted["summary"] = { + "offered": offered, + "requested": requested, + } + formatted["pending"] = offer.get_pending_amounts() + del formatted["offer"] + return formatted + + @classmethod + def from_json_dict_convenience(cls, record: Dict[str, Any], offer: str = "") -> "TradeRecord": + new_record = record.copy() + new_record["status"] = TradeStatus[record["status"]].value + del new_record["summary"] + del new_record["pending"] + new_record["offer"] = offer + return cls.from_json_dict(new_record) diff --git a/chia/wallet/trading/offer.py b/chia/wallet/trading/offer.py new file mode 100644 index 000000000000..474e320b9462 --- /dev/null +++ b/chia/wallet/trading/offer.py @@ -0,0 +1,398 @@ +from dataclasses import dataclass +from typing import List, Optional, Dict, Set, Tuple +from blspy import G2Element + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program +from chia.types.announcement import Announcement +from chia.types.coin_spend import CoinSpend +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint64 +from chia.wallet.cat_wallet.cat_utils import ( + CAT_MOD, + SpendableCAT, + construct_cat_puzzle, + match_cat_puzzle, + unsigned_spend_bundle_for_spendable_cats, +) +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.payment import Payment + +OFFER_MOD = load_clvm("settlement_payments.clvm") +ZERO_32 = bytes32([0] * 32) + + +@dataclass(frozen=True) +class NotarizedPayment(Payment): + nonce: bytes32 = ZERO_32 + + @classmethod + def from_condition_and_nonce(cls, condition: Program, nonce: bytes32) -> "NotarizedPayment": + with_opcode: Program = Program.to((51, condition)) # Gotta do this because the super class is expecting it + p = Payment.from_condition(with_opcode) + puzzle_hash, amount, memos = tuple(p.as_condition_args()) + return cls(puzzle_hash, amount, memos, nonce) + + +@dataclass(frozen=True) +class Offer: + requested_payments: Dict[ + Optional[bytes32], List[NotarizedPayment] + ] # The key is the asset id of the asset being requested + bundle: SpendBundle + + @staticmethod + def ph(): + return OFFER_MOD.get_tree_hash() + + @staticmethod + def notarize_payments( + requested_payments: Dict[Optional[bytes32], List[Payment]], # `None` means you are requesting XCH + coins: List[Coin], + ) -> Dict[Optional[bytes32], List[NotarizedPayment]]: + # This sort should be reproducible in CLVM with `>s` + sorted_coins: List[Coin] = sorted(coins, key=Coin.name) + sorted_coin_list: List[List] = [c.as_list() for c in sorted_coins] + nonce: bytes32 = Program.to(sorted_coin_list).get_tree_hash() + + notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {} + for tail_hash, payments in requested_payments.items(): + notarized_payments[tail_hash] = [] + for p in payments: + puzzle_hash, amount, memos = tuple(p.as_condition_args()) + notarized_payments[tail_hash].append(NotarizedPayment(puzzle_hash, amount, memos, nonce)) + + return notarized_payments + + # The announcements returned from this function must be asserted in whatever spend bundle is created by the wallet + @staticmethod + def calculate_announcements( + notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]], + ) -> List[Announcement]: + announcements: List[Announcement] = [] + for tail, payments in notarized_payments.items(): + if tail is not None: + settlement_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail, OFFER_MOD).get_tree_hash() + else: + settlement_ph = OFFER_MOD.get_tree_hash() + + msg: bytes32 = Program.to((payments[0].nonce, [p.as_condition_args() for p in payments])).get_tree_hash() + announcements.append(Announcement(settlement_ph, msg)) + + return announcements + + def __post_init__(self): + # Verify that there is at least something being offered + offered_coins: Dict[bytes32, List[Coin]] = self.get_offered_coins() + if offered_coins == {}: + raise ValueError("Bundle is not offering anything") + + # Verify that there are no duplicate payments + for payments in self.requested_payments.values(): + payment_programs: List[bytes32] = [p.name() for p in payments] + if len(set(payment_programs)) != len(payment_programs): + raise ValueError("Bundle has duplicate requested payments") + + # This method does not get every coin that is being offered, only the `settlement_payment` children + def get_offered_coins(self) -> Dict[Optional[bytes32], List[Coin]]: + offered_coins: Dict[Optional[bytes32], List[Coin]] = {} + + for addition in self.bundle.additions(): + # Get the parent puzzle + parent_puzzle: Program = list( + filter(lambda cs: cs.coin.name() == addition.parent_coin_info, self.bundle.coin_spends) + )[0].puzzle_reveal.to_program() + + # Determine it's TAIL (or lack of) + matched, curried_args = match_cat_puzzle(parent_puzzle) + tail_hash: Optional[bytes32] = None + if matched: + _, tail_hash_program, _ = curried_args + tail_hash = bytes32(tail_hash_program.as_python()) + offer_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD).get_tree_hash() + else: + tail_hash = None + offer_ph = OFFER_MOD.get_tree_hash() + + # Check if the puzzle_hash matches the hypothetical `settlement_payments` puzzle hash + if addition.puzzle_hash == offer_ph: + if tail_hash in offered_coins: + offered_coins[tail_hash].append(addition) + else: + offered_coins[tail_hash] = [addition] + + return offered_coins + + def get_offered_amounts(self) -> Dict[Optional[bytes32], int]: + offered_coins: Dict[Optional[bytes32], List[Coin]] = self.get_offered_coins() + offered_amounts: Dict[Optional[bytes32], int] = {} + for asset_id, coins in offered_coins.items(): + offered_amounts[asset_id] = uint64(sum([c.amount for c in coins])) + return offered_amounts + + def get_requested_payments(self) -> Dict[Optional[bytes32], List[NotarizedPayment]]: + return self.requested_payments + + def get_requested_amounts(self) -> Dict[Optional[bytes32], int]: + requested_amounts: Dict[Optional[bytes32], int] = {} + for asset_id, coins in self.get_requested_payments().items(): + requested_amounts[asset_id] = uint64(sum([c.amount for c in coins])) + return requested_amounts + + def arbitrage(self) -> Dict[Optional[bytes32], int]: + offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts() + requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts() + + arbitrage_dict: Dict[Optional[bytes32], int] = {} + for asset_id in [*requested_amounts.keys(), *offered_amounts.keys()]: + arbitrage_dict[asset_id] = offered_amounts.get(asset_id, 0) - requested_amounts.get(asset_id, 0) + + return arbitrage_dict + + # This is a method mostly for the UI that creates a JSON summary of the offer + def summary(self) -> Tuple[Dict[str, int], Dict[str, int]]: + offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts() + requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts() + + def keys_to_strings(dic: Dict[Optional[bytes32], int]) -> Dict[str, int]: + new_dic: Dict[str, int] = {} + for key in dic: + if key is None: + new_dic["xch"] = dic[key] + else: + new_dic[key.hex()] = dic[key] + return new_dic + + return keys_to_strings(offered_amounts), keys_to_strings(requested_amounts) + + # Also mostly for the UI, returns a dictionary of assets and how much of them is pended for this offer + # This method is also imperfect for sufficiently complex spends + def get_pending_amounts(self) -> Dict[str, int]: + all_additions: List[Coin] = self.bundle.additions() + all_removals: List[Coin] = self.bundle.removals() + non_ephemeral_removals: List[Coin] = list(filter(lambda c: c not in all_additions, all_removals)) + + pending_dict: Dict[str, int] = {} + # First we add up the amounts of all coins that share an ancestor with the offered coins (i.e. a primary coin) + for asset_id, coins in self.get_offered_coins().items(): + name = "xch" if asset_id is None else asset_id.hex() + pending_dict[name] = 0 + for coin in coins: + root_removal: Coin = self.get_root_removal(coin) + + for addition in filter(lambda c: c.parent_coin_info == root_removal.name(), all_additions): + pending_dict[name] += addition.amount + + # Then we add a potential fee as pending XCH + fee: int = sum(c.amount for c in all_removals) - sum(c.amount for c in all_additions) + if fee > 0: + pending_dict.setdefault("xch", 0) + pending_dict["xch"] += fee + + # Then we gather anything else as unknown + sum_of_additions_so_far: int = sum(pending_dict.values()) + unknown: int = sum([c.amount for c in non_ephemeral_removals]) - sum_of_additions_so_far + if unknown > 0: + pending_dict["unknown"] = unknown + + return pending_dict + + # This method returns all of the coins that are being used in the offer (without which it would be invalid) + def get_involved_coins(self) -> List[Coin]: + additions = self.bundle.additions() + return list(filter(lambda c: c not in additions, self.bundle.removals())) + + # This returns the non-ephemeral removal that is an ancestor of the specified coin + # This should maybe move to the SpendBundle object at some point + def get_root_removal(self, coin: Coin) -> Coin: + all_removals: Set[Coin] = set(self.bundle.removals()) + all_removal_ids: Set[bytes32] = {c.name() for c in all_removals} + non_ephemeral_removals: Set[Coin] = { + c for c in all_removals if c.parent_coin_info not in {r.name() for r in all_removals} + } + if coin.name() not in all_removal_ids and coin.parent_coin_info not in all_removal_ids: + raise ValueError("The specified coin is not a coin in this bundle") + + while coin not in non_ephemeral_removals: + coin = next(c for c in all_removals if c.name() == coin.parent_coin_info) + + return coin + + # This will only return coins that are ancestors of settlement payments + def get_primary_coins(self) -> List[Coin]: + primary_coins: Set[Coin] = set() + for _, coins in self.get_offered_coins().items(): + for coin in coins: + primary_coins.add(self.get_root_removal(coin)) + return list(primary_coins) + + @classmethod + def aggregate(cls, offers: List["Offer"]) -> "Offer": + total_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {} + total_bundle = SpendBundle([], G2Element()) + for offer in offers: + # First check for any overlap in inputs + total_inputs: Set[Coin] = {cs.coin for cs in total_bundle.coin_spends} + offer_inputs: Set[Coin] = {cs.coin for cs in offer.bundle.coin_spends} + if total_inputs & offer_inputs: + raise ValueError("The aggregated offers overlap inputs") + + # Next, do the aggregation + for tail, payments in offer.requested_payments.items(): + if tail in total_requested_payments: + total_requested_payments[tail].extend(payments) + else: + total_requested_payments[tail] = payments + + total_bundle = SpendBundle.aggregate([total_bundle, offer.bundle]) + + return cls(total_requested_payments, total_bundle) + + # Validity is defined by having enough funds within the offer to satisfy both sides + def is_valid(self) -> bool: + return all([value >= 0 for value in self.arbitrage().values()]) + + # A "valid" spend means that this bundle can be pushed to the network and will succeed + # This differs from the `to_spend_bundle` method which deliberately creates an invalid SpendBundle + def to_valid_spend(self, arbitrage_ph: Optional[bytes32] = None) -> SpendBundle: + if not self.is_valid(): + raise ValueError("Offer is currently incomplete") + + completion_spends: List[CoinSpend] = [] + for tail_hash, payments in self.requested_payments.items(): + offered_coins: List[Coin] = self.get_offered_coins()[tail_hash] + + # Because of CAT supply laws, we must specify a place for the leftovers to go + arbitrage_amount: int = self.arbitrage()[tail_hash] + all_payments: List[NotarizedPayment] = payments.copy() + if arbitrage_amount > 0: + assert arbitrage_amount is not None + assert arbitrage_ph is not None + all_payments.append(NotarizedPayment(arbitrage_ph, uint64(arbitrage_amount), [])) + + for coin in offered_coins: + inner_solutions = [] + if coin == offered_coins[0]: + nonces: List[bytes32] = [p.nonce for p in all_payments] + for nonce in list(dict.fromkeys(nonces)): # dedup without messing with order + nonce_payments: List[NotarizedPayment] = list(filter(lambda p: p.nonce == nonce, all_payments)) + inner_solutions.append((nonce, [np.as_condition_args() for np in nonce_payments])) + + if tail_hash: + # CATs have a special way to be solved so we have to do some calculation before getting the solution + parent_spend: CoinSpend = list( + filter(lambda cs: cs.coin.name() == coin.parent_coin_info, self.bundle.coin_spends) + )[0] + parent_coin: Coin = parent_spend.coin + matched, curried_args = match_cat_puzzle(parent_spend.puzzle_reveal.to_program()) + assert matched + _, _, inner_puzzle = curried_args + spendable_cat = SpendableCAT( + coin, + tail_hash, + OFFER_MOD, + Program.to(inner_solutions), + lineage_proof=LineageProof( + parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount + ), + ) + solution: Program = ( + unsigned_spend_bundle_for_spendable_cats(CAT_MOD, [spendable_cat]) + .coin_spends[0] + .solution.to_program() + ) + else: + solution = Program.to(inner_solutions) + + completion_spends.append( + CoinSpend( + coin, + construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD, + solution, + ) + ) + + return SpendBundle.aggregate([SpendBundle(completion_spends, G2Element()), self.bundle]) + + def to_spend_bundle(self) -> SpendBundle: + # Before we serialze this as a SpendBundle, we need to serialze the `requested_payments` as dummy CoinSpends + additional_coin_spends: List[CoinSpend] = [] + for tail_hash, payments in self.requested_payments.items(): + puzzle_reveal: Program = construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD + inner_solutions = [] + nonces: List[bytes32] = [p.nonce for p in payments] + for nonce in list(dict.fromkeys(nonces)): # dedup without messing with order + nonce_payments: List[NotarizedPayment] = list(filter(lambda p: p.nonce == nonce, payments)) + inner_solutions.append((nonce, [np.as_condition_args() for np in nonce_payments])) + + additional_coin_spends.append( + CoinSpend( + Coin( + ZERO_32, + puzzle_reveal.get_tree_hash(), + uint64(0), + ), + puzzle_reveal, + Program.to(inner_solutions), + ) + ) + + return SpendBundle.aggregate( + [ + SpendBundle(additional_coin_spends, G2Element()), + self.bundle, + ] + ) + + @classmethod + def from_spend_bundle(cls, bundle: SpendBundle) -> "Offer": + # Because of the `to_spend_bundle` method, we need to parse the dummy CoinSpends as `requested_payments` + requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {} + leftover_coin_spends: List[CoinSpend] = [] + for coin_spend in bundle.coin_spends: + if coin_spend.coin.parent_coin_info == ZERO_32: + matched, curried_args = match_cat_puzzle(coin_spend.puzzle_reveal.to_program()) + if matched: + _, tail_hash_program, _ = curried_args + tail_hash: Optional[bytes32] = bytes32(tail_hash_program.as_python()) + else: + tail_hash = None + + notarized_payments: List[NotarizedPayment] = [] + for payment_group in coin_spend.solution.to_program().as_iter(): + nonce = bytes32(payment_group.first().as_python()) + payment_args_list: List[Program] = payment_group.rest().as_iter() + notarized_payments.extend( + [NotarizedPayment.from_condition_and_nonce(condition, nonce) for condition in payment_args_list] + ) + requested_payments[tail_hash] = notarized_payments + + else: + leftover_coin_spends.append(coin_spend) + + return cls(requested_payments, SpendBundle(leftover_coin_spends, bundle.aggregated_signature)) + + def name(self) -> bytes32: + return self.to_spend_bundle().name() + + # Methods to make this a valid Streamable member + # We basically hijack the SpendBundle versions for most of it + @classmethod + def parse(cls, f) -> "Offer": + parsed_bundle = SpendBundle.parse(f) + return cls.from_bytes(bytes(parsed_bundle)) + + def stream(self, f): + as_spend_bundle = SpendBundle.from_bytes(bytes(self)) + as_spend_bundle.stream(f) + + def __bytes__(self) -> bytes: + return bytes(self.to_spend_bundle()) + + @classmethod + def from_bytes(cls, as_bytes: bytes) -> "Offer": + # Because of the __bytes__ method, we need to parse the dummy CoinSpends as `requested_payments` + bundle = SpendBundle.from_bytes(as_bytes) + return cls.from_spend_bundle(bundle) diff --git a/chia/wallet/trading/trade_status.py b/chia/wallet/trading/trade_status.py index 579ed39e3894..6d368bb3b3c1 100644 --- a/chia/wallet/trading/trade_status.py +++ b/chia/wallet/trading/trade_status.py @@ -5,6 +5,6 @@ class TradeStatus(Enum): PENDING_ACCEPT = 0 PENDING_CONFIRM = 1 PENDING_CANCEL = 2 - CANCELED = 3 + CANCELLED = 3 CONFIRMED = 4 FAILED = 5 diff --git a/chia/wallet/trading/trade_store.py b/chia/wallet/trading/trade_store.py index 71f08d3d5aa1..d055a43d4aa6 100644 --- a/chia/wallet/trading/trade_store.py +++ b/chia/wallet/trading/trade_store.py @@ -1,4 +1,5 @@ from typing import List, Optional +from operator import attrgetter import aiosqlite @@ -91,12 +92,11 @@ async def set_status(self, trade_id: bytes32, status: TradeStatus, in_transactio confirmed_at_index=confirmed_at_index, accepted_at_time=current.accepted_at_time, created_at_time=current.created_at_time, - my_offer=current.my_offer, + is_my_offer=current.is_my_offer, sent=current.sent, - spend_bundle=current.spend_bundle, - tx_spend_bundle=current.tx_spend_bundle, - additions=current.additions, - removals=current.removals, + offer=current.offer, + taken_offer=current.taken_offer, + coins_of_interest=current.coins_of_interest, trade_id=current.trade_id, status=uint32(status.value), sent_to=current.sent_to, @@ -133,12 +133,11 @@ async def increment_sent( confirmed_at_index=current.confirmed_at_index, accepted_at_time=current.accepted_at_time, created_at_time=current.created_at_time, - my_offer=current.my_offer, + is_my_offer=current.is_my_offer, sent=uint32(current.sent + 1), - spend_bundle=current.spend_bundle, - tx_spend_bundle=current.tx_spend_bundle, - additions=current.additions, - removals=current.removals, + offer=current.offer, + taken_offer=current.taken_offer, + coins_of_interest=current.coins_of_interest, trade_id=current.trade_id, status=current.status, sent_to=sent_to, @@ -160,12 +159,11 @@ async def set_not_sent(self, id: bytes32): confirmed_at_index=current.confirmed_at_index, accepted_at_time=current.accepted_at_time, created_at_time=current.created_at_time, - my_offer=current.my_offer, + is_my_offer=current.is_my_offer, sent=uint32(0), - spend_bundle=current.spend_bundle, - tx_spend_bundle=current.tx_spend_bundle, - additions=current.additions, - removals=current.removals, + offer=current.offer, + taken_offer=current.taken_offer, + coins_of_interest=current.coins_of_interest, trade_id=current.trade_id, status=uint32(TradeStatus.PENDING_CONFIRM.value), sent_to=[], @@ -252,6 +250,40 @@ async def get_all_trades(self) -> List[TradeRecord]: return records + async def get_trades_between( + self, start: int, end: int, sort_key: Optional[str] = None, reverse: bool = False + ) -> List[TradeRecord]: + """ + Return a list of trades sorted by a key and between a start and end index. + """ + records = await self.get_all_trades() + + # Sort + records = sorted(records, key=attrgetter("trade_id")) # For determinism + if sort_key is None or sort_key == "CONFIRMED_AT_HEIGHT": + records = sorted(records, key=attrgetter("confirmed_at_index"), reverse=(not reverse)) + elif sort_key == "RELEVANCE": + sorted_records = sorted(records, key=attrgetter("created_at_time"), reverse=(not reverse)) + sorted_records = sorted(sorted_records, key=attrgetter("confirmed_at_index"), reverse=(not reverse)) + # custom sort of the statuses here + records = [] + statuses = ["PENDING", "CONFIRMED", "CANCELLED", "FAILED"] + if reverse: + statuses.reverse() + statuses.append("") # This is a catch all for any statuses we have not explicitly designated + for status in statuses: + for record in sorted_records: + if status in TradeStatus(record.status).name and record not in records: + records.append(record) + else: + raise ValueError(f"No known sort {sort_key}") + + # Paginate + if start > len(records) - 1: + return [] + else: + return records[max(start, 0) : min(end, len(records))] + async def get_trades_above(self, height: uint32) -> List[TradeRecord]: cursor = await self.db_connection.execute("SELECT * from trade_records WHERE confirmed_at_index>?", (height,)) rows = await cursor.fetchall() diff --git a/chia/wallet/transaction_record.py b/chia/wallet/transaction_record.py index 6209ab7c2ba4..f43907f1ba25 100644 --- a/chia/wallet/transaction_record.py +++ b/chia/wallet/transaction_record.py @@ -1,11 +1,12 @@ from dataclasses import dataclass -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Dict from chia.consensus.coinbase import pool_parent_id, farmer_parent_id from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.spend_bundle import SpendBundle +from chia.util.bech32m import encode_puzzle_hash, decode_puzzle_hash from chia.util.ints import uint8, uint32, uint64 from chia.util.streamable import Streamable, streamable from chia.wallet.util.transaction_type import TransactionType @@ -36,6 +37,7 @@ class TransactionRecord(Streamable): trade_id: Optional[bytes32] type: uint32 # TransactionType name: bytes32 + memos: List[Tuple[bytes32, List[bytes]]] def is_in_mempool(self) -> bool: # If one of the nodes we sent it to responded with success, we set it to success @@ -59,3 +61,39 @@ def height_farmed(self, genesis_challenge: bytes32) -> Optional[uint32]: if farmer_parent == self.additions[0].parent_coin_info: return uint32(block_index) return None + + def get_memos(self) -> Dict[bytes32, List[bytes]]: + return {coin_id: ms for coin_id, ms in self.memos} + + @classmethod + def from_json_dict_convenience(cls, modified_tx_input: Dict): + modified_tx = modified_tx_input.copy() + if "to_address" in modified_tx: + modified_tx["to_puzzle_hash"] = decode_puzzle_hash(modified_tx["to_address"]).hex() + if "to_address" in modified_tx: + del modified_tx["to_address"] + # Converts memos from a flat dict into a nested list + memos_dict: Dict[str, List[str]] = {} + memos_list: List = [] + if "memos" in modified_tx: + for coin_id, memo in modified_tx["memos"].items(): + if coin_id not in memos_dict: + memos_dict[coin_id] = [] + memos_dict[coin_id].append(memo) + for coin_id, memos in memos_dict.items(): + memos_list.append((coin_id, memos)) + modified_tx["memos"] = memos_list + return cls.from_json_dict(modified_tx) + + def to_json_dict_convenience(self, config: Dict) -> Dict: + selected = config["selected_network"] + prefix = config["network_overrides"]["config"][selected]["address_prefix"] + formatted = self.to_json_dict() + formatted["to_address"] = encode_puzzle_hash(self.to_puzzle_hash, prefix) + formatted["memos"] = { + coin_id.hex(): memo.hex() + for coin_id, memos in self.get_memos().items() + for memo in memos + if memo is not None + } + return formatted diff --git a/chia/wallet/util/debug_spend_bundle.py b/chia/wallet/util/debug_spend_bundle.py index c22f5cd04882..ea925443358c 100644 --- a/chia/wallet/util/debug_spend_bundle.py +++ b/chia/wallet/util/debug_spend_bundle.py @@ -1,4 +1,4 @@ -from typing import Iterable, List, Tuple +from typing import List from blspy import AugSchemeMPL, G1Element from clvm import KEYWORD_FROM_ATOM @@ -6,7 +6,6 @@ from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program, INFINITE_COST -from chia.types.blockchain_format.sized_bytes import bytes32 from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.condition_opcodes import ConditionOpcode from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict @@ -76,8 +75,8 @@ def debug_spend_bundle(spend_bundle, agg_sig_additional_data=DEFAULT_CONSTANTS.A if error: print(f"*** error {error}") elif conditions is not None: - for pk, m in pkm_pairs_for_conditions_dict(conditions, coin_name, agg_sig_additional_data): - pks.append(G1Element.from_bytes(pk)) + for pk_bytes, m in pkm_pairs_for_conditions_dict(conditions, coin_name, agg_sig_additional_data): + pks.append(G1Element.from_bytes(pk_bytes)) msgs.append(m) print() cost, r = puzzle_reveal.run_with_cost(INFINITE_COST, solution) # type: ignore @@ -189,10 +188,3 @@ def debug_spend_bundle(spend_bundle, agg_sig_additional_data=DEFAULT_CONSTANTS.A print(f" coin_ids: {[msg.hex()[-128:-64] for msg in msgs]}") print(f" add_data: {[msg.hex()[-64:] for msg in msgs]}") print(f"signature: {spend_bundle.aggregated_signature}") - - -def solution_for_pay_to_any(puzzle_hash_amount_pairs: Iterable[Tuple[bytes32, int]]) -> Program: - output_conditions = [ - [ConditionOpcode.CREATE_COIN, puzzle_hash, amount] for puzzle_hash, amount in puzzle_hash_amount_pairs - ] - return Program.to(output_conditions) diff --git a/chia/wallet/util/trade_utils.py b/chia/wallet/util/trade_utils.py deleted file mode 100644 index 3fd50726fe60..000000000000 --- a/chia/wallet/util/trade_utils.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Any, Dict, Optional, Tuple - -from chia.types.blockchain_format.program import Program, INFINITE_COST -from chia.types.condition_opcodes import ConditionOpcode -from chia.types.spend_bundle import SpendBundle -from chia.util.condition_tools import conditions_dict_for_solution -from chia.wallet.cc_wallet import cc_utils -from chia.wallet.trade_record import TradeRecord -from chia.wallet.trading.trade_status import TradeStatus - - -def trade_status_ui_string(status: TradeStatus): - if status is TradeStatus.PENDING_CONFIRM: - return "Pending Confirmation" - elif status is TradeStatus.CANCELED: - return "Canceled" - elif status is TradeStatus.CONFIRMED: - return "Confirmed" - elif status is TradeStatus.PENDING_CANCEL: - return "Pending Cancellation" - elif status is TradeStatus.FAILED: - return "Failed" - elif status is TradeStatus.PENDING_ACCEPT: - return "Pending" - - -def trade_record_to_dict(record: TradeRecord) -> Dict[str, Any]: - """Convenience function to return only part of trade record we care about and show correct status to the ui""" - result: Dict[str, Any] = {} - result["trade_id"] = record.trade_id.hex() - result["sent"] = record.sent - result["my_offer"] = record.my_offer - result["created_at_time"] = record.created_at_time - result["accepted_at_time"] = record.accepted_at_time - result["confirmed_at_index"] = record.confirmed_at_index - result["status"] = trade_status_ui_string(TradeStatus(record.status)) - success, offer_dict, error = get_discrepancies_for_spend_bundle(record.spend_bundle) - if success is False or offer_dict is None: - raise ValueError(error) - result["offer_dict"] = offer_dict - return result - - -# Returns the relative difference in value between the amount outputted by a puzzle and solution and a coin's amount -def get_output_discrepancy_for_puzzle_and_solution(coin, puzzle, solution): - discrepancy = coin.amount - get_output_amount_for_puzzle_and_solution(puzzle, solution) - return discrepancy - - # Returns the amount of value outputted by a puzzle and solution - - -def get_output_amount_for_puzzle_and_solution(puzzle: Program, solution: Program) -> int: - error, conditions, cost = conditions_dict_for_solution(puzzle, solution, INFINITE_COST) - total = 0 - if conditions: - for _ in conditions.get(ConditionOpcode.CREATE_COIN, []): - total += Program.to(_.vars[1]).as_int() - return total - - -def get_discrepancies_for_spend_bundle( - trade_offer: SpendBundle, -) -> Tuple[bool, Optional[Dict], Optional[Exception]]: - try: - cc_discrepancies: Dict[str, int] = dict() - for coinsol in trade_offer.coin_spends: - puzzle: Program = Program.from_bytes(bytes(coinsol.puzzle_reveal)) - solution: Program = Program.from_bytes(bytes(coinsol.solution)) - # work out the deficits between coin amount and expected output for each - r = cc_utils.uncurry_cc(puzzle) - if r: - # Calculate output amounts - mod_hash, genesis_checker, inner_puzzle = r - innersol = solution.first() - - total = get_output_amount_for_puzzle_and_solution(inner_puzzle, innersol) - colour = bytes(genesis_checker).hex() - if colour in cc_discrepancies: - cc_discrepancies[colour] += coinsol.coin.amount - total - else: - cc_discrepancies[colour] = coinsol.coin.amount - total - else: - coin_amount = coinsol.coin.amount - out_amount = get_output_amount_for_puzzle_and_solution(puzzle, solution) - diff = coin_amount - out_amount - if "chia" in cc_discrepancies: - cc_discrepancies["chia"] = cc_discrepancies["chia"] + diff - else: - cc_discrepancies["chia"] = diff - - return True, cc_discrepancies, None - except Exception as e: - return False, None, e diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py new file mode 100644 index 000000000000..8a8e5090ce1e --- /dev/null +++ b/chia/wallet/util/wallet_sync_utils.py @@ -0,0 +1,208 @@ +from typing import List, Optional, Tuple, Union, Dict + +from chia.consensus.constants import ConsensusConstants +from chia.protocols.wallet_protocol import ( + RequestAdditions, + RespondAdditions, + RejectAdditionsRequest, + RejectRemovalsRequest, + RespondRemovals, + RequestRemovals, +) +from chia.types.blockchain_format.coin import hash_coin_list, Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.full_block import FullBlock +from chia.util.merkle_set import confirm_not_included_already_hashed, confirm_included_already_hashed, MerkleSet + + +def validate_additions( + coins: List[Tuple[bytes32, List[Coin]]], + proofs: Optional[List[Tuple[bytes32, bytes, Optional[bytes]]]], + root, +): + if proofs is None: + # Verify root + additions_merkle_set = MerkleSet() + + # Addition Merkle set contains puzzlehash and hash of all coins with that puzzlehash + for puzzle_hash, coins_l in coins: + additions_merkle_set.add_already_hashed(puzzle_hash) + additions_merkle_set.add_already_hashed(hash_coin_list(coins_l)) + + additions_root = additions_merkle_set.get_root() + if root != additions_root: + return False + else: + for i in range(len(coins)): + assert coins[i][0] == proofs[i][0] + coin_list_1: List[Coin] = coins[i][1] + puzzle_hash_proof: Optional[bytes] = proofs[i][1] + coin_list_proof: Optional[bytes] = proofs[i][2] + if len(coin_list_1) == 0: + # Verify exclusion proof for puzzle hash + assert puzzle_hash_proof is not None + not_included = confirm_not_included_already_hashed( + root, + coins[i][0], + puzzle_hash_proof, + ) + if not_included is False: + return False + else: + try: + # Verify inclusion proof for coin list + assert coin_list_proof is not None + included = confirm_included_already_hashed( + root, + hash_coin_list(coin_list_1), + coin_list_proof, + ) + if included is False: + return False + except AssertionError: + return False + try: + # Verify inclusion proof for puzzle hash + assert puzzle_hash_proof is not None + included = confirm_included_already_hashed( + root, + coins[i][0], + puzzle_hash_proof, + ) + if included is False: + return False + except AssertionError: + return False + + return True + + +def validate_removals(coins, proofs, root): + if proofs is None: + # If there are no proofs, it means all removals were returned in the response. + # we must find the ones relevant to our wallets. + + # Verify removals root + removals_merkle_set = MerkleSet() + for name_coin in coins: + name, coin = name_coin + if coin is not None: + removals_merkle_set.add_already_hashed(coin.name()) + removals_root = removals_merkle_set.get_root() + if root != removals_root: + return False + else: + # This means the full node has responded only with the relevant removals + # for our wallet. Each merkle proof must be verified. + if len(coins) != len(proofs): + return False + for i in range(len(coins)): + # Coins are in the same order as proofs + if coins[i][0] != proofs[i][0]: + return False + coin = coins[i][1] + if coin is None: + # Verifies merkle proof of exclusion + not_included = confirm_not_included_already_hashed( + root, + coins[i][0], + proofs[i][1], + ) + if not_included is False: + return False + else: + # Verifies merkle proof of inclusion of coin name + if coins[i][0] != coin.name(): + return False + included = confirm_included_already_hashed( + root, + coin.name(), + proofs[i][1], + ) + if included is False: + return False + return True + + +async def request_and_validate_removals(peer, height, header_hash, coin_name, removals_root) -> bool: + removals_request = RequestRemovals(height, header_hash, [coin_name]) + + removals_res: Optional[Union[RespondRemovals, RejectRemovalsRequest]] = await peer.request_removals( + removals_request + ) + if removals_res is None or isinstance(removals_res, RejectRemovalsRequest): + return False + return validate_removals(removals_res.coins, removals_res.proofs, removals_root) + + +async def request_and_validate_additions(peer, height, header_hash, puzzle_hash, additions_root): + additions_request = RequestAdditions(height, header_hash, [puzzle_hash]) + additions_res: Optional[Union[RespondAdditions, RejectAdditionsRequest]] = await peer.request_additions( + additions_request + ) + if additions_res is None or isinstance(additions_res, RejectAdditionsRequest): + return False + + validated = validate_additions( + additions_res.coins, + additions_res.proofs, + additions_root, + ) + return validated + + +def get_block_challenge( + constants: ConsensusConstants, + header_block: FullBlock, + all_blocks: Dict[bytes32, FullBlock], + genesis_block: bool, + overflow: bool, + skip_overflow_last_ss_validation: bool, +) -> Optional[bytes32]: + if len(header_block.finished_sub_slots) > 0: + if overflow: + # New sub-slot with overflow block + if skip_overflow_last_ss_validation: + # In this case, we are missing the final sub-slot bundle (it's not finished yet), however + # There is a whole empty slot before this block is infused + challenge: bytes32 = header_block.finished_sub_slots[-1].challenge_chain.get_hash() + else: + challenge = header_block.finished_sub_slots[ + -1 + ].challenge_chain.challenge_chain_end_of_slot_vdf.challenge + else: + # No overflow, new slot with a new challenge + challenge = header_block.finished_sub_slots[-1].challenge_chain.get_hash() + else: + if genesis_block: + challenge = constants.GENESIS_CHALLENGE + else: + if overflow: + if skip_overflow_last_ss_validation: + # Overflow infusion without the new slot, so get the last challenge + challenges_to_look_for = 1 + else: + # Overflow infusion, so get the second to last challenge. skip_overflow_last_ss_validation is False, + # Which means no sub slots are omitted + challenges_to_look_for = 2 + else: + challenges_to_look_for = 1 + reversed_challenge_hashes: List[bytes32] = [] + if header_block.height == 0: + return constants.GENESIS_CHALLENGE + if header_block.prev_header_hash not in all_blocks: + return None + curr: Optional[FullBlock] = all_blocks[header_block.prev_header_hash] + while len(reversed_challenge_hashes) < challenges_to_look_for: + if curr is None: + return None + if len(curr.finished_sub_slots) > 0: + reversed_challenge_hashes += reversed( + [slot.challenge_chain.get_hash() for slot in curr.finished_sub_slots] + ) + if curr.height == 0: + return constants.GENESIS_CHALLENGE + + curr = all_blocks.get(curr.prev_header_hash, None) + challenge = reversed_challenge_hashes[challenges_to_look_for - 1] + return challenge diff --git a/chia/wallet/util/wallet_types.py b/chia/wallet/util/wallet_types.py index 76df74b016ca..f5330c674ec7 100644 --- a/chia/wallet/util/wallet_types.py +++ b/chia/wallet/util/wallet_types.py @@ -1,4 +1,5 @@ from enum import IntEnum +from typing import List from typing_extensions import TypedDict @@ -14,7 +15,7 @@ class WalletType(IntEnum): AUTHORIZED_PAYEE = 3 MULTI_SIG = 4 CUSTODY = 5 - COLOURED_COIN = 6 + CAT = 6 RECOVERABLE = 7 DISTRIBUTED_ID = 8 POOLING_WALLET = 9 @@ -23,3 +24,4 @@ class WalletType(IntEnum): class AmountWithPuzzlehash(TypedDict): amount: uint64 puzzlehash: bytes32 + memos: List[bytes] diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 3ecbf435f84c..063edf1d984c 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -37,7 +37,7 @@ from chia.wallet.sign_coin_spends import sign_coin_spends from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType -from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType +from chia.wallet.util.wallet_types import WalletType, AmountWithPuzzlehash from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo @@ -127,7 +127,7 @@ async def get_pending_change_balance(self) -> uint64: for record in unconfirmed_tx: if not record.is_in_mempool(): - self.log.warning(f"Record: {record} not in mempool") + self.log.warning(f"Record: {record} not in mempool, {record.sent_to}") continue our_spend = False for coin in record.removals: @@ -147,6 +147,9 @@ async def get_pending_change_balance(self) -> uint64: def puzzle_for_pk(self, pubkey: bytes) -> Program: return puzzle_for_pk(pubkey) + async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: + return puzzle_hash # Looks unimpressive, but it's more complicated in other wallets + async def hack_populate_secret_key_for_puzzle_hash(self, puzzle_hash: bytes32) -> G1Element: maybe = await self.wallet_state_manager.get_keys(puzzle_hash) if maybe is None: @@ -195,10 +198,10 @@ async def get_new_puzzlehash(self, in_transaction: bool = False) -> bytes32: def make_solution( self, - primaries: Optional[List[AmountWithPuzzlehash]] = None, + primaries: List[AmountWithPuzzlehash], min_time=0, me=None, - coin_announcements: Optional[Set[bytes32]] = None, + coin_announcements: Optional[Set[bytes]] = None, coin_announcements_to_assert: Optional[Set[bytes32]] = None, puzzle_announcements: Optional[Set[bytes32]] = None, puzzle_announcements_to_assert: Optional[Set[bytes32]] = None, @@ -206,9 +209,15 @@ def make_solution( ) -> Program: assert fee >= 0 condition_list = [] - if primaries: + if len(primaries) > 0: for primary in primaries: - condition_list.append(make_create_coin_condition(primary["puzzlehash"], primary["amount"])) + if "memos" in primary: + memos: Optional[List[bytes]] = primary["memos"] + if memos is not None and len(memos) == 0: + memos = None + else: + memos = None + condition_list.append(make_create_coin_condition(primary["puzzlehash"], primary["amount"], memos)) if min_time > 0: condition_list.append(make_assert_absolute_seconds_exceeds_condition(min_time)) if me: @@ -229,6 +238,11 @@ def make_solution( condition_list.append(make_assert_puzzle_announcement(announcement_hash)) return solution_for_conditions(condition_list) + def add_condition_to_solution(self, condition: Program, solution: Program) -> Program: + python_program = solution.as_python() + python_program[1].append(condition) + return Program.to(python_program) + async def select_coins(self, amount, exclude: List[Coin] = None) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. @@ -292,15 +306,17 @@ async def _generate_unsigned_transaction( coins: Set[Coin] = None, primaries_input: Optional[List[AmountWithPuzzlehash]] = None, ignore_max_send_amount: bool = False, - announcements_to_consume: Set[Announcement] = None, + coin_announcements_to_consume: Set[Announcement] = None, + puzzle_announcements_to_consume: Set[Announcement] = None, + memos: Optional[List[bytes]] = None, + negative_change_allowed: bool = False, ) -> List[CoinSpend]: """ Generates a unsigned transaction in form of List(Puzzle, Solutions) Note: this must be called under a wallet state manager lock """ - primaries: Optional[List[AmountWithPuzzlehash]] if primaries_input is None: - primaries = None + primaries: Optional[List[AmountWithPuzzlehash]] = None total_amount = amount + fee else: primaries = primaries_input.copy() @@ -317,12 +333,24 @@ async def _generate_unsigned_transaction( if coins is None: coins = await self.select_coins(total_amount) assert len(coins) > 0 - self.log.info(f"coins is not None {coins}") spend_value = sum([coin.amount for coin in coins]) + change = spend_value - total_amount + if negative_change_allowed: + change = max(0, change) + assert change >= 0 + if coin_announcements_to_consume is not None: + coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume} + else: + coin_announcements_bytes = None + if puzzle_announcements_to_consume is not None: + puzzle_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in puzzle_announcements_to_consume} + else: + puzzle_announcements_bytes = None + spends: List[CoinSpend] = [] primary_announcement_hash: Optional[bytes32] = None @@ -331,39 +359,39 @@ async def _generate_unsigned_transaction( all_primaries_list = [(p["puzzlehash"], p["amount"]) for p in primaries] + [(newpuzzlehash, amount)] if len(set(all_primaries_list)) != len(all_primaries_list): raise ValueError("Cannot create two identical coins") - + if memos is None: + memos = [] + assert memos is not None for coin in coins: - self.log.info(f"coin from coins {coin}") + self.log.info(f"coin from coins: {coin.name()} {coin}") puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) - # Only one coin creates outputs if primary_announcement_hash is None and origin_id in (None, coin.name()): if primaries is None: - primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}] + if amount > 0: + primaries = [{"puzzlehash": newpuzzlehash, "amount": uint64(amount), "memos": memos}] + else: + primaries = [] else: - primaries.append({"puzzlehash": newpuzzlehash, "amount": amount}) + primaries.append({"puzzlehash": newpuzzlehash, "amount": uint64(amount), "memos": memos}) if change > 0: change_puzzle_hash: bytes32 = await self.get_new_puzzlehash() - primaries.append({"puzzlehash": change_puzzle_hash, "amount": uint64(change)}) + primaries.append({"puzzlehash": change_puzzle_hash, "amount": uint64(change), "memos": []}) message_list: List[bytes32] = [c.name() for c in coins] for primary in primaries: message_list.append(Coin(coin.name(), primary["puzzlehash"], primary["amount"]).name()) message: bytes32 = std_hash(b"".join(message_list)) - # TODO: address hint error and remove ignore - # error: Argument "coin_announcements_to_assert" to "make_solution" of "Wallet" has incompatible - # type "Optional[Set[Announcement]]"; expected "Optional[Set[bytes32]]" [arg-type] solution: Program = self.make_solution( primaries=primaries, fee=fee, coin_announcements={message}, - coin_announcements_to_assert=announcements_to_consume, # type: ignore[arg-type] + coin_announcements_to_assert=coin_announcements_bytes, + puzzle_announcements_to_assert=puzzle_announcements_bytes, ) primary_announcement_hash = Announcement(coin.name(), message).name() else: - # TODO: address hint error and remove ignore - # error: Argument 1 to has incompatible type "Optional[bytes32]"; expected "bytes32" - # [arg-type] - solution = self.make_solution(coin_announcements_to_assert={primary_announcement_hash}) # type: ignore[arg-type] # noqa: E501 + assert primary_announcement_hash is not None + solution = self.make_solution(coin_announcements_to_assert={primary_announcement_hash}, primaries=[]) spends.append( CoinSpend( @@ -391,22 +419,33 @@ async def generate_signed_transaction( coins: Set[Coin] = None, primaries: Optional[List[AmountWithPuzzlehash]] = None, ignore_max_send_amount: bool = False, - announcements_to_consume: Set[bytes32] = None, + coin_announcements_to_consume: Set[Announcement] = None, + puzzle_announcements_to_consume: Set[Announcement] = None, + memos: Optional[List[bytes]] = None, + negative_change_allowed: bool = False, ) -> TransactionRecord: """ Use this to generate transaction. Note: this must be called under a wallet state manager lock + The first output is (amount, puzzle_hash, memos), and the rest of the outputs are in primaries. """ if primaries is None: non_change_amount = amount else: non_change_amount = uint64(amount + sum(p["amount"] for p in primaries)) - # TODO: address hint error and remove ignore - # error: Argument 8 to "_generate_unsigned_transaction" of "Wallet" has incompatible type - # "Optional[Set[bytes32]]"; expected "Optional[Set[Announcement]]" [arg-type] transaction = await self._generate_unsigned_transaction( - amount, puzzle_hash, fee, origin_id, coins, primaries, ignore_max_send_amount, announcements_to_consume # type: ignore[arg-type] # noqa: E501 + amount, + puzzle_hash, + fee, + origin_id, + coins, + primaries, + ignore_max_send_amount, + coin_announcements_to_consume, + puzzle_announcements_to_consume, + memos, + negative_change_allowed, ) assert len(transaction) > 0 @@ -422,7 +461,13 @@ async def generate_signed_transaction( now = uint64(int(time.time())) add_list: List[Coin] = list(spend_bundle.additions()) rem_list: List[Coin] = list(spend_bundle.removals()) - assert sum(a.amount for a in add_list) + fee == sum(r.amount for r in rem_list) + + output_amount = sum(a.amount for a in add_list) + fee + input_amount = sum(r.amount for r in rem_list) + if negative_change_allowed: + assert output_amount >= input_amount + else: + assert output_amount == input_amount return TransactionRecord( confirmed_at_height=uint32(0), @@ -440,11 +485,13 @@ async def generate_signed_transaction( trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), + memos=list(spend_bundle.get_memos().items()), ) async def push_transaction(self, tx: TransactionRecord) -> None: """Use this API to send transactions.""" await self.wallet_state_manager.add_pending_transaction(tx) + await self.wallet_state_manager.wallet_node.update_ui() # This is to be aggregated together with a coloured coin offer to ensure that the trade happens async def create_spend_bundle_relative_chia(self, chia_amount: int, exclude: List[Coin]) -> SpendBundle: @@ -470,7 +517,9 @@ async def create_spend_bundle_relative_chia(self, chia_amount: int, exclude: Lis puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) if output_created is None: newpuzhash = await self.get_new_puzzlehash() - primaries: List[AmountWithPuzzlehash] = [{"puzzlehash": newpuzhash, "amount": uint64(chia_amount)}] + primaries: List[AmountWithPuzzlehash] = [ + {"puzzlehash": newpuzhash, "amount": uint64(chia_amount), "memos": []} + ] solution = self.make_solution(primaries=primaries) output_created = coin list_of_solutions.append(CoinSpend(coin, puzzle, solution)) diff --git a/chia/wallet/wallet_action.py b/chia/wallet/wallet_action.py index 162c9c9c752e..3c94e4701277 100644 --- a/chia/wallet/wallet_action.py +++ b/chia/wallet/wallet_action.py @@ -12,7 +12,7 @@ class WalletAction: Purpose: Some wallets require wallet node to perform a certain action when event happens. - For Example, coloured coin wallet needs to fetch solutions once it receives a coin. + For Example, CAT wallet needs to fetch solutions once it receives a coin. In order to be safe from losing connection, closing the app, etc, those actions need to be persisted. id: auto-incremented for every added action diff --git a/chia/wallet/wallet_block_store.py b/chia/wallet/wallet_block_store.py deleted file mode 100644 index 8ea79e1abe49..000000000000 --- a/chia/wallet/wallet_block_store.py +++ /dev/null @@ -1,321 +0,0 @@ -from dataclasses import dataclass -from typing import Dict, List, Optional, Tuple - -import aiosqlite - -from chia.consensus.block_record import BlockRecord -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary -from chia.types.coin_spend import CoinSpend -from chia.types.header_block import HeaderBlock -from chia.util.db_wrapper import DBWrapper -from chia.util.ints import uint32, uint64 -from chia.util.lru_cache import LRUCache -from chia.util.streamable import Streamable, streamable -from chia.wallet.block_record import HeaderBlockRecord - - -@dataclass(frozen=True) -@streamable -class AdditionalCoinSpends(Streamable): - coin_spends_list: List[CoinSpend] - - -class WalletBlockStore: - """ - This object handles HeaderBlocks and Blocks stored in DB used by wallet. - """ - - db: aiosqlite.Connection - db_wrapper: DBWrapper - block_cache: LRUCache - - @classmethod - async def create(cls, db_wrapper: DBWrapper): - self = cls() - - self.db_wrapper = db_wrapper - self.db = db_wrapper.db - await self.db.execute( - "CREATE TABLE IF NOT EXISTS header_blocks(header_hash text PRIMARY KEY, height int," - " timestamp int, block blob)" - ) - - await self.db.execute("CREATE INDEX IF NOT EXISTS header_hash on header_blocks(header_hash)") - - await self.db.execute("CREATE INDEX IF NOT EXISTS timestamp on header_blocks(timestamp)") - - await self.db.execute("CREATE INDEX IF NOT EXISTS height on header_blocks(height)") - - # Block records - await self.db.execute( - "CREATE TABLE IF NOT EXISTS block_records(header_hash " - "text PRIMARY KEY, prev_hash text, height bigint, weight bigint, total_iters text," - "block blob, sub_epoch_summary blob, is_peak tinyint)" - ) - - await self.db.execute( - "CREATE TABLE IF NOT EXISTS additional_coin_spends(header_hash text PRIMARY KEY, spends_list_blob blob)" - ) - - # Height index so we can look up in order of height for sync purposes - await self.db.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)") - - await self.db.execute("CREATE INDEX IF NOT EXISTS hh on block_records(header_hash)") - await self.db.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak)") - await self.db.commit() - self.block_cache = LRUCache(1000) - return self - - async def _clear_database(self): - cursor_2 = await self.db.execute("DELETE FROM header_blocks") - await cursor_2.close() - await self.db.commit() - - async def add_block_record( - self, - header_block_record: HeaderBlockRecord, - block_record: BlockRecord, - additional_coin_spends: List[CoinSpend], - ): - """ - Adds a block record to the database. This block record is assumed to be connected - to the chain, but it may or may not be in the LCA path. - """ - cached = self.block_cache.get(header_block_record.header_hash) - if cached is not None: - # Since write to db can fail, we remove from cache here to avoid potential inconsistency - # Adding to cache only from reading - self.block_cache.put(header_block_record.header_hash, None) - - if header_block_record.header.foliage_transaction_block is not None: - timestamp = header_block_record.header.foliage_transaction_block.timestamp - else: - timestamp = uint64(0) - cursor = await self.db.execute( - "INSERT OR REPLACE INTO header_blocks VALUES(?, ?, ?, ?)", - ( - header_block_record.header_hash.hex(), - header_block_record.height, - timestamp, - bytes(header_block_record), - ), - ) - - await cursor.close() - cursor_2 = await self.db.execute( - "INSERT OR REPLACE INTO block_records VALUES(?, ?, ?, ?, ?, ?, ?,?)", - ( - header_block_record.header.header_hash.hex(), - header_block_record.header.prev_header_hash.hex(), - header_block_record.header.height, - header_block_record.header.weight.to_bytes(128 // 8, "big", signed=False).hex(), - header_block_record.header.total_iters.to_bytes(128 // 8, "big", signed=False).hex(), - bytes(block_record), - None - if block_record.sub_epoch_summary_included is None - else bytes(block_record.sub_epoch_summary_included), - False, - ), - ) - await cursor_2.close() - - if len(additional_coin_spends) > 0: - blob: bytes = bytes(AdditionalCoinSpends(additional_coin_spends)) - cursor_3 = await self.db.execute( - "INSERT OR REPLACE INTO additional_coin_spends VALUES(?, ?)", - (header_block_record.header_hash.hex(), blob), - ) - await cursor_3.close() - - async def get_header_block_at(self, heights: List[uint32]) -> List[HeaderBlock]: - if len(heights) == 0: - return [] - - heights_db = tuple(heights) - formatted_str = f'SELECT block from header_blocks WHERE height in ({"?," * (len(heights_db) - 1)}?)' - cursor = await self.db.execute(formatted_str, heights_db) - rows = await cursor.fetchall() - await cursor.close() - return [HeaderBlock.from_bytes(row[0]) for row in rows] - - async def get_header_block_record(self, header_hash: bytes32) -> Optional[HeaderBlockRecord]: - """Gets a block record from the database, if present""" - cached = self.block_cache.get(header_hash) - if cached is not None: - return cached - cursor = await self.db.execute("SELECT block from header_blocks WHERE header_hash=?", (header_hash.hex(),)) - row = await cursor.fetchone() - await cursor.close() - if row is not None: - hbr: HeaderBlockRecord = HeaderBlockRecord.from_bytes(row[0]) - self.block_cache.put(hbr.header_hash, hbr) - return hbr - else: - return None - - async def get_additional_coin_spends(self, header_hash: bytes32) -> Optional[List[CoinSpend]]: - cursor = await self.db.execute( - "SELECT spends_list_blob from additional_coin_spends WHERE header_hash=?", (header_hash.hex(),) - ) - row = await cursor.fetchone() - await cursor.close() - if row is not None: - coin_spends: AdditionalCoinSpends = AdditionalCoinSpends.from_bytes(row[0]) - return coin_spends.coin_spends_list - else: - return None - - async def get_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]: - cursor = await self.db.execute( - "SELECT block from block_records WHERE header_hash=?", - (header_hash.hex(),), - ) - row = await cursor.fetchone() - await cursor.close() - if row is not None: - return BlockRecord.from_bytes(row[0]) - return None - - async def get_block_records( - self, - ) -> Tuple[Dict[bytes32, BlockRecord], Optional[bytes32]]: - """ - Returns a dictionary with all blocks, as well as the header hash of the peak, - if present. - """ - cursor = await self.db.execute("SELECT header_hash, block, is_peak from block_records") - rows = await cursor.fetchall() - await cursor.close() - ret: Dict[bytes32, BlockRecord] = {} - peak: Optional[bytes32] = None - for row in rows: - header_hash_bytes, block_record_bytes, is_peak = row - header_hash = bytes32.fromhex(header_hash_bytes) - ret[header_hash] = BlockRecord.from_bytes(block_record_bytes) - if is_peak: - assert peak is None # Sanity check, only one peak - peak = header_hash - return ret, peak - - def rollback_cache_block(self, header_hash: bytes32): - self.block_cache.remove(header_hash) - - async def set_peak(self, header_hash: bytes32) -> None: - cursor_1 = await self.db.execute("UPDATE block_records SET is_peak=0 WHERE is_peak=1") - await cursor_1.close() - cursor_2 = await self.db.execute( - "UPDATE block_records SET is_peak=1 WHERE header_hash=?", - (header_hash.hex(),), - ) - await cursor_2.close() - - async def get_block_records_close_to_peak( - self, blocks_n: int - ) -> Tuple[Dict[bytes32, BlockRecord], Optional[bytes32]]: - """ - Returns a dictionary with all blocks, as well as the header hash of the peak, - if present. - """ - - res = await self.db.execute("SELECT header_hash, height from block_records WHERE is_peak = 1") - row = await res.fetchone() - await res.close() - if row is None: - return {}, None - header_hash_bytes, peak_height = row - peak: bytes32 = bytes32(bytes.fromhex(header_hash_bytes)) - - formatted_str = f"SELECT header_hash, block from block_records WHERE height >= {peak_height - blocks_n}" - cursor = await self.db.execute(formatted_str) - rows = await cursor.fetchall() - await cursor.close() - ret: Dict[bytes32, BlockRecord] = {} - for row in rows: - header_hash_bytes, block_record_bytes = row - header_hash = bytes32.fromhex(header_hash_bytes) - ret[header_hash] = BlockRecord.from_bytes(block_record_bytes) - return ret, peak - - async def get_header_blocks_in_range( - self, - start: int, - stop: int, - ) -> Dict[bytes32, HeaderBlock]: - - formatted_str = f"SELECT header_hash, block from header_blocks WHERE height >= {start} and height <= {stop}" - - cursor = await self.db.execute(formatted_str) - rows = await cursor.fetchall() - await cursor.close() - ret: Dict[bytes32, HeaderBlock] = {} - for row in rows: - header_hash_bytes, block_record_bytes = row - header_hash = bytes32.fromhex(header_hash_bytes) - ret[header_hash] = HeaderBlock.from_bytes(block_record_bytes) - - return ret - - async def get_block_records_in_range( - self, - start: int, - stop: int, - ) -> Dict[bytes32, BlockRecord]: - """ - Returns a dictionary with all blocks, as well as the header hash of the peak, - if present. - """ - - formatted_str = f"SELECT header_hash, block from block_records WHERE height >= {start} and height <= {stop}" - - cursor = await self.db.execute(formatted_str) - rows = await cursor.fetchall() - await cursor.close() - ret: Dict[bytes32, BlockRecord] = {} - for row in rows: - header_hash_bytes, block_record_bytes = row - header_hash = bytes32.fromhex(header_hash_bytes) - ret[header_hash] = BlockRecord.from_bytes(block_record_bytes) - - return ret - - async def get_peak_heights_dicts(self) -> Tuple[Dict[uint32, bytes32], Dict[uint32, SubEpochSummary]]: - """ - Returns a dictionary with all blocks, as well as the header hash of the peak, - if present. - """ - - res = await self.db.execute("SELECT header_hash from block_records WHERE is_peak = 1") - row = await res.fetchone() - await res.close() - if row is None: - return {}, {} - - peak: bytes32 = bytes32.fromhex(row[0]) - cursor = await self.db.execute("SELECT header_hash,prev_hash,height,sub_epoch_summary from block_records") - rows = await cursor.fetchall() - await cursor.close() - hash_to_prev_hash: Dict[bytes32, bytes32] = {} - hash_to_height: Dict[bytes32, uint32] = {} - hash_to_summary: Dict[bytes32, SubEpochSummary] = {} - - for row in rows: - hash_to_prev_hash[bytes32.fromhex(row[0])] = bytes32.fromhex(row[1]) - hash_to_height[bytes32.fromhex(row[0])] = row[2] - if row[3] is not None: - hash_to_summary[bytes32.fromhex(row[0])] = SubEpochSummary.from_bytes(row[3]) - - height_to_hash: Dict[uint32, bytes32] = {} - sub_epoch_summaries: Dict[uint32, SubEpochSummary] = {} - - curr_header_hash = peak - curr_height = hash_to_height[curr_header_hash] - while True: - height_to_hash[curr_height] = curr_header_hash - if curr_header_hash in hash_to_summary: - sub_epoch_summaries[curr_height] = hash_to_summary[curr_header_hash] - if curr_height == 0: - break - curr_header_hash = hash_to_prev_hash[curr_header_hash] - curr_height = hash_to_height[curr_header_hash] - return height_to_hash, sub_epoch_summaries diff --git a/chia/wallet/wallet_blockchain.py b/chia/wallet/wallet_blockchain.py index 1a0e68ebe022..6b55efbb6b86 100644 --- a/chia/wallet/wallet_blockchain.py +++ b/chia/wallet/wallet_blockchain.py @@ -1,94 +1,41 @@ -import asyncio -import dataclasses import logging -import multiprocessing -from concurrent.futures.process import ProcessPoolExecutor -from enum import Enum -from typing import Any, Callable, Dict, List, Optional, Set, Tuple - -from chia.consensus.block_header_validation import validate_finished_header_block, validate_unfinished_header_block +from typing import Dict, Optional, Tuple, List +from chia.consensus.block_header_validation import validate_finished_header_block from chia.consensus.block_record import BlockRecord +from chia.consensus.blockchain import ReceiveBlockResult from chia.consensus.blockchain_interface import BlockchainInterface from chia.consensus.constants import ConsensusConstants -from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty from chia.consensus.find_fork_point import find_fork_point_in_chain from chia.consensus.full_block_to_block_record import block_to_block_record -from chia.consensus.multiprocess_validation import PreValidationResult, pre_validate_blocks_multiprocessing from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary -from chia.types.coin_spend import CoinSpend from chia.types.header_block import HeaderBlock -from chia.types.unfinished_header_block import UnfinishedHeaderBlock -from chia.util.errors import Err, ValidationError +from chia.types.weight_proof import WeightProof +from chia.util.errors import Err from chia.util.ints import uint32, uint64 -from chia.util.streamable import recurse_jsonify -from chia.wallet.block_record import HeaderBlockRecord -from chia.wallet.wallet_block_store import WalletBlockStore -from chia.wallet.wallet_coin_store import WalletCoinStore -from chia.wallet.wallet_pool_store import WalletPoolStore -from chia.wallet.wallet_transaction_store import WalletTransactionStore +from chia.wallet.key_val_store import KeyValStore +from chia.wallet.wallet_weight_proof_handler import WalletWeightProofHandler log = logging.getLogger(__name__) -class ReceiveBlockResult(Enum): - """ - When Blockchain.receive_block(b) is called, one of these results is returned, - showing whether the block was added to the chain (extending the peak), - and if not, why it was not added. - """ - - NEW_PEAK = 1 # Added to the peak of the blockchain - ADDED_AS_ORPHAN = 2 # Added as an orphan/stale block (not a new peak of the chain) - INVALID_BLOCK = 3 # Block was not added because it was invalid - ALREADY_HAVE_BLOCK = 4 # Block is already present in this blockchain - DISCONNECTED_BLOCK = 5 # Block's parent (previous pointer) is not in this blockchain - - class WalletBlockchain(BlockchainInterface): constants: ConsensusConstants - constants_json: Dict - # peak of the blockchain - _peak_height: Optional[uint32] - # All blocks in peak path are guaranteed to be included, can include orphan blocks - __block_records: Dict[bytes32, BlockRecord] - # Defines the path from genesis to the peak, no orphan blocks - __height_to_hash: Dict[uint32, bytes32] - # all hashes of blocks in block_record by height, used for garbage collection - __heights_in_cache: Dict[uint32, Set[bytes32]] - # All sub-epoch summaries that have been included in the blockchain from the beginning until and including the peak - # (height_included, SubEpochSummary). Note: ONLY for the blocks in the path to the peak - __sub_epoch_summaries: Dict[uint32, SubEpochSummary] = {} - # Stores - coin_store: WalletCoinStore - tx_store: WalletTransactionStore - pool_store: WalletPoolStore - block_store: WalletBlockStore - # Used to verify blocks in parallel - pool: ProcessPoolExecutor - - new_transaction_block_callback: Any - reorg_rollback: Any - wallet_state_manager_lock: asyncio.Lock + _basic_store: KeyValStore + _weight_proof_handler: WalletWeightProofHandler - # Whether blockchain is shut down or not - _shut_down: bool + synced_weight_proof: Optional[WeightProof] - # Lock to prevent simultaneous reads and writes - lock: asyncio.Lock - log: logging.Logger + _peak: Optional[HeaderBlock] + _height_to_hash: Dict[uint32, bytes32] + _block_records: Dict[bytes32, BlockRecord] + _latest_timestamp: uint64 + _sub_slot_iters: uint64 + _difficulty: uint64 + CACHE_SIZE: int @staticmethod async def create( - block_store: WalletBlockStore, - coin_store: WalletCoinStore, - tx_store: WalletTransactionStore, - pool_store: WalletPoolStore, - consensus_constants: ConsensusConstants, - new_transaction_block_callback: Callable, # f(removals: List[Coin], additions: List[Coin], height: uint32) - reorg_rollback: Callable, - lock: asyncio.Lock, - reserved_cores: int, + _basic_store: KeyValStore, constants: ConsensusConstants, weight_proof_handler: WalletWeightProofHandler ): """ Initializes a blockchain with the BlockRecords from disk, assuming they have all been @@ -96,401 +43,173 @@ async def create( in the consensus constants config. """ self = WalletBlockchain() - self.lock = asyncio.Lock() - self.coin_store = coin_store - self.tx_store = tx_store - self.pool_store = pool_store - cpu_count = multiprocessing.cpu_count() - if cpu_count > 61: - cpu_count = 61 # Windows Server 2016 has an issue https://bugs.python.org/issue26903 - num_workers = max(cpu_count - reserved_cores, 1) - self.pool = ProcessPoolExecutor(max_workers=num_workers) - log.info(f"Started {num_workers} processes for block validation") - self.constants = consensus_constants - self.constants_json = recurse_jsonify(dataclasses.asdict(self.constants)) - self.block_store = block_store - self._shut_down = False - self.new_transaction_block_callback = new_transaction_block_callback - self.reorg_rollback = reorg_rollback - self.log = logging.getLogger(__name__) - self.wallet_state_manager_lock = lock - await self._load_chain_from_store() - return self - - def shut_down(self): - self._shut_down = True - self.pool.shutdown(wait=True) - - async def _load_chain_from_store(self) -> None: - """ - Initializes the state of the Blockchain class from the database. - """ - height_to_hash, sub_epoch_summaries = await self.block_store.get_peak_heights_dicts() - self.__height_to_hash = height_to_hash - self.__sub_epoch_summaries = sub_epoch_summaries - self.__block_records = {} - self.__heights_in_cache = {} - blocks, peak = await self.block_store.get_block_records_close_to_peak(self.constants.BLOCKS_CACHE_SIZE) - for block_record in blocks.values(): - self.add_block_record(block_record) + self._basic_store = _basic_store + self.constants = constants + self.CACHE_SIZE = constants.SUB_EPOCH_BLOCKS + 100 + self._weight_proof_handler = weight_proof_handler + self.synced_weight_proof = await self._basic_store.get_object("SYNCED_WEIGHT_PROOF", WeightProof) + self._peak = None + self._peak = await self.get_peak_block() + self._latest_timestamp = uint64(0) + self._height_to_hash = {} + self._block_records = {} + if self.synced_weight_proof is not None: + await self.new_weight_proof(self.synced_weight_proof) + else: + self._sub_slot_iters = constants.SUB_SLOT_ITERS_STARTING + self._difficulty = constants.DIFFICULTY_STARTING - if len(blocks) == 0: - assert peak is None - self._peak_height = None - return None + return self - assert peak is not None - self._peak_height = self.block_record(peak).height - assert len(self.__height_to_hash) == self._peak_height + 1 + async def new_weight_proof(self, weight_proof: WeightProof, records: Optional[List[BlockRecord]] = None) -> None: + peak: Optional[HeaderBlock] = await self.get_peak_block() - def get_peak(self) -> Optional[BlockRecord]: - """ - Return the peak of the blockchain - """ - if self._peak_height is None: + if peak is not None and weight_proof.recent_chain_data[-1].weight <= peak.weight: + # No update, don't change anything return None - return self.height_to_block_record(self._peak_height) - - async def receive_block( - self, - header_block_record: HeaderBlockRecord, - pre_validation_result: Optional[PreValidationResult] = None, - trusted: bool = False, - fork_point_with_peak: Optional[uint32] = None, - additional_coin_spends: List[CoinSpend] = None, - ) -> Tuple[ReceiveBlockResult, Optional[Err], Optional[uint32]]: - """ - Adds a new block into the blockchain, if it's valid and connected to the current - blockchain, regardless of whether it is the child of a head, or another block. - Returns a header if block is added to head. Returns an error if the block is - invalid. Also returns the fork height, in the case of a new peak. - """ - - if additional_coin_spends is None: - additional_coin_spends = [] - block = header_block_record.header - genesis: bool = block.height == 0 - + self.synced_weight_proof = weight_proof + await self._basic_store.set_object("SYNCED_WEIGHT_PROOF", weight_proof) + + latest_timestamp = self._latest_timestamp + + if records is None: + success, _, _, records = await self._weight_proof_handler.validate_weight_proof(weight_proof, True) + assert success + assert records is not None and len(records) > 1 + + for record in records: + self._height_to_hash[record.height] = record.header_hash + self.add_block_record(record) + if record.is_transaction_block: + assert record.timestamp is not None + if record.timestamp > latest_timestamp: + latest_timestamp = record.timestamp + + self._sub_slot_iters = records[-1].sub_slot_iters + self._difficulty = uint64(records[-1].weight - records[-2].weight) + await self.set_peak_block(weight_proof.recent_chain_data[-1], latest_timestamp) + self.clean_block_records() + + async def receive_block(self, block: HeaderBlock) -> Tuple[ReceiveBlockResult, Optional[Err]]: if self.contains_block(block.header_hash): - return ReceiveBlockResult.ALREADY_HAVE_BLOCK, None, None - - if not self.contains_block(block.prev_header_hash) and not genesis: - return ( - ReceiveBlockResult.DISCONNECTED_BLOCK, - Err.INVALID_PREV_BLOCK_HASH, - None, - ) - - if block.height == 0: - prev_b: Optional[BlockRecord] = None + return ReceiveBlockResult.ALREADY_HAVE_BLOCK, None + if not self.contains_block(block.prev_header_hash) and block.height > 0: + return ReceiveBlockResult.DISCONNECTED_BLOCK, None + if ( + len(block.finished_sub_slots) > 0 + and block.finished_sub_slots[0].challenge_chain.new_sub_slot_iters is not None + ): + assert block.finished_sub_slots[0].challenge_chain.new_difficulty is not None # They both change together + sub_slot_iters: uint64 = block.finished_sub_slots[0].challenge_chain.new_sub_slot_iters + difficulty: uint64 = block.finished_sub_slots[0].challenge_chain.new_difficulty else: - prev_b = self.block_record(block.prev_header_hash) - sub_slot_iters, difficulty = get_next_sub_slot_iters_and_difficulty( - self.constants, len(block.finished_sub_slots) > 0, prev_b, self + sub_slot_iters = self._sub_slot_iters + difficulty = self._difficulty + required_iters, error = validate_finished_header_block( + self.constants, self, block, False, difficulty, sub_slot_iters, False ) - - if trusted is False and pre_validation_result is None: - required_iters, error = validate_finished_header_block( - self.constants, self, block, False, difficulty, sub_slot_iters - ) - elif trusted: - unfinished_header_block = UnfinishedHeaderBlock( - block.finished_sub_slots, - block.reward_chain_block.get_unfinished(), - block.challenge_chain_sp_proof, - block.reward_chain_sp_proof, - block.foliage, - block.foliage_transaction_block, - block.transactions_filter, - ) - - required_iters, val_error = validate_unfinished_header_block( - self.constants, self, unfinished_header_block, False, difficulty, sub_slot_iters, False, True - ) - error = val_error if val_error is not None else None - else: - assert pre_validation_result is not None - required_iters = pre_validation_result.required_iters - error = ( - ValidationError(Err(pre_validation_result.error)) if pre_validation_result.error is not None else None - ) - if error is not None: - return ReceiveBlockResult.INVALID_BLOCK, error.code, None - assert required_iters is not None + return ReceiveBlockResult.INVALID_BLOCK, error.code + if required_iters is None: + return ReceiveBlockResult.INVALID_BLOCK, Err.INVALID_POSPACE - block_record = block_to_block_record( - self.constants, - self, - required_iters, - None, - block, + block_record: BlockRecord = block_to_block_record( + self.constants, self, required_iters, None, block, sub_slot_iters ) - heights_changed: Set[Tuple[uint32, Optional[bytes32]]] = set() - # Always add the block to the database - async with self.wallet_state_manager_lock: - async with self.block_store.db_wrapper.lock: - try: - await self.block_store.db_wrapper.begin_transaction() - await self.block_store.add_block_record(header_block_record, block_record, additional_coin_spends) - self.add_block_record(block_record) - self.clean_block_record(block_record.height - self.constants.BLOCKS_CACHE_SIZE) - fork_height, records_to_add = await self._reconsider_peak( - block_record, genesis, fork_point_with_peak, additional_coin_spends, heights_changed - ) - for record in records_to_add: - if record.sub_epoch_summary_included is not None: - self.__sub_epoch_summaries[record.height] = record.sub_epoch_summary_included - await self.block_store.db_wrapper.commit_transaction() - except BaseException as e: - self.log.error(f"Error during db transaction: {e}") - if self.block_store.db_wrapper.db._connection is not None: - await self.block_store.db_wrapper.rollback_transaction() - self.remove_block_record(block_record.header_hash) - self.block_store.rollback_cache_block(block_record.header_hash) - await self.coin_store.rebuild_wallet_cache() - await self.tx_store.rebuild_tx_cache() - await self.pool_store.rebuild_cache() - for height, replaced in heights_changed: - # If it was replaced change back to the previous value otherwise pop the change - if replaced is not None: - self.__height_to_hash[height] = replaced - else: - self.__height_to_hash.pop(height) - raise - if fork_height is not None: - self.log.info(f"💰 Updated wallet peak to height {block_record.height}, weight {block_record.weight}, ") - return ReceiveBlockResult.NEW_PEAK, None, fork_height + self.add_block_record(block_record) + if self._peak is None: + if block_record.is_transaction_block: + latest_timestamp = block_record.timestamp else: - return ReceiveBlockResult.ADDED_AS_ORPHAN, None, None - - async def _reconsider_peak( - self, - block_record: BlockRecord, - genesis: bool, - fork_point_with_peak: Optional[uint32], - additional_coin_spends_from_wallet: Optional[List[CoinSpend]], - heights_changed: Set[Tuple[uint32, Optional[bytes32]]], - ) -> Tuple[Optional[uint32], List[BlockRecord]]: - """ - When a new block is added, this is called, to check if the new block is the new peak of the chain. - This also handles reorgs by reverting blocks which are not in the heaviest chain. - It returns the height of the fork between the previous chain and the new chain, or returns - None if there was no update to the heaviest chain. - """ - peak = self.get_peak() - if genesis: - if peak is None: - block: Optional[HeaderBlockRecord] = await self.block_store.get_header_block_record( - block_record.header_hash - ) - assert block is not None - replaced = None - if uint32(0) in self.__height_to_hash: - replaced = self.__height_to_hash[uint32(0)] - self.__height_to_hash[uint32(0)] = block.header_hash - heights_changed.add((uint32(0), replaced)) - assert len(block.additions) == 0 and len(block.removals) == 0 - await self.new_transaction_block_callback(block.removals, block.additions, block_record, []) - self._peak_height = uint32(0) - return uint32(0), [block_record] - return None, [] - - assert peak is not None - if block_record.weight > peak.weight: - # Find the fork. if the block is just being appended, it will return the peak - # If no blocks in common, returns -1, and reverts all blocks - if fork_point_with_peak is not None: - fork_h: int = fork_point_with_peak + latest_timestamp = None + self._height_to_hash[block_record.height] = block_record.header_hash + await self.set_peak_block(block, latest_timestamp) + return ReceiveBlockResult.NEW_PEAK, None + elif block_record.weight > self._peak.weight: + if block_record.prev_hash == self._peak.header_hash: + fork_height: int = self._peak.height else: - fork_h = find_fork_point_in_chain(self, block_record, peak) - - # Rollback to fork - self.log.debug(f"fork_h: {fork_h}, SB: {block_record.height}, peak: {peak.height}") - if block_record.prev_hash != peak.header_hash: - await self.reorg_rollback(fork_h) - - # Rollback sub_epoch_summaries - heights_to_delete = [] - for ses_included_height in self.__sub_epoch_summaries.keys(): - if ses_included_height > fork_h: - heights_to_delete.append(ses_included_height) - for height in heights_to_delete: - del self.__sub_epoch_summaries[height] - - # Collect all blocks from fork point to new peak - blocks_to_add: List[Tuple[HeaderBlockRecord, BlockRecord, List[CoinSpend]]] = [] - curr = block_record.header_hash - while fork_h < 0 or curr != self.height_to_hash(uint32(fork_h)): - fetched_header_block: Optional[HeaderBlockRecord] = await self.block_store.get_header_block_record(curr) - fetched_block_record: Optional[BlockRecord] = await self.block_store.get_block_record(curr) - if curr == block_record.header_hash: - additional_coin_spends = additional_coin_spends_from_wallet - else: - additional_coin_spends = await self.block_store.get_additional_coin_spends(curr) - if additional_coin_spends is None: - additional_coin_spends = [] - assert fetched_header_block is not None - assert fetched_block_record is not None - blocks_to_add.append((fetched_header_block, fetched_block_record, additional_coin_spends)) - if fetched_header_block.height == 0: - # Doing a full reorg, starting at height 0 + fork_height = find_fork_point_in_chain(self, block_record, self._peak) + await self._rollback_to_height(fork_height) + curr_record: BlockRecord = block_record + latest_timestamp = self._latest_timestamp + while curr_record.height > fork_height: + self._height_to_hash[curr_record.height] = curr_record.header_hash + if curr_record.timestamp is not None and curr_record.timestamp > latest_timestamp: + latest_timestamp = curr_record.timestamp + if curr_record.height == 0: break - curr = fetched_block_record.prev_hash - - records_to_add: List[BlockRecord] = [] - for fetched_header_block, fetched_block_record, additional_coin_spends in reversed(blocks_to_add): - replaced = None - if fetched_block_record.height in self.__height_to_hash: - replaced = self.__height_to_hash[fetched_block_record.height] - self.__height_to_hash[fetched_block_record.height] = fetched_block_record.header_hash - heights_changed.add((fetched_block_record.height, replaced)) - records_to_add.append(fetched_block_record) - if fetched_block_record.is_transaction_block: - await self.new_transaction_block_callback( - fetched_header_block.removals, - fetched_header_block.additions, - fetched_block_record, - additional_coin_spends, - ) - - # Changes the peak to be the new peak - await self.block_store.set_peak(block_record.header_hash) - self._peak_height = block_record.height - if fork_h < 0: - return None, records_to_add - return uint32(fork_h), records_to_add - - # This is not a heavier block than the heaviest we have seen, so we don't change the coin set - return None, [] - - def get_next_difficulty(self, header_hash: bytes32, new_slot: bool) -> uint64: - assert self.contains_block(header_hash) - curr = self.block_record(header_hash) - if curr.height <= 2: - return self.constants.DIFFICULTY_STARTING - return get_next_sub_slot_iters_and_difficulty(self.constants, new_slot, curr, self)[1] - - def get_next_slot_iters(self, header_hash: bytes32, new_slot: bool) -> uint64: - assert self.contains_block(header_hash) - curr = self.block_record(header_hash) - if curr.height <= 2: - return self.constants.SUB_SLOT_ITERS_STARTING - return get_next_sub_slot_iters_and_difficulty(self.constants, new_slot, curr, self)[0] - - async def pre_validate_blocks_multiprocessing( - self, blocks: List[HeaderBlock], batch_size: int = 4 - ) -> Optional[List[PreValidationResult]]: - return await pre_validate_blocks_multiprocessing( - self.constants, self.constants_json, self, blocks, self.pool, True, {}, None, batch_size - ) + curr_record = self.block_record(curr_record.prev_hash) + self._sub_slot_iters = block_record.sub_slot_iters + self._difficulty = uint64(block_record.weight - self.block_record(block_record.prev_hash).weight) + await self.set_peak_block(block, latest_timestamp) + self.clean_block_records() + return ReceiveBlockResult.NEW_PEAK, None + return ReceiveBlockResult.ADDED_AS_ORPHAN, None + + async def _rollback_to_height(self, height: int): + if self._peak is None: + return + for h in range(max(0, height + 1), self._peak.height + 1): + del self._height_to_hash[uint32(h)] + + await self._basic_store.remove_object("PEAK_BLOCK") + + def get_peak_height(self) -> uint32: + if self._peak is None: + return uint32(0) + return self._peak.height + + async def set_peak_block(self, block: HeaderBlock, timestamp: Optional[uint64] = None): + await self._basic_store.set_object("PEAK_BLOCK", block) + self._peak = block + if timestamp is not None: + self._latest_timestamp = timestamp + elif block.foliage_transaction_block is not None: + self._latest_timestamp = block.foliage_transaction_block.timestamp + log.info(f"Peak set to : {self._peak.height} timestamp: {self._latest_timestamp}") + + async def get_peak_block(self) -> Optional[HeaderBlock]: + if self._peak is not None: + return self._peak + return await self._basic_store.get_object("PEAK_BLOCK", HeaderBlock) + + def get_latest_timestamp(self) -> uint64: + return self._latest_timestamp def contains_block(self, header_hash: bytes32) -> bool: - """ - True if we have already added this block to the chain. This may return false for orphan blocks - that we have added but no longer keep in memory. - """ - return header_hash in self.__block_records - - def block_record(self, header_hash: bytes32) -> BlockRecord: - return self.__block_records[header_hash] - - def height_to_block_record(self, height: uint32, check_db=False) -> BlockRecord: - header_hash = self.height_to_hash(height) - # TODO: address hint error and remove ignore - # error: Argument 1 to "block_record" of "WalletBlockchain" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - return self.block_record(header_hash) # type: ignore[arg-type] - - def get_ses_heights(self) -> List[uint32]: - return sorted(self.__sub_epoch_summaries.keys()) - - def get_ses(self, height: uint32) -> SubEpochSummary: - return self.__sub_epoch_summaries[height] - - def height_to_hash(self, height: uint32) -> Optional[bytes32]: - return self.__height_to_hash[height] + return header_hash in self._block_records def contains_height(self, height: uint32) -> bool: - return height in self.__height_to_hash - - def get_peak_height(self) -> Optional[uint32]: - return self._peak_height - - async def warmup(self, fork_point: uint32): - """ - Loads blocks into the cache. The blocks loaded include all blocks from - fork point - BLOCKS_CACHE_SIZE up to and including the fork_point. + return height in self._height_to_hash - Args: - fork_point: the last block height to load in the cache + def height_to_hash(self, height: uint32) -> bytes32: + return self._height_to_hash[height] - """ + def try_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]: + if self.contains_block(header_hash): + return self.block_record(header_hash) + return None - if self._peak_height is None: - return None - blocks = await self.block_store.get_block_records_in_range( - fork_point - self.constants.BLOCKS_CACHE_SIZE, self._peak_height - ) - for block_record in blocks.values(): - self.add_block_record(block_record) - - def clean_block_record(self, height: int): - """ - Clears all block records in the cache which have block_record < height. - Args: - height: Minimum height that we need to keep in the cache - """ - - if height < 0: - return None - blocks_to_remove = self.__heights_in_cache.get(uint32(height), None) - while blocks_to_remove is not None and height >= 0: - for header_hash in blocks_to_remove: - del self.__block_records[header_hash] - del self.__heights_in_cache[uint32(height)] # remove height from heights in cache + def block_record(self, header_hash: bytes32) -> BlockRecord: + return self._block_records[header_hash] - if height == 0: - break - height -= 1 - blocks_to_remove = self.__heights_in_cache.get(uint32(height), None) + def add_block_record(self, block_record: BlockRecord): + self._block_records[block_record.header_hash] = block_record def clean_block_records(self): """ - Cleans the cache so that we only maintain relevant blocks. - This removes block records that have height < peak - BLOCKS_CACHE_SIZE. - These blocks are necessary for calculating future difficulty adjustments. + Cleans the cache so that we only maintain relevant blocks. This removes + block records that have height < peak - CACHE_SIZE. """ - - if len(self.__block_records) < self.constants.BLOCKS_CACHE_SIZE: - return None - - peak = self.get_peak() - assert peak is not None - if peak.height - self.constants.BLOCKS_CACHE_SIZE < 0: + height_limit = max(0, self.get_peak_height() - self.CACHE_SIZE) + if len(self._block_records) < self.CACHE_SIZE: return None - self.clean_block_record(peak.height - self.constants.BLOCKS_CACHE_SIZE) - async def get_block_records_in_range(self, start: int, stop: int) -> Dict[bytes32, BlockRecord]: - return await self.block_store.get_block_records_in_range(start, stop) + to_remove: List[bytes32] = [] + for header_hash, block_record in self._block_records.items(): + if block_record.height < height_limit: + to_remove.append(header_hash) - async def get_header_blocks_in_range( - self, start: int, stop: int, tx_filter: bool = True - ) -> Dict[bytes32, HeaderBlock]: - return await self.block_store.get_header_blocks_in_range(start, stop) - - async def get_block_record_from_db(self, header_hash: bytes32) -> Optional[BlockRecord]: - if header_hash in self.__block_records: - return self.__block_records[header_hash] - return await self.block_store.get_block_record(header_hash) - - def remove_block_record(self, header_hash: bytes32): - sbr = self.block_record(header_hash) - del self.__block_records[header_hash] - self.__heights_in_cache[sbr.height].remove(header_hash) - - def add_block_record(self, block_record: BlockRecord): - self.__block_records[block_record.header_hash] = block_record - if block_record.height not in self.__heights_in_cache.keys(): - self.__heights_in_cache[block_record.height] = set() - self.__heights_in_cache[block_record.height].add(block_record.header_hash) + for header_hash in to_remove: + del self._block_records[header_hash] diff --git a/chia/wallet/wallet_coin_store.py b/chia/wallet/wallet_coin_store.py index 3046ea270e89..f217ed684d4d 100644 --- a/chia/wallet/wallet_coin_store.py +++ b/chia/wallet/wallet_coin_store.py @@ -82,6 +82,20 @@ async def rebuild_wallet_cache(self): self.unspent_coin_wallet_cache[coin_record.wallet_id] = {} self.unspent_coin_wallet_cache[coin_record.wallet_id][name] = coin_record + async def get_multiple_coin_records(self, coin_names: List[bytes32]) -> List[WalletCoinRecord]: + """Return WalletCoinRecord(s) that have a coin name in the specified list""" + if set(coin_names).issubset(set(self.coin_record_cache.keys())): + return list(filter(lambda cr: cr.coin.name() in coin_names, self.coin_record_cache.values())) + else: + as_hexes = [cn.hex() for cn in coin_names] + cursor = await self.db_connection.execute( + f'SELECT * from coin_record WHERE coin_name in ({"?," * (len(as_hexes) - 1)}?)', tuple(as_hexes) + ) + rows = await cursor.fetchall() + await cursor.close() + + return [self.coin_record_from_row(row) for row in rows] + # Store CoinRecord in DB and ram cache async def add_coin_record(self, record: WalletCoinRecord) -> None: # update wallet cache @@ -114,6 +128,18 @@ async def add_coin_record(self, record: WalletCoinRecord) -> None: ) await cursor.close() + # Sometimes we realize that a coin is actually not interesting to us so we need to delete it + async def delete_coin_record(self, coin_name: bytes32) -> None: + if coin_name in self.coin_record_cache: + coin_record = self.coin_record_cache.pop(coin_name) + if coin_record.wallet_id in self.unspent_coin_wallet_cache: + coin_cache = self.unspent_coin_wallet_cache[coin_record.wallet_id] + if coin_name in coin_cache: + coin_cache.pop(coin_record.coin.name()) + + c = await self.db_connection.execute("DELETE FROM coin_record WHERE coin_name=?", (coin_name.hex(),)) + await c.close() + # Update coin_record to be spent in DB async def set_spent(self, coin_name: bytes32, height: uint32) -> WalletCoinRecord: current: Optional[WalletCoinRecord] = await self.get_coin_record(coin_name) @@ -200,6 +226,20 @@ async def get_all_coins(self) -> Set[WalletCoinRecord]: return set(self.coin_record_from_row(row) for row in rows) + async def get_coins_to_check(self, check_height) -> Set[WalletCoinRecord]: + """Returns set of all CoinRecords.""" + cursor = await self.db_connection.execute( + "SELECT * from coin_record where spent_height=0 or spent_height>? or confirmed_height>?", + ( + check_height, + check_height, + ), + ) + rows = await cursor.fetchall() + await cursor.close() + + return set(self.coin_record_from_row(row) for row in rows) + # Checks DB and DiffStores for CoinRecords with puzzle_hash and returns them async def get_coin_records_by_puzzle_hash(self, puzzle_hash: bytes32) -> List[WalletCoinRecord]: """Returns a list of all coin records with the given puzzle hash""" @@ -209,6 +249,17 @@ async def get_coin_records_by_puzzle_hash(self, puzzle_hash: bytes32) -> List[Wa return [self.coin_record_from_row(row) for row in rows] + # Checks DB and DiffStores for CoinRecords with parent_coin_info and returns them + async def get_coin_records_by_parent_id(self, parent_coin_info: bytes32) -> List[WalletCoinRecord]: + """Returns a list of all coin records with the given parent id""" + cursor = await self.db_connection.execute( + "SELECT * from coin_record WHERE coin_parent=?", (parent_coin_info.hex(),) + ) + rows = await cursor.fetchall() + await cursor.close() + + return [self.coin_record_from_row(row) for row in rows] + async def rollback_to_block(self, height: int): """ Rolls back the blockchain to block_index. All blocks confirmed after this point @@ -229,7 +280,8 @@ async def rollback_to_block(self, height: int): coin_record.wallet_id, ) self.coin_record_cache[coin_record.coin.name()] = new_record - self.unspent_coin_wallet_cache[coin_record.wallet_id][coin_record.coin.name()] = new_record + if coin_record.wallet_id in self.unspent_coin_wallet_cache: + self.unspent_coin_wallet_cache[coin_record.wallet_id][coin_record.coin.name()] = new_record if coin_record.confirmed_block_height > height: delete_queue.append(coin_record) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 5a62397ac9a5..01bb3786dd8d 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -4,83 +4,98 @@ import time import traceback from pathlib import Path -from typing import Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any + +from blspy import PrivateKey, AugSchemeMPL +from packaging.version import Version -from blspy import PrivateKey from chia.consensus.block_record import BlockRecord -from chia.consensus.blockchain_interface import BlockchainInterface +from chia.consensus.blockchain import ReceiveBlockResult from chia.consensus.constants import ConsensusConstants -from chia.consensus.multiprocess_validation import PreValidationResult +from chia.consensus.find_fork_point import find_fork_point_in_chain from chia.daemon.keychain_proxy import ( - KeychainProxy, KeychainProxyConnectionFailure, - KeyringIsEmpty, - KeyringIsLocked, connect_to_keychain_and_validate, wrap_local_keychain, + KeychainProxy, + KeyringIsEmpty, ) -from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH +from chia.full_node.weight_proof import chunks +from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH, solution_to_pool_state +from chia.pools.pool_wallet import PoolWallet from chia.protocols import wallet_protocol from chia.protocols.full_node_protocol import RequestProofOfWeight, RespondProofOfWeight from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.protocols.wallet_protocol import ( + RespondToCoinUpdates, + CoinState, + RespondToPhUpdates, + RespondBlockHeader, + RequestAdditions, + RespondAdditions, RejectAdditionsRequest, + RequestSESInfo, + RespondSESInfo, + RespondRemovals, RejectRemovalsRequest, - RequestAdditions, RequestHeaderBlocks, - RespondAdditions, - RespondBlockHeader, RespondHeaderBlocks, - RespondRemovals, ) from chia.server.node_discovery import WalletPeers from chia.server.outbound_message import Message, NodeType, make_msg from chia.server.peer_store_resolver import PeerStoreResolver from chia.server.server import ChiaServer from chia.server.ws_connection import WSChiaConnection -from chia.types.blockchain_format.coin import Coin, hash_coin_list +from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary from chia.types.coin_spend import CoinSpend from chia.types.header_block import HeaderBlock from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.peer_info import PeerInfo +from chia.types.weight_proof import WeightProof, SubEpochData from chia.util.byte_types import hexstr_to_bytes -from chia.util.check_fork_next_block import check_fork_next_block -from chia.util.config import WALLET_PEERS_PATH_KEY_DEPRECATED, load_config -from chia.util.errors import Err, ValidationError -from chia.util.ints import uint32, uint128 -from chia.util.keychain import Keychain -from chia.util.lru_cache import LRUCache -from chia.util.merkle_set import MerkleSet, confirm_included_already_hashed, confirm_not_included_already_hashed +from chia.util.config import WALLET_PEERS_PATH_KEY_DEPRECATED +from chia.util.ints import uint32, uint64 +from chia.util.keychain import KeyringIsLocked, Keychain from chia.util.network import get_host_addr from chia.util.path import mkdir, path_from_root -from chia.wallet.block_record import HeaderBlockRecord from chia.wallet.derivation_record import DerivationRecord -from chia.wallet.settings.settings_objects import BackupInitialized +from chia.wallet.util.wallet_sync_utils import ( + validate_additions, + validate_removals, + request_and_validate_removals, + request_and_validate_additions, +) +from chia.wallet.wallet_coin_record import WalletCoinRecord +from chia.wallet.wallet_state_manager import WalletStateManager from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.backup_utils import open_backup_file from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_action import WalletAction -from chia.wallet.wallet_blockchain import ReceiveBlockResult -from chia.wallet.wallet_state_manager import WalletStateManager from chia.util.profiler import profile_task +class PeerRequestCache: + blocks: Dict[uint32, HeaderBlock] + block_requests: Dict[bytes32, Any] + ses_requests: Dict[bytes32, Any] + states_validated: Dict[bytes32, CoinState] + + def __init__(self): + self.blocks = {} + self.ses_requests = {} + self.block_requests = {} + self.states_validated = {} + + class WalletNode: key_config: Dict config: Dict constants: ConsensusConstants - keychain_proxy: Optional[KeychainProxy] - local_keychain: Optional[Keychain] # For testing only. KeychainProxy is used in normal cases server: Optional[ChiaServer] log: logging.Logger - wallet_peers: WalletPeers # Maintains the state of the wallet (blockchain and transactions), handles DB connections wallet_state_manager: Optional[WalletStateManager] - - # How far away from LCA we must be to perform a full sync. Before then, do a short sync, - # which is consecutive requests for the previous block - short_sync_threshold: int _shut_down: bool root_path: Path state_changed_callback: Optional[Callable] @@ -89,6 +104,8 @@ class WalletNode: peer_task: Optional[asyncio.Task] logged_in: bool wallet_peers_initialized: bool + keychain_proxy: Optional[KeychainProxy] + wallet_peers: Optional[WalletPeers] def __init__( self, @@ -100,10 +117,7 @@ def __init__( ): self.config = config self.constants = consensus_constants - self.keychain_proxy = None - self.local_keychain = local_keychain self.root_path = root_path - self.base_config = load_config(root_path, "config.yaml") self.log = logging.getLogger(name if name else __name__) # Normal operation data self.cached_blocks: Dict = {} @@ -112,22 +126,22 @@ def __init__( # Sync data self._shut_down = False self.proof_hashes: List = [] - self.header_hashes: List = [] - self.header_hashes_error = False - self.short_sync_threshold = 15 # Change the test when changing this - self.potential_blocks_received: Dict = {} - self.potential_header_hashes: Dict = {} self.state_changed_callback = None self.wallet_state_manager = None - self.backup_initialized = False # Delay first launch sync after user imports backup info or decides to skip self.server = None self.wsm_close_task = None self.sync_task: Optional[asyncio.Task] = None self.logged_in_fingerprint: Optional[int] = None self.peer_task = None self.logged_in = False + self.keychain_proxy = None + self.local_keychain = local_keychain + self.height_to_time: Dict[uint32, uint64] = {} + self.synced_peers: Set[bytes32] = set() + self.wallet_peers = None self.wallet_peers_initialized = False - self.last_new_peak_messages = LRUCache(5) + self.valid_wp_cache: Dict[bytes32, Any] = {} + self.untrusted_caches: Dict[bytes32, Any] = {} async def ensure_keychain_proxy(self) -> KeychainProxy: if not self.keychain_proxy: @@ -140,7 +154,6 @@ async def ensure_keychain_proxy(self) -> KeychainProxy: return self.keychain_proxy async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: - key: PrivateKey = None try: keychain_proxy = await self.ensure_keychain_proxy() key = await keychain_proxy.get_key_for_fingerprint(fingerprint) @@ -159,16 +172,9 @@ async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ async def _start( self, fingerprint: Optional[int] = None, - new_wallet: bool = False, - backup_file: Optional[Path] = None, - skip_backup_import: bool = False, ) -> bool: - try: - private_key = await self.get_key_for_fingerprint(fingerprint) - except KeychainProxyConnectionFailure: - self.log.error("Failed to connect to keychain service") - return False - + self.synced_peers = set() + private_key = await self.get_key_for_fingerprint(fingerprint) if private_key is None: self.logged_in = False return False @@ -182,52 +188,29 @@ async def _start( .replace("CHALLENGE", self.config["selected_network"]) .replace("KEY", db_path_key_suffix) ) - path = path_from_root(self.root_path, db_path_replaced) + path = path_from_root(self.root_path, f"{db_path_replaced}_new") mkdir(path.parent) self.new_peak_lock = asyncio.Lock() assert self.server is not None self.wallet_state_manager = await WalletStateManager.create( - private_key, self.config, path, self.constants, self.server, self.root_path + private_key, + self.config, + path, + self.constants, + self.server, + self.root_path, + self.new_puzzle_hash_created, + self.get_coin_state, + self.subscribe_to_coin_updates, + self, ) - self.wsm_close_task = None - assert self.wallet_state_manager is not None - backup_settings: BackupInitialized = self.wallet_state_manager.user_settings.get_backup_settings() - if backup_settings.user_initialized is False: - if new_wallet is True: - await self.wallet_state_manager.user_settings.user_created_new_wallet() - elif skip_backup_import is True: - await self.wallet_state_manager.user_settings.user_skipped_backup_import() - elif backup_file is not None: - await self.wallet_state_manager.import_backup_info(backup_file) - else: - self.backup_initialized = False - await self.wallet_state_manager.close_all_stores() - self.wallet_state_manager = None - self.logged_in = False - return False - - self.backup_initialized = True + self.config["starting_height"] = 0 - # Start peers here after the backup initialization has finished - # We only want to do this once per instantiation - # However, doing it earlier before backup initialization causes - # the wallet to spam the introducer - if self.wallet_peers_initialized is False: - asyncio.create_task(self.wallet_peers.start()) - self.wallet_peers_initialized = True - - if backup_file is not None: - json_dict = open_backup_file(backup_file, self.wallet_state_manager.private_key) - if "start_height" in json_dict["data"]: - start_height = json_dict["data"]["start_height"] - self.config["starting_height"] = max(0, start_height - self.config["start_height_buffer"]) - else: - self.config["starting_height"] = 0 - else: - self.config["starting_height"] = 0 + if self.wallet_peers is None: + self.initialize_wallet_peers() if self.state_changed_callback is not None: self.wallet_state_manager.set_callback(self.state_changed_callback) @@ -237,14 +220,28 @@ async def _start( self.peer_task = asyncio.create_task(self._periodically_check_full_node()) self.sync_event = asyncio.Event() - self.sync_task = asyncio.create_task(self.sync_job()) if fingerprint is None: self.logged_in_fingerprint = private_key.get_g1().get_fingerprint() else: self.logged_in_fingerprint = fingerprint self.logged_in = True + self.wallet_state_manager.set_sync_mode(False) + + async with self.wallet_state_manager.puzzle_store.lock: + index = await self.wallet_state_manager.puzzle_store.get_last_derivation_path() + if index is None or index < self.config["initial_num_public_keys"] - 1: + await self.wallet_state_manager.create_more_puzzle_hashes(from_zero=True) + self.wsm_close_task = None return True + async def new_puzzle_hash_created(self, puzzle_hashes: List[bytes32]): + if len(puzzle_hashes) == 0: + return + assert self.server is not None + full_nodes: Dict[bytes32, WSChiaConnection] = self.server.connection_by_type.get(NodeType.FULL_NODE, {}) + for node_id, node in full_nodes.copy().items(): + await self.subscribe_to_phs(puzzle_hashes, node) + def _close(self): self.log.info("self._close") self.logged_in_fingerprint = None @@ -252,18 +249,15 @@ def _close(self): async def _await_closed(self): self.log.info("self._await_closed") - await self.server.close_all_connections() - asyncio.create_task(self.wallet_peers.ensure_is_closed()) + if self.server is not None: + await self.server.close_all_connections() + if self.wallet_peers is not None: + await self.wallet_peers.ensure_is_closed() if self.wallet_state_manager is not None: - await self.wallet_state_manager.close_all_stores() + await self.wallet_state_manager._await_closed() self.wallet_state_manager = None - if self.sync_task is not None: - self.sync_task.cancel() - self.sync_task = None - if self.peer_task is not None: - self.peer_task.cancel() - self.peer_task = None self.logged_in = False + self.wallet_peers = None def _set_state_changed_callback(self, callback: Callable): self.state_changed_callback = callback @@ -273,12 +267,12 @@ def _set_state_changed_callback(self, callback: Callable): self.wallet_state_manager.set_pending_callback(self._pending_tx_handler) def _pending_tx_handler(self): - if self.wallet_state_manager is None or self.backup_initialized is False: + if self.wallet_state_manager is None: return None asyncio.create_task(self._resend_queue()) async def _action_messages(self) -> List[Message]: - if self.wallet_state_manager is None or self.backup_initialized is False: + if self.wallet_state_manager is None: return [] actions: List[WalletAction] = await self.wallet_state_manager.action_store.get_all_pending_actions() result: List[Message] = [] @@ -297,21 +291,11 @@ async def _action_messages(self) -> List[Message]: return result async def _resend_queue(self): - if ( - self._shut_down - or self.server is None - or self.wallet_state_manager is None - or self.backup_initialized is None - ): + if self._shut_down or self.server is None or self.wallet_state_manager is None: return None for msg, sent_peers in await self._messages_to_resend(): - if ( - self._shut_down - or self.server is None - or self.wallet_state_manager is None - or self.backup_initialized is None - ): + if self._shut_down or self.server is None or self.wallet_state_manager is None: return None full_nodes = self.server.get_full_node_connections() for peer in full_nodes: @@ -320,17 +304,12 @@ async def _resend_queue(self): await peer.send_message(msg) for msg in await self._action_messages(): - if ( - self._shut_down - or self.server is None - or self.wallet_state_manager is None - or self.backup_initialized is None - ): + if self._shut_down or self.server is None or self.wallet_state_manager is None: return None await self.server.send_to_all([msg], NodeType.FULL_NODE) async def _messages_to_resend(self) -> List[Tuple[Message, Set[bytes32]]]: - if self.wallet_state_manager is None or self.backup_initialized is False or self._shut_down: + if self.wallet_state_manager is None or self._shut_down: return [] messages: List[Tuple[Message, Set[bytes32]]] = [] @@ -353,45 +332,218 @@ async def _messages_to_resend(self) -> List[Tuple[Message, Set[bytes32]]]: def set_server(self, server: ChiaServer): self.server = server - DNS_SERVERS_EMPTY: list = [] - network_name: str = self.config["selected_network"] - # TODO: Perhaps use a different set of DNS seeders for wallets, to split the traffic. - self.wallet_peers = WalletPeers( - self.server, - self.config["target_peer_count"], - PeerStoreResolver( - self.root_path, - self.config, - selected_network=network_name, - peers_file_path_key="wallet_peers_file_path", - legacy_peer_db_path_key=WALLET_PEERS_PATH_KEY_DEPRECATED, - default_peers_file_path="wallet/db/wallet_peers.dat", - ), - self.config["introducer_peer"], - DNS_SERVERS_EMPTY, - self.config["peer_connect_interval"], - network_name, - None, - self.log, - ) + self.initialize_wallet_peers() + + def initialize_wallet_peers(self): + self.server.on_connect = self.on_connect + network_name = self.config["selected_network"] + + connect_to_unknown_peers = self.config.get("connect_to_unknown_peers", False) + if connect_to_unknown_peers: + self.wallet_peers = WalletPeers( + self.server, + self.config["target_peer_count"], + PeerStoreResolver( + self.root_path, + self.config, + selected_network=network_name, + peers_file_path_key="wallet_peers_file_path", + legacy_peer_db_path_key=WALLET_PEERS_PATH_KEY_DEPRECATED, + default_peers_file_path="wallet/db/wallet_peers.dat", + ), + self.config["introducer_peer"], + self.config["dns_servers"], + self.config["peer_connect_interval"], + network_name, + None, + self.log, + ) + asyncio.create_task(self.wallet_peers.start()) + + def on_disconnect(self, peer: WSChiaConnection): + if peer.peer_node_id in self.untrusted_caches: + self.untrusted_caches.pop(peer.peer_node_id) async def on_connect(self, peer: WSChiaConnection): - if self.wallet_state_manager is None or self.backup_initialized is False: + if self.wallet_state_manager is None: return None + + if Version(peer.protocol_version) < Version("0.0.33"): + self.log.info("Disconnecting, full node running old software") + await peer.close() + + trusted = self.is_trusted(peer) + self.log.info(f"Connected peer {peer} is {trusted}") messages_peer_ids = await self._messages_to_resend() self.wallet_state_manager.state_changed("add_connection") for msg, peer_ids in messages_peer_ids: if peer.peer_node_id in peer_ids: continue await peer.send_message(msg) + if not self.has_full_node() and self.wallet_peers is not None: asyncio.create_task(self.wallet_peers.on_connect(peer)) + async def trusted_sync(self, full_node: WSChiaConnection): + """ + Performs a one-time sync with each trusted peer, subscribing to interested puzzle hashes and coin ids. + """ + self.log.info("Starting trusted sync") + assert self.wallet_state_manager is not None + self.wallet_state_manager.set_sync_mode(True) + start_time = time.time() + current_height: uint32 = self.wallet_state_manager.blockchain.get_peak_height() + request_height: uint32 = uint32(max(0, current_height - 1000)) + + already_checked: Set[bytes32] = set() + continue_while: bool = True + while continue_while: + # Get all phs from puzzle store + all_puzzle_hashes: List[bytes32] = await self.get_puzzle_hashes_to_subscribe() + to_check: List[bytes32] = [] + for ph in all_puzzle_hashes: + if ph in already_checked: + continue + else: + to_check.append(ph) + already_checked.add(ph) + if len(to_check) == 1000: + break + + await self.subscribe_to_phs(to_check, full_node, request_height) + + # Check if new puzzle hashed have been created + check_again = await self.get_puzzle_hashes_to_subscribe() + await self.wallet_state_manager.create_more_puzzle_hashes() + + continue_while = False + for ph in check_again: + if ph not in already_checked: + continue_while = True + break + + all_coins: Set[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_coins_to_check(request_height) + all_coin_names: List[bytes32] = [coin_record.name() for coin_record in all_coins] + removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() + all_coin_names.extend(removed_dict.keys()) + + one_k_chunks = chunks(all_coin_names, 1000) + for chunk in one_k_chunks: + await self.subscribe_to_coin_updates(chunk, full_node, request_height) + self.wallet_state_manager.set_sync_mode(False) + end_time = time.time() + duration = end_time - start_time + self.log.info(f"Trusted sync duration was: {duration}") + # Refresh wallets + for wallet_id, wallet in self.wallet_state_manager.wallets.items(): + self.wallet_state_manager.state_changed("coin_removed", wallet_id) + self.wallet_state_manager.state_changed("coin_added", wallet_id) + self.synced_peers.add(full_node.peer_node_id) + + async def subscribe_to_phs(self, puzzle_hashes: List[bytes32], peer: WSChiaConnection, height=uint32(0)): + """ + Tell full nodes that we are interested in puzzle hashes, and for trusted connections, add the new coin state + for the puzzle hashes. + """ + + msg = wallet_protocol.RegisterForPhUpdates(puzzle_hashes, height) + all_state: Optional[RespondToPhUpdates] = await peer.register_interest_in_puzzle_hash(msg) + # State for untrusted sync is processed only in wp sync | or short sync backwards + if all_state is not None and self.is_trusted(peer): + assert self.wallet_state_manager is not None + await self.wallet_state_manager.new_coin_state(all_state.coin_states, peer) + + async def subscribe_to_coin_updates(self, coin_names, peer, height=uint32(0)): + """ + Tell full nodes that we are interested in coin ids, and for trusted connections, add the new coin state + for the coin changes. + """ + msg = wallet_protocol.RegisterForCoinUpdates(coin_names, height) + all_coins_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg) + # State for untrusted sync is processed only in wp sync | or short sync backwards + if all_coins_state is not None and self.is_trusted(peer): + await self.wallet_state_manager.new_coin_state(all_coins_state.coin_states, peer) + + async def get_coin_state(self, coin_names: List[bytes32]) -> List[CoinState]: + assert self.server is not None + # TODO Use trusted peer, otherwise try untrusted + all_nodes = self.server.connection_by_type[NodeType.FULL_NODE] + if len(all_nodes.keys()) == 0: + raise ValueError("Not connected to the full node") + first_node = list(all_nodes.values())[0] + msg = wallet_protocol.RegisterForCoinUpdates(coin_names, uint32(0)) + coin_state: Optional[RespondToCoinUpdates] = await first_node.register_interest_in_coin(msg) + # TODO validate state if received from untrusted peer + assert coin_state is not None + return coin_state.coin_states + + async def get_coins_with_puzzle_hash(self, puzzle_hash) -> List[CoinState]: + assert self.wallet_state_manager is not None + assert self.server is not None + all_nodes = self.server.connection_by_type[NodeType.FULL_NODE] + if len(all_nodes.keys()) == 0: + raise ValueError("Not connected to the full node") + first_node = list(all_nodes.values())[0] + msg = wallet_protocol.RegisterForPhUpdates(puzzle_hash, uint32(0)) + coin_state: Optional[RespondToPhUpdates] = await first_node.register_interest_in_puzzle_hash(msg) + assert coin_state is not None + return coin_state.coin_states + + def is_trusted(self, peer): + return self.server.is_trusted_peer(peer, self.config["trusted_peers"]) + + async def state_update_received(self, request: wallet_protocol.CoinStateUpdate, peer: WSChiaConnection): + assert self.wallet_state_manager is not None + assert self.server is not None + async with self.new_peak_lock: + async with self.wallet_state_manager.lock: + if self.is_trusted(peer): + await self.wallet_state_manager.new_coin_state( + request.items, peer, request.fork_height, request.height + ) + await self.update_ui() + else: + # Ignore state_update_received if untrusted, we'll sync from block messages where we check filter + for coin_state in request.items: + info = await self.wallet_state_manager.puzzle_store.wallet_info_for_puzzle_hash( + coin_state.coin.puzzle_hash + ) + if coin_state.created_height is None or info is not None: + continue + + # We need to check the hints and see if there is a new CAT sent to us, so we can create + # a new CAT wallet + wallet_id, wallet_type = await self.wallet_state_manager.fetch_parent_and_check_for_cat( + peer, coin_state + ) + + if wallet_id is not None: + # If there is a new wallet, check if we have this height already in the blockchain + if self.wallet_state_manager.blockchain.contains_height(request.height): + # If we do, complete the blocks + header_blocks: Optional[RespondHeaderBlocks] = await peer.request_header_blocks( + wallet_protocol.RequestHeaderBlocks( + request.height, self.wallet_state_manager.blockchain.get_peak_height() + ) + ) + assert header_blocks is not None and isinstance( + header_blocks, wallet_protocol.RespondHeaderBlocks + ) + # re-check the block filter for any new addition /removals, for all of the blocks + # that have been added to the blockchain since this CAT was created + await self.complete_blocks(header_blocks.header_blocks, peer) + + def get_full_node_peer(self): + nodes = self.server.get_full_node_connections() + if len(nodes) > 0: + return nodes[0] + else: + return None + async def _periodically_check_full_node(self) -> None: tries = 0 while not self._shut_down and tries < 5: if self.has_full_node(): - await self.wallet_peers.ensure_is_closed() if self.wallet_state_manager is not None: self.wallet_state_manager.state_changed("add_connection") break @@ -413,7 +565,7 @@ def has_full_node(self) -> bool: full_node_resolved = full_node_peer else: full_node_resolved = PeerInfo( - get_host_addr(full_node_peer.host, self.base_config.get("prefer_ipv6")), full_node_peer.port + get_host_addr(full_node_peer.host, self.config.get("prefer_ipv6")), full_node_peer.port ) if full_node_peer in peers or full_node_resolved in peers: self.log.info(f"Will not attempt to connect to other nodes, already connected to {full_node_peer}") @@ -422,133 +574,204 @@ def has_full_node(self) -> bool: connection.get_peer_info() != full_node_peer and connection.get_peer_info() != full_node_resolved ): - self.log.info(f"Closing unnecessary connection to {connection.get_peer_logging()}.") + self.log.info(f"Closing unnecessary connection to {connection.get_peer_info()}.") asyncio.create_task(connection.close()) return True return False - async def complete_blocks(self, header_blocks: List[HeaderBlock], peer: WSChiaConnection): - if self.wallet_state_manager is None: - return None - header_block_records: List[HeaderBlockRecord] = [] - assert self.server - trusted = self.server.is_trusted_peer(peer, self.config["trusted_peers"]) - async with self.wallet_state_manager.blockchain.lock: - for block in header_blocks: - if block.is_transaction_block: - # Find additions and removals - (additions, removals,) = await self.wallet_state_manager.get_filter_additions_removals( - block, block.transactions_filter, None - ) - - # Get Additions - added_coins = await self.get_additions(peer, block, additions) - if added_coins is None: - raise ValueError("Failed to fetch additions") - - # Get removals - removed_coins = await self.get_removals(peer, block, added_coins, removals) - if removed_coins is None: - raise ValueError("Failed to fetch removals") + async def fetch_last_tx_from_peer(self, height: uint32, peer: WSChiaConnection) -> Optional[HeaderBlock]: + request_height = height + while True: + if request_height == 0: + return None + request = wallet_protocol.RequestBlockHeader(request_height) + response: Optional[RespondBlockHeader] = await peer.request_block_header(request) + if response is not None and isinstance(response, RespondBlockHeader): + if response.header_block.is_transaction_block: + return response.header_block + else: + break + request_height = uint32(request_height - 1) + return None - # If there is a launcher created, or we have a singleton spent, fetches the required solutions - additional_coin_spends: List[CoinSpend] = await self.get_additional_coin_spends( - peer, block, added_coins, removed_coins - ) + async def get_timestamp_for_height(self, height: uint32) -> uint64: + """ + Returns the timestamp for transaction block at h=height, if not transaction block, backtracks until it finds + a transaction block + """ + if height in self.height_to_time: + return self.height_to_time[height] - hbr = HeaderBlockRecord(block, added_coins, removed_coins) - else: - hbr = HeaderBlockRecord(block, [], []) - header_block_records.append(hbr) - additional_coin_spends = [] - (result, error, fork_h,) = await self.wallet_state_manager.blockchain.receive_block( - hbr, trusted=trusted, additional_coin_spends=additional_coin_spends - ) - if result == ReceiveBlockResult.NEW_PEAK: - if not self.wallet_state_manager.sync_mode: - self.wallet_state_manager.blockchain.clean_block_records() - self.wallet_state_manager.state_changed("new_block") - self.wallet_state_manager.state_changed("sync_changed") - await self.wallet_state_manager.new_peak() - elif result == ReceiveBlockResult.INVALID_BLOCK: - self.log.info(f"Invalid block from peer: {peer.get_peer_logging()} {error}") - await peer.close() - return - else: - self.log.debug(f"Result: {result}") + peer = self.get_full_node_peer() + assert peer is not None + curr_height: uint32 = height + while True: + request = wallet_protocol.RequestBlockHeader(curr_height) + response: Optional[RespondBlockHeader] = await peer.request_block_header(request) + if response is None or not isinstance(response, RespondBlockHeader): + raise ValueError(f"Invalid response from {peer}, {response}") + if response.header_block.foliage_transaction_block is not None: + self.height_to_time[height] = response.header_block.foliage_transaction_block.timestamp + return response.header_block.foliage_transaction_block.timestamp + curr_height = uint32(curr_height - 1) async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChiaConnection): - if self.wallet_state_manager is None: - return - - if self.wallet_state_manager.blockchain.contains_block(peak.header_hash): - self.log.debug(f"known peak {peak.header_hash}") - return - - if self.wallet_state_manager.sync_mode: - self.last_new_peak_messages.put(peer, peak) - return - + assert self.wallet_state_manager is not None + assert self.server is not None async with self.new_peak_lock: - curr_peak = self.wallet_state_manager.blockchain.get_peak() - if curr_peak is not None and curr_peak.weight >= peak.weight: + if self.wallet_state_manager is None: + # When logging out of wallet return + if self.is_trusted(peer): + async with self.wallet_state_manager.lock: + request = wallet_protocol.RequestBlockHeader(peak.height) + header_response: Optional[RespondBlockHeader] = await peer.request_block_header(request) + assert header_response is not None + + last_tx: Optional[HeaderBlock] = await self.fetch_last_tx_from_peer(peak.height, peer) + latest_timestamp: Optional[uint64] = None + if last_tx is not None: + assert last_tx.foliage_transaction_block is not None + latest_timestamp = last_tx.foliage_transaction_block.timestamp + + if peer.peer_node_id not in self.synced_peers: + await self.trusted_sync(peer) + + await self.wallet_state_manager.blockchain.set_peak_block( + header_response.header_block, latest_timestamp + ) - request = wallet_protocol.RequestBlockHeader(peak.height) - response: Optional[RespondBlockHeader] = await peer.request_block_header(request) - if response is None or not isinstance(response, RespondBlockHeader) or response.header_block is None: - self.log.warning(f"bad peak response from peer {response}") - return - header_block = response.header_block - curr_peak_height = 0 if curr_peak is None else curr_peak.height - if (curr_peak_height == 0 and peak.height < self.constants.WEIGHT_PROOF_RECENT_BLOCKS) or ( - curr_peak_height > peak.height - 200 - ): + self.wallet_state_manager.state_changed("new_block") + self.wallet_state_manager.set_sync_mode(False) + else: + request = wallet_protocol.RequestBlockHeader(peak.height) + response: Optional[RespondBlockHeader] = await peer.request_block_header(request) + if response is None or not isinstance(response, RespondBlockHeader) or response.header_block is None: + self.log.debug(f"bad peak response from peer {response}, perhaps connection was closed") + return + peak_block = response.header_block + current_peak: Optional[HeaderBlock] = await self.wallet_state_manager.blockchain.get_peak_block() + if current_peak is not None and peak_block.weight < current_peak.weight: + if peak_block.height < current_peak.height - 20: + await peer.close(120) + return - if peak.height <= curr_peak_height + self.config["short_sync_blocks_behind_threshold"]: - await self.wallet_short_sync_backtrack(header_block, peer) + # don't sync if full node is not synced it self, since we want to fully sync to a few peers + if ( + not response.header_block.is_transaction_block + and current_peak is not None + and peak_block.prev_header_hash == current_peak.header_hash + ): + # This block is after our peak, so we don't need to check if node is synced + pass else: - await self.batch_sync_to_peak(curr_peak_height, peak) - elif peak.height >= self.constants.WEIGHT_PROOF_RECENT_BLOCKS: - # Request weight proof - # Sync if PoW validates - weight_request = RequestProofOfWeight(peak.height, peak.header_hash) - weight_proof_response: RespondProofOfWeight = await peer.request_proof_of_weight( - weight_request, timeout=360 + if not response.header_block.is_transaction_block: + last_tx_block = await self.fetch_last_tx_from_peer(response.header_block.height, peer) + else: + last_tx_block = response.header_block + + if last_tx_block is None: + return + assert last_tx_block is not None + assert last_tx_block.foliage_transaction_block is not None + if ( + self.config["testing"] is False + and last_tx_block.foliage_transaction_block.timestamp < int(time.time()) - 600 + ): + # Full node not synced, don't sync to it + self.log.info("Peer we connected to is not fully synced, dropping connection...") + await peer.close() + return + + long_sync_threshold = 100 + far_behind: bool = ( + peak.height - self.wallet_state_manager.blockchain.get_peak_height() > long_sync_threshold ) - if weight_proof_response is None: - return + fork_point = -1 + if current_peak is not None: + # Force a long sync if it's a very deep reorg + try: + fork_point = find_fork_point_in_chain( + self.wallet_state_manager.blockchain, peak_block, current_peak + ) + if peak.height - fork_point > long_sync_threshold: + far_behind = True + except KeyError: + # If we don't have the blocks to find fork point, it's a deep reorg + far_behind = True + + # check if claimed peak is heavier or same as our current peak + # if we haven't synced fully to this peer sync again + if ( + peer.peer_node_id not in self.synced_peers or far_behind + ) and peak.height >= self.constants.WEIGHT_PROOF_RECENT_BLOCKS: + syncing = False + if far_behind or len(self.synced_peers) == 0: + syncing = True + self.wallet_state_manager.set_sync_mode(True) + try: + ( + valid_weight_proof, + weight_proof, + summaries, + block_records, + ) = await self.fetch_and_validate_the_weight_proof(peer, response.header_block) + if valid_weight_proof is False: + if syncing: + self.wallet_state_manager.set_sync_mode(False) + await peer.close() + return + assert weight_proof is not None + if syncing: + async with self.wallet_state_manager.lock: + await self.untrusted_sync_to_peer(peer, weight_proof, syncing, fork_point) + else: + await self.untrusted_sync_to_peer(peer, weight_proof, syncing, fork_point) + if ( + self.wallet_state_manager.blockchain.synced_weight_proof is None + or weight_proof.recent_chain_data[-1].weight + > self.wallet_state_manager.blockchain.synced_weight_proof.recent_chain_data[-1].weight + ): + await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records) + + self.synced_peers.add(peer.peer_node_id) + + self.wallet_state_manager.state_changed("new_block") + await self.update_ui() + except Exception: + if syncing: + self.wallet_state_manager.set_sync_mode(False) + tb = traceback.format_exc() + self.log.error(f"Error syncing to {peer.get_peer_info()} {tb}") + await peer.close() + return + if syncing: + self.wallet_state_manager.set_sync_mode(False) - weight_proof = weight_proof_response.wp - if self.wallet_state_manager is None: - return - if self.server is not None and self.server.is_trusted_peer(peer, self.config["trusted_peers"]): - valid, fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point_no_validations( - weight_proof - ) else: - valid, fork_point, _ = await self.wallet_state_manager.weight_proof_handler.validate_weight_proof( - weight_proof - ) - if not valid: - self.log.error( - f"invalid weight proof, num of epochs {len(weight_proof.sub_epochs)}" - f" recent blocks num ,{len(weight_proof.recent_chain_data)}" - ) - self.log.debug(f"{weight_proof}") - return - self.log.info(f"Validated, fork point is {fork_point}") - self.wallet_state_manager.sync_store.add_potential_fork_point( - header_block.header_hash, uint32(fork_point) - ) - self.wallet_state_manager.sync_store.add_potential_peak(header_block) - self.start_sync() + if peer.peer_node_id not in self.synced_peers: + # Edge case, we still want to subscribe for all phs + # (Hints are not in filter) + await self.untrusted_subscribe_to_puzzle_hashes(peer, False, None, None) + self.synced_peers.add(peer.peer_node_id) + await self.wallet_short_sync_backtrack(peak_block, peer) + self.wallet_state_manager.set_sync_mode(False) + self.wallet_state_manager.state_changed("new_block") + + await self.wallet_state_manager.new_peak(peak) + self._pending_tx_handler() + + async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer): + assert self.wallet_state_manager is not None - async def wallet_short_sync_backtrack(self, header_block, peer): top = header_block blocks = [top] # Fetch blocks backwards until we hit the one that we have, # then complete them with additions / removals going forward + fork_height = 0 + if self.wallet_state_manager.blockchain.contains_block(header_block.prev_header_hash): + fork_height = header_block.height - 1 + while not self.wallet_state_manager.blockchain.contains_block(top.prev_header_hash) and top.height > 0: request_prev = wallet_protocol.RequestBlockHeader(top.height - 1) response_prev: Optional[RespondBlockHeader] = await peer.request_block_header(request_prev) @@ -557,401 +780,167 @@ async def wallet_short_sync_backtrack(self, header_block, peer): prev_head = response_prev.header_block blocks.append(prev_head) top = prev_head + fork_height = top.height - 1 + blocks.reverse() + # Roll back coins and transactions + await self.wallet_state_manager.reorg_rollback(fork_height) + peak = await self.wallet_state_manager.blockchain.get_peak_block() + if peak is not None: + assert header_block.weight >= peak.weight + for block in blocks: + # Set blockchain to the latest peak + res, err = await self.wallet_state_manager.blockchain.receive_block(block) + if res == ReceiveBlockResult.INVALID_BLOCK: + raise ValueError(err) + + # Add new coins and transactions await self.complete_blocks(blocks, peer) - await self.wallet_state_manager.create_more_puzzle_hashes() - - async def batch_sync_to_peak(self, fork_height, peak): - advanced_peak = False - batch_size = self.constants.MAX_BLOCK_COUNT_PER_REQUESTS - for i in range(max(0, fork_height - 1), peak.height, batch_size): - start_height = i - end_height = min(peak.height, start_height + batch_size) - peers = self.server.get_full_node_connections() - added = False - for peer in peers: - try: - added, advanced_peak = await self.fetch_blocks_and_validate( - peer, uint32(start_height), uint32(end_height), None if advanced_peak else fork_height - ) - if added: - break - except Exception as e: - await peer.close() - exc = traceback.format_exc() - self.log.error(f"Error while trying to fetch from peer:{e} {exc}") - if not added: - raise RuntimeError(f"Was not able to add blocks {start_height}-{end_height}") - - curr_peak = self.wallet_state_manager.blockchain.get_peak() - assert peak is not None - self.wallet_state_manager.blockchain.clean_block_record( - min(end_height, curr_peak.height) - self.constants.BLOCKS_CACHE_SIZE - ) - def start_sync(self) -> None: - self.log.info("self.sync_event.set()") - self.sync_event.set() - - async def check_new_peak(self) -> None: + async def complete_blocks(self, header_blocks: List[HeaderBlock], peer: WSChiaConnection): if self.wallet_state_manager is None: return None + all_outgoing_per_wallet: Dict[int, List[TransactionRecord]] = {} - current_peak: Optional[BlockRecord] = self.wallet_state_manager.blockchain.get_peak() - if current_peak is None: - return None - potential_peaks: List[ - Tuple[bytes32, HeaderBlock] - ] = self.wallet_state_manager.sync_store.get_potential_peaks_tuples() - for _, block in potential_peaks: - if current_peak.weight < block.weight: - await asyncio.sleep(5) - self.start_sync() - return None - - async def sync_job(self) -> None: - while True: - self.log.info("Loop start in sync job") - if self._shut_down is True: - break - asyncio.create_task(self.check_new_peak()) - await self.sync_event.wait() - self.last_new_peak_messages = LRUCache(5) - self.sync_event.clear() - - if self._shut_down is True: - break - try: - assert self.wallet_state_manager is not None - self.wallet_state_manager.set_sync_mode(True) - await self._sync() - except Exception as e: - tb = traceback.format_exc() - self.log.error(f"Loop exception in sync {e}. {tb}") - finally: - if self.wallet_state_manager is not None: - self.wallet_state_manager.set_sync_mode(False) - for peer, peak in self.last_new_peak_messages.cache.items(): - asyncio.create_task(self.new_peak_wallet(peak, peer)) - self.log.info("Loop end in sync job") - - async def _sync(self) -> None: - """ - Wallet has fallen far behind (or is starting up for the first time), and must be synced - up to the LCA of the blockchain. - """ - if self.wallet_state_manager is None or self.backup_initialized is False or self.server is None: - return None - - highest_weight: uint128 = uint128(0) - peak_height: uint32 = uint32(0) - peak: Optional[HeaderBlock] = None - potential_peaks: List[ - Tuple[bytes32, HeaderBlock] - ] = self.wallet_state_manager.sync_store.get_potential_peaks_tuples() - - self.log.info(f"Have collected {len(potential_peaks)} potential peaks") - - for header_hash, potential_peak_block in potential_peaks: - if potential_peak_block.weight > highest_weight: - highest_weight = potential_peak_block.weight - peak_height = potential_peak_block.height - peak = potential_peak_block - - if peak_height is None or peak_height == 0: - return None - - if self.wallet_state_manager.peak is not None and highest_weight <= self.wallet_state_manager.peak.weight: - self.log.info("Not performing sync, already caught up.") - return None - - peers: List[WSChiaConnection] = self.server.get_full_node_connections() - if len(peers) == 0: - self.log.info("No peers to sync to") - return None - - async with self.wallet_state_manager.blockchain.lock: - fork_height = None - if peak is not None: - fork_height = self.wallet_state_manager.sync_store.get_potential_fork_point(peak.header_hash) - assert fork_height is not None - # This is the fork point in SES in the case where no fork was detected - peers = self.server.get_full_node_connections() - fork_height = await check_fork_next_block( - self.wallet_state_manager.blockchain, fork_height, peers, wallet_next_block_check - ) - - if fork_height is None: - fork_height = uint32(0) - await self.wallet_state_manager.blockchain.warmup(fork_height) - await self.batch_sync_to_peak(fork_height, peak) - - async def fetch_blocks_and_validate( - self, - peer: WSChiaConnection, - height_start: uint32, - height_end: uint32, - fork_point_with_peak: Optional[uint32], - ) -> Tuple[bool, bool]: - """ - Returns whether the blocks validated, and whether the peak was advanced - """ - if self.wallet_state_manager is None: - return False, False - - self.log.info(f"Requesting blocks {height_start}-{height_end}") - request = RequestHeaderBlocks(uint32(height_start), uint32(height_end)) - res: Optional[RespondHeaderBlocks] = await peer.request_header_blocks(request) - if res is None or not isinstance(res, RespondHeaderBlocks): - raise ValueError("Peer returned no response") - header_blocks: List[HeaderBlock] = res.header_blocks - advanced_peak = False - if header_blocks is None: - raise ValueError(f"No response from peer {peer}") - assert self.server - trusted = self.server.is_trusted_peer(peer, self.config["trusted_peers"]) - pre_validation_results: Optional[List[PreValidationResult]] = None - if not trusted: - pre_validation_results = await self.wallet_state_manager.blockchain.pre_validate_blocks_multiprocessing( - header_blocks - ) - if pre_validation_results is None: - return False, advanced_peak - assert len(header_blocks) == len(pre_validation_results) - - for i in range(len(header_blocks)): - header_block = header_blocks[i] - if not trusted and pre_validation_results is not None and pre_validation_results[i].error is not None: - raise ValidationError(Err(pre_validation_results[i].error)) - - fork_point_with_old_peak = None if advanced_peak else fork_point_with_peak - if header_block.is_transaction_block: + for block in header_blocks: + if block.is_transaction_block: # Find additions and removals (additions, removals,) = await self.wallet_state_manager.get_filter_additions_removals( - header_block, header_block.transactions_filter, fork_point_with_old_peak + block, block.transactions_filter, None ) # Get Additions - added_coins = await self.get_additions(peer, header_block, additions) + added_coins = await self.get_additions(peer, block, additions) if added_coins is None: raise ValueError("Failed to fetch additions") # Get removals - removed_coins = await self.get_removals(peer, header_block, added_coins, removals) + removed_coins = await self.get_removals(peer, block, added_coins, removals) if removed_coins is None: raise ValueError("Failed to fetch removals") - # If there is a launcher created, or we have a singleton spent, fetches the required solutions - additional_coin_spends: List[CoinSpend] = await self.get_additional_coin_spends( - peer, header_block, added_coins, removed_coins - ) - - header_block_record = HeaderBlockRecord(header_block, added_coins, removed_coins) - else: - header_block_record = HeaderBlockRecord(header_block, [], []) - additional_coin_spends = [] - start_t = time.time() - if trusted: - (result, error, fork_h,) = await self.wallet_state_manager.blockchain.receive_block( - header_block_record, - None, - trusted, - fork_point_with_old_peak, - additional_coin_spends=additional_coin_spends, - ) - else: - assert pre_validation_results is not None - (result, error, fork_h,) = await self.wallet_state_manager.blockchain.receive_block( - header_block_record, - pre_validation_results[i], - trusted, - fork_point_with_old_peak, - additional_coin_spends=additional_coin_spends, - ) - self.log.debug( - f"Time taken to validate {header_block.height} with fork " - f"{fork_point_with_old_peak}: {time.time() - start_t}" - ) - if result == ReceiveBlockResult.NEW_PEAK: - advanced_peak = True - self.wallet_state_manager.state_changed("new_block") - elif result == ReceiveBlockResult.INVALID_BLOCK: - raise ValueError("Value error peer sent us invalid block") - if advanced_peak: - await self.wallet_state_manager.create_more_puzzle_hashes() - return True, advanced_peak + for added_coin in added_coins: + self.log.info(f"coin added {added_coin}") + wallet_info = await self.wallet_state_manager.get_wallet_id_for_puzzle_hash(added_coin.puzzle_hash) + if wallet_info is None: + continue + wallet_id, wallet_type = wallet_info + if wallet_id in all_outgoing_per_wallet: + all_outgoing = all_outgoing_per_wallet[wallet_id] + else: + all_outgoing = await self.wallet_state_manager.tx_store.get_all_transactions_for_wallet( + wallet_id + ) + all_outgoing_per_wallet[wallet_id] = all_outgoing + derivation_index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash( + added_coin.puzzle_hash + ) + if derivation_index is not None: + await self.wallet_state_manager.puzzle_store.set_used_up_to(derivation_index, False) + await self.wallet_state_manager.coin_added( + added_coin, block.height, all_outgoing, wallet_id, wallet_type + ) - def validate_additions( - self, - coins: List[Tuple[bytes32, List[Coin]]], - proofs: Optional[List[Tuple[bytes32, bytes, Optional[bytes]]]], - root, - ): - if proofs is None: - # Verify root - additions_merkle_set = MerkleSet() + all_unconfirmed: List[ + TransactionRecord + ] = await self.wallet_state_manager.tx_store.get_all_unconfirmed() - # Addition Merkle set contains puzzlehash and hash of all coins with that puzzlehash - for puzzle_hash, coins_l in coins: - additions_merkle_set.add_already_hashed(puzzle_hash) - additions_merkle_set.add_already_hashed(hash_coin_list(coins_l)) + all_removed_coins = None + trade_removals = await self.wallet_state_manager.trade_manager.get_coins_of_interest() - additions_root = additions_merkle_set.get_root() - if root != additions_root: - return False - else: - for i in range(len(coins)): - assert coins[i][0] == proofs[i][0] - coin_list_1: List[Coin] = coins[i][1] - # TODO: address hint error and remove ignore - # error: Incompatible types in assignment (expression has type "bytes", variable has type - # "bytes32") [assignment] - puzzle_hash_proof: bytes32 = proofs[i][1] # type: ignore[assignment] - # TODO: address hint error and remove ignore - # error: Incompatible types in assignment (expression has type "Optional[bytes]", variable has - # type "Optional[bytes32]") [assignment] - coin_list_proof: Optional[bytes32] = proofs[i][2] # type: ignore[assignment] - if len(coin_list_1) == 0: - # Verify exclusion proof for puzzle hash - not_included = confirm_not_included_already_hashed( - root, - coins[i][0], - puzzle_hash_proof, - ) - if not_included is False: - return False - else: - try: - # Verify inclusion proof for coin list - # TODO: address hint error and remove ignore - # error: Argument 3 to "confirm_included_already_hashed" has incompatible type - # "Optional[bytes32]"; expected "bytes32" [arg-type] - included = confirm_included_already_hashed( - root, - hash_coin_list(coin_list_1), - coin_list_proof, # type: ignore[arg-type] + for removed_coin in removed_coins: + self.log.info(f"coin removed {removed_coin}") + if removed_coin.name() in trade_removals: + await self.wallet_state_manager.trade_manager.coins_of_interest_farmed( + CoinState(removed_coin, block.height, None) # `None` is a lie but it shouldn't matter ) - if included is False: - return False - except AssertionError: - return False - try: - # Verify inclusion proof for puzzle hash - included = confirm_included_already_hashed( - root, - coins[i][0], - puzzle_hash_proof, + for unconfirmed_record in all_unconfirmed: + if removed_coin in unconfirmed_record.removals: + self.log.info(f"Setting tx_id: {unconfirmed_record.name} to confirmed") + await self.wallet_state_manager.tx_store.set_confirmed( + unconfirmed_record.name, block.height + ) + + record = await self.wallet_state_manager.coin_store.get_coin_record(removed_coin.name()) + if record is None: + continue + await self.wallet_state_manager.coin_store.set_spent(removed_coin.name(), block.height) + removed_record = await self.wallet_state_manager.coin_store.get_coin_record(removed_coin.name()) + + if removed_record is not None and removed_record.wallet_type == WalletType.POOLING_WALLET: + if all_removed_coins is None: + all_removed_coins = await self.get_removals(peer, block, added_coins, removals, True) + pool_spend = await self.fetch_puzzle_solution(peer, block.height, removed_coin) + if len(pool_spend.additions()) > 0: + pool_added_coin = pool_spend.additions()[0] + await self.wallet_state_manager.coin_added( + pool_added_coin, + block.height, + [], + uint32(removed_record.wallet_id), + removed_record.wallet_type, + ) + pool_wallet = self.wallet_state_manager.wallets[uint32(removed_record.wallet_id)] + await pool_wallet.apply_state_transitions(pool_spend, block.height) + assert all_removed_coins is not None + if pool_added_coin in all_removed_coins: + pool_spend_2 = await self.fetch_puzzle_solution(peer, block.height, pool_added_coin) + if len(pool_spend_2.additions()) > 0: + pool_added_coin_2 = pool_spend_2.additions()[0] + await self.wallet_state_manager.coin_added( + pool_added_coin_2, + block.height, + [], + uint32(removed_record.wallet_id), + removed_record.wallet_type, + ) + pool_wallet = self.wallet_state_manager.wallets[uint32(removed_record.wallet_id)] + await pool_wallet.apply_state_transitions(pool_spend_2, block.height) + + # Check if we have created a pool wallet + children: List[CoinState] = await self.fetch_children(peer, removed_coin.name(), None) + for child in children: + if child.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH: + continue + if await self.wallet_state_manager.have_a_pool_wallet_with_launched_id(child.coin.name()): + continue + if child.spent_height is None: + continue + + launcher_spend: CoinSpend = await self.fetch_puzzle_solution(peer, block.height, child.coin) + pool_state = None + try: + pool_state = solution_to_pool_state(launcher_spend) + except Exception as e: + self.log.debug(f"Not a pool wallet launcher {e}") + continue + assert pool_state is not None + assert child.spent_height is not None + pool_wallet = await PoolWallet.create( + self.wallet_state_manager, + self.wallet_state_manager.main_wallet, + child.coin.name(), + [launcher_spend], + child.spent_height, + False, + "pool_wallet", + ) + await pool_wallet.apply_state_transitions(launcher_spend, block.height) + pool_added_coin = launcher_spend.additions()[0] + await self.wallet_state_manager.coin_added( + pool_added_coin, + block.height, + [], + uint32(pool_wallet.wallet_id), + WalletType(pool_wallet.type()), ) - if included is False: - return False - except AssertionError: - return False - - return True - - def validate_removals(self, coins, proofs, root): - if proofs is None: - # If there are no proofs, it means all removals were returned in the response. - # we must find the ones relevant to our wallets. - - # Verify removals root - removals_merkle_set = MerkleSet() - for name_coin in coins: - # TODO review all verification - name, coin = name_coin - if coin is not None: - removals_merkle_set.add_already_hashed(coin.name()) - removals_root = removals_merkle_set.get_root() - if root != removals_root: - return False - else: - # This means the full node has responded only with the relevant removals - # for our wallet. Each merkle proof must be verified. - if len(coins) != len(proofs): - return False - for i in range(len(coins)): - # Coins are in the same order as proofs - if coins[i][0] != proofs[i][0]: - return False - coin = coins[i][1] - if coin is None: - # Verifies merkle proof of exclusion - not_included = confirm_not_included_already_hashed( - root, - coins[i][0], - proofs[i][1], - ) - if not_included is False: - return False - else: - # Verifies merkle proof of inclusion of coin name - if coins[i][0] != coin.name(): - return False - included = confirm_included_already_hashed( - root, - coin.name(), - proofs[i][1], - ) - if included is False: - return False - return True - - async def fetch_puzzle_solution(self, peer, height: uint32, coin: Coin) -> CoinSpend: - solution_response = await peer.request_puzzle_solution( - wallet_protocol.RequestPuzzleSolution(coin.name(), height) - ) - if solution_response is None or not isinstance(solution_response, wallet_protocol.RespondPuzzleSolution): - raise ValueError(f"Was not able to obtain solution {solution_response}") - return CoinSpend(coin, solution_response.response.puzzle, solution_response.response.solution) - async def get_additional_coin_spends( - self, peer, block, added_coins: List[Coin], removed_coins: List[Coin] - ) -> List[CoinSpend]: - assert self.wallet_state_manager is not None - additional_coin_spends: List[CoinSpend] = [] - if len(removed_coins) > 0: - removed_coin_ids = set([coin.name() for coin in removed_coins]) - all_added_coins = await self.get_additions(peer, block, [], get_all_additions=True) - assert all_added_coins is not None - if all_added_coins is not None: - all_added_coin_parents = [c.parent_coin_info for c in all_added_coins] - for coin in all_added_coins: - # This searches specifically for a launcher being created, and adds the solution of the launcher - if ( - coin.puzzle_hash == SINGLETON_LAUNCHER_HASH # Check that it's a launcher - and coin.name() in all_added_coin_parents # Check that it's ephemermal - and coin.parent_coin_info in removed_coin_ids # Check that an interesting coin created it - ): - cs: CoinSpend = await self.fetch_puzzle_solution(peer, block.height, coin) - additional_coin_spends.append(cs) - # Apply this coin solution, which might add things to interested list - await self.wallet_state_manager.get_next_interesting_coin_ids(cs, False) + await self.update_ui() - all_removed_coins: Optional[List[Coin]] = await self.get_removals( - peer, block, added_coins, removed_coins, request_all_removals=True - ) - assert all_removed_coins is not None - all_removed_coins_dict: Dict[bytes32, Coin] = {coin.name(): coin for coin in all_removed_coins} - keep_searching = True - while keep_searching: - # This keeps fetching solutions for coins we are interested list, in this block, until - # there are no more interested things to fetch - keep_searching = False - interested_ids: List[ - bytes32 - ] = await self.wallet_state_manager.interested_store.get_interested_coin_ids() - for coin_id in interested_ids: - if coin_id in all_removed_coins_dict: - coin = all_removed_coins_dict[coin_id] - cs = await self.fetch_puzzle_solution(peer, block.height, coin) - - # Apply this coin solution, which might add things to interested list - await self.wallet_state_manager.get_next_interesting_coin_ids(cs, False) - additional_coin_spends.append(cs) - keep_searching = True - all_removed_coins_dict.pop(coin_id) - break - return additional_coin_spends + async def update_ui(self): + for wallet_id, wallet in self.wallet_state_manager.wallets.items(): + self.wallet_state_manager.state_changed("coin_removed", wallet_id) + self.wallet_state_manager.state_changed("coin_added", wallet_id) async def get_additions( self, peer: WSChiaConnection, block_i, additions: Optional[List[bytes32]], get_all_additions: bool = False @@ -967,7 +956,7 @@ async def get_additions( await peer.close() return None elif isinstance(additions_res, RespondAdditions): - validated = self.validate_additions( + validated = validate_additions( additions_res.coins, additions_res.proofs, block_i.foliage_transaction_block.additions_root, @@ -997,8 +986,7 @@ async def get_removals( record_info: Optional[DerivationRecord] = await puzzle_store.get_derivation_record_for_puzzle_hash( coin.puzzle_hash ) - if record_info is not None and record_info.wallet_type == WalletType.COLOURED_COIN: - # TODO why ? + if record_info is not None and record_info.wallet_type == WalletType.CAT: request_all_removals = True break if record_info is not None and record_info.wallet_type == WalletType.DISTRIBUTED_ID: @@ -1015,7 +1003,7 @@ async def get_removals( if removals_res is None: return None elif isinstance(removals_res, RespondRemovals): - validated = self.validate_removals( + validated = validate_removals( removals_res.coins, removals_res.proofs, block_i.foliage_transaction_block.removals_root, @@ -1037,15 +1025,483 @@ async def get_removals( else: return [] + async def fetch_and_validate_the_weight_proof( + self, peer: WSChiaConnection, peak: HeaderBlock + ) -> Tuple[bool, Optional[WeightProof], List[SubEpochSummary], List[BlockRecord]]: + assert self.wallet_state_manager is not None + assert self.wallet_state_manager.weight_proof_handler is not None + + weight_request = RequestProofOfWeight(peak.height, peak.header_hash) + weight_proof_response: RespondProofOfWeight = await peer.request_proof_of_weight(weight_request, timeout=60) + + if weight_proof_response is None: + return False, None, [], [] + start_validation = time.time() + + weight_proof = weight_proof_response.wp + + if weight_proof.recent_chain_data[-1].reward_chain_block.height != peak.height: + return False, None, [], [] + if weight_proof.recent_chain_data[-1].reward_chain_block.weight != peak.weight: + return False, None, [], [] + + if weight_proof.get_hash() in self.valid_wp_cache: + valid, fork_point, summaries, block_records = self.valid_wp_cache[weight_proof.get_hash()] + else: + start_validation = time.time() + ( + valid, + fork_point, + summaries, + block_records, + ) = await self.wallet_state_manager.weight_proof_handler.validate_weight_proof(weight_proof) + if valid: + self.valid_wp_cache[weight_proof.get_hash()] = valid, fork_point, summaries, block_records + + end_validation = time.time() + self.log.info(f"It took {end_validation - start_validation} time to validate the weight proof") + return valid, weight_proof, summaries, block_records + + async def get_puzzle_hashes_to_subscribe(self) -> List[bytes32]: + assert self.wallet_state_manager is not None + all_puzzle_hashes = list(await self.wallet_state_manager.puzzle_store.get_all_puzzle_hashes()) + # Get all phs from interested store + interested_puzzle_hashes = [ + t[0] for t in await self.wallet_state_manager.interested_store.get_interested_puzzle_hashes() + ] + all_puzzle_hashes.extend(interested_puzzle_hashes) + return all_puzzle_hashes + + async def untrusted_subscribe_to_puzzle_hashes( + self, + peer: WSChiaConnection, + save_state: bool, + peer_request_cache: Optional[PeerRequestCache], + weight_proof: Optional[WeightProof], + ): + assert self.wallet_state_manager is not None + already_checked = set() + continue_while = True + while continue_while: + all_puzzle_hashes = await self.get_puzzle_hashes_to_subscribe() + to_check = [] + for ph in all_puzzle_hashes: + if ph in already_checked: + continue + else: + to_check.append(ph) + already_checked.add(ph) + if len(to_check) == 1000: + break + msg = wallet_protocol.RegisterForPhUpdates(to_check, uint32(0)) + all_state: Optional[RespondToPhUpdates] = await peer.register_interest_in_puzzle_hash(msg) + assert all_state is not None + + if save_state: + assert weight_proof is not None + assert peer_request_cache is not None + validated_state = await self.validate_received_state_from_peer( + all_state.coin_states, peer, weight_proof, peer_request_cache, False + ) + await self.wallet_state_manager.new_coin_state(validated_state, peer, weight_proof=weight_proof) + + # Check if new puzzle hashed have been created + check_again = await self.get_puzzle_hashes_to_subscribe() -async def wallet_next_block_check( - peer: WSChiaConnection, potential_peek: uint32, blockchain: BlockchainInterface -) -> bool: - block_response = await peer.request_header_blocks( - wallet_protocol.RequestHeaderBlocks(potential_peek, potential_peek) - ) - if block_response is not None and isinstance(block_response, wallet_protocol.RespondHeaderBlocks): - our_peak = blockchain.get_peak() - if our_peak is not None and block_response.header_blocks[0].prev_header_hash == our_peak.header_hash: + continue_while = False + for ph in check_again: + if ph not in already_checked: + continue_while = True + break + + async def untrusted_sync_to_peer( + self, peer: WSChiaConnection, weight_proof: WeightProof, syncing: bool, fork_height: int + ): + assert self.wallet_state_manager is not None + # If new weight proof is higher than the old one, rollback to the fork point and than apply new coin_states + if fork_height == -1: + wp_fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point( + old_wp=self.wallet_state_manager.blockchain.synced_weight_proof, new_wp=weight_proof + ) + # Extra conservative + fork_height = max(0, wp_fork_point - 10) + self.log.info(f"Starting untrusted sync to: {peer.get_peer_info()}, syncing: {syncing}, fork at: {fork_height}") + if syncing: + self.log.info(f"Rollback for {fork_height}") + await self.wallet_state_manager.reorg_rollback(fork_height) + + start_time: float = time.time() + peer_request_cache: PeerRequestCache = PeerRequestCache() + self.untrusted_caches[peer.peer_node_id] = peer_request_cache + # Always sync fully from untrusted + # Get state for puzzle hashes + self.log.debug("Start untrusted_subscribe_to_puzzle_hashes ") + await self.untrusted_subscribe_to_puzzle_hashes(peer, True, peer_request_cache, weight_proof) + self.log.debug("End untrusted_subscribe_to_puzzle_hashes ") + + checked_call_coins = False + checked_coins: Set[bytes32] = set() + while not checked_call_coins: + # Get state for coins ids + all_coins = await self.wallet_state_manager.coin_store.get_coins_to_check(uint32(0)) + all_coin_names = [coin_record.name() for coin_record in all_coins] + removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() + all_coin_names.extend(removed_dict.keys()) + + to_check: List[bytes32] = [] + for coin_name in all_coin_names: + if coin_name in checked_coins: + continue + else: + to_check.append(coin_name) + checked_coins.add(coin_name) + if len(to_check) == 1000: + break + + msg1 = wallet_protocol.RegisterForCoinUpdates(to_check, uint32(0)) + new_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg1) + + assert new_state is not None + if syncing: + # If syncing, completely change over to this peer's information + coin_state_before_fork: List[CoinState] = new_state.coin_states + else: + # Otherwise, we only want to apply changes before the fork point, since we are synced to another peer + # We are just validating that there is no missing information + coin_state_before_fork = [] + for coin_state_entry in new_state.coin_states: + if coin_state_entry.spent_height is not None: + if coin_state_entry.spent_height <= fork_height: + coin_state_before_fork.append(coin_state_entry) + elif coin_state_entry.created_height is not None: + if coin_state_entry.created_height <= fork_height: + coin_state_before_fork.append(coin_state_entry) + + validated_state = await self.validate_received_state_from_peer( + coin_state_before_fork, peer, weight_proof, peer_request_cache, False + ) + # Apply validated state + await self.wallet_state_manager.new_coin_state(validated_state, peer, weight_proof=weight_proof) + + all_coins = await self.wallet_state_manager.coin_store.get_coins_to_check(uint32(0)) + all_coin_names = [coin_record.name() for coin_record in all_coins] + removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() + all_coin_names.extend(removed_dict.keys()) + + checked_call_coins = True + for coin_name in all_coin_names: + if coin_name not in checked_coins: + checked_call_coins = False + break + + end_time = time.time() + duration = end_time - start_time + self.log.info(f"Sync duration was: {duration}") + + async def validate_received_state_from_peer( + self, + coin_states: List[CoinState], + peer, + weight_proof: WeightProof, + peer_request_cache: PeerRequestCache, + return_old_state: bool, + ) -> List[CoinState]: + """ + Returns all state that is valid and included in the blockchain proved by the weight proof. If return_old_states + is False, only new states that are not in the coin_store are returned. + """ + assert self.wallet_state_manager is not None + all_validated_states = [] + total = len(coin_states) + for coin_idx, coin_state in enumerate(coin_states): + looked_up_coin: Optional[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_coin_record( + coin_state.coin.name() + ) + if ( + looked_up_coin is not None + and coin_state.created_height is not None + and looked_up_coin.confirmed_block_height == coin_state.created_height + ): + if looked_up_coin.spent: + if looked_up_coin.spent_block_height == coin_state.spent_height: + # Both are spent and created at same height, no need to validate + if return_old_state: + all_validated_states.append(coin_state) + continue + else: + if coin_state.spent_height is None: + # Both are not spent, no need to validate + if return_old_state: + all_validated_states.append(coin_state) + continue + if coin_state.get_hash() in peer_request_cache.states_validated: + all_validated_states.append(coin_state) + continue + self.log.info(f"Validating {coin_idx + 1} of {total}") + spent_height = coin_state.spent_height + confirmed_height = coin_state.created_height + + current = await self.wallet_state_manager.coin_store.get_coin_record(coin_state.coin.name()) + # if remote state is same as current local state we skip validation + + # CoinRecord unspent = height 0, coin state = None. We adjust for comparison bellow + current_spent_height = None + if current is not None and current.spent_block_height != 0: + current_spent_height = current.spent_block_height + + # It's possible that new state has been added before we finished validating weight proof + # We'll just ignore it here, backward sync will pick it up + wp_tip_height = weight_proof.recent_chain_data[-1].height + if (confirmed_height is not None and confirmed_height > wp_tip_height) or ( + spent_height is not None and spent_height > wp_tip_height + ): + continue + elif ( + current is not None + and current_spent_height == spent_height + and current.confirmed_block_height == confirmed_height + ): + all_validated_states.append(coin_state) + continue + else: + # Full info validation + if confirmed_height is None: + # We shouldn't receive state for non-existing coin unless we specifically ask for it + peer.close(9999) + raise ValueError("Should not receive state for non-existing coin") + + self.log.debug(f"Validating state: {coin_state}") + # request header block for created height + if confirmed_height in peer_request_cache.blocks: + state_block: HeaderBlock = peer_request_cache.blocks[confirmed_height] + else: + request = RequestHeaderBlocks(confirmed_height, confirmed_height) + res = await peer.request_header_blocks(request) + state_block = res.header_blocks[0] + peer_request_cache.blocks[confirmed_height] = state_block + + # get proof of inclusion + assert state_block.foliage_transaction_block is not None + validate_additions_result = await request_and_validate_additions( + peer, + state_block.height, + state_block.header_hash, + coin_state.coin.puzzle_hash, + state_block.foliage_transaction_block.additions_root, + ) + + if validate_additions_result is False: + peer.close(9999) + raise ValueError(f"Addition did not validate: {state_block}, {coin_state}") + + # get blocks on top of this block + + validated = await self.validate_state(weight_proof, state_block, peer, peer_request_cache) + if not validated: + raise ValueError("Validation failed") + + if spent_height is None and current is not None and current.spent_block_height != 0: + # Peer is telling us that coin that was previously known to be spent is not spent anymore + # Check old state + if spent_height in peer_request_cache.blocks: + spent_state_block: HeaderBlock = peer_request_cache.blocks[current.spent_block_height] + else: + request = RequestHeaderBlocks(current.spent_block_height, current.spent_block_height) + res = await peer.request_header_blocks(request) + spent_state_block = res.header_blocks[0] + assert spent_state_block.height == current.spent_block_height + peer_request_cache.blocks[current.spent_block_height] = spent_state_block + assert spent_state_block.foliage_transaction_block is not None + validate_removals_result: bool = await request_and_validate_removals( + peer, + current.spent_block_height, + spent_state_block.header_hash, + coin_state.coin.name(), + spent_state_block.foliage_transaction_block.removals_root, + ) + if validate_removals_result is False: + peer.close(9999) + raise ValueError("Validation failed") + validated = await self.validate_state(weight_proof, spent_state_block, peer, peer_request_cache) + if not validated: + raise ValueError("Validation failed") + + if spent_height is not None: + # request header block for created height + if spent_height in peer_request_cache.blocks: + spent_state_block = peer_request_cache.blocks[spent_height] + else: + request = RequestHeaderBlocks(spent_height, spent_height) + res = await peer.request_header_blocks(request) + spent_state_block = res.header_blocks[0] + assert spent_state_block.height == spent_height + peer_request_cache.blocks[spent_height] = spent_state_block + assert spent_state_block.foliage_transaction_block is not None + validate_removals_result = await request_and_validate_removals( + peer, + spent_state_block.height, + spent_state_block.header_hash, + coin_state.coin.name(), + spent_state_block.foliage_transaction_block.removals_root, + ) + if validate_removals_result is False: + peer.close(9999) + raise ValueError(f"Removals did not validate {spent_state_block}, {coin_state}") + validated = await self.validate_state(weight_proof, spent_state_block, peer, peer_request_cache) + if not validated: + raise ValueError("Validation failed") + all_validated_states.append(coin_state) + peer_request_cache.states_validated[coin_state.get_hash()] = coin_state + return all_validated_states + + async def validate_state( + self, weight_proof: WeightProof, block: HeaderBlock, peer, peer_request_cache: PeerRequestCache + ) -> bool: + assert self.wallet_state_manager is not None + + if block.height >= weight_proof.recent_chain_data[0].height: + # this was already validated as part of the wp validation + index = block.height - weight_proof.recent_chain_data[0].height + if weight_proof.recent_chain_data[index].header_hash != block.header_hash: + self.log.error("Failed validation 1") + return False return True - return False + else: + start = block.height + 1 + compare_to_recent = False + current_ses: Optional[SubEpochData] = None + inserted: Optional[SubEpochData] = None + first_height_recent = weight_proof.recent_chain_data[0].height + if start > first_height_recent - 1000: + compare_to_recent = True + end = first_height_recent + else: + request = RequestSESInfo(block.height, block.height + 32) + if request.get_hash() in peer_request_cache.ses_requests: + res_ses: RespondSESInfo = peer_request_cache.ses_requests[request.get_hash()] + else: + res_ses = await peer.request_ses_hashes(request) + ses_0 = res_ses.reward_chain_hash[0] + last_height = res_ses.heights[0][-1] # Last height in sub epoch + end = last_height + for idx, ses in enumerate(weight_proof.sub_epochs): + if idx > len(weight_proof.sub_epochs) - 3: + break + if ses.reward_chain_hash == ses_0: + current_ses = ses + inserted = weight_proof.sub_epochs[idx + 2] + break + if current_ses is None: + self.log.error("Failed validation 2") + return False + + blocks = [] + + for i in range(start - (start % 32), end + 1, 32): + request_start = min(uint32(i), end) + request_end = min(uint32(i + 31), end) + request_h_response = RequestHeaderBlocks(request_start, request_end) + if request_h_response.get_hash() in peer_request_cache.block_requests: + res_h_blocks: RespondHeaderBlocks = peer_request_cache.block_requests[request_h_response.get_hash()] + else: + res_h_blocks = await peer.request_header_blocks(request_h_response) + peer_request_cache.block_requests[request_h_response.get_hash()] = res_h_blocks + self.log.info(f"Fetching blocks: {request_start} - {request_end}") + blocks.extend([bl for bl in res_h_blocks.header_blocks if bl.height >= start]) + + if compare_to_recent and weight_proof.recent_chain_data[0].header_hash != blocks[-1].header_hash: + self.log.error("Failed validation 3") + return False + + reversed_blocks = blocks.copy() + reversed_blocks.reverse() + + if not compare_to_recent: + last = reversed_blocks[0].finished_sub_slots[-1].reward_chain.get_hash() + if inserted is None or last != inserted.reward_chain_hash: + self.log.error("Failed validation 4") + return False + + for idx, en_block in enumerate(reversed_blocks): + if idx == len(reversed_blocks) - 1: + next_block_rc_hash = block.reward_chain_block.get_hash() + prev_hash = block.header_hash + else: + next_block_rc_hash = reversed_blocks[idx + 1].reward_chain_block.get_hash() + prev_hash = reversed_blocks[idx + 1].header_hash + + if not en_block.prev_header_hash == prev_hash: + self.log.error("Failed validation 5") + return False + + if len(en_block.finished_sub_slots) > 0: + # What to do here + reversed_slots = en_block.finished_sub_slots.copy() + reversed_slots.reverse() + for slot_idx, slot in enumerate(reversed_slots[:-1]): + hash_val = reversed_slots[slot_idx + 1].reward_chain.get_hash() + if not hash_val == slot.reward_chain.end_of_slot_vdf.challenge: + self.log.error("Failed validation 6") + return False + if not next_block_rc_hash == reversed_slots[-1].reward_chain.end_of_slot_vdf.challenge: + self.log.error("Failed validation 7") + return False + else: + if not next_block_rc_hash == en_block.reward_chain_block.reward_chain_ip_vdf.challenge: + self.log.error("Failed validation 8") + return False + + if idx > len(reversed_blocks) - 50: + if not AugSchemeMPL.verify( + en_block.reward_chain_block.proof_of_space.plot_public_key, + en_block.foliage.foliage_block_data.get_hash(), + en_block.foliage.foliage_block_data_signature, + ): + self.log.error("Failed validation 9") + return False + return True + + async def fetch_puzzle_solution(self, peer, height: uint32, coin: Coin) -> CoinSpend: + solution_response = await peer.request_puzzle_solution( + wallet_protocol.RequestPuzzleSolution(coin.name(), height) + ) + if solution_response is None or not isinstance(solution_response, wallet_protocol.RespondPuzzleSolution): + raise ValueError(f"Was not able to obtain solution {solution_response}") + assert solution_response.response.puzzle.get_tree_hash() == coin.puzzle_hash + assert solution_response.response.coin_name == coin.name() + + return CoinSpend( + coin, + solution_response.response.puzzle.to_serialized_program(), + solution_response.response.solution.to_serialized_program(), + ) + + async def fetch_children_and_validate( + self, peer, coin_name, weight_proof: Optional[WeightProof] + ) -> List[CoinState]: + response: Optional[wallet_protocol.RespondChildren] = await peer.request_children( + wallet_protocol.RequestChildren(coin_name) + ) + if response is None or not isinstance(response, wallet_protocol.RespondChildren): + raise ValueError(f"Was not able to obtain children {response}") + if not self.is_trusted(peer): + if peer.peer_node_id in self.untrusted_caches: + request_cache = self.untrusted_caches[peer.peer_node_id] + else: + request_cache = PeerRequestCache() + assert weight_proof is not None + validated_states = await self.validate_received_state_from_peer( + response.coin_states, peer, weight_proof, request_cache, True + ) + return validated_states + + return response.coin_states + + async def fetch_children(self, peer, coin_name, weight_proof: Optional[WeightProof]) -> List[CoinState]: + response: Optional[wallet_protocol.RespondChildren] = await peer.request_children( + wallet_protocol.RequestChildren(coin_name) + ) + if response is None or not isinstance(response, wallet_protocol.RespondChildren): + raise ValueError(f"Was not able to obtain children {response}") + + return response.coin_states diff --git a/chia/wallet/wallet_node_api.py b/chia/wallet/wallet_node_api.py index 979af9df57f5..b8bc630971d4 100644 --- a/chia/wallet/wallet_node_api.py +++ b/chia/wallet/wallet_node_api.py @@ -78,13 +78,17 @@ async def transaction_ack(self, ack: wallet_protocol.TransactionAck, peer: WSChi assert peer.peer_node_id is not None name = peer.peer_node_id.hex() status = MempoolInclusionStatus(ack.status) - if self.wallet_node.wallet_state_manager is None or self.wallet_node.backup_initialized is False: + if self.wallet_node.wallet_state_manager is None: return None if status == MempoolInclusionStatus.SUCCESS: self.wallet_node.log.info(f"SpendBundle has been received and accepted to mempool by the FullNode. {ack}") elif status == MempoolInclusionStatus.PENDING: self.wallet_node.log.info(f"SpendBundle has been received (and is pending) by the FullNode. {ack}") else: + if not self.wallet_node.is_trusted(peer) and ack.error == Err.NO_TRANSACTIONS_WHILE_SYNCING.name: + self.wallet_node.log.info(f"Peer {peer.get_peer_info()} is not synced, closing connection") + await peer.close() + return self.wallet_node.log.warning(f"SpendBundle has been rejected by the FullNode. {ack}") if ack.error is not None: await self.wallet_node.wallet_state_manager.remove_from_queue(ack.txid, name, status, Err[ack.error]) @@ -96,10 +100,11 @@ async def transaction_ack(self, ack: wallet_protocol.TransactionAck, peer: WSChi async def respond_peers_introducer( self, request: introducer_protocol.RespondPeersIntroducer, peer: WSChiaConnection ): - if not self.wallet_node.has_full_node(): - await self.wallet_node.wallet_peers.respond_peers(request, peer.get_peer_info(), False) - else: - await self.wallet_node.wallet_peers.ensure_is_closed() + if self.wallet_node.wallet_peers is not None: + if not self.wallet_node.has_full_node(): + await self.wallet_node.wallet_peers.respond_peers(request, peer.get_peer_info(), False) + else: + await self.wallet_node.wallet_peers.ensure_is_closed() if peer is not None and peer.connection_type is NodeType.INTRODUCER: await peer.close() @@ -107,6 +112,8 @@ async def respond_peers_introducer( @peer_required @api_request async def respond_peers(self, request: full_node_protocol.RespondPeers, peer: WSChiaConnection): + if self.wallet_node.wallet_peers is None: + return None if not self.wallet_node.has_full_node(): self.log.info(f"Wallet received {len(request.peer_list)} peers.") await self.wallet_node.wallet_peers.respond_peers(request, peer.get_peer_info(), True) @@ -117,7 +124,7 @@ async def respond_peers(self, request: full_node_protocol.RespondPeers, peer: WS @api_request async def respond_puzzle_solution(self, request: wallet_protocol.RespondPuzzleSolution): - if self.wallet_node.wallet_state_manager is None or self.wallet_node.backup_initialized is False: + if self.wallet_node.wallet_state_manager is None: return None await self.wallet_node.wallet_state_manager.puzzle_solution_received(request) @@ -132,3 +139,28 @@ async def respond_header_blocks(self, request: wallet_protocol.RespondHeaderBloc @api_request async def reject_header_blocks(self, request: wallet_protocol.RejectHeaderBlocks): self.log.warning(f"Reject header blocks: {request}") + + @peer_required + @api_request + async def coin_state_update(self, request: wallet_protocol.CoinStateUpdate, peer: WSChiaConnection): + await self.wallet_node.state_update_received(request, peer) + + @api_request + async def respond_to_ph_update(self, request: wallet_protocol.RespondToPhUpdates): + pass + + @api_request + async def respond_to_coin_update(self, request: wallet_protocol.RespondToCoinUpdates): + pass + + @api_request + async def respond_children(self, request: wallet_protocol.RespondChildren): + pass + + @api_request + async def respond_ses_hashes(self, request: wallet_protocol.RespondSESInfo): + pass + + @api_request + async def respond_blocks(self, request: full_node_protocol.RespondBlocks) -> None: + pass diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index 0e0be16eb4c8..8034a7237810 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -43,7 +43,8 @@ async def create(cls, db_wrapper: DBWrapper, cache_size: uint32 = uint32(600000) " puzzle_hash text PRIMARY_KEY," " wallet_type int," " wallet_id int," - " used tinyint)" + " used tinyint," + " hardened tinyint)" ) ) await self.db_connection.execute( @@ -88,6 +89,10 @@ async def add_derivation_paths(self, records: List[DerivationRecord], in_transac sql_records = [] for record in records: self.all_puzzle_hashes.add(record.puzzle_hash) + if record.hardened: + hardened = 1 + else: + hardened = 0 sql_records.append( ( record.index, @@ -96,11 +101,12 @@ async def add_derivation_paths(self, records: List[DerivationRecord], in_transac record.wallet_type, record.wallet_id, 0, + hardened, ), ) cursor = await self.db_connection.executemany( - "INSERT OR REPLACE INTO derivation_paths VALUES(?, ?, ?, ?, ?, ?)", + "INSERT OR REPLACE INTO derivation_paths VALUES(?, ?, ?, ?, ?, ?, ?)", sql_records, ) @@ -110,16 +116,19 @@ async def add_derivation_paths(self, records: List[DerivationRecord], in_transac await self.db_connection.commit() self.db_wrapper.lock.release() - async def get_derivation_record(self, index: uint32, wallet_id: uint32) -> Optional[DerivationRecord]: + async def get_derivation_record( + self, index: uint32, wallet_id: uint32, hardened: bool + ) -> Optional[DerivationRecord]: """ Returns the derivation record by index and wallet id. """ + if hardened: + hard = 1 + else: + hard = 0 cursor = await self.db_connection.execute( - "SELECT * FROM derivation_paths WHERE derivation_index=? and wallet_id=?;", - ( - index, - wallet_id, - ), + "SELECT * FROM derivation_paths WHERE derivation_index=? and wallet_id=? and hardened=?;", + (index, wallet_id, hard), ) row = await cursor.fetchone() await cursor.close() @@ -131,6 +140,7 @@ async def get_derivation_record(self, index: uint32, wallet_id: uint32) -> Optio G1Element.from_bytes(bytes.fromhex(row[1])), WalletType(row[3]), uint32(row[4]), + bool(row[5]), ) return None @@ -153,6 +163,7 @@ async def get_derivation_record_for_puzzle_hash(self, puzzle_hash: bytes32) -> O G1Element.from_bytes(bytes.fromhex(row[1])), WalletType(row[3]), uint32(row[4]), + bool(row[6]), ) return None @@ -201,6 +212,16 @@ async def one_of_puzzle_hashes_exists(self, puzzle_hashes: List[bytes32]) -> boo return False + def row_to_record(self, row) -> DerivationRecord: + return DerivationRecord( + uint32(row[0]), + bytes32.fromhex(row[2]), + G1Element.from_bytes(bytes.fromhex(row[1])), + WalletType(row[3]), + uint32(row[4]), + bool(row[6]), + ) + async def index_for_pubkey(self, pubkey: G1Element) -> Optional[uint32]: """ Returns derivation paths for the given pubkey. @@ -218,6 +239,23 @@ async def index_for_pubkey(self, pubkey: G1Element) -> Optional[uint32]: return None + async def record_for_pubkey(self, pubkey: G1Element) -> Optional[DerivationRecord]: + """ + Returns derivation record for the given pubkey. + Returns None if not present. + """ + + cursor = await self.db_connection.execute( + "SELECT * from derivation_paths WHERE pubkey=?", (bytes(pubkey).hex(),) + ) + row = await cursor.fetchone() + await cursor.close() + + if row is not None: + return self.row_to_record(row) + + return None + async def index_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[uint32]: """ Returns the derivation path for the puzzle_hash. @@ -234,6 +272,22 @@ async def index_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[uint32]: return None + async def record_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[DerivationRecord]: + """ + Returns the derivation path for the puzzle_hash. + Returns None if not present. + """ + cursor = await self.db_connection.execute( + "SELECT * from derivation_paths WHERE puzzle_hash=?", (puzzle_hash.hex(),) + ) + row = await cursor.fetchone() + await cursor.close() + + if row is not None and row[0] is not None: + return self.row_to_record(row) + + return None + async def index_for_puzzle_hash_and_wallet(self, puzzle_hash: bytes32, wallet_id: uint32) -> Optional[uint32]: """ Returns the derivation path for the puzzle_hash. @@ -322,14 +376,14 @@ async def get_current_derivation_record_for_wallet(self, wallet_id: uint32) -> O """ cursor = await self.db_connection.execute( - f"SELECT MAX(derivation_index) FROM derivation_paths WHERE wallet_id={wallet_id} and used=1;" + f"SELECT MAX(derivation_index) FROM derivation_paths WHERE wallet_id={wallet_id} and used=1 and hardened=0;" ) row = await cursor.fetchone() await cursor.close() if row is not None and row[0] is not None: index = uint32(row[0]) - return await self.get_derivation_record(index, wallet_id) + return await self.get_derivation_record(index, wallet_id, False) return None @@ -337,7 +391,9 @@ async def get_unused_derivation_path(self) -> Optional[uint32]: """ Returns the first unused derivation path by derivation_index. """ - cursor = await self.db_connection.execute("SELECT MIN(derivation_index) FROM derivation_paths WHERE used=0;") + cursor = await self.db_connection.execute( + "SELECT MIN(derivation_index) FROM derivation_paths WHERE used=0 and hardened=0;" + ) row = await cursor.fetchone() await cursor.close() diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index f06b2609a1e8..283609774e14 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1,26 +1,22 @@ import asyncio -import base64 import json import logging import time from collections import defaultdict from pathlib import Path +from secrets import token_bytes from typing import Any, Callable, Dict, List, Optional, Set, Tuple import aiosqlite -from blspy import AugSchemeMPL, G1Element, PrivateKey +from blspy import G1Element, PrivateKey from chiabip158 import PyBIP158 -from cryptography.fernet import Fernet -from chia import __version__ -from chia.consensus.block_record import BlockRecord from chia.consensus.coinbase import pool_parent_id, farmer_parent_id from chia.consensus.constants import ConsensusConstants -from chia.consensus.find_fork_point import find_fork_point_in_chain -from chia.full_node.weight_proof import WeightProofHandler from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH, solution_to_pool_state from chia.pools.pool_wallet import PoolWallet -from chia.protocols.wallet_protocol import PuzzleSolutionResponse, RespondPuzzleSolution +from chia.protocols import wallet_protocol +from chia.protocols.wallet_protocol import PuzzleSolutionResponse, RespondPuzzleSolution, CoinState from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 @@ -28,32 +24,32 @@ from chia.types.full_block import FullBlock from chia.types.header_block import HeaderBlock from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.types.weight_proof import WeightProof from chia.util.byte_types import hexstr_to_bytes from chia.util.db_wrapper import DBWrapper from chia.util.errors import Err -from chia.util.hash import std_hash -from chia.util.ints import uint32, uint64, uint128 +from chia.util.ints import uint32, uint64, uint128, uint8 from chia.util.db_synchronous import db_synchronous_on -from chia.wallet.block_record import HeaderBlockRecord -from chia.wallet.cc_wallet.cc_wallet import CCWallet +from chia.wallet.cat_wallet.cat_utils import match_cat_puzzle, construct_cat_puzzle +from chia.wallet.cat_wallet.cat_wallet import CATWallet +from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.derivation_record import DerivationRecord -from chia.wallet.derive_keys import master_sk_to_backup_sk, master_sk_to_wallet_sk +from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened from chia.wallet.key_val_store import KeyValStore +from chia.wallet.puzzles.cat_loader import CAT_MOD from chia.wallet.rl_wallet.rl_wallet import RLWallet from chia.wallet.settings.user_settings import UserSettings from chia.wallet.trade_manager import TradeManager from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.backup_utils import open_backup_file from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet import Wallet from chia.wallet.wallet_action import WalletAction from chia.wallet.wallet_action_store import WalletActionStore -from chia.wallet.wallet_block_store import WalletBlockStore from chia.wallet.wallet_blockchain import WalletBlockchain from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_coin_store import WalletCoinStore -from chia.wallet.wallet_info import WalletInfo, WalletInfoBackup +from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_interested_store import WalletInterestedStore from chia.wallet.wallet_pool_store import WalletPoolStore from chia.wallet.wallet_puzzle_store import WalletPuzzleStore @@ -62,6 +58,7 @@ from chia.wallet.wallet_user_store import WalletUserStore from chia.server.server import ChiaServer from chia.wallet.did_wallet.did_wallet import DIDWallet +from chia.wallet.wallet_weight_proof_handler import WalletWeightProofHandler def get_balance_from_coin_records(coin_records: Set[WalletCoinRecord]) -> uint128: @@ -95,8 +92,10 @@ class WalletStateManager: state_changed_callback: Optional[Callable] pending_tx_callback: Optional[Callable] + subscribe_to_new_puzzle_hash: Any + subscribe_to_coin_ids_update: Any + get_coin_state: Any puzzle_hash_created_callbacks: Dict = defaultdict(lambda *x: None) - new_peak_callbacks: Dict = defaultdict(lambda *x: None) db_path: Path db_connection: aiosqlite.Connection db_wrapper: DBWrapper @@ -108,15 +107,16 @@ class WalletStateManager: trade_manager: TradeManager new_wallet: bool user_settings: UserSettings - blockchain: Any - block_store: WalletBlockStore + blockchain: WalletBlockchain coin_store: WalletCoinStore sync_store: WalletSyncStore interested_store: WalletInterestedStore - pool_store: WalletPoolStore - weight_proof_handler: Any + weight_proof_handler: WalletWeightProofHandler server: ChiaServer root_path: Path + wallet_node: Any + pool_store: WalletPoolStore + default_cats: Dict[str, Any] @staticmethod async def create( @@ -126,9 +126,17 @@ async def create( constants: ConsensusConstants, server: ChiaServer, root_path: Path, + subscribe_to_new_puzzle_hash, + get_coin_state, + subscribe_to_coin_ids, + wallet_node, name: str = None, ): self = WalletStateManager() + self.subscribe_to_new_puzzle_hash = subscribe_to_new_puzzle_hash + self.get_coin_state = get_coin_state + self.subscribe_to_coin_ids_update = subscribe_to_coin_ids + self.new_wallet = False self.config = config self.constants = constants self.server = server @@ -152,27 +160,14 @@ async def create( self.basic_store = await KeyValStore.create(self.db_wrapper) self.trade_manager = await TradeManager.create(self, self.db_wrapper) self.user_settings = await UserSettings.create(self.basic_store) - self.block_store = await WalletBlockStore.create(self.db_wrapper) - self.interested_store = await WalletInterestedStore.create(self.db_wrapper) self.pool_store = await WalletPoolStore.create(self.db_wrapper) + self.interested_store = await WalletInterestedStore.create(self.db_wrapper) + self.default_cats = DEFAULT_CATS - reserved_cores = self.config.get("reserved_cores", 2) - - self.blockchain = await WalletBlockchain.create( - self.block_store, - self.coin_store, - self.tx_store, - self.pool_store, - self.constants, - self.new_transaction_block_callback, - self.reorg_rollback, - self.lock, - reserved_cores, - ) - self.weight_proof_handler = WeightProofHandler(self.constants, self.blockchain) - + self.wallet_node = wallet_node self.sync_mode = False - self.sync_store = await WalletSyncStore.create() + self.weight_proof_handler = WalletWeightProofHandler(self.constants) + self.blockchain = await WalletBlockchain.create(self.basic_store, self.constants, self.weight_proof_handler) self.state_changed_callback = None self.pending_tx_callback = None @@ -192,8 +187,8 @@ async def create( if wallet_info.id == 1: continue wallet = await Wallet.create(config, wallet_info) - elif wallet_info.type == WalletType.COLOURED_COIN: - wallet = await CCWallet.create( + elif wallet_info.type == WalletType.CAT: + wallet = await CATWallet.create( self, self.main_wallet, wallet_info, @@ -215,28 +210,24 @@ async def create( if wallet is not None: self.wallets[wallet_info.id] = wallet - async with self.puzzle_store.lock: - index = await self.puzzle_store.get_last_derivation_path() - if index is None or index < self.config["initial_num_public_keys"] - 1: - await self.create_more_puzzle_hashes(from_zero=True) - return self - @property - def peak(self) -> Optional[BlockRecord]: - peak = self.blockchain.get_peak() - return peak - def get_derivation_index(self, pubkey: G1Element, max_depth: int = 1000) -> int: for i in range(0, max_depth): derived = self.get_public_key(uint32(i)) if derived == pubkey: return i + derived = self.get_public_key_unhardened(uint32(i)) + if derived == pubkey: + return i return -1 def get_public_key(self, index: uint32) -> G1Element: return master_sk_to_wallet_sk(self.private_key, index).get_g1() + def get_public_key_unhardened(self, index: uint32) -> G1Element: + return master_sk_to_wallet_sk_unhardened(self.private_key, index).get_g1() + async def load_wallets(self): for wallet_info in await self.get_all_wallet_info_entries(): if wallet_info.id in self.wallets: @@ -247,8 +238,8 @@ async def load_wallets(self): wallet = await Wallet.create(self.config, wallet_info) self.wallets[wallet_info.id] = wallet # TODO add RL AND DiD WALLETS HERE - elif wallet_info.type == WalletType.COLOURED_COIN: - wallet = await CCWallet.create( + elif wallet_info.type == WalletType.CAT: + wallet = await CATWallet.create( self, self.main_wallet, wallet_info, @@ -263,10 +254,14 @@ async def load_wallets(self): self.wallets[wallet_info.id] = wallet async def get_keys(self, puzzle_hash: bytes32) -> Optional[Tuple[G1Element, PrivateKey]]: - index_for_puzzlehash = await self.puzzle_store.index_for_puzzle_hash(puzzle_hash) - if index_for_puzzlehash is None: + record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash) + if record is None: raise ValueError(f"No key for this puzzlehash {puzzle_hash})") - private = master_sk_to_wallet_sk(self.private_key, index_for_puzzlehash) + if record.hardened: + private = master_sk_to_wallet_sk(self.private_key, record.index) + pubkey = private.get_g1() + return pubkey, private + private = master_sk_to_wallet_sk_unhardened(self.private_key, record.index) pubkey = private.get_g1() return pubkey, private @@ -305,50 +300,43 @@ async def create_more_puzzle_hashes(self, from_zero: bool = False, in_transactio for index in range(start_index, unused + to_generate): if WalletType(target_wallet.type()) == WalletType.POOLING_WALLET: continue - if WalletType(target_wallet.type()) == WalletType.RATE_LIMITED: - if target_wallet.rl_info.initialized is False: - break - wallet_type = target_wallet.rl_info.type - if wallet_type == "user": - rl_pubkey = G1Element.from_bytes(target_wallet.rl_info.user_pubkey) - else: - rl_pubkey = G1Element.from_bytes(target_wallet.rl_info.admin_pubkey) - rl_puzzle: Program = target_wallet.puzzle_for_pk(rl_pubkey) - puzzle_hash: bytes32 = rl_puzzle.get_tree_hash() - - rl_index = self.get_derivation_index(rl_pubkey) - if rl_index == -1: - break - - derivation_paths.append( - DerivationRecord( - uint32(rl_index), - puzzle_hash, - rl_pubkey, - target_wallet.type(), - uint32(target_wallet.id()), - ) - ) - break + # Hardened pubkey: G1Element = self.get_public_key(uint32(index)) puzzle: Program = target_wallet.puzzle_for_pk(bytes(pubkey)) if puzzle is None: - self.log.warning(f"Unable to create puzzles with wallet {target_wallet}") + self.log.error(f"Unable to create puzzles with wallet {target_wallet}") break puzzlehash: bytes32 = puzzle.get_tree_hash() self.log.info(f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash.hex()}") + derivation_paths.append( + DerivationRecord( + uint32(index), puzzlehash, pubkey, target_wallet.type(), uint32(target_wallet.id()), True + ) + ) + # Unhardened + pubkey_unhardened: G1Element = self.get_public_key_unhardened(uint32(index)) + puzzle_unhardened: Program = target_wallet.puzzle_for_pk(bytes(pubkey_unhardened)) + if puzzle_unhardened is None: + self.log.error(f"Unable to create puzzles with wallet {target_wallet}") + break + puzzlehash_unhardened: bytes32 = puzzle_unhardened.get_tree_hash() + self.log.info( + f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" + ) derivation_paths.append( DerivationRecord( uint32(index), - puzzlehash, - pubkey, + puzzlehash_unhardened, + pubkey_unhardened, target_wallet.type(), uint32(target_wallet.id()), + False, ) ) - + puzzle_hashes = [record.puzzle_hash for record in derivation_paths] await self.puzzle_store.add_derivation_paths(derivation_paths, in_transaction) + await self.subscribe_to_new_puzzle_hash(puzzle_hashes) if unused > 0: await self.puzzle_store.set_used_up_to(uint32(unused - 1), in_transaction) @@ -364,7 +352,8 @@ async def update_wallet_puzzle_hashes(self, wallet_id): # This handles the case where the database is empty unused = uint32(0) for index in range(unused, last): - pubkey: G1Element = self.get_public_key(uint32(index)) + # Since DID are not released yet we can assume they are only using unhardened keys derivation + pubkey: G1Element = self.get_public_key_unhardened(uint32(index)) puzzle: Program = target_wallet.puzzle_for_pk(bytes(pubkey)) puzzlehash: bytes32 = puzzle.get_tree_hash() self.log.info(f"Generating public key at index {index} puzzle hash {puzzlehash.hex()}") @@ -375,11 +364,14 @@ async def update_wallet_puzzle_hashes(self, wallet_id): pubkey, target_wallet.wallet_info.type, uint32(target_wallet.wallet_info.id), + False, ) ) await self.puzzle_store.add_derivation_paths(derivation_paths) - async def get_unused_derivation_record(self, wallet_id: uint32, in_transaction=False) -> DerivationRecord: + async def get_unused_derivation_record( + self, wallet_id: uint32, in_transaction=False, hardened=False + ) -> DerivationRecord: """ Creates a puzzle hash for the given wallet, and then makes more puzzle hashes for every wallet to ensure we always have more in the database. Never reusue the @@ -394,7 +386,9 @@ async def get_unused_derivation_record(self, wallet_id: uint32, in_transaction=F # Now we must have unused public keys unused = await self.puzzle_store.get_unused_derivation_path() assert unused is not None - record: Optional[DerivationRecord] = await self.puzzle_store.get_derivation_record(unused, wallet_id) + record: Optional[DerivationRecord] = await self.puzzle_store.get_derivation_record( + unused, wallet_id, hardened + ) assert record is not None # Set this key to used so we never use it again @@ -430,12 +424,6 @@ def set_coin_with_puzzlehash_created_callback(self, puzzlehash: bytes32, callbac """ self.puzzle_hash_created_callbacks[puzzlehash] = callback - def set_new_peak_callback(self, wallet_id: int, callback: Callable): - """ - Callback to be called when blockchain adds new peak - """ - self.new_peak_callbacks[wallet_id] = callback - async def puzzle_hash_created(self, coin: Coin): callback = self.puzzle_hash_created_callbacks[coin.puzzle_hash] if callback is None: @@ -462,18 +450,13 @@ def tx_pending_changed(self) -> None: self.pending_tx_callback() async def synced(self): - if self.sync_mode is True: - return False - peak: Optional[BlockRecord] = self.blockchain.get_peak() - if peak is None: + latest = await self.blockchain.get_peak_block() + if latest is None: return False - curr = peak - while not curr.is_transaction_block and not curr.height == 0: - curr = self.blockchain.try_block_record(curr.prev_hash) - if curr is None: - return False - if curr.is_transaction_block and curr.timestamp > int(time.time()) - 7 * 60: + latest_timestamp = self.blockchain.get_latest_timestamp() + + if latest_timestamp > int(time.time()) - 10 * 60: return True return False @@ -528,19 +511,13 @@ async def get_confirmed_balance_for_wallet( """ Returns the confirmed balance, including coinbase rewards that are not spendable. """ - # lock only if unspent_coin_records is None. - # This API should change so that get_balance_from_coin_records is called for Set[WalletCoinRecord] - # and this method is called only for the unspent_coin_records==None case. + # lock only if unspent_coin_records is None if unspent_coin_records is None: - unspent_coin_records = await self.get_confirmed_balance_for_wallet_with_lock(wallet_id) - return get_balance_from_coin_records(unspent_coin_records) - - async def get_confirmed_balance_for_wallet_with_lock(self, wallet_id: int) -> Set[WalletCoinRecord]: - if self.lock.locked() is True: - # raise AssertionError("expected wallet_state_manager to be unlocked") - pass - async with self.lock: - return await self.coin_store.get_unspent_coins_for_wallet(wallet_id) + unspent_coin_records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id) + amount: uint128 = uint128(0) + for record in unspent_coin_records: + amount = uint128(amount + record.coin.amount) + return uint128(amount) async def get_unconfirmed_balance( self, wallet_id, unspent_coin_records: Optional[Set[WalletCoinRecord]] = None @@ -599,225 +576,386 @@ async def unconfirmed_removals_for_wallet(self, wallet_id: int) -> Dict[bytes32, removals[coin.name()] = coin return removals - async def new_transaction_block_callback( - self, - removals: List[Coin], - additions: List[Coin], - block: BlockRecord, - additional_coin_spends: List[CoinSpend], - ): - height: uint32 = block.height - for coin in additions: - await self.puzzle_hash_created(coin) - trade_additions, added = await self.coins_of_interest_added(additions, block) - trade_removals, removed = await self.coins_of_interest_removed(removals, height) - if len(trade_additions) > 0 or len(trade_removals) > 0: - await self.trade_manager.coins_of_interest_farmed(trade_removals, trade_additions, height) - - if len(additional_coin_spends) > 0: - created_pool_wallet_ids: List[int] = [] - for cs in additional_coin_spends: - if cs.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH: - already_have = False - pool_state = None - for wallet_id, wallet in self.wallets.items(): - if ( - wallet.type() == WalletType.POOLING_WALLET - and (await wallet.get_current_state()).launcher_id == cs.coin.name() - ): - self.log.warning("Already have, not recreating") - already_have = True - if not already_have: - try: - pool_state = solution_to_pool_state(cs) - except Exception as e: - self.log.debug(f"Not a pool wallet launcher {e}") - continue - if pool_state is None: - self.log.debug("Not a pool wallet launcher") - continue - self.log.info("Found created launcher. Creating pool wallet") - pool_wallet = await PoolWallet.create( - self, self.main_wallet, cs.coin.name(), additional_coin_spends, height, True, "pool_wallet" - ) - created_pool_wallet_ids.append(pool_wallet.wallet_id) + async def fetch_parent_and_check_for_cat(self, peer, coin_state) -> Tuple[Optional[uint32], Optional[WalletType]]: + if self.is_pool_reward(coin_state.created_height, coin_state.coin.parent_coin_info) or self.is_farmer_reward( + coin_state.created_height, coin_state.coin.parent_coin_info + ): + return None, None - for wallet_id, wallet in self.wallets.items(): - if wallet.type() == WalletType.POOLING_WALLET: - await wallet.apply_state_transitions(additional_coin_spends, height) + response: List[CoinState] = await self.wallet_node.get_coin_state([coin_state.coin.parent_coin_info]) + if len(response) == 0: + return None, None + parent_coin_state = response[0] + assert parent_coin_state.spent_height == coin_state.created_height + wallet_id = None + wallet_type = None + cs: CoinSpend = await self.wallet_node.fetch_puzzle_solution( + peer, parent_coin_state.spent_height, parent_coin_state.coin + ) + matched, curried_args = match_cat_puzzle(Program.from_bytes(bytes(cs.puzzle_reveal))) - added_notified = set() - removed_notified = set() - for coin_record in added: - if coin_record.wallet_id in added_notified: - continue - added_notified.add(coin_record.wallet_id) - self.state_changed("coin_added", coin_record.wallet_id) - for coin_record in removed: - if coin_record.wallet_id in removed_notified: - continue - removed_notified.add(coin_record.wallet_id) - self.state_changed("coin_removed", coin_record.wallet_id) + if matched: + mod_hash, tail_hash, inner_puzzle = curried_args + inner_puzzle_hash = inner_puzzle.get_tree_hash() + self.log.info( + f"parent: {parent_coin_state.coin.name()} inner_puzzle_hash for parent is {inner_puzzle_hash}" + ) - self.tx_pending_changed() + hint_list = cs.hints() + derivation_record = None + for hint in hint_list: + derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(bytes32(hint)) + if derivation_record is not None: + break - async def coins_of_interest_added( - self, coins: List[Coin], block: BlockRecord - ) -> Tuple[List[Coin], List[WalletCoinRecord]]: - ( - trade_removals, - trade_additions, - ) = await self.trade_manager.get_coins_of_interest() - trade_adds: List[Coin] = [] - height = block.height - - pool_rewards = set() - farmer_rewards = set() - added = [] - - prev = await self.blockchain.get_block_record_from_db(block.prev_hash) - # [block 1] [block 2] [tx block 3] [block 4] [block 5] [tx block 6] - # [tx block 6] will contain rewards for [block 1] [block 2] [tx block 3] - while prev is not None: - # step 1 find previous block - if prev.is_transaction_block: - break - prev = await self.blockchain.get_block_record_from_db(prev.prev_hash) - - if prev is not None: - # include last block - pool_parent = pool_parent_id(uint32(prev.height), self.constants.GENESIS_CHALLENGE) - farmer_parent = farmer_parent_id(uint32(prev.height), self.constants.GENESIS_CHALLENGE) - pool_rewards.add(pool_parent) - farmer_rewards.add(farmer_parent) - prev = await self.blockchain.get_block_record_from_db(prev.prev_hash) - - while prev is not None: - # step 2 traverse from previous block to the block before it - pool_parent = pool_parent_id(uint32(prev.height), self.constants.GENESIS_CHALLENGE) - farmer_parent = farmer_parent_id(uint32(prev.height), self.constants.GENESIS_CHALLENGE) - pool_rewards.add(pool_parent) - farmer_rewards.add(farmer_parent) - if prev.is_transaction_block: - break - prev = await self.blockchain.get_block_record_from_db(prev.prev_hash) - wallet_ids: Set[int] = set() - for coin in coins: - info = await self.puzzle_store.wallet_info_for_puzzle_hash(coin.puzzle_hash) - if info is not None: - wallet_ids.add(info[0]) + if derivation_record is None: + self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}") + else: + our_inner_puzzle: Program = self.main_wallet.puzzle_for_pk(bytes(derivation_record.pubkey)) + cat_puzzle = construct_cat_puzzle(CAT_MOD, bytes32(bytes(tail_hash)[1:]), our_inner_puzzle) + if cat_puzzle.get_tree_hash() != coin_state.coin.puzzle_hash: + return None, None + if bytes(tail_hash).hex()[2:] in self.default_cats: + cat_wallet = await CATWallet.create_wallet_for_cat( + self, self.main_wallet, bytes(tail_hash).hex()[2:] + ) + wallet_id = cat_wallet.id() + wallet_type = WalletType(cat_wallet.type()) + self.state_changed("wallet_created") - all_outgoing_tx: Dict[int, List[TransactionRecord]] = {} - for wallet_id in wallet_ids: - all_outgoing_tx[wallet_id] = await self.tx_store.get_all_transactions_for_wallet( - wallet_id, TransactionType.OUTGOING_TX - ) + return wallet_id, wallet_type + + async def new_coin_state( + self, + coin_states: List[CoinState], + peer, + fork_height: Optional[uint32] = None, + current_height: Optional[uint32] = None, + weight_proof: Optional[WeightProof] = None, + ): + created_h_none = [] + for coin_st in coin_states.copy(): + if coin_st.created_height is None: + coin_states.remove(coin_st) + created_h_none.append(coin_st) + coin_states.sort(key=lambda x: x.created_height, reverse=False) # type: ignore + coin_states.extend(created_h_none) + all_outgoing_per_wallet: Dict[int, List[TransactionRecord]] = {} + trade_removals = await self.trade_manager.get_coins_of_interest() + all_unconfirmed: List[TransactionRecord] = await self.tx_store.get_all_unconfirmed() + trade_coin_removed: List[CoinState] = [] - for coin in coins: - if coin.name() in trade_additions: - trade_adds.append(coin) + if fork_height is not None and current_height is not None and fork_height != current_height - 1: + # This only applies to trusted mode + await self.reorg_rollback(fork_height) - is_coinbase = False - is_fee_reward = False - if coin.parent_coin_info in pool_rewards: - is_coinbase = True - if coin.parent_coin_info in farmer_rewards: - is_fee_reward = True + for coin_state_idx, coin_state in enumerate(coin_states): + info = await self.get_wallet_id_for_puzzle_hash(coin_state.coin.puzzle_hash) + local_record: Optional[WalletCoinRecord] = await self.coin_store.get_coin_record(coin_state.coin.name()) + self.log.info(f"new_coin_state received ({coin_state_idx + 1} / {len(coin_states)})") + self.log.debug(f"{coin_state.coin.name()}: {coin_state}") - info = await self.puzzle_store.wallet_info_for_puzzle_hash(coin.puzzle_hash) + wallet_id = None + wallet_type = None if info is not None: wallet_id, wallet_type = info - added_coin_record = await self.coin_added( - coin, - is_coinbase, - is_fee_reward, - uint32(wallet_id), - wallet_type, - height, - all_outgoing_tx.get(wallet_id, []), - ) - added.append(added_coin_record) + elif local_record is not None: + wallet_id = uint32(local_record.wallet_id) + wallet_type = local_record.wallet_type + elif coin_state.created_height is not None: + wallet_id, wallet_type = await self.fetch_parent_and_check_for_cat(peer, coin_state) + + if wallet_id is None or wallet_type is None: + self.log.info(f"No wallet for coin state: {coin_state}") + continue + + if wallet_id in all_outgoing_per_wallet: + all_outgoing = all_outgoing_per_wallet[wallet_id] else: - interested_wallet_id = await self.interested_store.get_interested_puzzle_hash_wallet_id( - puzzle_hash=coin.puzzle_hash + all_outgoing = await self.tx_store.get_all_transactions_for_wallet( + wallet_id, TransactionType.OUTGOING_TX ) - if interested_wallet_id is not None: - wallet_type = self.wallets[uint32(interested_wallet_id)].type() - added_coin_record = await self.coin_added( - coin, - is_coinbase, - is_fee_reward, - uint32(interested_wallet_id), - wallet_type, - height, - all_outgoing_tx.get(interested_wallet_id, []), - ) - added.append(added_coin_record) + all_outgoing_per_wallet[wallet_id] = all_outgoing - derivation_index = await self.puzzle_store.index_for_puzzle_hash(coin.puzzle_hash) + derivation_index = await self.puzzle_store.index_for_puzzle_hash(coin_state.coin.puzzle_hash) if derivation_index is not None: await self.puzzle_store.set_used_up_to(derivation_index, True) - return trade_adds, added - - async def coins_of_interest_removed( - self, coins: List[Coin], height: uint32 - ) -> Tuple[List[Coin], List[WalletCoinRecord]]: - # This gets called when coins of our interest are spent on chain - if len(coins) > 0: - self.log.info(f"Coins removed {coins} at height: {height}") - ( - trade_removals, - trade_additions, - ) = await self.trade_manager.get_coins_of_interest() - - # Keep track of trade coins that are removed - trade_coin_removed: List[Coin] = [] - removed = [] - all_unconfirmed: List[TransactionRecord] = await self.tx_store.get_all_unconfirmed() - for coin in coins: - record = await self.coin_store.get_coin_record(coin.name()) - if coin.name() in trade_removals: - trade_coin_removed.append(coin) - if record is None: - self.log.info(f"Record for removed coin {coin.name()} is None. (ephemeral)") + if coin_state.created_height is None: + # TODO implements this coin got reorged + pass + elif coin_state.created_height is not None and coin_state.spent_height is None: + await self.coin_added(coin_state.coin, coin_state.created_height, all_outgoing, wallet_id, wallet_type) + elif coin_state.created_height is not None and coin_state.spent_height is not None: + self.log.info(f"Coin Removed: {coin_state}") + record = await self.coin_store.get_coin_record(coin_state.coin.name()) + if coin_state.coin.name() in trade_removals: + trade_coin_removed.append(coin_state) + children: Optional[List[CoinState]] = None + if record is None: + farmer_reward = False + pool_reward = False + if self.is_farmer_reward(coin_state.created_height, coin_state.coin.parent_coin_info): + farmer_reward = True + elif self.is_pool_reward(coin_state.created_height, coin_state.coin.parent_coin_info): + pool_reward = True + record = WalletCoinRecord( + coin_state.coin, + coin_state.created_height, + coin_state.spent_height, + True, + farmer_reward or pool_reward, + wallet_type, + wallet_id, + ) + await self.coin_store.add_coin_record(record) + # Coin first received + coin_record: Optional[WalletCoinRecord] = await self.coin_store.get_coin_record( + coin_state.coin.parent_coin_info + ) + if coin_record is not None and wallet_type.value == coin_record.wallet_type: + change = True + else: + change = False + + if not change: + created_timestamp = await self.wallet_node.get_timestamp_for_height(coin_state.created_height) + tx_record = TransactionRecord( + confirmed_at_height=coin_state.created_height, + created_at_time=uint64(created_timestamp), + to_puzzle_hash=coin_state.coin.puzzle_hash, + amount=uint64(coin_state.coin.amount), + fee_amount=uint64(0), + confirmed=True, + sent=uint32(0), + spend_bundle=None, + additions=[coin_state.coin], + removals=[], + wallet_id=wallet_id, + sent_to=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=bytes32(token_bytes()), + memos=[], + ) + await self.tx_store.add_transaction_record(tx_record, False) + + children = await self.wallet_node.fetch_children(peer, coin_state.coin.name(), weight_proof) + assert children is not None + additions = [state.coin for state in children] + if len(children) > 0: + cs: CoinSpend = await self.wallet_node.fetch_puzzle_solution( + peer, coin_state.spent_height, coin_state.coin + ) + + fee = cs.reserved_fee() + + to_puzzle_hash = None + # Find coin that doesn't belong to us + amount = 0 + for coin in additions: + derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash( + coin.puzzle_hash + ) + if derivation_record is None: + to_puzzle_hash = coin.puzzle_hash + amount += coin.amount + + if to_puzzle_hash is None: + to_puzzle_hash = additions[0].puzzle_hash + + spent_timestamp = await self.wallet_node.get_timestamp_for_height(coin_state.spent_height) + + # Reorg rollback adds reorged transactions so it's possible there is tx_record already + # Even though we are just adding coin record to the db (after reorg) + tx_records: List[TransactionRecord] = [] + for out_tx_record in all_outgoing: + for rem_coin in out_tx_record.removals: + if rem_coin.name() == coin_state.coin.name(): + tx_records.append(out_tx_record) + + if len(tx_records) > 0: + for tx_record in tx_records: + await self.tx_store.set_confirmed(tx_record.name, coin_state.spent_height) + else: + tx_record = TransactionRecord( + confirmed_at_height=coin_state.spent_height, + created_at_time=uint64(spent_timestamp), + to_puzzle_hash=to_puzzle_hash, + amount=uint64(int(amount)), + fee_amount=uint64(fee), + confirmed=True, + sent=uint32(0), + spend_bundle=None, + additions=additions, + removals=[coin_state.coin], + wallet_id=wallet_id, + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=bytes32(token_bytes()), + memos=[], + ) + + await self.tx_store.add_transaction_record(tx_record, False) + else: + await self.coin_store.set_spent(coin_state.coin.name(), coin_state.spent_height) + rem_tx_records: List[TransactionRecord] = [] + for out_tx_record in all_outgoing: + for rem_coin in out_tx_record.removals: + if rem_coin.name() == coin_state.coin.name(): + rem_tx_records.append(out_tx_record) + + for tx_record in rem_tx_records: + await self.tx_store.set_confirmed(tx_record.name, coin_state.spent_height) + await self.coin_store.db_connection.commit() + for unconfirmed_record in all_unconfirmed: + for rem_coin in unconfirmed_record.removals: + if rem_coin.name() == coin_state.coin.name(): + self.log.info(f"Setting tx_id: {unconfirmed_record.name} to confirmed") + await self.tx_store.set_confirmed(unconfirmed_record.name, coin_state.spent_height) + + if record.wallet_type == WalletType.POOLING_WALLET: + cs = await self.wallet_node.fetch_puzzle_solution(peer, coin_state.spent_height, coin_state.coin) + wallet = self.wallets[uint32(record.wallet_id)] + await wallet.apply_state_transitions(cs, coin_state.spent_height) + if len(cs.additions()) > 0: + added_pool_coin = cs.additions()[0] + await self.coin_added( + added_pool_coin, + coin_state.spent_height, + [], + uint32(record.wallet_id), + record.wallet_type, + ) + await self.add_interested_coin_id(added_pool_coin.name()) + + # Check if a child is a singleton launcher + if children is None: + children = await self.wallet_node.fetch_children(peer, coin_state.coin.name(), weight_proof) + assert children is not None + for child in children: + if child.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH: + continue + if await self.have_a_pool_wallet_with_launched_id(child.coin.name()): + continue + launcher_spend: CoinSpend = await self.wallet_node.fetch_puzzle_solution( + peer, coin_state.spent_height, child.coin + ) + pool_state = None + try: + pool_state = solution_to_pool_state(launcher_spend) + except Exception as e: + self.log.debug(f"Not a pool wallet launcher {e}") + continue + assert pool_state is not None + assert child.spent_height is not None + pool_wallet = await PoolWallet.create( + self, + self.main_wallet, + child.coin.name(), + [launcher_spend], + child.spent_height, + False, + "pool_wallet", + ) + await pool_wallet.apply_state_transitions(launcher_spend, coin_state.spent_height) + coin_added = launcher_spend.additions()[0] + await self.coin_added( + coin_added, coin_state.spent_height, [], pool_wallet.id(), WalletType(pool_wallet.type()) + ) + await self.add_interested_coin_id(coin_added.name()) + else: - await self.coin_store.set_spent(coin.name(), height) - for unconfirmed_record in all_unconfirmed: - for rem_coin in unconfirmed_record.removals: - if rem_coin.name() == coin.name(): - self.log.info(f"Setting tx_id: {unconfirmed_record.name} to confirmed") - await self.tx_store.set_confirmed(unconfirmed_record.name, height) - if record is not None: - removed.append(record) + raise RuntimeError("All cases already handled") # Logic error, all cases handled + + for coin_state_removed in trade_coin_removed: + await self.trade_manager.coins_of_interest_farmed(coin_state_removed) + + async def have_a_pool_wallet_with_launched_id(self, launcher_id: bytes32) -> bool: + for wallet_id, wallet in self.wallets.items(): + if ( + wallet.type() == WalletType.POOLING_WALLET + and (await wallet.get_current_state()).launcher_id == launcher_id + ): + self.log.warning("Already have, not recreating") + return True + return False - return trade_coin_removed, removed + def is_pool_reward(self, created_height, parent_id): + for i in range(0, 30): + try_height = created_height - i + if try_height < 0: + break + calculated = pool_parent_id(try_height, self.constants.GENESIS_CHALLENGE) + if calculated == parent_id: + return True + return False + + def is_farmer_reward(self, created_height, parent_id): + for i in range(0, 30): + try_height = created_height - i + if try_height < 0: + break + calculated = farmer_parent_id(try_height, self.constants.GENESIS_CHALLENGE) + if calculated == parent_id: + return True + return False + + async def get_wallet_id_for_puzzle_hash(self, puzzle_hash) -> Optional[Tuple[uint32, WalletType]]: + info = await self.puzzle_store.wallet_info_for_puzzle_hash(puzzle_hash) + if info is not None: + wallet_id, wallet_type = info + return wallet_id, wallet_type + + interested_wallet_id = await self.interested_store.get_interested_puzzle_hash_wallet_id(puzzle_hash=puzzle_hash) + if interested_wallet_id is not None: + wallet_id = uint32(interested_wallet_id) + wallet_type = WalletType(self.wallets[uint32(wallet_id)].type()) + return wallet_id, wallet_type + return None async def coin_added( self, coin: Coin, - coinbase: bool, - fee_reward: bool, - wallet_id: uint32, - wallet_type: WalletType, height: uint32, all_outgoing_transaction_records: List[TransactionRecord], - ) -> WalletCoinRecord: + wallet_id: uint32, + wallet_type: WalletType, + ) -> Optional[WalletCoinRecord]: """ - Adding coin to DB + Adding coin to DB, return wallet coin record if it get's added """ - self.log.info(f"Adding coin: {coin} at {height}") + existing: Optional[WalletCoinRecord] = await self.coin_store.get_coin_record(coin.name()) + if existing is not None: + return None + + self.log.info(f"Adding coin: {coin} at {height} wallet_id:{wallet_id}") + farmer_reward = False + pool_reward = False + if self.is_farmer_reward(height, coin.parent_coin_info): + farmer_reward = True + elif self.is_pool_reward(height, coin.parent_coin_info): + pool_reward = True + farm_reward = False - if coinbase or fee_reward: + coin_record: Optional[WalletCoinRecord] = await self.coin_store.get_coin_record(coin.parent_coin_info) + if coin_record is not None and wallet_type.value == coin_record.wallet_type: + change = True + else: + change = False + + if farmer_reward or pool_reward: farm_reward = True - now = uint64(int(time.time())) - if coinbase: + if pool_reward: tx_type: int = TransactionType.COINBASE_REWARD.value else: tx_type = TransactionType.FEE_REWARD.value + timestamp = await self.wallet_node.get_timestamp_for_height(height) + tx_record = TransactionRecord( confirmed_at_height=uint32(height), - created_at_time=now, + created_at_time=timestamp, to_puzzle_hash=coin.puzzle_hash, amount=coin.amount, fee_amount=uint64(0), @@ -831,6 +969,7 @@ async def coin_added( trade_id=None, type=uint32(tx_type), name=coin.name(), + memos=[], ) await self.tx_store.add_transaction_record(tx_record, True) else: @@ -845,11 +984,11 @@ async def coin_added( for record in records: if record.confirmed is False: await self.tx_store.set_confirmed(record.name, height) - else: - now = uint64(int(time.time())) + elif not change: + timestamp = await self.wallet_node.get_timestamp_for_height(height) tx_record = TransactionRecord( confirmed_at_height=uint32(height), - created_at_time=now, + created_at_time=timestamp, to_puzzle_hash=coin.puzzle_hash, amount=coin.amount, fee_amount=uint64(0), @@ -863,20 +1002,22 @@ async def coin_added( trade_id=None, type=uint32(TransactionType.INCOMING_TX.value), name=coin.name(), + memos=[], ) if coin.amount > 0: await self.tx_store.add_transaction_record(tx_record, True) - coin_record: WalletCoinRecord = WalletCoinRecord( + coin_record_1: WalletCoinRecord = WalletCoinRecord( coin, height, uint32(0), False, farm_reward, wallet_type, wallet_id ) - await self.coin_store.add_coin_record(coin_record) + await self.coin_store.add_coin_record(coin_record_1) - if wallet_type == WalletType.COLOURED_COIN or wallet_type == WalletType.DISTRIBUTED_ID: + if wallet_type == WalletType.CAT or wallet_type == WalletType.DISTRIBUTED_ID: wallet = self.wallets[wallet_id] await wallet.coin_added(coin, height) - return coin_record + await self.create_more_puzzle_hashes() + return coin_record_1 async def add_pending_transaction(self, tx_record: TransactionRecord): """ @@ -884,6 +1025,13 @@ async def add_pending_transaction(self, tx_record: TransactionRecord): """ # Wallet node will use this queue to retry sending this transaction until full nodes receives it await self.tx_store.add_transaction_record(tx_record, False) + all_coins_names = [] + all_coins_names.extend([coin.name() for coin in tx_record.additions]) + all_coins_names.extend([coin.name() for coin in tx_record.removals]) + + nodes = self.server.get_full_node_connections() + for node in nodes: + await self.subscribe_to_coin_ids_update(all_coins_names, node) self.tx_pending_changed() self.state_changed("pending_transaction", tx_record.wallet_id) @@ -920,97 +1068,6 @@ async def get_all_transactions(self, wallet_id: int) -> List[TransactionRecord]: async def get_transaction(self, tx_id: bytes32) -> Optional[TransactionRecord]: return await self.tx_store.get_transaction_record(tx_id) - async def get_filter_additions_removals( - self, new_block: HeaderBlock, transactions_filter: bytes, fork_point_with_peak: Optional[uint32] - ) -> Tuple[List[bytes32], List[bytes32]]: - """Returns a list of our coin ids, and a list of puzzle_hashes that positively match with provided filter.""" - # assert new_block.prev_header_hash in self.blockchain.blocks - - tx_filter = PyBIP158([b for b in transactions_filter]) - - # Find fork point - if fork_point_with_peak is not None: - fork_h: int = fork_point_with_peak - elif new_block.prev_header_hash != self.constants.GENESIS_CHALLENGE and self.peak is not None: - block_record = await self.blockchain.get_block_record_from_db(self.peak.header_hash) - # this may return -1, in case there is no shared ancestor block - fork_h = find_fork_point_in_chain( - self.blockchain, - block_record, - new_block, - ) - else: - fork_h = 0 - - # Get all unspent coins - my_coin_records: Set[WalletCoinRecord] = await self.coin_store.get_unspent_coins_at_height( - uint32(fork_h) if fork_h >= 0 else None - ) - - # Filter coins up to and including fork point - unspent_coin_names: Set[bytes32] = set() - for coin in my_coin_records: - if coin.confirmed_block_height <= fork_h: - unspent_coin_names.add(coin.name()) - - # Get all blocks after fork point up to but not including this block - if new_block.height > 0: - curr: BlockRecord = self.blockchain.block_record(new_block.prev_hash) - reorg_blocks: List[HeaderBlockRecord] = [] - while curr.height > fork_h: - header_block_record = await self.block_store.get_header_block_record(curr.header_hash) - assert header_block_record is not None - reorg_blocks.append(header_block_record) - if curr.height == 0: - break - curr = await self.blockchain.get_block_record_from_db(curr.prev_hash) - reorg_blocks.reverse() - - # For each block, process additions to get all Coins, then process removals to get unspent coins - for reorg_block in reorg_blocks: - for addition in reorg_block.additions: - unspent_coin_names.add(addition.name()) - for removal in reorg_block.removals: - record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(removal.puzzle_hash) - if record is None: - continue - unspent_coin_names.remove(removal.name()) - - my_puzzle_hashes = self.puzzle_store.all_puzzle_hashes - - removals_of_interest: List[bytes32] = [] - additions_of_interest: List[bytes32] = [] - - ( - trade_removals, - trade_additions, - ) = await self.trade_manager.get_coins_of_interest() - for name, trade_coin in trade_removals.items(): - if tx_filter.Match(bytearray(trade_coin.name())): - removals_of_interest.append(trade_coin.name()) - - for name, trade_coin in trade_additions.items(): - if tx_filter.Match(bytearray(trade_coin.puzzle_hash)): - additions_of_interest.append(trade_coin.puzzle_hash) - - for coin_name in unspent_coin_names: - if tx_filter.Match(bytearray(coin_name)): - removals_of_interest.append(coin_name) - - for puzzle_hash in my_puzzle_hashes: - if tx_filter.Match(bytearray(puzzle_hash)): - additions_of_interest.append(puzzle_hash) - - for coin_id in await self.interested_store.get_interested_coin_ids(): - if tx_filter.Match(bytearray(coin_id)): - removals_of_interest.append(coin_id) - - for puzzle_hash, _ in await self.interested_store.get_interested_puzzle_hashes(): - if tx_filter.Match(bytearray(puzzle_hash)): - additions_of_interest.append(puzzle_hash) - - return additions_of_interest, removals_of_interest - async def is_addition_relevant(self, addition: Coin): """ Check whether we care about a new addition (puzzle_hash). Returns true if we @@ -1044,6 +1101,7 @@ async def reorg_rollback(self, height: int): TransactionType.INCOMING_TRADE, ]: await self.tx_store.tx_reorged(record) + self.tx_pending_changed() # Removes wallets that were created from a blockchain transaction which got reorged. remove_ids = [] @@ -1055,19 +1113,11 @@ async def reorg_rollback(self, height: int): for wallet_id in remove_ids: await self.user_store.delete_wallet(wallet_id, in_transaction=True) self.wallets.pop(wallet_id) - self.new_peak_callbacks.pop(wallet_id) - async def close_all_stores(self) -> None: - if self.blockchain is not None: - self.blockchain.shut_down() + async def _await_closed(self) -> None: await self.db_connection.close() - - async def clear_all_stores(self): - await self.coin_store._clear_database() - await self.tx_store._clear_database() - await self.puzzle_store._clear_database() - await self.user_store._clear_database() - await self.basic_store._clear_database() + if self.weight_proof_handler is not None: + self.weight_proof_handler.cancel_weight_proof_tasks() def unlink_db(self): Path(self.db_path).unlink() @@ -1081,69 +1131,13 @@ async def get_start_height(self): otherwise use the peak """ - first_coin_height = await self.coin_store.get_first_coin_height() - if first_coin_height is None: - start_height = self.blockchain.get_peak() - else: - start_height = first_coin_height - - return start_height - - async def create_wallet_backup(self, file_path: Path): - all_wallets = await self.get_all_wallet_info_entries() - for wallet in all_wallets: - if wallet.id == 1: - all_wallets.remove(wallet) - break - - backup_pk = master_sk_to_backup_sk(self.private_key) - now = uint64(int(time.time())) - wallet_backup = WalletInfoBackup(all_wallets) - - backup: Dict[str, Any] = {} - - data = wallet_backup.to_json_dict() - data["version"] = __version__ - data["fingerprint"] = self.private_key.get_g1().get_fingerprint() - data["timestamp"] = now - data["start_height"] = await self.get_start_height() - key_base_64 = base64.b64encode(bytes(backup_pk)) - f = Fernet(key_base_64) - data_bytes = json.dumps(data).encode() - encrypted = f.encrypt(data_bytes) - - meta_data: Dict[str, Any] = {"timestamp": now, "pubkey": bytes(backup_pk.get_g1()).hex()} - - meta_data_bytes = json.dumps(meta_data).encode() - signature = bytes(AugSchemeMPL.sign(backup_pk, std_hash(encrypted) + std_hash(meta_data_bytes))).hex() - - backup["data"] = encrypted.decode() - backup["meta_data"] = meta_data - backup["signature"] = signature - - backup_file_text = json.dumps(backup) - file_path.write_text(backup_file_text) - - async def import_backup_info(self, file_path) -> None: - json_dict = open_backup_file(file_path, self.private_key) - wallet_list_json = json_dict["data"]["wallet_list"] + return 0 - for wallet_info in wallet_list_json: - await self.user_store.create_wallet( - wallet_info["name"], - wallet_info["type"], - wallet_info["data"], - wallet_info["id"], - ) - await self.load_wallets() - await self.user_settings.user_imported_backup() - await self.create_more_puzzle_hashes(from_zero=True) - - async def get_wallet_for_colour(self, colour): + async def get_wallet_for_asset_id(self, asset_id): for wallet_id in self.wallets: wallet = self.wallets[wallet_id] - if wallet.type() == WalletType.COLOURED_COIN: - if bytes(wallet.cc_info.my_genesis_checker).hex() == colour: + if wallet.type() == WalletType.CAT: + if bytes(wallet.cat_info.limitations_program_hash).hex() == asset_id: return wallet return None @@ -1152,33 +1146,7 @@ async def add_new_wallet(self, wallet: Any, wallet_id: int, create_puzzle_hashes if create_puzzle_hashes: await self.create_more_puzzle_hashes() - # search through the blockrecords and return the most recent coin to use a given puzzlehash - async def search_blockrecords_for_puzzlehash(self, puzzlehash: bytes32): - header_hash_of_interest = None - highest_block_height = 0 - peak: Optional[BlockRecord] = self.blockchain.get_peak() - if peak is None: - return None, None - peak_block: Optional[HeaderBlockRecord] = await self.blockchain.block_store.get_header_block_record( - peak.header_hash - ) - while peak_block is not None: - tx_filter = PyBIP158([b for b in peak_block.header.transactions_filter]) - if tx_filter.Match(bytearray(puzzlehash)) and peak_block.height > highest_block_height: - header_hash_of_interest = peak_block.header_hash - highest_block_height = peak_block.height - break - else: - peak_block = await self.blockchain.block_store.get_header_block_record( - peak_block.header.prev_header_hash - ) - - return highest_block_height, header_hash_of_interest - async def get_spendable_coins_for_wallet(self, wallet_id: int, records=None) -> Set[WalletCoinRecord]: - if self.peak is None: - return set() - if records is None: records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id) @@ -1249,19 +1217,76 @@ async def puzzle_solution_received(self, response: RespondPuzzleSolution): callback = getattr(wallet, callback_str) await callback(unwrapped, action.id) - def get_peak(self) -> Optional[BlockRecord]: - return self.blockchain.get_peak() + async def new_peak(self, peak: wallet_protocol.NewPeakWallet): + for wallet_id, wallet in self.wallets.items(): + if wallet.type() == uint8(WalletType.POOLING_WALLET): + await wallet.new_peak(peak.height) + + async def add_interested_puzzle_hash( + self, puzzle_hash: bytes32, wallet_id: int, in_transaction: bool = False + ) -> None: + await self.interested_store.add_interested_puzzle_hash(puzzle_hash, wallet_id, in_transaction) + await self.subscribe_to_new_puzzle_hash([puzzle_hash]) + + async def add_interested_coin_id(self, coin_id: bytes32) -> None: + nodes = self.server.get_full_node_connections() + for node in nodes: + await self.subscribe_to_coin_ids_update([coin_id], node) + + async def get_filter_additions_removals( + self, new_block: HeaderBlock, transactions_filter: bytes, fork_point_with_peak: Optional[uint32] + ) -> Tuple[List[bytes32], List[bytes32]]: + """Returns a list of our coin ids, and a list of puzzle_hashes that positively match with provided filter.""" + # assert new_block.prev_header_hash in self.blockchain.blocks + + tx_filter = PyBIP158([b for b in transactions_filter]) + + # Get all unspent coins + my_coin_records: Set[WalletCoinRecord] = await self.coin_store.get_unspent_coins_at_height(None) + + # Get additions on unconfirmed transactions + unconfirmed_additions: Set[Coin] = set() + for tx_record in await self.tx_store.get_all_unconfirmed(): + unconfirmed_additions.update(set(tx_record.additions)) + + # Filter coins up to and including fork point + unspent_coin_names: Set[bytes32] = set() + for coin in my_coin_records: + unspent_coin_names.add(coin.name()) + + my_puzzle_hashes = self.puzzle_store.all_puzzle_hashes + + removals_of_interest: List[bytes32] = [] + additions_of_interest: List[bytes32] = [] + + trade_removals = await self.trade_manager.get_coins_of_interest() + for name, trade_coin in trade_removals.items(): + if tx_filter.Match(bytearray(trade_coin.name())): + removals_of_interest.append(trade_coin.name()) + + for addition in unconfirmed_additions: + if tx_filter.Match(bytearray(addition.name())): + additions_of_interest.append(addition.name()) - async def get_next_interesting_coin_ids(self, spend: CoinSpend, in_transaction: bool) -> List[bytes32]: - pool_wallet_interested: List[bytes32] = PoolWallet.get_next_interesting_coin_ids(spend) - for coin_id in pool_wallet_interested: - await self.interested_store.add_interested_coin_id(coin_id, in_transaction) - return pool_wallet_interested + for coin_name in unspent_coin_names: + if tx_filter.Match(bytearray(coin_name)): + removals_of_interest.append(coin_name) - async def new_peak(self): - peak: Optional[BlockRecord] = self.get_peak() - if peak is None: - return + for puzzle_hash in my_puzzle_hashes: + if tx_filter.Match(bytearray(puzzle_hash)): + additions_of_interest.append(puzzle_hash) + + for coin_id in await self.interested_store.get_interested_coin_ids(): + if tx_filter.Match(bytearray(coin_id)): + removals_of_interest.append(coin_id) + + for puzzle_hash, _ in await self.interested_store.get_interested_puzzle_hashes(): + if tx_filter.Match(bytearray(puzzle_hash)): + additions_of_interest.append(puzzle_hash) + + return additions_of_interest, removals_of_interest - for wallet_id, callback in self.new_peak_callbacks.items(): - await callback(peak) + async def delete_trade_transactions(self, trade_id: bytes32): + txs: List[TransactionRecord] = await self.tx_store.get_transactions_by_trade_id(trade_id) + for tx in txs: + await self.tx_store.delete_transaction_record(tx.name) diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index 3dd66d8f0b7b..52953be607a5 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -142,6 +142,17 @@ async def add_transaction_record(self, record: TransactionRecord, in_transaction if not in_transaction: self.db_wrapper.lock.release() + async def delete_transaction_record(self, tx_id: bytes32) -> None: + if tx_id in self.tx_record_cache: + tx_record = self.tx_record_cache.pop(tx_id) + if tx_record.wallet_id in self.unconfirmed_for_wallet: + tx_cache = self.unconfirmed_for_wallet[tx_record.wallet_id] + if tx_id in tx_cache: + tx_cache.pop(tx_id) + + c = await self.db_connection.execute("DELETE FROM transaction_record WHERE bundle_id=?", (tx_id,)) + await c.close() + async def set_confirmed(self, tx_id: bytes32, height: uint32): """ Updates transaction to be confirmed. @@ -164,9 +175,10 @@ async def set_confirmed(self, tx_id: bytes32, height: uint32): removals=current.removals, wallet_id=current.wallet_id, sent_to=current.sent_to, - trade_id=None, + trade_id=current.trade_id, type=current.type, name=current.name, + memos=current.memos, ) await self.add_transaction_record(tx, True) @@ -217,6 +229,7 @@ async def increment_sent( trade_id=None, type=current.type, name=current.name, + memos=current.memos, ) await self.add_transaction_record(tx, False) @@ -242,6 +255,7 @@ async def tx_reorged(self, record: TransactionRecord): trade_id=None, type=record.type, name=record.name, + memos=record.memos, ) await self.add_transaction_record(tx, True) @@ -437,6 +451,18 @@ async def get_transaction_above(self, height: int) -> List[TransactionRecord]: return records + async def get_transactions_by_trade_id(self, trade_id: bytes32) -> List[TransactionRecord]: + cursor = await self.db_connection.execute("SELECT * from transaction_record WHERE trade_id=?", (trade_id,)) + rows = await cursor.fetchall() + await cursor.close() + records = [] + + for row in rows: + record = TransactionRecord.from_bytes(row[0]) + records.append(record) + + return records + async def rollback_to_block(self, height: int): # Delete from storage to_delete = [] diff --git a/chia/wallet/wallet_weight_proof_handler.py b/chia/wallet/wallet_weight_proof_handler.py new file mode 100644 index 000000000000..7e3664644a1d --- /dev/null +++ b/chia/wallet/wallet_weight_proof_handler.py @@ -0,0 +1,194 @@ +import asyncio +import logging +import pathlib +import random +import tempfile +from concurrent.futures.process import ProcessPoolExecutor +from typing import IO, List, Tuple, Optional + +from chia.consensus.block_record import BlockRecord +from chia.consensus.constants import ConsensusConstants +from chia.full_node.weight_proof import ( + _validate_sub_epoch_summaries, + vars_to_bytes, + validate_sub_epoch_sampling, + _validate_sub_epoch_segments, + _validate_recent_blocks_and_get_records, + chunks, + _validate_vdf_batch, +) +from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary + +from chia.types.weight_proof import ( + WeightProof, +) + +from chia.util.ints import uint32 + +log = logging.getLogger(__name__) + + +def _create_shutdown_file() -> IO: + return tempfile.NamedTemporaryFile(prefix="chia_executor_shutdown_trigger") + + +class WalletWeightProofHandler: + + LAMBDA_L = 100 + C = 0.5 + MAX_SAMPLES = 20 + + def __init__( + self, + constants: ConsensusConstants, + ): + self._constants = constants + self._num_processes = 4 + self._executor_shutdown_tempfile: IO = _create_shutdown_file() + self._executor: ProcessPoolExecutor = ProcessPoolExecutor(self._num_processes) + self._weight_proof_tasks: List[asyncio.Task] = [] + + def cancel_weight_proof_tasks(self): + for task in self._weight_proof_tasks: + if not task.done(): + task.cancel() + self._weight_proof_tasks = [] + self._executor_shutdown_tempfile.close() + self._executor.shutdown(wait=True) + + async def validate_weight_proof( + self, weight_proof: WeightProof, skip_segment_validation=False + ) -> Tuple[bool, uint32, List[SubEpochSummary], List[BlockRecord]]: + task: asyncio.Task = asyncio.create_task( + self._validate_weight_proof_inner(weight_proof, skip_segment_validation) + ) + self._weight_proof_tasks.append(task) + valid, fork_point, summaries, block_records = await task + self._weight_proof_tasks.remove(task) + return valid, fork_point, summaries, block_records + + async def _validate_weight_proof_inner( + self, weight_proof: WeightProof, skip_segment_validation: bool + ) -> Tuple[bool, uint32, List[SubEpochSummary], List[BlockRecord]]: + assert len(weight_proof.sub_epochs) > 0 + if len(weight_proof.sub_epochs) == 0: + return False, uint32(0), [], [] + + peak_height = weight_proof.recent_chain_data[-1].reward_chain_block.height + log.info(f"validate weight proof peak height {peak_height}") + + summaries, sub_epoch_weight_list = _validate_sub_epoch_summaries(self._constants, weight_proof) + if summaries is None: + log.error("weight proof failed sub epoch data validation") + return False, uint32(0), [], [] + + seed = summaries[-2].get_hash() + rng = random.Random(seed) + if not validate_sub_epoch_sampling(rng, sub_epoch_weight_list, weight_proof): + log.error("failed weight proof sub epoch sample validation") + return False, uint32(0), [], [] + + constants, summary_bytes, wp_segment_bytes, wp_recent_chain_bytes = vars_to_bytes( + self._constants, summaries, weight_proof + ) + + vdf_tasks: List[asyncio.Future] = [] + recent_blocks_validation_task: asyncio.Future = asyncio.get_running_loop().run_in_executor( + self._executor, + _validate_recent_blocks_and_get_records, + constants, + wp_recent_chain_bytes, + summary_bytes, + pathlib.Path(self._executor_shutdown_tempfile.name), + ) + try: + if not skip_segment_validation: + segments_validated, vdfs_to_validate = _validate_sub_epoch_segments( + constants, rng, wp_segment_bytes, summary_bytes + ) + + if not segments_validated: + return False, uint32(0), [], [] + + vdf_chunks = chunks(vdfs_to_validate, self._num_processes) + for chunk in vdf_chunks: + byte_chunks = [] + for vdf_proof, classgroup, vdf_info in chunk: + byte_chunks.append((bytes(vdf_proof), bytes(classgroup), bytes(vdf_info))) + + vdf_task: asyncio.Future = asyncio.get_running_loop().run_in_executor( + self._executor, + _validate_vdf_batch, + constants, + byte_chunks, + pathlib.Path(self._executor_shutdown_tempfile.name), + ) + vdf_tasks.append(vdf_task) + + for vdf_task in vdf_tasks: + validated = await vdf_task + if not validated: + return False, uint32(0), [], [] + + valid_recent_blocks, records_bytes = await recent_blocks_validation_task + finally: + recent_blocks_validation_task.cancel() + for vdf_task in vdf_tasks: + vdf_task.cancel() + + if not valid_recent_blocks: + log.error("failed validating weight proof recent blocks") + # Verify the data + return False, uint32(0), [], [] + + records = [BlockRecord.from_bytes(b) for b in records_bytes] + + # TODO fix find fork point + return True, uint32(0), summaries, records + + def get_fork_point(self, old_wp: Optional[WeightProof], new_wp: WeightProof) -> uint32: + """ + iterate through sub epoch summaries to find fork point. This method is conservative, it does not return the + actual fork point, it can return a height that is before the actual fork point. + """ + + if old_wp is None: + return uint32(0) + + old_ses = set() + + for ses in old_wp.sub_epochs: + old_ses.add(ses.reward_chain_hash) + + overflow = 0 + count = 0 + for idx, new_ses in enumerate(new_wp.sub_epochs): + if new_ses.reward_chain_hash in old_ses: + count += 1 + overflow += new_ses.num_blocks_overflow + continue + else: + break + + # Try to find an exact fork point + if new_wp.recent_chain_data[0].height >= old_wp.recent_chain_data[0].height: + left_wp = old_wp + right_wp = new_wp + else: + left_wp = new_wp + right_wp = old_wp + + r_index = 0 + l_index = 0 + while r_index < len(right_wp.recent_chain_data) and l_index < len(left_wp.recent_chain_data): + if right_wp.recent_chain_data[r_index].header_hash == left_wp.recent_chain_data[l_index].header_hash: + r_index += 1 + continue + # Keep incrementing left pointer until we find a match + l_index += 1 + if r_index != 0: + # We found a matching block, this is the last matching block + return right_wp.recent_chain_data[r_index - 1].height + + # Just return the matching sub epoch height + return uint32((self._constants.SUB_EPOCH_BLOCKS * count) - overflow) diff --git a/setup.py b/setup.py index 2d016a15cb48..39931f93f00c 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ "dnslib==0.9.14", # dns lib "typing-extensions==4.0.1", # typing backports like Protocol and TypedDict "zstd==1.5.0.4", + "packaging==21.0", ] upnp_dependencies = [ @@ -100,7 +101,7 @@ "chia.wallet", "chia.wallet.puzzles", "chia.wallet.rl_wallet", - "chia.wallet.cc_wallet", + "chia.wallet.cat_wallet", "chia.wallet.did_wallet", "chia.wallet.settings", "chia.wallet.trading", diff --git a/tests/clvm/benchmark_costs.py b/tests/clvm/benchmark_costs.py new file mode 100644 index 000000000000..29a8d5379edb --- /dev/null +++ b/tests/clvm/benchmark_costs.py @@ -0,0 +1,16 @@ +from chia.types.blockchain_format.program import INFINITE_COST +from chia.types.spend_bundle import SpendBundle +from chia.types.generator_types import BlockGenerator +from chia.consensus.cost_calculator import calculate_cost_of_program, NPCResult +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.full_node.bundle_tools import simple_solution_generator +from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions + + +def cost_of_spend_bundle(spend_bundle: SpendBundle) -> int: + program: BlockGenerator = simple_solution_generator(spend_bundle) + npc_result: NPCResult = get_name_puzzle_conditions( + program, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=True + ) + cost: int = calculate_cost_of_program(program.program, npc_result, DEFAULT_CONSTANTS.COST_PER_BYTE) + return cost diff --git a/tests/clvm/test_clvm_compilation.py b/tests/clvm/test_clvm_compilation.py index b42f102ba32a..412d244da722 100644 --- a/tests/clvm/test_clvm_compilation.py +++ b/tests/clvm/test_clvm_compilation.py @@ -8,12 +8,10 @@ wallet_program_files = set( [ "chia/wallet/puzzles/calculate_synthetic_public_key.clvm", - "chia/wallet/puzzles/cc.clvm", + "chia/wallet/puzzles/cat.clvm", "chia/wallet/puzzles/chialisp_deserialisation.clvm", "chia/wallet/puzzles/rom_bootstrap_generator.clvm", "chia/wallet/puzzles/generator_for_single_coin.clvm", - "chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm", - "chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm", "chia/wallet/puzzles/lock.inner.puzzle.clvm", "chia/wallet/puzzles/p2_conditions.clvm", "chia/wallet/puzzles/p2_delegated_conditions.clvm", @@ -37,6 +35,14 @@ "chia/wallet/puzzles/pool_member_innerpuz.clvm", "chia/wallet/puzzles/singleton_launcher.clvm", "chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm", + "chia/wallet/puzzles/genesis_by_puzzle_hash.clvm", + "chia/wallet/puzzles/everything_with_signature.clvm", + "chia/wallet/puzzles/delegated_tail.clvm", + "chia/wallet/puzzles/settlement_payments.clvm", + "chia/wallet/puzzles/genesis_by_coin_id.clvm", + "chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm", + "chia/wallet/puzzles/delegated_genesis_checker.clvm", + "chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm", ] ) diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index ad4d40b67113..b08631014b17 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -6,6 +6,7 @@ import time from secrets import token_bytes from typing import Dict, Optional, List +from blspy import G2Element import pytest @@ -21,8 +22,9 @@ from chia.server.outbound_message import Message from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.blockchain_format.classgroup import ClassgroupElement -from chia.types.blockchain_format.program import SerializedProgram +from chia.types.blockchain_format.program import Program, SerializedProgram from chia.types.blockchain_format.vdf import CompressibleVDFField, VDFProof +from chia.types.coin_spend import CoinSpend from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs from chia.types.full_block import FullBlock @@ -38,7 +40,7 @@ from chia.util.recursive_replace import recursive_replace from chia.util.vdf_prover import get_vdf_info_and_proof from tests.wallet_tools import WalletTool -from chia.wallet.cc_wallet.cc_wallet import CCWallet +from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.transaction_record import TransactionRecord from tests.connection_utils import add_dummy_connection, connect_and_get_peer @@ -192,6 +194,13 @@ async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockch await time_out_assert(10, node_height_at_least, True, full_node_2, 5) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 5) + async def check_transaction_confirmed(transaction) -> bool: + tx = await wallet_node_1.wallet_state_manager.get_transaction(transaction.name) + return tx.confirmed + + await time_out_assert(10, check_transaction_confirmed, True, tr) + await asyncio.sleep(0.5) + # Confirm generator is not compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator assert program is not None @@ -217,6 +226,9 @@ async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockch await time_out_assert(10, node_height_at_least, True, full_node_2, 6) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 6) + await time_out_assert(10, check_transaction_confirmed, True, tr) + await asyncio.sleep(0.5) + # Confirm generator is compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator assert program is not None @@ -284,33 +296,43 @@ async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockch await time_out_assert(10, node_height_at_least, True, full_node_2, 9) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 9) + await time_out_assert(10, check_transaction_confirmed, True, tr) + await asyncio.sleep(0.5) + # Confirm generator is compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator assert program is not None assert detect_potential_template_generator(uint32(9), program) is None assert len((await full_node_1.get_all_full_blocks())[-1].transactions_generator_ref_list) > 0 - # Creates a cc wallet - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node_1.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node_1.wallet_state_manager.tx_store.get_not_sent() - tr = tx_queue[0] - await time_out_assert( - 10, - full_node_1.full_node.mempool_manager.get_spendbundle, - tr.spend_bundle, - tr.spend_bundle.name(), - ) - + # Creates a standard_transaction and an anyone-can-spend tx tr: TransactionRecord = await wallet.generate_signed_transaction( 30000, - ph, + Program.to(1).get_tree_hash(), + ) + extra_spend = SpendBundle( + [ + CoinSpend( + next(coin for coin in tr.additions if coin.puzzle_hash == Program.to(1).get_tree_hash()), + Program.to(1), + Program.to([[51, ph, 30000]]), + ) + ], + G2Element(), ) - await wallet.push_transaction(tx=tr) + new_spend_bundle = SpendBundle.aggregate([tr.spend_bundle, extra_spend]) + new_tr = dataclasses.replace( + tr, + spend_bundle=new_spend_bundle, + additions=new_spend_bundle.additions(), + removals=new_spend_bundle.removals(), + ) + await wallet.push_transaction(tx=new_tr) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, - tr.spend_bundle, - tr.name, + new_tr.spend_bundle, + new_tr.spend_bundle.name(), ) # Farm a block @@ -319,32 +341,43 @@ async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockch await time_out_assert(10, node_height_at_least, True, full_node_2, 10) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 10) - # Confirm generator is compressed - program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator + await time_out_assert(10, check_transaction_confirmed, True, new_tr) + await asyncio.sleep(0.5) + + # Confirm generator is not compressed, #CAT creation has a cat spend + all_blocks = await full_node_1.get_all_full_blocks() + program: Optional[SerializedProgram] = all_blocks[-1].transactions_generator assert program is not None - assert detect_potential_template_generator(uint32(10), program) is None - assert len((await full_node_1.get_all_full_blocks())[-1].transactions_generator_ref_list) > 0 + assert len(all_blocks[-1].transactions_generator_ref_list) == 0 - # Make a cc transaction - tr: TransactionRecord = await cc_wallet.generate_signed_transaction([uint64(60)], [ph]) - await wallet.wallet_state_manager.add_pending_transaction(tr) - await time_out_assert( - 10, - full_node_2.full_node.mempool_manager.get_spendbundle, - tr.spend_bundle, - tr.name, - ) - # Make a standard transaction + # Make a standard transaction and an anyone-can-spend transaction tr: TransactionRecord = await wallet.generate_signed_transaction( 30000, - ph, + Program.to(1).get_tree_hash(), + ) + extra_spend = SpendBundle( + [ + CoinSpend( + next(coin for coin in tr.additions if coin.puzzle_hash == Program.to(1).get_tree_hash()), + Program.to(1), + Program.to([[51, ph, 30000]]), + ) + ], + G2Element(), ) - await wallet.push_transaction(tx=tr) + new_spend_bundle = SpendBundle.aggregate([tr.spend_bundle, extra_spend]) + new_tr = dataclasses.replace( + tr, + spend_bundle=new_spend_bundle, + additions=new_spend_bundle.additions(), + removals=new_spend_bundle.removals(), + ) + await wallet.push_transaction(tx=new_tr) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, - tr.spend_bundle, - tr.name, + new_tr.spend_bundle, + new_tr.spend_bundle.name(), ) # Farm a block diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 32795c775ea6..29db1745d407 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -2307,7 +2307,7 @@ def test_agg_sig_me(self): ) ] pks, msgs = pkm_pairs(npc_list, b"foobar") - assert pks == [bytes(self.pk1), bytes(self.pk2)] + assert [bytes(pk) for pk in pks] == [bytes(self.pk1), bytes(self.pk2)] assert msgs == [b"msg1" + self.h1 + b"foobar", b"msg2" + self.h1 + b"foobar"] def test_agg_sig_unsafe(self): @@ -2327,7 +2327,7 @@ def test_agg_sig_unsafe(self): ) ] pks, msgs = pkm_pairs(npc_list, b"foobar") - assert pks == [bytes(self.pk1), bytes(self.pk2)] + assert [bytes(pk) for pk in pks] == [bytes(self.pk1), bytes(self.pk2)] assert msgs == [b"msg1", b"msg2"] def test_agg_sig_mixed(self): @@ -2336,5 +2336,5 @@ def test_agg_sig_mixed(self): NPC(self.h1, self.h2, [(self.ASU, [ConditionWithArgs(self.ASU, [bytes(self.pk2), b"msg2"])])]), ] pks, msgs = pkm_pairs(npc_list, b"foobar") - assert pks == [bytes(self.pk1), bytes(self.pk2)] + assert [bytes(pk) for pk in pks] == [bytes(self.pk1), bytes(self.pk2)] assert msgs == [b"msg1" + self.h1 + b"foobar", b"msg2"] diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 0950be2a3a2e..9645ede634e1 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -8,6 +8,7 @@ from blspy import G1Element from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward +from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH from chia.pools.pool_wallet_info import PoolWalletInfo, PoolSingletonState from chia.protocols import full_node_protocol from chia.protocols.full_node_protocol import RespondBlock @@ -19,6 +20,7 @@ from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash +from chia.util.byte_types import hexstr_to_bytes from tests.block_tools import get_plot_dir from chia.util.config import load_config from chia.util.ints import uint16, uint32 @@ -27,7 +29,6 @@ from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt from tests.time_out_assert import time_out_assert - # TODO: Compare deducted fees in all tests against reported total_fee log = logging.getLogger(__name__) FEE_AMOUNT = 10 @@ -43,12 +44,15 @@ async def create_pool_plot(p2_singleton_puzzle_hash: bytes32) -> Optional[bytes3 return plot_id -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def event_loop(): loop = asyncio.get_event_loop() yield loop +PREFARMED_BLOCKS = 4 + + class TestPoolWalletRpc: @pytest.fixture(scope="function") async def two_wallet_nodes(self): @@ -61,16 +65,12 @@ async def one_wallet_node_and_rpc(self): async for nodes in setup_simulators_and_wallets(1, 1, {}): full_nodes, wallets = nodes full_node_api = full_nodes[0] - full_node_server = full_node_api.server wallet_node_0, wallet_server_0 = wallets[0] - await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) wallet_0 = wallet_node_0.wallet_state_manager.main_wallet our_ph = await wallet_0.get_new_puzzlehash() - await self.farm_blocks(full_node_api, our_ph, 4) - total_block_rewards = await self.get_total_block_rewards(4) + await self.farm_blocks(full_node_api, our_ph, PREFARMED_BLOCKS) - await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) api_user = WalletRpcApi(wallet_node_0) config = bt.config hostname = config["self_hostname"] @@ -99,16 +99,12 @@ async def one_wallet_node_and_rpc(self): async def setup(self, two_wallet_nodes): rmtree(get_pool_plot_dir(), ignore_errors=True) full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server wallet_node_0, wallet_server_0 = wallets[0] wallet_node_1, wallet_server_1 = wallets[1] - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - wallet_1 = wallet_node_1.wallet_state_manager.main_wallet - our_ph = await wallet_0.get_new_puzzlehash() - pool_ph = await wallet_1.get_new_puzzlehash() - - await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + our_ph_record = await wallet_node_0.wallet_state_manager.get_unused_derivation_record(1, False, True) + pool_ph_record = await wallet_node_1.wallet_state_manager.get_unused_derivation_record(1, False, True) + our_ph = our_ph_record.puzzle_hash + pool_ph = pool_ph_record.puzzle_hash api_user = WalletRpcApi(wallet_node_0) config = bt.config hostname = config["self_hostname"] @@ -129,7 +125,7 @@ async def setup(self, two_wallet_nodes): return ( full_nodes, - [wallet_0, wallet_1], + [wallet_node_0, wallet_node_1], [our_ph, pool_ph], client, # wallet rpc client rpc_cleanup, @@ -148,10 +144,25 @@ async def farm_blocks(self, full_node_api, ph: bytes32, num_blocks: int): # TODO also return calculated block rewards @pytest.mark.asyncio + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, fee): + async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, fee, trusted): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + + await wallet_node_0.server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) + total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + our_ph = await wallet_0.get_new_puzzlehash() summaries_response = await client.get_wallets() for summary in summaries_response: @@ -199,18 +210,35 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f == "0xb3c4b513600729c6b2cf776d8786d620b6acc88f86f9d6f489fa0a0aff81d634262d5348fb7ba304db55185bb4c5c8a4" ) # It can be one of multiple launcher IDs, due to selecting a different coin - assert pool_config["launcher_id"] in { - "0x78a1eadf583a2f27a129d7aeba076ec6a5200e1ec8225a72c9d4180342bf91a7", - "0x2bcab0310e78a7ab04e251ac6bdd5dfc80ce6895132e64f97265029db3d8309a", - "0x09edf686c318c138cd3461c38e9b4e10e7f21fc476a0929b4480e126b6efcb81", - } + launcher_id = None + for addition in creation_tx.additions: + if addition.puzzle_hash == SINGLETON_LAUNCHER_HASH: + launcher_id = addition.name() + break + assert hexstr_to_bytes(pool_config["launcher_id"]) == launcher_id assert pool_config["pool_url"] == "" @pytest.mark.asyncio + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc, fee): + async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc, fee, trusted): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + + await wallet_node_0.server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) + total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + + await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + our_ph = await wallet_0.get_new_puzzlehash() summaries_response = await client.get_wallets() for summary in summaries_response: @@ -258,18 +286,34 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc == "0xb3c4b513600729c6b2cf776d8786d620b6acc88f86f9d6f489fa0a0aff81d634262d5348fb7ba304db55185bb4c5c8a4" ) # It can be one of multiple launcher IDs, due to selecting a different coin - assert pool_config["launcher_id"] in { - "0x78a1eadf583a2f27a129d7aeba076ec6a5200e1ec8225a72c9d4180342bf91a7", - "0x2bcab0310e78a7ab04e251ac6bdd5dfc80ce6895132e64f97265029db3d8309a", - "0x09edf686c318c138cd3461c38e9b4e10e7f21fc476a0929b4480e126b6efcb81", - } + launcher_id = None + for addition in creation_tx.additions: + if addition.puzzle_hash == SINGLETON_LAUNCHER_HASH: + launcher_id = addition.name() + break + assert hexstr_to_bytes(pool_config["launcher_id"]) == launcher_id assert pool_config["pool_url"] == "http://pool.example.com" @pytest.mark.asyncio + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee): + async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, trusted): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + + await wallet_node_0.server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) + total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + our_ph_1 = await wallet_0.get_new_puzzlehash() our_ph_2 = await wallet_0.get_new_puzzlehash() summaries_response = await client.get_wallets() @@ -315,18 +359,6 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee): pool_list: List[Dict] = full_config["pool"]["pool_list"] assert len(pool_list) == 2 - p2_singleton_ph_2: bytes32 = status_2.p2_singleton_puzzle_hash - p2_singleton_ph_3: bytes32 = status_3.p2_singleton_puzzle_hash - assert ( - await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( - p2_singleton_ph_2 - ) - ) is not None - assert ( - await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( - p2_singleton_ph_3 - ) - ) is not None assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(3)) == 0 # Doing a reorg reverts and removes the pool wallets @@ -339,23 +371,27 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee): await client.pw_status(2) with pytest.raises(ValueError): await client.pw_status(3) - # It also removed interested PH, so we can recreated the pool wallet with another wallet_id later - assert ( - await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( - p2_singleton_ph_2 - ) - ) is None - assert ( - await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( - p2_singleton_ph_3 - ) - ) is None @pytest.mark.asyncio - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_absorb_self(self, one_wallet_node_and_rpc, fee): + @pytest.mark.parametrize("trusted", [True]) + @pytest.mark.parametrize("fee", [0]) + async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + + await wallet_node_0.server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + our_ph = await wallet_0.get_new_puzzlehash() summaries_response = await client.get_wallets() for summary in summaries_response: @@ -413,13 +449,14 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee): assert bal["confirmed_wallet_balance"] == 1 * 1750000000000 # Claim another 1.75 - absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2, fee) - absorb_tx.spend_bundle.debug() + absorb_tx1: TransactionRecord = await client.pw_absorb_rewards(2, fee) + absorb_tx1.spend_bundle.debug() + await time_out_assert( - 5, + 10, full_node_api.full_node.mempool_manager.get_spendbundle, - absorb_tx.spend_bundle, - absorb_tx.name, + absorb_tx1.spend_bundle, + absorb_tx1.name, ) await self.farm_blocks(full_node_api, our_ph, 2) @@ -432,6 +469,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee): tr: TransactionRecord = await client.send_transaction( 1, 100, encode_puzzle_hash(status.p2_singleton_puzzle_hash, "txch") ) + await time_out_assert( 10, full_node_api.full_node.mempool_manager.get_spendbundle, @@ -449,10 +487,25 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee): await bt.delete_plot(plot_id) @pytest.mark.asyncio + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee): + async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + + await wallet_node_0.server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + our_ph = await wallet_0.get_new_puzzlehash() summaries_response = await client.get_wallets() for summary in summaries_response: @@ -530,21 +583,35 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee): await bt.delete_plot(plot_id) assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 assert ( - wallet_node_0.wallet_state_manager.get_peak().height == full_node_api.full_node.blockchain.get_peak().height + wallet_node_0.wallet_state_manager.blockchain.get_peak_height() + == full_node_api.full_node.blockchain.get_peak().height ) # Balance stars at 6 XCH and 5 more blocks are farmed, total 22 XCH assert (await wallet_0.get_confirmed_balance()) == 21999999999999 @pytest.mark.asyncio - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_self_pooling_to_pooling(self, setup, fee): + @pytest.mark.parametrize("trusted", [True]) + @pytest.mark.parametrize("fee", [0]) + async def test_self_pooling_to_pooling(self, setup, fee, trusted): """This tests self-pooling -> pooling""" num_blocks = 4 # Num blocks to farm at a time total_blocks = 0 # Total blocks farmed so far - full_nodes, wallets, receive_address, client, rpc_cleanup = setup + full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup + wallets = [wallet_n.wallet_state_manager.main_wallet for wallet_n in wallet_nodes] + wallet_node_0 = wallet_nodes[0] our_ph = receive_address[0] pool_ph = receive_address[1] full_node_api = full_nodes[0] + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + + await wallet_node_0.server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) try: total_blocks += await self.farm_blocks(full_node_api, our_ph, num_blocks) @@ -592,10 +659,11 @@ async def test_self_pooling_to_pooling(self, setup, fee): wallet_id_2 = summary["id"] else: wallet_id = summary["id"] + await asyncio.sleep(1) assert wallet_id is not None assert wallet_id_2 is not None status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] - status_2: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + status_2: PoolWalletInfo = (await client.pw_status(wallet_id_2))[0] assert status.current.state == PoolSingletonState.SELF_POOLING.value assert status_2.current.state == PoolSingletonState.SELF_POOLING.value @@ -655,19 +723,29 @@ async def status_is_farming_to_pool(w_id: int): await rpc_cleanup() @pytest.mark.asyncio + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize( - "fee,target_puzzle_hash", - [ - (0, "0x9ba327777484b8300d60427e4f3b776ac81948dfedd069a8d3f55834e101696e"), - (FEE_AMOUNT, "0x9ba327777484b8300d60427e4f3b776ac81948dfedd069a8d3f55834e101696e"), - ], + "fee", + [0, FEE_AMOUNT], ) - async def test_leave_pool(self, setup, fee, target_puzzle_hash): + async def test_leave_pool(self, setup, fee, trusted): """This tests self-pooling -> pooling -> escaping -> self pooling""" - full_nodes, wallets, receive_address, client, rpc_cleanup = setup + full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] + wallets = [wallet_n.wallet_state_manager.main_wallet for wallet_n in wallet_nodes] pool_ph = receive_address[1] full_node_api = full_nodes[0] + if trusted: + wallet_nodes[0].config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_nodes[0].config["trusted_peers"] = {} + + await wallet_nodes[0].server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) + WAIT_SECS = 200 try: @@ -719,22 +797,15 @@ async def have_chia(): status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] assert status.current.state == PoolSingletonState.SELF_POOLING.value - assert status.current.to_json_dict() == { - "owner_pubkey": "0xb286bbf7a10fa058d2a2a758921377ef00bb7f8143e1bd40dd195ae918dbef42cfc481140f01b9eae13b430a0c8fe304", # noqa: E501 - "pool_url": None, - "relative_lock_height": 0, - "state": 1, - "target_puzzle_hash": "0x738127e26cb61ffe5530ce0cef02b5eeadb1264aa423e82204a6d6bf9f31c2b7", - "version": 1, - } - assert status.target.to_json_dict() == { - "owner_pubkey": "0xb286bbf7a10fa058d2a2a758921377ef00bb7f8143e1bd40dd195ae918dbef42cfc481140f01b9eae13b430a0c8fe304", # noqa: E501 - "pool_url": "https://pool.example.com", - "relative_lock_height": 5, - "state": 3, - "target_puzzle_hash": target_puzzle_hash, - "version": 1, - } + assert status.current.pool_url is None + assert status.current.relative_lock_height == 0 + assert status.current.state == 1 + assert status.current.version == 1 + + assert status.target.pool_url == "https://pool.example.com" + assert status.target.relative_lock_height == 5 + assert status.target.state == 3 + assert status.target.version == 1 async def status_is_farming_to_pool(): await self.farm_blocks(full_node_api, our_ph, 1) @@ -771,17 +842,29 @@ async def status_is_self_pooling(): await rpc_cleanup() @pytest.mark.asyncio + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_change_pools(self, setup, fee): + async def test_change_pools(self, setup, fee, trusted): """This tests Pool A -> escaping -> Pool B""" - full_nodes, wallets, receive_address, client, rpc_cleanup = setup + full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] pool_a_ph = receive_address[1] + wallets = [wallet_n.wallet_state_manager.main_wallet for wallet_n in wallet_nodes] pool_b_ph = await wallets[1].get_new_puzzlehash() - full_node_api = full_nodes[0] - WAIT_SECS = 200 + if trusted: + wallet_nodes[0].config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_nodes[0].config["trusted_peers"] = {} + + await wallet_nodes[0].server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) + + WAIT_SECS = 200 try: summaries_response = await client.get_wallets() for summary in summaries_response: @@ -860,16 +943,27 @@ async def status_is_leaving(): await rpc_cleanup() @pytest.mark.asyncio + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_change_pools_reorg(self, setup, fee): + async def test_change_pools_reorg(self, setup, fee, trusted): """This tests Pool A -> escaping -> reorg -> escaping -> Pool B""" - full_nodes, wallets, receive_address, client, rpc_cleanup = setup + full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] pool_a_ph = receive_address[1] + wallets = [wallet_n.wallet_state_manager.main_wallet for wallet_n in wallet_nodes] pool_b_ph = await wallets[1].get_new_puzzlehash() - full_node_api = full_nodes[0] WAIT_SECS = 30 + if trusted: + wallet_nodes[0].config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_nodes[0].config["trusted_peers"] = {} + + await wallet_nodes[0].server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) try: summaries_response = await client.get_wallets() diff --git a/tests/pools/test_pool_wallet.py b/tests/pools/test_pool_wallet.py deleted file mode 100644 index 547ff4654622..000000000000 --- a/tests/pools/test_pool_wallet.py +++ /dev/null @@ -1,70 +0,0 @@ -import asyncio -import logging -from typing import List - -import pytest -from blspy import PrivateKey - -from chia.pools.pool_wallet import PoolWallet -from chia.pools.pool_wallet_info import PoolState, FARMING_TO_POOL -from chia.simulator.simulator_protocol import FarmNewBlockProtocol -from chia.types.coin_spend import CoinSpend -from chia.types.full_block import FullBlock -from chia.types.peer_info import PeerInfo -from chia.util.ints import uint16, uint32 -from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk -from chia.wallet.wallet_state_manager import WalletStateManager -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets - - -log = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - -class TestPoolWallet2: - @pytest.fixture(scope="function") - async def one_wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - @pytest.mark.asyncio - async def test_create_new_pool_wallet(self, one_wallet_node): - full_nodes, wallets = one_wallet_node - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node_0, wallet_server_0 = wallets[0] - wsm: WalletStateManager = wallet_node_0.wallet_state_manager - - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - ph = await wallet_0.get_new_puzzlehash() - await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - - for i in range(3): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - all_blocks: List[FullBlock] = await full_node_api.get_all_full_blocks() - h: uint32 = all_blocks[-1].height - - await asyncio.sleep(3) - owner_sk: PrivateKey = master_sk_to_singleton_owner_sk(wsm.private_key, 3) - initial_state = PoolState(1, FARMING_TO_POOL, ph, owner_sk.get_g1(), "pool.com", uint32(10)) - tx_record, _, _ = await PoolWallet.create_new_pool_wallet_transaction(wsm, wallet_0, initial_state) - - launcher_spend: CoinSpend = tx_record.spend_bundle.coin_spends[1] - - async with wsm.db_wrapper.lock: - pw = await PoolWallet.create( - wsm, wallet_0, launcher_spend.coin.name(), tx_record.spend_bundle.coin_spends, h, True - ) - - log.warning(await pw.get_current_state()) - - # Claim rewards - # Escape pool - # Claim rewards - # Self pool diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 4203ac8e9dd9..a5cb2d1e4a89 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -187,7 +187,7 @@ async def setup_wallet_node( service = Service(**kwargs) - await service.start(new_wallet=True) + await service.start() yield service._node, service._node.server diff --git a/tests/wallet/cc_wallet/__init__.py b/tests/wallet/cat_wallet/__init__.py similarity index 100% rename from tests/wallet/cc_wallet/__init__.py rename to tests/wallet/cat_wallet/__init__.py diff --git a/tests/wallet/cat_wallet/test_cat_lifecycle.py b/tests/wallet/cat_wallet/test_cat_lifecycle.py new file mode 100644 index 000000000000..9fdcdf3063b1 --- /dev/null +++ b/tests/wallet/cat_wallet/test_cat_lifecycle.py @@ -0,0 +1,637 @@ +import pytest + +from typing import List, Tuple, Optional, Dict +from blspy import PrivateKey, AugSchemeMPL, G2Element +from clvm.casts import int_to_bytes + +from chia.clvm.spend_sim import SpendSim, SimClient +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.spend_bundle import SpendBundle +from chia.types.coin_spend import CoinSpend +from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.util.errors import Err +from chia.util.ints import uint64 +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.cat_wallet.cat_utils import ( + CAT_MOD, + SpendableCAT, + construct_cat_puzzle, + unsigned_spend_bundle_for_spendable_cats, +) +from chia.wallet.puzzles.tails import ( + GenesisById, + GenesisByPuzhash, + EverythingWithSig, + DelegatedLimitations, +) + +from tests.clvm.test_puzzles import secret_exponent_for_index +from tests.clvm.benchmark_costs import cost_of_spend_bundle + +acs = Program.to(1) +acs_ph = acs.get_tree_hash() +NO_LINEAGE_PROOF = LineageProof() + + +class TestCATLifecycle: + cost: Dict[str, int] = {} + + @pytest.fixture(scope="function") + async def setup_sim(self): + sim = await SpendSim.create() + sim_client = SimClient(sim) + await sim.farm_block() + return sim, sim_client + + async def do_spend( + self, + sim: SpendSim, + sim_client: SimClient, + tail: Program, + coins: List[Coin], + lineage_proofs: List[Program], + inner_solutions: List[Program], + expected_result: Tuple[MempoolInclusionStatus, Err], + reveal_limitations_program: bool = True, + signatures: List[G2Element] = [], + extra_deltas: Optional[List[int]] = None, + additional_spends: List[SpendBundle] = [], + limitations_solutions: Optional[List[Program]] = None, + cost_str: str = "", + ): + if limitations_solutions is None: + limitations_solutions = [Program.to([])] * len(coins) + if extra_deltas is None: + extra_deltas = [0] * len(coins) + + spendable_cat_list: List[SpendableCAT] = [] + for coin, innersol, proof, limitations_solution, extra_delta in zip( + coins, inner_solutions, lineage_proofs, limitations_solutions, extra_deltas + ): + spendable_cat_list.append( + SpendableCAT( + coin, + tail.get_tree_hash(), + acs, + innersol, + limitations_solution=limitations_solution, + lineage_proof=proof, + extra_delta=extra_delta, + limitations_program_reveal=tail if reveal_limitations_program else Program.to([]), + ) + ) + + spend_bundle: SpendBundle = unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + spendable_cat_list, + ) + agg_sig = AugSchemeMPL.aggregate(signatures) + result = await sim_client.push_tx( + SpendBundle.aggregate( + [ + *additional_spends, + spend_bundle, + SpendBundle([], agg_sig), # "Signing" the spend bundle + ] + ) + ) + assert result == expected_result + self.cost[cost_str] = cost_of_spend_bundle(spend_bundle) + await sim.farm_block() + + @pytest.mark.asyncio() + async def test_cat_mod(self, setup_sim): + sim, sim_client = setup_sim + + try: + tail = Program.to([]) + checker_solution = Program.to([]) + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + cat_ph: bytes32 = cat_puzzle.get_tree_hash() + await sim.farm_block(cat_ph) + starting_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(cat_ph))[0].coin + + # Testing the eve spend + await self.do_spend( + sim, + sim_client, + tail, + [starting_coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), starting_coin.amount - 3, [b"memo"]], + [51, acs.get_tree_hash(), 1], + [51, acs.get_tree_hash(), 2], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution], + cost_str="Eve Spend", + ) + + # There's 4 total coins at this point. A farming reward and the three children of the spend above. + + # Testing a combination of two + coins: List[Coin] = [ + record.coin + for record in (await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False)) + ] + coins = [coins[0], coins[1]] + await self.do_spend( + sim, + sim_client, + tail, + coins, + [NO_LINEAGE_PROOF] * 2, + [ + Program.to( + [ + [51, acs.get_tree_hash(), coins[0].amount + coins[1].amount], + [51, 0, -113, tail, checker_solution], + ] + ), + Program.to([[51, 0, -113, tail, checker_solution]]), + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution] * 2, + cost_str="Two CATs", + ) + + # Testing a combination of three + coins = [ + record.coin + for record in (await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False)) + ] + total_amount: uint64 = uint64(sum([c.amount for c in coins])) + await self.do_spend( + sim, + sim_client, + tail, + coins, + [NO_LINEAGE_PROOF] * 3, + [ + Program.to( + [ + [51, acs.get_tree_hash(), total_amount], + [51, 0, -113, tail, checker_solution], + ] + ), + Program.to([[51, 0, -113, tail, checker_solution]]), + Program.to([[51, 0, -113, tail, checker_solution]]), + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution] * 3, + cost_str="Three CATs", + ) + + # Spend with a standard lineage proof + parent_coin: Coin = coins[0] # The first one is the one we didn't light on fire + _, curried_args = cat_puzzle.uncurry() + _, _, innerpuzzle = curried_args.as_iter() + lineage_proof = LineageProof(parent_coin.parent_coin_info, innerpuzzle.get_tree_hash(), parent_coin.amount) + await self.do_spend( + sim, + sim_client, + tail, + [(await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin], + [lineage_proof], + [Program.to([[51, acs.get_tree_hash(), total_amount]])], + (MempoolInclusionStatus.SUCCESS, None), + reveal_limitations_program=False, + cost_str="Standard Lineage Check", + ) + + # Melt some value + await self.do_spend( + sim, + sim_client, + tail, + [(await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), total_amount - 1], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + extra_deltas=[-1], + limitations_solutions=[checker_solution], + cost_str="Melting Value", + ) + + # Mint some value + temp_p = Program.to(1) + temp_ph: bytes32 = temp_p.get_tree_hash() + await sim.farm_block(temp_ph) + acs_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(temp_ph, include_spent_coins=False))[ + 0 + ].coin + acs_bundle = SpendBundle( + [ + CoinSpend( + acs_coin, + temp_p, + Program.to([]), + ) + ], + G2Element(), + ) + await self.do_spend( + sim, + sim_client, + tail, + [(await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), total_amount], + [51, 0, -113, tail, checker_solution], + ] + ) + ], # We subtracted 1 last time so it's normal now + (MempoolInclusionStatus.SUCCESS, None), + extra_deltas=[1], + additional_spends=[acs_bundle], + limitations_solutions=[checker_solution], + cost_str="Mint Value", + ) + + finally: + await sim.close() + + @pytest.mark.asyncio() + async def test_complex_spend(self, setup_sim): + sim, sim_client = setup_sim + + try: + tail = Program.to([]) + checker_solution = Program.to([]) + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + cat_ph: bytes32 = cat_puzzle.get_tree_hash() + await sim.farm_block(cat_ph) + await sim.farm_block(cat_ph) + + cat_records = await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False) + parent_of_mint = cat_records[0].coin + parent_of_melt = cat_records[1].coin + eve_to_mint = cat_records[2].coin + eve_to_melt = cat_records[3].coin + + # Spend two of them to make them non-eve + await self.do_spend( + sim, + sim_client, + tail, + [parent_of_mint, parent_of_melt], + [NO_LINEAGE_PROOF, NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), parent_of_mint.amount], + [51, 0, -113, tail, checker_solution], + ] + ), + Program.to( + [ + [51, acs.get_tree_hash(), parent_of_melt.amount], + [51, 0, -113, tail, checker_solution], + ] + ), + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution] * 2, + cost_str="Spend two eves", + ) + + # Make the lineage proofs for the non-eves + mint_lineage = LineageProof(parent_of_mint.parent_coin_info, acs_ph, parent_of_mint.amount) + melt_lineage = LineageProof(parent_of_melt.parent_coin_info, acs_ph, parent_of_melt.amount) + + # Find the two new coins + all_cats = await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False) + all_cat_coins = [cr.coin for cr in all_cats] + standard_to_mint = list(filter(lambda cr: cr.parent_coin_info == parent_of_mint.name(), all_cat_coins))[0] + standard_to_melt = list(filter(lambda cr: cr.parent_coin_info == parent_of_melt.name(), all_cat_coins))[0] + + # Do the complex spend + # We have both and eve and non-eve doing both minting and melting + await self.do_spend( + sim, + sim_client, + tail, + [eve_to_mint, eve_to_melt, standard_to_mint, standard_to_melt], + [NO_LINEAGE_PROOF, NO_LINEAGE_PROOF, mint_lineage, melt_lineage], + [ + Program.to( + [ + [51, acs.get_tree_hash(), eve_to_mint.amount + 13], + [51, 0, -113, tail, checker_solution], + ] + ), + Program.to( + [ + [51, acs.get_tree_hash(), eve_to_melt.amount - 21], + [51, 0, -113, tail, checker_solution], + ] + ), + Program.to( + [ + [51, acs.get_tree_hash(), standard_to_mint.amount + 21], + [51, 0, -113, tail, checker_solution], + ] + ), + Program.to( + [ + [51, acs.get_tree_hash(), standard_to_melt.amount - 13], + [51, 0, -113, tail, checker_solution], + ] + ), + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution] * 4, + extra_deltas=[13, -21, 21, -13], + cost_str="Complex Spend", + ) + finally: + await sim.close() + + @pytest.mark.asyncio() + async def test_genesis_by_id(self, setup_sim): + sim, sim_client = setup_sim + + try: + standard_acs = Program.to(1) + standard_acs_ph: bytes32 = standard_acs.get_tree_hash() + await sim.farm_block(standard_acs_ph) + + starting_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(standard_acs_ph))[0].coin + tail: Program = GenesisById.construct([Program.to(starting_coin.name())]) + checker_solution: Program = GenesisById.solve([], {}) + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + cat_ph: bytes32 = cat_puzzle.get_tree_hash() + + await sim_client.push_tx( + SpendBundle( + [CoinSpend(starting_coin, standard_acs, Program.to([[51, cat_ph, starting_coin.amount]]))], + G2Element(), + ) + ) + await sim.farm_block() + + await self.do_spend( + sim, + sim_client, + tail, + [(await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), starting_coin.amount], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution], + cost_str="Genesis by ID", + ) + + finally: + await sim.close() + + @pytest.mark.asyncio() + async def test_genesis_by_puzhash(self, setup_sim): + sim, sim_client = setup_sim + + try: + standard_acs = Program.to(1) + standard_acs_ph: bytes32 = standard_acs.get_tree_hash() + await sim.farm_block(standard_acs_ph) + + starting_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(standard_acs_ph))[0].coin + tail: Program = GenesisByPuzhash.construct([Program.to(starting_coin.puzzle_hash)]) + checker_solution: Program = GenesisByPuzhash.solve([], starting_coin.to_json_dict()) + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + cat_ph: bytes32 = cat_puzzle.get_tree_hash() + + await sim_client.push_tx( + SpendBundle( + [CoinSpend(starting_coin, standard_acs, Program.to([[51, cat_ph, starting_coin.amount]]))], + G2Element(), + ) + ) + await sim.farm_block() + + await self.do_spend( + sim, + sim_client, + tail, + [(await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), starting_coin.amount], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution], + cost_str="Genesis by Puzhash", + ) + + finally: + await sim.close() + + @pytest.mark.asyncio() + async def test_everything_with_signature(self, setup_sim): + sim, sim_client = setup_sim + + try: + sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) + tail: Program = EverythingWithSig.construct([Program.to(sk.get_g1())]) + checker_solution: Program = EverythingWithSig.solve([], {}) + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + cat_ph: bytes32 = cat_puzzle.get_tree_hash() + await sim.farm_block(cat_ph) + + # Test eve spend + # We don't sign any message data because CLVM 0 translates to b'' apparently + starting_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(cat_ph))[0].coin + signature: G2Element = AugSchemeMPL.sign( + sk, (starting_coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA) + ) + + await self.do_spend( + sim, + sim_client, + tail, + [starting_coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), starting_coin.amount], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + limitations_solutions=[checker_solution], + signatures=[signature], + cost_str="Signature Issuance", + ) + + # Test melting value + coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin + signature = AugSchemeMPL.sign( + sk, (int_to_bytes(-1) + coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA) + ) + + await self.do_spend( + sim, + sim_client, + tail, + [coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), coin.amount - 1], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + extra_deltas=[-1], + limitations_solutions=[checker_solution], + signatures=[signature], + cost_str="Signature Melt", + ) + + # Test minting value + coin = (await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin + signature = AugSchemeMPL.sign(sk, (int_to_bytes(1) + coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA)) + + # Need something to fund the minting + temp_p = Program.to(1) + temp_ph: bytes32 = temp_p.get_tree_hash() + await sim.farm_block(temp_ph) + acs_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(temp_ph, include_spent_coins=False))[ + 0 + ].coin + acs_bundle = SpendBundle( + [ + CoinSpend( + acs_coin, + temp_p, + Program.to([]), + ) + ], + G2Element(), + ) + + await self.do_spend( + sim, + sim_client, + tail, + [coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), coin.amount + 1], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + extra_deltas=[1], + limitations_solutions=[checker_solution], + signatures=[signature], + additional_spends=[acs_bundle], + cost_str="Signature Mint", + ) + + finally: + await sim.close() + + @pytest.mark.asyncio() + async def test_delegated_tail(self, setup_sim): + sim, sim_client = setup_sim + + try: + standard_acs = Program.to(1) + standard_acs_ph: bytes32 = standard_acs.get_tree_hash() + await sim.farm_block(standard_acs_ph) + + starting_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(standard_acs_ph))[0].coin + sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) + tail: Program = DelegatedLimitations.construct([Program.to(sk.get_g1())]) + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + cat_ph: bytes32 = cat_puzzle.get_tree_hash() + + await sim_client.push_tx( + SpendBundle( + [CoinSpend(starting_coin, standard_acs, Program.to([[51, cat_ph, starting_coin.amount]]))], + G2Element(), + ) + ) + await sim.farm_block() + + # We're signing a different tail to use here + name_as_program = Program.to(starting_coin.name()) + new_tail: Program = GenesisById.construct([name_as_program]) + checker_solution: Program = DelegatedLimitations.solve( + [name_as_program], + { + "signed_program": { + "identifier": "genesis_by_id", + "args": [str(name_as_program)], + }, + "program_arguments": {}, + }, + ) + signature: G2Element = AugSchemeMPL.sign(sk, new_tail.get_tree_hash()) + + await self.do_spend( + sim, + sim_client, + tail, + [(await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False))[0].coin], + [NO_LINEAGE_PROOF], + [ + Program.to( + [ + [51, acs.get_tree_hash(), starting_coin.amount], + [51, 0, -113, tail, checker_solution], + ] + ) + ], + (MempoolInclusionStatus.SUCCESS, None), + signatures=[signature], + limitations_solutions=[checker_solution], + cost_str="Delegated Genesis", + ) + + finally: + await sim.close() + + def test_cost(self): + import json + import logging + + log = logging.getLogger(__name__) + log.warning(json.dumps(self.cost)) diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py new file mode 100644 index 000000000000..124af7cd2d18 --- /dev/null +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -0,0 +1,787 @@ +import asyncio +from typing import List + +import pytest + +from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward +from chia.full_node.mempool_manager import MempoolManager +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.peer_info import PeerInfo +from chia.util.ints import uint16, uint32, uint64 +from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle +from chia.wallet.cat_wallet.cat_wallet import CATWallet +from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS +from chia.wallet.puzzles.cat_loader import CAT_MOD +from chia.wallet.transaction_record import TransactionRecord +from tests.setup_nodes import setup_simulators_and_wallets +from tests.time_out_assert import time_out_assert + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32): + tx = mempool.get_spendbundle(tx_id) + if tx is None: + return False + return True + + +class TestCATWallet: + @pytest.fixture(scope="function") + async def wallet_node(self): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + @pytest.fixture(scope="function") + async def two_wallet_nodes(self): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + + @pytest.fixture(scope="function") + async def three_wallet_nodes(self): + async for _ in setup_simulators_and_wallets(1, 3, {}): + yield _ + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_cat_creation(self, two_wallet_nodes, trusted): + num_blocks = 3 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet = wallet_node.wallet_state_manager.main_wallet + + ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} + + await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100) + ) + # The next 2 lines are basically a noop, it just adds test coverage + cat_wallet = await CATWallet.create(wallet_node.wallet_state_manager, wallet, cat_wallet.wallet_info) + await wallet_node.wallet_state_manager.add_new_wallet(cat_wallet, cat_wallet.id()) + + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() + tx_record = tx_queue[0] + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 100) + await time_out_assert(15, cat_wallet.get_spendable_balance, 100) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 100) + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_cat_spend(self, two_wallet_nodes, trusted): + num_blocks = 3 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + wallet2 = wallet_node_2.wallet_state_manager.main_wallet + + ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + wallet_node_2.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} + await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100) + ) + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() + tx_record = tx_queue[0] + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 100) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 100) + + assert cat_wallet.cat_info.limitations_program_hash is not None + asset_id = cat_wallet.get_asset_id() + + cat_wallet_2: CATWallet = await CATWallet.create_wallet_for_cat( + wallet_node_2.wallet_state_manager, wallet2, asset_id + ) + + assert cat_wallet.cat_info.limitations_program_hash == cat_wallet_2.cat_info.limitations_program_hash + + cat_2_hash = await cat_wallet_2.get_new_inner_hash() + tx_records = await cat_wallet.generate_signed_transaction([uint64(60)], [cat_2_hash], fee=uint64(1)) + for tx_record in tx_records: + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + if tx_record.spend_bundle is not None: + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + await time_out_assert(15, cat_wallet.get_pending_change_balance, 40) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await time_out_assert(30, wallet.get_confirmed_balance, funds * 2 - 101) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 40) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 40) + + await time_out_assert(30, cat_wallet_2.get_confirmed_balance, 60) + await time_out_assert(30, cat_wallet_2.get_unconfirmed_balance, 60) + + cat_hash = await cat_wallet.get_new_inner_hash() + tx_records = await cat_wallet_2.generate_signed_transaction([uint64(15)], [cat_hash]) + for tx_record in tx_records: + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 55) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 55) + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_get_wallet_for_asset_id(self, two_wallet_nodes, trusted): + num_blocks = 3 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet = wallet_node.wallet_state_manager.main_wallet + + ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} + await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100) + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + asset_id = cat_wallet.get_asset_id() + await cat_wallet.set_tail_program(bytes(cat_wallet.cat_info.my_tail).hex()) + assert await wallet_node.wallet_state_manager.get_wallet_for_asset_id(asset_id) == cat_wallet + + # Test that the a default CAT will initialize correctly + asset = DEFAULT_CATS[next(iter(DEFAULT_CATS))] + asset_id = asset["asset_id"] + cat_wallet_2 = await CATWallet.create_wallet_for_cat(wallet_node.wallet_state_manager, wallet, asset_id) + assert await cat_wallet_2.get_name() == asset["name"] + await cat_wallet_2.set_name("Test Name") + assert await cat_wallet_2.get_name() == "Test Name" + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_cat_doesnt_see_eve(self, two_wallet_nodes, trusted): + num_blocks = 3 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + wallet2 = wallet_node_2.wallet_state_manager.main_wallet + + ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + wallet_node_2.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} + await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100) + ) + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() + tx_record = tx_queue[0] + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 100) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 100) + + assert cat_wallet.cat_info.limitations_program_hash is not None + asset_id = cat_wallet.get_asset_id() + + cat_wallet_2: CATWallet = await CATWallet.create_wallet_for_cat( + wallet_node_2.wallet_state_manager, wallet2, asset_id + ) + + assert cat_wallet.cat_info.limitations_program_hash == cat_wallet_2.cat_info.limitations_program_hash + + cat_2_hash = await cat_wallet_2.get_new_inner_hash() + tx_records = await cat_wallet.generate_signed_transaction([uint64(60)], [cat_2_hash]) + for tx_record in tx_records: + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 40) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 40) + + await time_out_assert(15, cat_wallet_2.get_confirmed_balance, 60) + await time_out_assert(15, cat_wallet_2.get_unconfirmed_balance, 60) + + cc2_ph = await cat_wallet_2.get_new_cat_puzzle_hash() + tx_record = await wallet.wallet_state_manager.main_wallet.generate_signed_transaction(10, cc2_ph, 0) + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + for i in range(0, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + id = cat_wallet_2.id() + wsm = cat_wallet_2.wallet_state_manager + + async def query_and_assert_transactions(wsm, id): + all_txs = await wsm.tx_store.get_all_transactions_for_wallet(id) + return len(list(filter(lambda tx: tx.amount == 10, all_txs))) + + await time_out_assert(15, query_and_assert_transactions, 0, wsm, id) + await time_out_assert(15, wsm.get_confirmed_balance_for_wallet, 60, id) + await time_out_assert(15, cat_wallet_2.get_confirmed_balance, 60) + await time_out_assert(15, cat_wallet_2.get_unconfirmed_balance, 60) + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_cat_spend_multiple(self, three_wallet_nodes, trusted): + num_blocks = 3 + full_nodes, wallets = three_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, wallet_server_0 = wallets[0] + wallet_node_1, wallet_server_1 = wallets[1] + wallet_node_2, wallet_server_2 = wallets[2] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + wallet_2 = wallet_node_2.wallet_state_manager.main_wallet + + ph = await wallet_0.get_new_puzzlehash() + if trusted: + wallet_node_0.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + wallet_node_1.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + wallet_node_2.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node_0.config["trusted_peers"] = {} + wallet_node_1.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} + await wallet_server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await wallet_server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await wallet_server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet_0.get_confirmed_balance, funds) + + async with wallet_node_0.wallet_state_manager.lock: + cat_wallet_0: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node_0.wallet_state_manager, wallet_0, {"identifier": "genesis_by_id"}, uint64(100) + ) + tx_queue: List[TransactionRecord] = await wallet_node_0.wallet_state_manager.tx_store.get_not_sent() + tx_record = tx_queue[0] + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet_0.get_confirmed_balance, 100) + await time_out_assert(15, cat_wallet_0.get_unconfirmed_balance, 100) + + assert cat_wallet_0.cat_info.limitations_program_hash is not None + asset_id = cat_wallet_0.get_asset_id() + + cat_wallet_1: CATWallet = await CATWallet.create_wallet_for_cat( + wallet_node_1.wallet_state_manager, wallet_1, asset_id + ) + + cat_wallet_2: CATWallet = await CATWallet.create_wallet_for_cat( + wallet_node_2.wallet_state_manager, wallet_2, asset_id + ) + + assert cat_wallet_0.cat_info.limitations_program_hash == cat_wallet_1.cat_info.limitations_program_hash + assert cat_wallet_0.cat_info.limitations_program_hash == cat_wallet_2.cat_info.limitations_program_hash + + cat_1_hash = await cat_wallet_1.get_new_inner_hash() + cat_2_hash = await cat_wallet_2.get_new_inner_hash() + + tx_records = await cat_wallet_0.generate_signed_transaction([uint64(60), uint64(20)], [cat_1_hash, cat_2_hash]) + for tx_record in tx_records: + await wallet_0.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet_0.get_confirmed_balance, 20) + await time_out_assert(15, cat_wallet_0.get_unconfirmed_balance, 20) + + await time_out_assert(30, cat_wallet_1.get_confirmed_balance, 60) + await time_out_assert(30, cat_wallet_1.get_unconfirmed_balance, 60) + + await time_out_assert(30, cat_wallet_2.get_confirmed_balance, 20) + await time_out_assert(30, cat_wallet_2.get_unconfirmed_balance, 20) + + cat_hash = await cat_wallet_0.get_new_inner_hash() + + tx_records = await cat_wallet_1.generate_signed_transaction([uint64(15)], [cat_hash]) + for tx_record in tx_records: + await wallet_1.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + tx_records_2 = await cat_wallet_2.generate_signed_transaction([uint64(20)], [cat_hash]) + for tx_record in tx_records_2: + await wallet_2.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet_0.get_confirmed_balance, 55) + await time_out_assert(15, cat_wallet_0.get_unconfirmed_balance, 55) + + await time_out_assert(30, cat_wallet_1.get_confirmed_balance, 45) + await time_out_assert(30, cat_wallet_1.get_unconfirmed_balance, 45) + + await time_out_assert(30, cat_wallet_2.get_confirmed_balance, 0) + await time_out_assert(30, cat_wallet_2.get_unconfirmed_balance, 0) + + txs = await wallet_1.wallet_state_manager.tx_store.get_transactions_between(cat_wallet_1.id(), 0, 100000) + print(len(txs)) + # Test with Memo + tx_records_3: TransactionRecord = await cat_wallet_1.generate_signed_transaction( + [uint64(30)], [cat_hash], memos=[[b"Markus Walburg"]] + ) + with pytest.raises(ValueError): + await cat_wallet_1.generate_signed_transaction( + [uint64(30)], [cat_hash], memos=[[b"too"], [b"many"], [b"memos"]] + ) + + for tx_record in tx_records_3: + await wallet_1.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + txs = await wallet_1.wallet_state_manager.tx_store.get_transactions_between(cat_wallet_1.id(), 0, 100000) + for tx in txs: + if tx.amount == 30: + memos = tx.get_memos() + assert len(memos) == 1 + assert b"Markus Walburg" in [v for v_list in memos.values() for v in v_list] + assert list(memos.keys())[0] in [a.name() for a in tx.spend_bundle.additions()] + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_cat_max_amount_send(self, two_wallet_nodes, trusted): + num_blocks = 3 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + + ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + wallet_node_2.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} + await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100000) + ) + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() + tx_record = tx_queue[0] + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 100000) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 100000) + + assert cat_wallet.cat_info.limitations_program_hash is not None + + cat_2 = await cat_wallet.get_new_inner_puzzle() + cat_2_hash = cat_2.get_tree_hash() + amounts = [] + puzzle_hashes = [] + for i in range(1, 50): + amounts.append(uint64(i)) + puzzle_hashes.append(cat_2_hash) + spent_coint = (await cat_wallet.get_cat_spendable_coins())[0].coin + tx_records = await cat_wallet.generate_signed_transaction(amounts, puzzle_hashes, coins={spent_coint}) + for tx_record in tx_records: + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await asyncio.sleep(2) + + async def check_all_there(): + spendable = await cat_wallet.get_cat_spendable_coins() + spendable_name_set = set() + for record in spendable: + spendable_name_set.add(record.coin.name()) + puzzle_hash = construct_cat_puzzle( + CAT_MOD, cat_wallet.cat_info.limitations_program_hash, cat_2 + ).get_tree_hash() + for i in range(1, 50): + coin = Coin(spent_coint.name(), puzzle_hash, i) + if coin.name() not in spendable_name_set: + return False + return True + + await time_out_assert(15, check_all_there, True) + await asyncio.sleep(5) + max_sent_amount = await cat_wallet.get_max_send_amount() + + # 1) Generate transaction that is under the limit + under_limit_txs = None + try: + under_limit_txs = await cat_wallet.generate_signed_transaction( + [max_sent_amount - 1], + [ph], + ) + except ValueError: + assert ValueError + + assert under_limit_txs is not None + + # 2) Generate transaction that is equal to limit + at_limit_txs = None + try: + at_limit_txs = await cat_wallet.generate_signed_transaction( + [max_sent_amount], + [ph], + ) + except ValueError: + assert ValueError + + assert at_limit_txs is not None + + # 3) Generate transaction that is greater than limit + above_limit_txs = None + try: + above_limit_txs = await cat_wallet.generate_signed_transaction( + [max_sent_amount + 1], + [ph], + ) + except ValueError: + pass + + assert above_limit_txs is None + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_cat_hint(self, two_wallet_nodes, trusted): + num_blocks = 3 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + wallet2 = wallet_node_2.wallet_state_manager.main_wallet + + ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + wallet_node_2.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} + await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100) + ) + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() + tx_record = tx_queue[0] + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 100) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 100) + assert cat_wallet.cat_info.limitations_program_hash is not None + + cat_2_hash = await wallet2.get_new_puzzlehash() + tx_records = await cat_wallet.generate_signed_transaction([uint64(60)], [cat_2_hash], memos=[[cat_2_hash]]) + + for tx_record in tx_records: + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 40) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 40) + + # First we test that no wallet was created + asyncio.sleep(10) + assert len(wallet_node_2.wallet_state_manager.wallets.keys()) == 1 + + # Then we update the wallet's default CATs + wallet_node_2.wallet_state_manager.default_cats = { + cat_wallet.cat_info.limitations_program_hash.hex(): { + "asset_id": cat_wallet.cat_info.limitations_program_hash.hex(), + "name": "Test", + "symbol": "TST", + } + } + + # Then we send another transaction + tx_records = await cat_wallet.generate_signed_transaction([uint64(10)], [cat_2_hash], memos=[[cat_2_hash]]) + + for tx_record in tx_records: + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 30) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 30) + + # Now we check that another wallet WAS created + async def check_wallets(wallet_node): + return len(wallet_node.wallet_state_manager.wallets.keys()) + + await time_out_assert(10, check_wallets, 2, wallet_node_2) + cat_wallet_2 = wallet_node_2.wallet_state_manager.wallets[2] + + await time_out_assert(30, cat_wallet_2.get_confirmed_balance, 10) + await time_out_assert(30, cat_wallet_2.get_unconfirmed_balance, 10) + + cat_hash = await cat_wallet.get_new_inner_hash() + tx_records = await cat_wallet_2.generate_signed_transaction([uint64(5)], [cat_hash]) + for tx_record in tx_records: + await wallet.wallet_state_manager.add_pending_transaction(tx_record) + + await time_out_assert( + 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + ) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await time_out_assert(15, cat_wallet.get_confirmed_balance, 35) + await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 35) + + # @pytest.mark.asyncio + + # async def test_cat_melt_and_mint(self, two_wallet_nodes): + # num_blocks = 3 + # full_nodes, wallets = two_wallet_nodes + # full_node_api = full_nodes[0] + # full_node_server = full_node_api.server + # wallet_node, server_2 = wallets[0] + # wallet_node_2, server_3 = wallets[1] + # wallet = wallet_node.wallet_state_manager.main_wallet + # + # ph = await wallet.get_new_puzzlehash() + # + # await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + # await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + # + # for i in range(1, num_blocks): + # await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + # + # funds = sum( + # [ + # calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + # for i in range(1, num_blocks - 1) + # ] + # ) + # + # await time_out_assert(15, wallet.get_confirmed_balance, funds) + # + # async with wallet_node.wallet_state_manager.lock: + # cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + # wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100000) + # ) + # tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() + # tx_record = tx_queue[0] + # await time_out_assert( + # 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() + # ) + # for i in range(1, num_blocks): + # await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + # + # await time_out_assert(15, cat_wallet.get_confirmed_balance, 100000) + # await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 100000) diff --git a/tests/wallet/cat_wallet/test_offer_lifecycle.py b/tests/wallet/cat_wallet/test_offer_lifecycle.py new file mode 100644 index 000000000000..3011c681c9d3 --- /dev/null +++ b/tests/wallet/cat_wallet/test_offer_lifecycle.py @@ -0,0 +1,293 @@ +import pytest + +from typing import Dict, Optional, List +from blspy import G2Element + +from chia.clvm.spend_sim import SpendSim, SimClient +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.program import Program +from chia.types.announcement import Announcement +from chia.types.spend_bundle import SpendBundle +from chia.types.coin_spend import CoinSpend +from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.util.ints import uint64 +from chia.wallet.cat_wallet.cat_utils import ( + CAT_MOD, + construct_cat_puzzle, + SpendableCAT, + unsigned_spend_bundle_for_spendable_cats, +) +from chia.wallet.payment import Payment +from chia.wallet.trading.offer import Offer, NotarizedPayment + +from tests.clvm.benchmark_costs import cost_of_spend_bundle + +acs = Program.to(1) +acs_ph = acs.get_tree_hash() + + +# Some methods mapping strings to CATs +def str_to_tail(tail_str: str) -> Program: + return Program.to([3, [], [1, tail_str], []]) + + +def str_to_tail_hash(tail_str: str) -> bytes32: + return Program.to([3, [], [1, tail_str], []]).get_tree_hash() + + +def str_to_cat_hash(tail_str: str) -> bytes32: + return construct_cat_puzzle(CAT_MOD, str_to_tail_hash(tail_str), acs).get_tree_hash() + + +class TestOfferLifecycle: + cost: Dict[str, int] = {} + + @pytest.fixture(scope="function") + async def setup_sim(self): + sim = await SpendSim.create() + sim_client = SimClient(sim) + await sim.farm_block() + return sim, sim_client + + # This method takes a dictionary of strings mapping to amounts and generates the appropriate CAT/XCH coins + async def generate_coins( + self, + sim, + sim_client, + requested_coins: Dict[Optional[str], List[uint64]], + ) -> Dict[Optional[str], List[Coin]]: + await sim.farm_block(acs_ph) + parent_coin: Coin = [cr.coin for cr in await (sim_client.get_coin_records_by_puzzle_hash(acs_ph))][0] + + # We need to gather a list of initial coins to create as well as spends that do the eve spend for every CAT + payments: List[Payment] = [] + cat_bundles: List[SpendBundle] = [] + for tail_str, amounts in requested_coins.items(): + for amount in amounts: + if tail_str: + tail: Program = str_to_tail(tail_str) # Making a fake but unique TAIL + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + payments.append(Payment(cat_puzzle.get_tree_hash(), amount, [])) + cat_bundles.append( + unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + [ + SpendableCAT( + Coin(parent_coin.name(), cat_puzzle.get_tree_hash(), amount), + tail.get_tree_hash(), + acs, + Program.to([[51, acs_ph, amount], [51, 0, -113, tail, []]]), + ) + ], + ) + ) + else: + payments.append(Payment(acs_ph, amount, [])) + + # This bundle create all of the initial coins + parent_bundle = SpendBundle( + [ + CoinSpend( + parent_coin, + acs, + Program.to([[51, p.puzzle_hash, p.amount] for p in payments]), + ) + ], + G2Element(), + ) + + # Then we aggregate it with all of the eve spends + await sim_client.push_tx(SpendBundle.aggregate([parent_bundle, *cat_bundles])) + await sim.farm_block() + + # Search for all of the coins and put them into a dictionary + coin_dict: Dict[Optional[str], List[Coin]] = {} + for tail_str, _ in requested_coins.items(): + if tail_str: + tail_hash: bytes32 = str_to_tail_hash(tail_str) + cat_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail_hash, acs).get_tree_hash() + coin_dict[tail_str] = [ + cr.coin + for cr in await (sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False)) + ] + else: + coin_dict[None] = list( + filter( + lambda c: c.amount < 250000000000, + [ + cr.coin + for cr in await ( + sim_client.get_coin_records_by_puzzle_hash(acs_ph, include_spent_coins=False) + ) + ], + ) + ) + + return coin_dict + + # This method simulates a wallet's `generate_signed_transaction` but doesn't bother with non-offer announcements + def generate_secure_bundle( + self, + selected_coins: List[Coin], + announcements: List[Announcement], + offered_amount: uint64, + tail_str: Optional[str] = None, + ) -> SpendBundle: + announcement_assertions: List[List] = [[63, a.name()] for a in announcements] + selected_coin_amount: int = sum([c.amount for c in selected_coins]) + non_primaries: List[Coin] = [] if len(selected_coins) < 2 else selected_coins[1:] + inner_solution: List[List] = [ + [51, Offer.ph(), offered_amount], # Offered coin + [51, acs_ph, uint64(selected_coin_amount - offered_amount)], # Change + *announcement_assertions, + ] + + if tail_str is None: + bundle = SpendBundle( + [ + CoinSpend( + selected_coins[0], + acs, + Program.to(inner_solution), + ), + *[CoinSpend(c, acs, Program.to([])) for c in non_primaries], + ], + G2Element(), + ) + else: + spendable_cats: List[SpendableCAT] = [ + SpendableCAT( + c, + str_to_tail_hash(tail_str), + acs, + Program.to( + [ + [51, 0, -113, str_to_tail(tail_str), Program.to([])], # Use the TAIL rather than lineage + *(inner_solution if c == selected_coins[0] else []), + ] + ), + ) + for c in selected_coins + ] + bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cats) + + return bundle + + @pytest.mark.asyncio() + async def test_complex_offer(self, setup_sim): + sim, sim_client = setup_sim + + try: + coins_needed: Dict[Optional[str], List[int]] = { + None: [500, 400, 300], + "red": [250, 100], + "blue": [3000], + } + all_coins: Dict[Optional[str], List[Coin]] = await self.generate_coins(sim, sim_client, coins_needed) + chia_coins: List[Coin] = all_coins[None] + red_coins: List[Coin] = all_coins["red"] + blue_coins: List[Coin] = all_coins["blue"] + + # Create an XCH Offer for RED + chia_requested_payments: Dict[Optional[bytes32], List[Payment]] = { + str_to_tail_hash("red"): [ + Payment(acs_ph, 100, [b"memo"]), + Payment(acs_ph, 200, [b"memo"]), + ] + } + + chia_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments( + chia_requested_payments, chia_coins + ) + chia_announcements: List[Announcement] = Offer.calculate_announcements(chia_requested_payments) + chia_secured_bundle: SpendBundle = self.generate_secure_bundle(chia_coins, chia_announcements, 1000) + chia_offer = Offer(chia_requested_payments, chia_secured_bundle) + assert not chia_offer.is_valid() + + # Create a RED Offer for XCH + red_requested_payments: Dict[Optional[bytes32], List[Payment]] = { + None: [ + Payment(acs_ph, 300, [b"red memo"]), + Payment(acs_ph, 400, [b"red memo"]), + ] + } + + red_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments( + red_requested_payments, red_coins + ) + red_announcements: List[Announcement] = Offer.calculate_announcements(red_requested_payments) + red_secured_bundle: SpendBundle = self.generate_secure_bundle( + red_coins, red_announcements, 350, tail_str="red" + ) + red_offer = Offer(red_requested_payments, red_secured_bundle) + assert not red_offer.is_valid() + + # Test aggregation of offers + new_offer = Offer.aggregate([chia_offer, red_offer]) + assert new_offer.get_offered_amounts() == {None: 1000, str_to_tail_hash("red"): 350} + assert new_offer.get_requested_amounts() == {None: 700, str_to_tail_hash("red"): 300} + assert new_offer.is_valid() + + # Create yet another offer of BLUE for XCH and RED + blue_requested_payments: Dict[Optional[bytes32], List[Payment]] = { + None: [ + Payment(acs_ph, 200, [b"blue memo"]), + ], + str_to_tail_hash("red"): [ + Payment(acs_ph, 50, [b"blue memo"]), + ], + } + + blue_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments( + blue_requested_payments, blue_coins + ) + blue_announcements: List[Announcement] = Offer.calculate_announcements(blue_requested_payments) + blue_secured_bundle: SpendBundle = self.generate_secure_bundle( + blue_coins, blue_announcements, 2000, tail_str="blue" + ) + blue_offer = Offer(blue_requested_payments, blue_secured_bundle) + assert not blue_offer.is_valid() + + # Test a re-aggregation + new_offer: Offer = Offer.aggregate([new_offer, blue_offer]) + assert new_offer.get_offered_amounts() == { + None: 1000, + str_to_tail_hash("red"): 350, + str_to_tail_hash("blue"): 2000, + } + assert new_offer.get_requested_amounts() == {None: 900, str_to_tail_hash("red"): 350} + assert new_offer.summary() == ( + { + "xch": 1000, + str_to_tail_hash("red").hex(): 350, + str_to_tail_hash("blue").hex(): 2000, + }, + {"xch": 900, str_to_tail_hash("red").hex(): 350}, + ) + assert new_offer.get_pending_amounts() == { + "xch": 1200, + str_to_tail_hash("red").hex(): 350, + str_to_tail_hash("blue").hex(): 3000, + } + assert new_offer.is_valid() + + # Test (de)serialization + assert Offer.from_bytes(bytes(new_offer)) == new_offer + + # Make sure we can actually spend the offer once it's valid + arbitrage_ph: bytes32 = Program.to([3, [], [], 1]).get_tree_hash() + offer_bundle: SpendBundle = new_offer.to_valid_spend(arbitrage_ph) + result = await sim_client.push_tx(offer_bundle) + assert result == (MempoolInclusionStatus.SUCCESS, None) + self.cost["complex offer"] = cost_of_spend_bundle(offer_bundle) + await sim.farm_block() + finally: + await sim.close() + + def test_cost(self): + import json + import logging + + log = logging.getLogger(__name__) + log.warning(json.dumps(self.cost)) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py new file mode 100644 index 000000000000..75fceb06961b --- /dev/null +++ b/tests/wallet/cat_wallet/test_trades.py @@ -0,0 +1,506 @@ +import asyncio +from secrets import token_bytes +from typing import List + +import pytest + +from chia.full_node.mempool_manager import MempoolManager +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.peer_info import PeerInfo +from chia.util.ints import uint16, uint64 +from chia.wallet.cat_wallet.cat_wallet import CATWallet +from chia.wallet.trading.offer import Offer +from chia.wallet.trading.trade_status import TradeStatus +from chia.wallet.transaction_record import TransactionRecord +from tests.setup_nodes import setup_simulators_and_wallets +from tests.time_out_assert import time_out_assert + + +async def tx_in_pool(mempool: MempoolManager, tx_id): + tx = mempool.get_spendbundle(tx_id) + if tx is None: + return False + return True + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +@pytest.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + + +buffer_blocks = 4 + + +@pytest.fixture(scope="function") +async def wallets_prefarm(two_wallet_nodes): + """ + Sets up the node with 10 blocks, and returns a payer and payee wallet. + """ + farm_blocks = 10 + buffer = 4 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, wallet_server_0 = wallets[0] + wallet_node_1, wallet_server_1 = wallets[1] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + + ph0 = await wallet_0.get_new_puzzlehash() + ph1 = await wallet_1.get_new_puzzlehash() + + await wallet_server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await wallet_server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(0, farm_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph0)) + + for i in range(0, farm_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + + for i in range(0, buffer): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + return wallet_node_0, wallet_node_1, full_node_api + + +@pytest.mark.parametrize( + "trusted", + [False], +) +class TestCATTrades: + @pytest.mark.asyncio + async def test_cat_trades(self, wallets_prefarm, trusted): + wallet_node_maker, wallet_node_taker, full_node = wallets_prefarm + wallet_maker = wallet_node_maker.wallet_state_manager.main_wallet + wallet_taker = wallet_node_taker.wallet_state_manager.main_wallet + + if trusted: + wallet_node_maker.config["trusted_peers"] = {full_node.server.node_id: full_node.server.node_id} + wallet_node_taker.config["trusted_peers"] = {full_node.server.node_id: full_node.server.node_id} + else: + wallet_node_maker.config["trusted_peers"] = {} + wallet_node_taker.config["trusted_peers"] = {} + + # Create two new CATs, one in each wallet + async with wallet_node_maker.wallet_state_manager.lock: + cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, uint64(100) + ) + await asyncio.sleep(1) + + async with wallet_node_taker.wallet_state_manager.lock: + new_cat_wallet_taker: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node_taker.wallet_state_manager, wallet_taker, {"identifier": "genesis_by_id"}, uint64(100) + ) + await asyncio.sleep(1) + + for i in range(1, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, 100) + await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, 100) + await time_out_assert(15, new_cat_wallet_taker.get_confirmed_balance, 100) + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, 100) + + # Add the taker's CAT to the maker's wallet + assert cat_wallet_maker.cat_info.my_tail is not None + assert new_cat_wallet_taker.cat_info.my_tail is not None + new_cat_wallet_maker: CATWallet = await CATWallet.create_wallet_for_cat( + wallet_node_maker.wallet_state_manager, wallet_maker, new_cat_wallet_taker.get_asset_id() + ) + + # Create the trade parameters + MAKER_CHIA_BALANCE = 20 * 1000000000000 - 100 + TAKER_CHIA_BALANCE = 20 * 1000000000000 - 100 + await time_out_assert(15, wallet_maker.get_confirmed_balance, MAKER_CHIA_BALANCE) + await time_out_assert(15, wallet_taker.get_unconfirmed_balance, TAKER_CHIA_BALANCE) + MAKER_CAT_BALANCE = 100 + MAKER_NEW_CAT_BALANCE = 0 + TAKER_CAT_BALANCE = 0 + TAKER_NEW_CAT_BALANCE = 100 + + chia_for_cat = { + wallet_maker.id(): -1, + new_cat_wallet_maker.id(): 2, # This is the CAT that the taker made + } + cat_for_chia = { + wallet_maker.id(): 3, + cat_wallet_maker.id(): -4, # The taker has no knowledge of this CAT yet + } + cat_for_cat = { + cat_wallet_maker.id(): -5, + new_cat_wallet_maker.id(): 6, + } + chia_for_multiple_cat = { + wallet_maker.id(): -7, + cat_wallet_maker.id(): 8, + new_cat_wallet_maker.id(): 9, + } + multiple_cat_for_chia = { + wallet_maker.id(): 10, + cat_wallet_maker.id(): -11, + new_cat_wallet_maker.id(): -12, + } + chia_and_cat_for_cat = { + wallet_maker.id(): -13, + cat_wallet_maker.id(): -14, + new_cat_wallet_maker.id(): 15, + } + + trade_manager_maker = wallet_node_maker.wallet_state_manager.trade_manager + trade_manager_taker = wallet_node_taker.wallet_state_manager.trade_manager + + # Execute all of the trades + # chia_for_cat + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, fee=uint64(1)) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + + success, trade_take, error = await trade_manager_taker.respond_to_offer( + Offer.from_bytes(trade_make.offer), fee=uint64(1) + ) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_take is not None + + MAKER_CHIA_BALANCE -= 2 # -1 and -1 for fee + MAKER_NEW_CAT_BALANCE += 2 + TAKER_CHIA_BALANCE += 0 # +1 and -1 for fee + TAKER_NEW_CAT_BALANCE -= 2 + + await time_out_assert(15, wallet_taker.get_unconfirmed_balance, TAKER_CHIA_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + + for i in range(0, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, wallet_maker.get_confirmed_balance, MAKER_CHIA_BALANCE) + await time_out_assert(15, wallet_maker.get_unconfirmed_balance, MAKER_CHIA_BALANCE) + await time_out_assert(15, new_cat_wallet_maker.get_confirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_maker.get_unconfirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, wallet_taker.get_confirmed_balance, TAKER_CHIA_BALANCE) + await time_out_assert(15, wallet_taker.get_unconfirmed_balance, TAKER_CHIA_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_confirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + + async def get_trade_and_status(trade_manager, trade) -> TradeStatus: + trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) + return TradeStatus(trade_rec.status) + + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) + + # cat_for_chia + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(cat_for_chia) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + + success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_take is not None + + MAKER_CAT_BALANCE -= 4 + MAKER_CHIA_BALANCE += 3 + TAKER_CAT_BALANCE += 4 + TAKER_CHIA_BALANCE -= 3 + + cat_wallet_taker: CATWallet = await wallet_node_taker.wallet_state_manager.get_wallet_for_asset_id( + cat_wallet_maker.get_asset_id() + ) + + await time_out_assert(15, wallet_taker.get_unconfirmed_balance, TAKER_CHIA_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + + for i in range(0, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, wallet_maker.get_confirmed_balance, MAKER_CHIA_BALANCE) + await time_out_assert(15, wallet_maker.get_unconfirmed_balance, MAKER_CHIA_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, wallet_taker.get_confirmed_balance, TAKER_CHIA_BALANCE) + await time_out_assert(15, wallet_taker.get_unconfirmed_balance, TAKER_CHIA_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_confirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) + + # cat_for_cat + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(cat_for_cat) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_take is not None + + MAKER_CAT_BALANCE -= 5 + MAKER_NEW_CAT_BALANCE += 6 + TAKER_CAT_BALANCE += 5 + TAKER_NEW_CAT_BALANCE -= 6 + + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + + for i in range(0, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, new_cat_wallet_maker.get_confirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_maker.get_unconfirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_confirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_confirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) + + # chia_for_multiple_cat + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_multiple_cat) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_take is not None + + MAKER_CHIA_BALANCE -= 7 + MAKER_CAT_BALANCE += 8 + MAKER_NEW_CAT_BALANCE += 9 + TAKER_CHIA_BALANCE += 7 + TAKER_CAT_BALANCE -= 8 + TAKER_NEW_CAT_BALANCE -= 9 + + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + + for i in range(0, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, new_cat_wallet_maker.get_confirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_maker.get_unconfirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_confirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_confirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) + + # multiple_cat_for_chia + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(multiple_cat_for_chia) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_take is not None + + MAKER_CAT_BALANCE -= 11 + MAKER_NEW_CAT_BALANCE -= 12 + MAKER_CHIA_BALANCE += 10 + TAKER_CAT_BALANCE += 11 + TAKER_NEW_CAT_BALANCE += 12 + TAKER_CHIA_BALANCE -= 10 + + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + + for i in range(0, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, new_cat_wallet_maker.get_confirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_maker.get_unconfirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_confirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_confirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) + + # chia_and_cat_for_cat + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_and_cat_for_cat) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_take is not None + + MAKER_CHIA_BALANCE -= 13 + MAKER_CAT_BALANCE -= 14 + MAKER_NEW_CAT_BALANCE += 15 + TAKER_CHIA_BALANCE += 13 + TAKER_CAT_BALANCE += 14 + TAKER_NEW_CAT_BALANCE -= 15 + + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + + for i in range(0, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, new_cat_wallet_maker.get_confirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_maker.get_unconfirmed_balance, MAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_confirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, new_cat_wallet_taker.get_unconfirmed_balance, TAKER_NEW_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_confirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) + await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) + + @pytest.mark.asyncio + async def test_trade_cancellation(self, wallets_prefarm, trusted): + wallet_node_maker, wallet_node_taker, full_node = wallets_prefarm + wallet_maker = wallet_node_maker.wallet_state_manager.main_wallet + wallet_taker = wallet_node_taker.wallet_state_manager.main_wallet + + if trusted: + wallet_node_maker.config["trusted_peers"] = {full_node.server.node_id.hex(): full_node.server.node_id.hex()} + wallet_node_taker.config["trusted_peers"] = {full_node.server.node_id.hex(): full_node.server.node_id.hex()} + else: + wallet_node_maker.config["trusted_peers"] = {} + wallet_node_taker.config["trusted_peers"] = {} + + async with wallet_node_maker.wallet_state_manager.lock: + cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, uint64(100) + ) + tx_queue: List[TransactionRecord] = await wallet_node_maker.wallet_state_manager.tx_store.get_not_sent() + await time_out_assert( + 15, tx_in_pool, True, full_node.full_node.mempool_manager, tx_queue[0].spend_bundle.name() + ) + + for i in range(1, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, 100) + await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, 100) + MAKER_CHIA_BALANCE = 20 * 1000000000000 - 100 + MAKER_CAT_BALANCE = 100 + TAKER_CHIA_BALANCE = 20 * 1000000000000 + await time_out_assert(15, wallet_maker.get_confirmed_balance, MAKER_CHIA_BALANCE) + + cat_for_chia = { + wallet_maker.id(): 1, + cat_wallet_maker.id(): -2, + } + + chia_for_cat = { + wallet_maker.id(): -3, + cat_wallet_maker.id(): 4, + } + + trade_manager_maker = wallet_node_maker.wallet_state_manager.trade_manager + trade_manager_taker = wallet_node_taker.wallet_state_manager.trade_manager + + async def get_trade_and_status(trade_manager, trade) -> TradeStatus: + trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) + return TradeStatus(trade_rec.status) + + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(cat_for_chia) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + + await trade_manager_maker.cancel_pending_offer(trade_make.trade_id) + await time_out_assert(15, get_trade_and_status, TradeStatus.CANCELLED, trade_manager_maker, trade_make) + + # Due to current mempool rules, trying to force a take out of the mempool with a cancel will not work. + # Uncomment this when/if it does + + # success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + # await asyncio.sleep(1) + # assert error is None + # assert success is True + # assert trade_take is not None + # await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CONFIRM, trade_manager_taker, trade_take) + # await time_out_assert( + # 15, + # tx_in_pool, + # True, + # full_node.full_node.mempool_manager, + # Offer.from_bytes(trade_take.offer).to_valid_spend().name(), + # ) + + FEE = uint64(2000000000000) + + txs = await trade_manager_maker.cancel_pending_offer_safely(trade_make.trade_id, fee=FEE) + await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) + for tx in txs: + if tx.spend_bundle is not None: + await time_out_assert(15, tx_in_pool, True, full_node.full_node.mempool_manager, tx.spend_bundle.name()) + + for i in range(1, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, get_trade_and_status, TradeStatus.CANCELLED, trade_manager_maker, trade_make) + # await time_out_assert(15, get_trade_and_status, TradeStatus.FAILED, trade_manager_taker, trade_take) + await time_out_assert(15, wallet_maker.get_pending_change_balance, 0) + await time_out_assert(15, wallet_maker.get_confirmed_balance, MAKER_CHIA_BALANCE - FEE) + await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, MAKER_CAT_BALANCE) + await time_out_assert(15, wallet_taker.get_confirmed_balance, TAKER_CHIA_BALANCE) + + success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + await asyncio.sleep(1) + assert error is not None + assert success is False + assert trade_take is None + + # Now we're going to create the other way around for test coverage sake + success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat) + await asyncio.sleep(1) + assert error is None + assert success is True + assert trade_make is not None + + # This take should fail since we have no CATs to fulfill it with + success, trade_take, error = await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer)) + await asyncio.sleep(1) + assert error is not None + assert success is False + assert trade_take is None + + txs = await trade_manager_maker.cancel_pending_offer_safely(trade_make.trade_id, fee=uint64(0)) + await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) + for tx in txs: + if tx.spend_bundle is not None: + await time_out_assert(15, tx_in_pool, True, full_node.full_node.mempool_manager, tx.spend_bundle.name()) + + for i in range(1, buffer_blocks): + await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + + await time_out_assert(15, get_trade_and_status, TradeStatus.CANCELLED, trade_manager_maker, trade_make) diff --git a/tests/wallet/cc_wallet/test_cc_wallet.py b/tests/wallet/cc_wallet/test_cc_wallet.py deleted file mode 100644 index 0c4afe009b3f..000000000000 --- a/tests/wallet/cc_wallet/test_cc_wallet.py +++ /dev/null @@ -1,549 +0,0 @@ -import asyncio -from typing import List - -import pytest - -from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward -from chia.full_node.mempool_manager import MempoolManager -from chia.simulator.simulator_protocol import FarmNewBlockProtocol -from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.peer_info import PeerInfo -from chia.util.ints import uint16, uint32, uint64 -from chia.wallet.cc_wallet.cc_utils import cc_puzzle_hash_for_inner_puzzle_hash -from chia.wallet.cc_wallet.cc_wallet import CCWallet -from chia.wallet.puzzles.cc_loader import CC_MOD -from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.wallet_coin_record import WalletCoinRecord -from tests.setup_nodes import setup_simulators_and_wallets -from tests.time_out_assert import time_out_assert - - -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - -async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32): - tx = mempool.get_spendbundle(tx_id) - if tx is None: - return False - return True - - -class TestCCWallet: - @pytest.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - @pytest.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - @pytest.fixture(scope="function") - async def three_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ - - @pytest.mark.asyncio - async def test_colour_creation(self, two_wallet_nodes): - num_blocks = 3 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node, server_2 = wallets[0] - wallet = wallet_node.wallet_state_manager.main_wallet - - ph = await wallet.get_new_puzzlehash() - - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - funds = sum( - [ - calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) - for i in range(1, num_blocks - 1) - ] - ) - - await time_out_assert(15, wallet.get_confirmed_balance, funds) - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() - tx_record = tx_queue[0] - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 100) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 100) - - @pytest.mark.asyncio - async def test_cc_spend(self, two_wallet_nodes): - num_blocks = 3 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node, server_2 = wallets[0] - wallet_node_2, server_3 = wallets[1] - wallet = wallet_node.wallet_state_manager.main_wallet - wallet2 = wallet_node_2.wallet_state_manager.main_wallet - - ph = await wallet.get_new_puzzlehash() - - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - funds = sum( - [ - calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) - for i in range(1, num_blocks - 1) - ] - ) - - await time_out_assert(15, wallet.get_confirmed_balance, funds) - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() - tx_record = tx_queue[0] - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 100) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 100) - - assert cc_wallet.cc_info.my_genesis_checker is not None - colour = cc_wallet.get_colour() - - cc_wallet_2: CCWallet = await CCWallet.create_wallet_for_cc(wallet_node_2.wallet_state_manager, wallet2, colour) - - assert cc_wallet.cc_info.my_genesis_checker == cc_wallet_2.cc_info.my_genesis_checker - - cc_2_hash = await cc_wallet_2.get_new_inner_hash() - tx_record = await cc_wallet.generate_signed_transaction([uint64(60)], [cc_2_hash]) - await wallet.wallet_state_manager.add_pending_transaction(tx_record) - - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 40) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 40) - - await time_out_assert(30, cc_wallet_2.get_confirmed_balance, 60) - await time_out_assert(30, cc_wallet_2.get_unconfirmed_balance, 60) - - cc_hash = await cc_wallet.get_new_inner_hash() - tx_record = await cc_wallet_2.generate_signed_transaction([uint64(15)], [cc_hash]) - await wallet.wallet_state_manager.add_pending_transaction(tx_record) - - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 55) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 55) - - @pytest.mark.asyncio - async def test_get_wallet_for_colour(self, two_wallet_nodes): - num_blocks = 3 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node, server_2 = wallets[0] - wallet = wallet_node.wallet_state_manager.main_wallet - - ph = await wallet.get_new_puzzlehash() - - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - funds = sum( - [ - calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) - for i in range(1, num_blocks - 1) - ] - ) - - await time_out_assert(15, wallet.get_confirmed_balance, funds) - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - colour = cc_wallet.get_colour() - assert await wallet_node.wallet_state_manager.get_wallet_for_colour(colour) == cc_wallet - - @pytest.mark.asyncio - async def test_generate_zero_val(self, two_wallet_nodes): - num_blocks = 4 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node, server_2 = wallets[0] - wallet_node_2, server_3 = wallets[1] - wallet = wallet_node.wallet_state_manager.main_wallet - wallet2 = wallet_node_2.wallet_state_manager.main_wallet - - ph = await wallet.get_new_puzzlehash() - - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - funds = sum( - [ - calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) - for i in range(1, num_blocks - 1) - ] - ) - await time_out_assert(15, wallet.get_confirmed_balance, funds) - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - await asyncio.sleep(1) - - ph = await wallet2.get_new_puzzlehash() - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 100) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 100) - - assert cc_wallet.cc_info.my_genesis_checker is not None - colour = cc_wallet.get_colour() - - cc_wallet_2: CCWallet = await CCWallet.create_wallet_for_cc(wallet_node_2.wallet_state_manager, wallet2, colour) - await asyncio.sleep(1) - - assert cc_wallet.cc_info.my_genesis_checker == cc_wallet_2.cc_info.my_genesis_checker - - spend_bundle = await cc_wallet_2.generate_zero_val_coin() - await asyncio.sleep(1) - await time_out_assert(15, tx_in_pool, True, full_node_api.full_node.mempool_manager, spend_bundle.name()) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - async def unspent_count(): - unspent: List[WalletCoinRecord] = list( - await cc_wallet_2.wallet_state_manager.get_spendable_coins_for_wallet(cc_wallet_2.id()) - ) - return len(unspent) - - await time_out_assert(15, unspent_count, 1) - unspent: List[WalletCoinRecord] = list( - await cc_wallet_2.wallet_state_manager.get_spendable_coins_for_wallet(cc_wallet_2.id()) - ) - assert unspent.pop().coin.amount == 0 - - @pytest.mark.asyncio - async def test_cc_spend_uncoloured(self, two_wallet_nodes): - num_blocks = 3 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node, server_2 = wallets[0] - wallet_node_2, server_3 = wallets[1] - wallet = wallet_node.wallet_state_manager.main_wallet - wallet2 = wallet_node_2.wallet_state_manager.main_wallet - - ph = await wallet.get_new_puzzlehash() - - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - funds = sum( - [ - calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) - for i in range(1, num_blocks - 1) - ] - ) - - await time_out_assert(15, wallet.get_confirmed_balance, funds) - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() - tx_record = tx_queue[0] - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 100) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 100) - - assert cc_wallet.cc_info.my_genesis_checker is not None - colour = cc_wallet.get_colour() - - cc_wallet_2: CCWallet = await CCWallet.create_wallet_for_cc(wallet_node_2.wallet_state_manager, wallet2, colour) - - assert cc_wallet.cc_info.my_genesis_checker == cc_wallet_2.cc_info.my_genesis_checker - - cc_2_hash = await cc_wallet_2.get_new_inner_hash() - tx_record = await cc_wallet.generate_signed_transaction([uint64(60)], [cc_2_hash]) - await wallet.wallet_state_manager.add_pending_transaction(tx_record) - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 40) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 40) - - await time_out_assert(15, cc_wallet_2.get_confirmed_balance, 60) - await time_out_assert(15, cc_wallet_2.get_unconfirmed_balance, 60) - - cc2_ph = await cc_wallet_2.get_new_cc_puzzle_hash() - tx_record = await wallet.wallet_state_manager.main_wallet.generate_signed_transaction(10, cc2_ph, 0) - await wallet.wallet_state_manager.add_pending_transaction(tx_record) - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(0, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - id = cc_wallet_2.id() - wsm = cc_wallet_2.wallet_state_manager - await time_out_assert(15, wsm.get_confirmed_balance_for_wallet, 70, id) - await time_out_assert(15, cc_wallet_2.get_confirmed_balance, 60) - await time_out_assert(15, cc_wallet_2.get_unconfirmed_balance, 60) - - @pytest.mark.asyncio - async def test_cc_spend_multiple(self, three_wallet_nodes): - num_blocks = 3 - full_nodes, wallets = three_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node_0, wallet_server_0 = wallets[0] - wallet_node_1, wallet_server_1 = wallets[1] - wallet_node_2, wallet_server_2 = wallets[2] - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - wallet_1 = wallet_node_1.wallet_state_manager.main_wallet - wallet_2 = wallet_node_2.wallet_state_manager.main_wallet - - ph = await wallet_0.get_new_puzzlehash() - - await wallet_server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await wallet_server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await wallet_server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - funds = sum( - [ - calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) - for i in range(1, num_blocks - 1) - ] - ) - - await time_out_assert(15, wallet_0.get_confirmed_balance, funds) - - cc_wallet_0: CCWallet = await CCWallet.create_new_cc(wallet_node_0.wallet_state_manager, wallet_0, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node_0.wallet_state_manager.tx_store.get_not_sent() - tx_record = tx_queue[0] - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet_0.get_confirmed_balance, 100) - await time_out_assert(15, cc_wallet_0.get_unconfirmed_balance, 100) - - assert cc_wallet_0.cc_info.my_genesis_checker is not None - colour = cc_wallet_0.get_colour() - - cc_wallet_1: CCWallet = await CCWallet.create_wallet_for_cc( - wallet_node_1.wallet_state_manager, wallet_1, colour - ) - - cc_wallet_2: CCWallet = await CCWallet.create_wallet_for_cc( - wallet_node_2.wallet_state_manager, wallet_2, colour - ) - - assert cc_wallet_0.cc_info.my_genesis_checker == cc_wallet_1.cc_info.my_genesis_checker - assert cc_wallet_0.cc_info.my_genesis_checker == cc_wallet_2.cc_info.my_genesis_checker - - cc_1_hash = await cc_wallet_1.get_new_inner_hash() - cc_2_hash = await cc_wallet_2.get_new_inner_hash() - - tx_record = await cc_wallet_0.generate_signed_transaction([uint64(60), uint64(20)], [cc_1_hash, cc_2_hash]) - await wallet_0.wallet_state_manager.add_pending_transaction(tx_record) - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet_0.get_confirmed_balance, 20) - await time_out_assert(15, cc_wallet_0.get_unconfirmed_balance, 20) - - await time_out_assert(30, cc_wallet_1.get_confirmed_balance, 60) - await time_out_assert(30, cc_wallet_1.get_unconfirmed_balance, 60) - - await time_out_assert(30, cc_wallet_2.get_confirmed_balance, 20) - await time_out_assert(30, cc_wallet_2.get_unconfirmed_balance, 20) - - cc_hash = await cc_wallet_0.get_new_inner_hash() - - tx_record = await cc_wallet_1.generate_signed_transaction([uint64(15)], [cc_hash]) - await wallet_1.wallet_state_manager.add_pending_transaction(tx_record) - - tx_record_2 = await cc_wallet_2.generate_signed_transaction([uint64(20)], [cc_hash]) - await wallet_2.wallet_state_manager.add_pending_transaction(tx_record_2) - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record_2.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet_0.get_confirmed_balance, 55) - await time_out_assert(15, cc_wallet_0.get_unconfirmed_balance, 55) - - await time_out_assert(30, cc_wallet_1.get_confirmed_balance, 45) - await time_out_assert(30, cc_wallet_1.get_unconfirmed_balance, 45) - - await time_out_assert(30, cc_wallet_2.get_confirmed_balance, 0) - await time_out_assert(30, cc_wallet_2.get_unconfirmed_balance, 0) - - @pytest.mark.asyncio - async def test_cc_max_amount_send(self, two_wallet_nodes): - num_blocks = 3 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node, server_2 = wallets[0] - wallet_node_2, server_3 = wallets[1] - wallet = wallet_node.wallet_state_manager.main_wallet - - ph = await wallet.get_new_puzzlehash() - - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - funds = sum( - [ - calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) - for i in range(1, num_blocks - 1) - ] - ) - - await time_out_assert(15, wallet.get_confirmed_balance, funds) - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100000)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() - tx_record = tx_queue[0] - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 100000) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 100000) - - assert cc_wallet.cc_info.my_genesis_checker is not None - - cc_2_hash = await cc_wallet.get_new_inner_hash() - amounts = [] - puzzle_hashes = [] - for i in range(1, 50): - amounts.append(uint64(i)) - puzzle_hashes.append(cc_2_hash) - spent_coint = (await cc_wallet.get_cc_spendable_coins())[0].coin - tx_record = await cc_wallet.generate_signed_transaction(amounts, puzzle_hashes, coins={spent_coint}) - await wallet.wallet_state_manager.add_pending_transaction(tx_record) - - await time_out_assert( - 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() - ) - - for i in range(1, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - await asyncio.sleep(2) - - async def check_all_there(): - spendable = await cc_wallet.get_cc_spendable_coins() - spendable_name_set = set() - for record in spendable: - spendable_name_set.add(record.coin.name()) - puzzle_hash = cc_puzzle_hash_for_inner_puzzle_hash(CC_MOD, cc_wallet.cc_info.my_genesis_checker, cc_2_hash) - for i in range(1, 50): - coin = Coin(spent_coint.name(), puzzle_hash, i) - if coin.name() not in spendable_name_set: - return False - return True - - await time_out_assert(15, check_all_there, True) - await asyncio.sleep(5) - max_sent_amount = await cc_wallet.get_max_send_amount() - - # 1) Generate transaction that is under the limit - under_limit_tx = None - try: - under_limit_tx = await cc_wallet.generate_signed_transaction( - [max_sent_amount - 1], - [ph], - ) - except ValueError: - assert ValueError - - assert under_limit_tx is not None - - # 2) Generate transaction that is equal to limit - at_limit_tx = None - try: - at_limit_tx = await cc_wallet.generate_signed_transaction( - [max_sent_amount], - [ph], - ) - except ValueError: - assert ValueError - - assert at_limit_tx is not None - - # 3) Generate transaction that is greater than limit - above_limit_tx = None - try: - above_limit_tx = await cc_wallet.generate_signed_transaction( - [max_sent_amount + 1], - [ph], - ) - except ValueError: - pass - - assert above_limit_tx is None diff --git a/tests/wallet/cc_wallet/test_trades.py b/tests/wallet/cc_wallet/test_trades.py deleted file mode 100644 index 4510490b846e..000000000000 --- a/tests/wallet/cc_wallet/test_trades.py +++ /dev/null @@ -1,469 +0,0 @@ -import asyncio -from pathlib import Path -from secrets import token_bytes - -import pytest - -from chia.simulator.simulator_protocol import FarmNewBlockProtocol -from chia.types.peer_info import PeerInfo -from chia.util.ints import uint16, uint64 -from chia.wallet.cc_wallet.cc_wallet import CCWallet -from chia.wallet.trade_manager import TradeManager -from chia.wallet.trading.trade_status import TradeStatus -from tests.setup_nodes import setup_simulators_and_wallets -from tests.time_out_assert import time_out_assert -from tests.wallet.sync.test_wallet_sync import wallet_height_at_least - - -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - -@pytest.fixture(scope="module") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - -buffer_blocks = 4 - - -@pytest.fixture(scope="module") -async def wallets_prefarm(two_wallet_nodes): - """ - Sets up the node with 10 blocks, and returns a payer and payee wallet. - """ - farm_blocks = 10 - buffer = 4 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node_0, wallet_server_0 = wallets[0] - wallet_node_1, wallet_server_1 = wallets[1] - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - wallet_1 = wallet_node_1.wallet_state_manager.main_wallet - - ph0 = await wallet_0.get_new_puzzlehash() - ph1 = await wallet_1.get_new_puzzlehash() - - await wallet_server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await wallet_server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - - for i in range(0, farm_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph0)) - - for i in range(0, farm_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) - - for i in range(0, buffer): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - return wallet_node_0, wallet_node_1, full_node_api - - -class TestCCTrades: - @pytest.mark.asyncio - async def test_cc_trade(self, wallets_prefarm): - wallet_node_0, wallet_node_1, full_node = wallets_prefarm - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - wallet_1 = wallet_node_1.wallet_state_manager.main_wallet - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node_0.wallet_state_manager, wallet_0, uint64(100)) - await asyncio.sleep(1) - - for i in range(1, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - await time_out_assert(15, wallet_height_at_least, True, wallet_node_0, 27) - await time_out_assert(15, cc_wallet.get_confirmed_balance, 100) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 100) - - assert cc_wallet.cc_info.my_genesis_checker is not None - colour = cc_wallet.get_colour() - - cc_wallet_2: CCWallet = await CCWallet.create_wallet_for_cc( - wallet_node_1.wallet_state_manager, wallet_1, colour - ) - await asyncio.sleep(1) - - assert cc_wallet.cc_info.my_genesis_checker == cc_wallet_2.cc_info.my_genesis_checker - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - await time_out_assert(15, wallet_height_at_least, True, wallet_node_0, 31) - # send cc_wallet 2 a coin - cc_hash = await cc_wallet_2.get_new_inner_hash() - tx_record = await cc_wallet.generate_signed_transaction([uint64(1)], [cc_hash]) - await wallet_0.wallet_state_manager.add_pending_transaction(tx_record) - await asyncio.sleep(1) - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - await time_out_assert(15, wallet_height_at_least, True, wallet_node_0, 35) - - trade_manager_0 = wallet_node_0.wallet_state_manager.trade_manager - trade_manager_1 = wallet_node_1.wallet_state_manager.trade_manager - - file = "test_offer_file.offer" - file_path = Path(file) - - if file_path.exists(): - file_path.unlink() - - offer_dict = {1: 10, 2: -30} - - success, trade_offer, error = await trade_manager_0.create_offer_for_ids(offer_dict, file) - await asyncio.sleep(1) - - assert success is True - assert trade_offer is not None - - success, offer, error = await trade_manager_1.get_discrepancies_for_offer(file_path) - await asyncio.sleep(1) - - assert error is None - assert success is True - assert offer is not None - - assert offer["chia"] == -10 - assert offer[colour] == 30 - - success, trade, reason = await trade_manager_1.respond_to_offer(file_path) - await asyncio.sleep(1) - - assert success is True - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(15, wallet_height_at_least, True, wallet_node_0, 39) - await time_out_assert(15, cc_wallet_2.get_confirmed_balance, 31) - await time_out_assert(15, cc_wallet_2.get_unconfirmed_balance, 31) - trade_2 = await trade_manager_0.get_trade_by_id(trade_offer.trade_id) - assert TradeStatus(trade_2.status) is TradeStatus.CONFIRMED - - @pytest.mark.asyncio - async def test_cc_trade_accept_with_zero(self, wallets_prefarm): - wallet_node_0, wallet_node_1, full_node = wallets_prefarm - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - wallet_1 = wallet_node_1.wallet_state_manager.main_wallet - - cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node_0.wallet_state_manager, wallet_0, uint64(100)) - await asyncio.sleep(1) - - for i in range(1, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(15, cc_wallet.get_confirmed_balance, 100) - await time_out_assert(15, cc_wallet.get_unconfirmed_balance, 100) - - assert cc_wallet.cc_info.my_genesis_checker is not None - colour = cc_wallet.get_colour() - - cc_wallet_2: CCWallet = await CCWallet.create_wallet_for_cc( - wallet_node_1.wallet_state_manager, wallet_1, colour - ) - await asyncio.sleep(1) - - assert cc_wallet.cc_info.my_genesis_checker == cc_wallet_2.cc_info.my_genesis_checker - - ph = await wallet_1.get_new_puzzlehash() - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - - trade_manager_0 = wallet_node_0.wallet_state_manager.trade_manager - trade_manager_1 = wallet_node_1.wallet_state_manager.trade_manager - - file = "test_offer_file.offer" - file_path = Path(file) - - if file_path.exists(): - file_path.unlink() - - offer_dict = {1: 10, 3: -30} - - success, trade_offer, error = await trade_manager_0.create_offer_for_ids(offer_dict, file) - await asyncio.sleep(1) - - assert success is True - assert trade_offer is not None - - success, offer, error = await trade_manager_1.get_discrepancies_for_offer(file_path) - await asyncio.sleep(1) - - assert error is None - assert success is True - assert offer is not None - - assert cc_wallet.get_colour() == cc_wallet_2.get_colour() - - assert offer["chia"] == -10 - assert offer[colour] == 30 - - success, trade, reason = await trade_manager_1.respond_to_offer(file_path) - await asyncio.sleep(1) - - assert success is True - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(15, cc_wallet_2.get_confirmed_balance, 30) - await time_out_assert(15, cc_wallet_2.get_unconfirmed_balance, 30) - trade_2 = await trade_manager_0.get_trade_by_id(trade_offer.trade_id) - assert TradeStatus(trade_2.status) is TradeStatus.CONFIRMED - - @pytest.mark.asyncio - async def test_cc_trade_with_multiple_colours(self, wallets_prefarm): - # This test start with CCWallet in both wallets. wall - # wallet1 {wallet_id: 2 = 70} - # wallet2 {wallet_id: 2 = 30} - - wallet_node_a, wallet_node_b, full_node = wallets_prefarm - wallet_a = wallet_node_a.wallet_state_manager.main_wallet - wallet_b = wallet_node_b.wallet_state_manager.main_wallet - - # cc_a_2 = coloured coin, Alice, wallet id = 2 - cc_a_2 = wallet_node_a.wallet_state_manager.wallets[2] - cc_b_2 = wallet_node_b.wallet_state_manager.wallets[2] - - cc_a_3: CCWallet = await CCWallet.create_new_cc(wallet_node_a.wallet_state_manager, wallet_a, uint64(100)) - await asyncio.sleep(1) - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(15, cc_a_3.get_confirmed_balance, 100) - await time_out_assert(15, cc_a_3.get_unconfirmed_balance, 100) - - # store these for asserting change later - cc_balance = await cc_a_2.get_unconfirmed_balance() - cc_balance_2 = await cc_b_2.get_unconfirmed_balance() - - assert cc_a_3.cc_info.my_genesis_checker is not None - red = cc_a_3.get_colour() - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - cc_b_3: CCWallet = await CCWallet.create_wallet_for_cc(wallet_node_b.wallet_state_manager, wallet_b, red) - await asyncio.sleep(1) - - assert cc_a_3.cc_info.my_genesis_checker == cc_b_3.cc_info.my_genesis_checker - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - trade_manager_0 = wallet_node_a.wallet_state_manager.trade_manager - trade_manager_1 = wallet_node_b.wallet_state_manager.trade_manager - - file = "test_offer_file.offer" - file_path = Path(file) - - if file_path.exists(): - file_path.unlink() - - # Wallet - offer_dict = {1: 1000, 2: -20, 4: -50} - - success, trade_offer, error = await trade_manager_0.create_offer_for_ids(offer_dict, file) - await asyncio.sleep(1) - - assert success is True - assert trade_offer is not None - - success, offer, error = await trade_manager_1.get_discrepancies_for_offer(file_path) - await asyncio.sleep(1) - assert error is None - assert success is True - assert offer is not None - assert offer["chia"] == -1000 - - colour_2 = cc_a_2.get_colour() - colour_3 = cc_a_3.get_colour() - - assert offer[colour_2] == 20 - assert offer[colour_3] == 50 - - success, trade, reason = await trade_manager_1.respond_to_offer(file_path) - await asyncio.sleep(1) - - assert success is True - for i in range(0, 10): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(15, cc_b_3.get_confirmed_balance, 50) - await time_out_assert(15, cc_b_3.get_unconfirmed_balance, 50) - - await time_out_assert(15, cc_a_3.get_confirmed_balance, 50) - await time_out_assert(15, cc_a_3.get_unconfirmed_balance, 50) - - await time_out_assert(15, cc_a_2.get_unconfirmed_balance, cc_balance - offer[colour_2]) - await time_out_assert(15, cc_b_2.get_unconfirmed_balance, cc_balance_2 + offer[colour_2]) - - trade = await trade_manager_0.get_trade_by_id(trade_offer.trade_id) - - status: TradeStatus = TradeStatus(trade.status) - - assert status is TradeStatus.CONFIRMED - - @pytest.mark.asyncio - async def test_create_offer_with_zero_val(self, wallets_prefarm): - # Wallet A Wallet B - # CCWallet id 2: 50 CCWallet id 2: 50 - # CCWallet id 3: 50 CCWallet id 2: 50 - # Wallet A will - # Wallet A will create a new CC and wallet B will create offer to buy that coin - - wallet_node_a, wallet_node_b, full_node = wallets_prefarm - wallet_a = wallet_node_a.wallet_state_manager.main_wallet - wallet_b = wallet_node_b.wallet_state_manager.main_wallet - trade_manager_a: TradeManager = wallet_node_a.wallet_state_manager.trade_manager - trade_manager_b: TradeManager = wallet_node_b.wallet_state_manager.trade_manager - - cc_a_4: CCWallet = await CCWallet.create_new_cc(wallet_node_a.wallet_state_manager, wallet_a, uint64(100)) - await asyncio.sleep(1) - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(15, cc_a_4.get_confirmed_balance, 100) - - colour = cc_a_4.get_colour() - - cc_b_4: CCWallet = await CCWallet.create_wallet_for_cc(wallet_node_b.wallet_state_manager, wallet_b, colour) - cc_balance = await cc_a_4.get_confirmed_balance() - cc_balance_2 = await cc_b_4.get_confirmed_balance() - offer_dict = {1: -30, cc_a_4.id(): 50} - - file = "test_offer_file.offer" - file_path = Path(file) - if file_path.exists(): - file_path.unlink() - - success, offer, error = await trade_manager_b.create_offer_for_ids(offer_dict, file) - - success, trade_a, reason = await trade_manager_a.respond_to_offer(file_path) - await asyncio.sleep(1) - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - await time_out_assert(15, cc_a_4.get_confirmed_balance, cc_balance - 50) - await time_out_assert(15, cc_b_4.get_confirmed_balance, cc_balance_2 + 50) - - async def assert_func(): - assert trade_a is not None - trade = await trade_manager_a.get_trade_by_id(trade_a.trade_id) - assert trade is not None - return trade.status - - async def assert_func_b(): - assert offer is not None - trade = await trade_manager_b.get_trade_by_id(offer.trade_id) - assert trade is not None - return trade.status - - await time_out_assert(15, assert_func, TradeStatus.CONFIRMED.value) - await time_out_assert(15, assert_func_b, TradeStatus.CONFIRMED.value) - - @pytest.mark.asyncio - async def test_cc_trade_cancel_insecure(self, wallets_prefarm): - # Wallet A Wallet B - # CCWallet id 2: 50 CCWallet id 2: 50 - # CCWallet id 3: 50 CCWallet id 3: 50 - # CCWallet id 4: 40 CCWallet id 4: 60 - # Wallet A will create offer, cancel it by deleting from db only - wallet_node_a, wallet_node_b, full_node = wallets_prefarm - wallet_a = wallet_node_a.wallet_state_manager.main_wallet - trade_manager_a: TradeManager = wallet_node_a.wallet_state_manager.trade_manager - - file = "test_offer_file.offer" - file_path = Path(file) - - if file_path.exists(): - file_path.unlink() - - spendable_chia = await wallet_a.get_spendable_balance() - - offer_dict = {1: 10, 2: -30, 3: 30} - - success, trade_offer, error = await trade_manager_a.create_offer_for_ids(offer_dict, file) - await asyncio.sleep(1) - - spendable_chia_after = await wallet_a.get_spendable_balance() - - locked_coin = await trade_manager_a.get_locked_coins(wallet_a.id()) - locked_sum = 0 - for name, record in locked_coin.items(): - locked_sum += record.coin.amount - - assert spendable_chia == spendable_chia_after + locked_sum - assert success is True - assert trade_offer is not None - - # Cancel offer 1 by just deleting from db - await trade_manager_a.cancel_pending_offer(trade_offer.trade_id) - await asyncio.sleep(1) - spendable_after_cancel_1 = await wallet_a.get_spendable_balance() - - # Spendable should be the same as it was before making offer 1 - assert spendable_chia == spendable_after_cancel_1 - - trade_a = await trade_manager_a.get_trade_by_id(trade_offer.trade_id) - assert trade_a is not None - assert trade_a.status == TradeStatus.CANCELED.value - - @pytest.mark.asyncio - async def test_cc_trade_cancel_secure(self, wallets_prefarm): - # Wallet A Wallet B - # CCWallet id 2: 50 CCWallet id 2: 50 - # CCWallet id 3: 50 CCWallet id 3: 50 - # CCWallet id 4: 40 CCWallet id 4: 60 - # Wallet A will create offer, cancel it by spending coins back to self - - wallet_node_a, wallet_node_b, full_node = wallets_prefarm - wallet_a = wallet_node_a.wallet_state_manager.main_wallet - trade_manager_a: TradeManager = wallet_node_a.wallet_state_manager.trade_manager - - file = "test_offer_file.offer" - file_path = Path(file) - - if file_path.exists(): - file_path.unlink() - - spendable_chia = await wallet_a.get_spendable_balance() - - offer_dict = {1: 10, 2: -30, 3: 30} - - success, trade_offer, error = await trade_manager_a.create_offer_for_ids(offer_dict, file) - await asyncio.sleep(1) - - spendable_chia_after = await wallet_a.get_spendable_balance() - - locked_coin = await trade_manager_a.get_locked_coins(wallet_a.id()) - locked_sum = 0 - for name, record in locked_coin.items(): - locked_sum += record.coin.amount - - assert spendable_chia == spendable_chia_after + locked_sum - assert success is True - assert trade_offer is not None - - # Cancel offer 1 by spending coins that were offered - await trade_manager_a.cancel_pending_offer_safely(trade_offer.trade_id) - await asyncio.sleep(1) - - for i in range(0, buffer_blocks): - await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(15, wallet_a.get_spendable_balance, spendable_chia) - - # Spendable should be the same as it was before making offer 1 - - async def get_status(): - assert trade_offer is not None - trade_a = await trade_manager_a.get_trade_by_id(trade_offer.trade_id) - assert trade_a is not None - return trade_a.status - - await time_out_assert(15, get_status, TradeStatus.CANCELED.value) diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index b1fb1ca793f6..69723a7d222e 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -9,7 +9,7 @@ from blspy import AugSchemeMPL from chia.types.spend_bundle import SpendBundle from chia.consensus.block_rewards import calculate_pool_reward, calculate_base_farmer_reward -from tests.time_out_assert import time_out_assert +from tests.time_out_assert import time_out_assert, time_out_assert_not_none pytestmark = pytest.mark.skip("TODO: Fix tests") @@ -444,7 +444,10 @@ async def test_did_attest_after_recovery(self, two_wallet_nodes): test_info_list, test_message_spend_bundle, ) = await did_wallet_4.load_attest_files_for_recovery_spend(["test.attest"]) - await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, test_message_spend_bundle) + spend_bundle = await did_wallet_4.recovery_spend( + coin, new_ph, test_info_list, pubkey, test_message_spend_bundle + ) + await time_out_assert_not_none(15, full_node_1.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) for i in range(1, num_blocks): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) diff --git a/tests/wallet/rl_wallet/test_rl_rpc.py b/tests/wallet/rl_wallet/test_rl_rpc.py index 4b18addd28c2..3ba07a2a84bd 100644 --- a/tests/wallet/rl_wallet/test_rl_rpc.py +++ b/tests/wallet/rl_wallet/test_rl_rpc.py @@ -10,6 +10,7 @@ from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash from chia.util.ints import uint16 +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType from tests.setup_nodes import self_hostname, setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -27,7 +28,7 @@ async def is_transaction_in_mempool(user_wallet_id, api, tx_id: bytes32) -> bool val = await api.get_transaction({"wallet_id": user_wallet_id, "transaction_id": tx_id.hex()}) except ValueError: return False - for _, mis, _ in val["transaction"].sent_to: + for _, mis, _ in TransactionRecord.from_json_dict_convenience(val["transaction"]).sent_to: if ( MempoolInclusionStatus(mis) == MempoolInclusionStatus.SUCCESS or MempoolInclusionStatus(mis) == MempoolInclusionStatus.PENDING @@ -41,7 +42,7 @@ async def is_transaction_confirmed(user_wallet_id, api, tx_id: bytes32) -> bool: val = await api.get_transaction({"wallet_id": user_wallet_id, "transaction_id": tx_id.hex()}) except ValueError: return False - return val["transaction"].confirmed + return TransactionRecord.from_json_dict_convenience(val["transaction"]).confirmed async def check_balance(api, wallet_id): diff --git a/tests/wallet/rl_wallet/test_rl_wallet.py b/tests/wallet/rl_wallet/test_rl_wallet.py index 9836fbf82c1e..7a92cbe0eba2 100644 --- a/tests/wallet/rl_wallet/test_rl_wallet.py +++ b/tests/wallet/rl_wallet/test_rl_wallet.py @@ -16,7 +16,7 @@ def event_loop(): yield loop -class TestCCWallet: +class TestCATWallet: @pytest.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 4cd2983275ea..1a027eba3956 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1,9 +1,14 @@ import asyncio +from typing import Optional -from operator import attrgetter +from blspy import G2Element + +from chia.types.coin_record import CoinRecord +from chia.types.coin_spend import CoinSpend +from chia.types.spend_bundle import SpendBundle from chia.util.config import load_config, save_config +from operator import attrgetter import logging -from pathlib import Path import pytest @@ -14,17 +19,19 @@ from chia.rpc.wallet_rpc_api import WalletRpcApi from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.announcement import Announcement +from chia.types.blockchain_format.program import Program from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash from chia.consensus.coinbase import create_puzzlehash_for_pk from chia.util.hash import std_hash from chia.wallet.derive_keys import master_sk_to_wallet_sk -from chia.util.ints import uint16, uint32 +from chia.util.ints import uint16, uint32, uint64 +from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from tests.setup_nodes import bt, setup_simulators_and_wallets, self_hostname from tests.time_out_assert import time_out_assert -from tests.util.rpc import validate_get_routes log = logging.getLogger(__name__) @@ -35,9 +42,14 @@ async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_make_transaction(self, two_wallet_nodes): + async def test_wallet_rpc(self, two_wallet_nodes, trusted): test_rpc_port = uint16(21529) + test_rpc_port_2 = uint16(21536) test_rpc_port_node = uint16(21530) num_blocks = 5 full_nodes, wallets = two_wallet_nodes @@ -51,6 +63,14 @@ async def test_wallet_make_transaction(self, two_wallet_nodes): ph_2 = await wallet_2.get_new_puzzlehash() await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + wallet_node_2.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -66,6 +86,7 @@ async def test_wallet_make_transaction(self, two_wallet_nodes): ) wallet_rpc_api = WalletRpcApi(wallet_node) + wallet_rpc_api_2 = WalletRpcApi(wallet_node_2) config = bt.config hostname = config["self_hostname"] @@ -96,14 +117,25 @@ def stop_node_cb(): config, connect_to_daemon=False, ) + rpc_cleanup_2 = await start_rpc_server( + wallet_rpc_api_2, + hostname, + daemon_port, + test_rpc_port_2, + stop_node_cb, + bt.root_path, + config, + connect_to_daemon=False, + ) await time_out_assert(5, wallet.get_confirmed_balance, initial_funds) await time_out_assert(5, wallet.get_unconfirmed_balance, initial_funds) client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) - await validate_get_routes(client, wallet_rpc_api) + client_2 = await WalletRpcClient.create(self_hostname, test_rpc_port_2, bt.root_path, config) client_node = await FullNodeRpcClient.create(self_hostname, test_rpc_port_node, bt.root_path, config) try: + await time_out_assert(5, client.get_synced) addr = encode_puzzle_hash(await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(), "xch") tx_amount = 15600000 try: @@ -113,7 +145,7 @@ def stop_node_cb(): pass # Tests sending a basic transaction - tx = await client.send_transaction("1", tx_amount, addr) + tx = await client.send_transaction("1", tx_amount, addr, memos=["this is a basic tx"]) transaction_id = tx.name async def tx_in_mempool(): @@ -131,6 +163,13 @@ async def tx_in_mempool(): async def eventual_balance(): return (await client.get_wallet_balance("1"))["confirmed_wallet_balance"] + # Checks that the memo can be retrieved + tx_confirmed = await client.get_transaction("1", transaction_id) + assert tx_confirmed.confirmed + assert len(tx_confirmed.get_memos()) == 1 + assert [b"this is a basic tx"] in tx_confirmed.get_memos().values() + assert list(tx_confirmed.get_memos().keys())[0] in [a.name() for a in tx.spend_bundle.additions()] + await time_out_assert(5, eventual_balance, initial_funds_eventually - tx_amount) # Tests offline signing @@ -140,11 +179,50 @@ async def eventual_balance(): # Test basic transaction to one output and coin announcement signed_tx_amount = 888000 - tx_coin_announcements = [std_hash(b"extra_stuff"), std_hash(b"more_stuff")] + tx_coin_announcements = [ + Announcement( + std_hash(b"coin_id_1"), + std_hash(b"message"), + b"\xca", + ), + Announcement( + std_hash(b"coin_id_2"), + bytes(Program.to("a string")), + ), + ] tx_res: TransactionRecord = await client.create_signed_transaction( [{"amount": signed_tx_amount, "puzzle_hash": ph_3}], coin_announcements=tx_coin_announcements ) + assert tx_res.fee_amount == 0 + assert tx_res.amount == signed_tx_amount + assert len(tx_res.additions) == 2 # The output and the change + assert any([addition.amount == signed_tx_amount for addition in tx_res.additions]) + # check error for a ASSERT_ANNOUNCE_CONSUMED_FAILED and if the error is not there throw a value error + try: + push_res = await client_node.push_tx(tx_res.spend_bundle) + except ValueError as error: + error_string = error.args[0]["error"] # noqa: # pylint: disable=E1126 + if error_string.find("ASSERT_ANNOUNCE_CONSUMED_FAILED") == -1: + raise ValueError from error + + # # Test basic transaction to one output and puzzle announcement + signed_tx_amount = 888000 + tx_puzzle_announcements = [ + Announcement( + std_hash(b"puzzle_hash_1"), + b"message", + b"\xca", + ), + Announcement( + std_hash(b"puzzle_hash_2"), + bytes(Program.to("a string")), + ), + ] + tx_res: TransactionRecord = await client.create_signed_transaction( + [{"amount": signed_tx_amount, "puzzle_hash": ph_3}], puzzle_announcements=tx_puzzle_announcements + ) + assert tx_res.fee_amount == 0 assert tx_res.amount == signed_tx_amount assert len(tx_res.additions) == 2 # The output and the change @@ -160,7 +238,7 @@ async def eventual_balance(): # Test basic transaction to one output signed_tx_amount = 888000 tx_res: TransactionRecord = await client.create_signed_transaction( - [{"amount": signed_tx_amount, "puzzle_hash": ph_3}] + [{"amount": signed_tx_amount, "puzzle_hash": ph_3, "memos": ["My memo"]}] ) assert tx_res.fee_amount == 0 @@ -188,7 +266,7 @@ async def eventual_balance(): assert coin_to_spend is not None tx_res = await client.create_signed_transaction( - [{"amount": 444, "puzzle_hash": ph_4}, {"amount": 999, "puzzle_hash": ph_5}], + [{"amount": 444, "puzzle_hash": ph_4, "memos": ["hhh"]}, {"amount": 999, "puzzle_hash": ph_5}], coins=[coin_to_spend], fee=100, ) @@ -205,11 +283,29 @@ async def eventual_balance(): await client.farm_block(encode_puzzle_hash(ph_2, "xch")) await asyncio.sleep(0.5) + found: bool = False + for addition in tx_res.spend_bundle.additions(): + if addition.amount == 444: + cr: Optional[CoinRecord] = await client_node.get_coin_record_by_name(addition.name()) + assert cr is not None + spend: CoinSpend = await client_node.get_puzzle_and_solution( + addition.parent_coin_info, cr.confirmed_block_index + ) + sb: SpendBundle = SpendBundle([spend], G2Element()) + assert sb.get_memos() == {addition.name(): [b"hhh"]} + found = True + assert found + new_balance = initial_funds_eventually - tx_amount - signed_tx_amount - 444 - 999 - 100 await time_out_assert(5, eventual_balance, new_balance) send_tx_res: TransactionRecord = await client.send_transaction_multi( - "1", [{"amount": 555, "puzzle_hash": ph_4}, {"amount": 666, "puzzle_hash": ph_5}], fee=200 + "1", + [ + {"amount": 555, "puzzle_hash": ph_4, "memos": ["FiMemo"]}, + {"amount": 666, "puzzle_hash": ph_5, "memos": ["SeMemo"]}, + ], + fee=200, ) assert send_tx_res is not None assert send_tx_res.fee_amount == 200 @@ -264,6 +360,168 @@ async def eventual_balance(): sorted_transactions = sorted(sorted_transactions, key=attrgetter("confirmed"), reverse=True) assert all_transactions == sorted_transactions + # Checks that the memo can be retrieved + tx_confirmed = await client.get_transaction("1", send_tx_res.name) + assert tx_confirmed.confirmed + assert len(tx_confirmed.get_memos()) == 2 + print(tx_confirmed.get_memos()) + assert [b"FiMemo"] in tx_confirmed.get_memos().values() + assert [b"SeMemo"] in tx_confirmed.get_memos().values() + assert list(tx_confirmed.get_memos().keys())[0] in [a.name() for a in send_tx_res.spend_bundle.additions()] + assert list(tx_confirmed.get_memos().keys())[1] in [a.name() for a in send_tx_res.spend_bundle.additions()] + + ############## + # CATS # + ############## + + # Creates a wallet and a CAT with 20 mojos + res = await client.create_new_cat_and_wallet(20) + assert res["success"] + cat_0_id = res["wallet_id"] + asset_id = bytes.fromhex(res["asset_id"]) + assert len(asset_id) > 0 + + bal_0 = await client.get_wallet_balance(cat_0_id) + assert bal_0["confirmed_wallet_balance"] == 0 + assert bal_0["pending_coin_removal_count"] == 1 + col = await client.get_cat_asset_id(cat_0_id) + assert col == asset_id + assert (await client.get_cat_name(cat_0_id)) == "CAT Wallet" + await client.set_cat_name(cat_0_id, "My cat") + assert (await client.get_cat_name(cat_0_id)) == "My cat" + wid, name = await client.cat_asset_id_to_name(col) + assert wid == cat_0_id + assert name == "My cat" + should_be_none = await client.cat_asset_id_to_name(bytes([0] * 32)) + assert should_be_none is None + + await asyncio.sleep(1) + for i in range(0, 5): + await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await asyncio.sleep(0.5) + + bal_0 = await client.get_wallet_balance(cat_0_id) + assert bal_0["confirmed_wallet_balance"] == 20 + assert bal_0["pending_coin_removal_count"] == 0 + assert bal_0["unspent_coin_count"] == 1 + + # Creates a second wallet with the same CAT + res = await client_2.create_wallet_for_existing_cat(asset_id) + assert res["success"] + cat_1_id = res["wallet_id"] + colour_1 = bytes.fromhex(res["asset_id"]) + assert colour_1 == asset_id + + await asyncio.sleep(1) + for i in range(0, 5): + await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await asyncio.sleep(0.5) + bal_1 = await client_2.get_wallet_balance(cat_1_id) + assert bal_1["confirmed_wallet_balance"] == 0 + + addr_0 = await client.get_next_address(cat_0_id, False) + addr_1 = await client_2.get_next_address(cat_1_id, False) + + assert addr_0 != addr_1 + + await client.cat_spend(cat_0_id, 4, addr_1, 0, ["the cat memo"]) + + await asyncio.sleep(1) + for i in range(0, 5): + await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await asyncio.sleep(0.5) + + bal_0 = await client.get_wallet_balance(cat_0_id) + bal_1 = await client_2.get_wallet_balance(cat_1_id) + + assert bal_0["confirmed_wallet_balance"] == 16 + assert bal_1["confirmed_wallet_balance"] == 4 + + ########## + # Offers # + ########## + + # Create an offer of 5 chia for one CAT + offer, trade_record = await client.create_offer_for_ids({uint32(1): -5, cat_0_id: 1}, validate_only=True) + all_offers = await client.get_all_offers() + assert len(all_offers) == 0 + assert offer is None + + offer, trade_record = await client.create_offer_for_ids({uint32(1): -5, cat_0_id: 1}, fee=uint64(1)) + + summary = await client.get_offer_summary(offer) + assert summary == {"offered": {"xch": 5}, "requested": {col.hex(): 1}} + + assert await client.check_offer_validity(offer) + + all_offers = await client.get_all_offers(file_contents=True) + assert len(all_offers) == 1 + assert TradeStatus(all_offers[0].status) == TradeStatus.PENDING_ACCEPT + assert all_offers[0].offer == bytes(offer) + + trade_record = await client_2.take_offer(offer, fee=uint64(1)) + assert TradeStatus(trade_record.status) == TradeStatus.PENDING_CONFIRM + + await client.cancel_offer(offer.name(), secure=False) + + trade_record = await client.get_offer(offer.name(), file_contents=True) + assert trade_record.offer == bytes(offer) + assert TradeStatus(trade_record.status) == TradeStatus.CANCELLED + + await client.cancel_offer(offer.name(), fee=uint64(1), secure=True) + + trade_record = await client.get_offer(offer.name()) + assert TradeStatus(trade_record.status) == TradeStatus.PENDING_CANCEL + + new_offer, new_trade_record = await client.create_offer_for_ids({uint32(1): -5, cat_0_id: 1}, fee=uint64(1)) + all_offers = await client.get_all_offers() + assert len(all_offers) == 2 + + await asyncio.sleep(1) + for i in range(0, 5): + await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await asyncio.sleep(0.5) + + # Test trade sorting + def only_ids(trades): + return [t.trade_id for t in trades] + + trade_record = await client.get_offer(offer.name()) + all_offers = await client.get_all_offers() # confirmed at index descending + assert len(all_offers) == 2 + assert only_ids(all_offers) == only_ids([trade_record, new_trade_record]) + all_offers = await client.get_all_offers(reverse=True) # confirmed at index ascending + assert only_ids(all_offers) == only_ids([new_trade_record, trade_record]) + all_offers = await client.get_all_offers(sort_key="RELEVANCE") # most relevant + assert only_ids(all_offers) == only_ids([new_trade_record, trade_record]) + all_offers = await client.get_all_offers(sort_key="RELEVANCE", reverse=True) # least relevant + assert only_ids(all_offers) == only_ids([trade_record, new_trade_record]) + # Test pagination + all_offers = await client.get_all_offers(start=0, end=1) + assert len(all_offers) == 1 + all_offers = await client.get_all_offers(start=-1, end=1) + assert len(all_offers) == 1 + all_offers = await client.get_all_offers(start=50) + assert len(all_offers) == 0 + all_offers = await client.get_all_offers(start=0, end=50) + assert len(all_offers) == 2 + + # Keys and addresses + + address = await client.get_next_address("1", True) + assert len(address) > 10 + + all_transactions = await client.get_transactions("1") + + some_transactions = await client.get_transactions("1", 0, 5) + some_transactions_2 = await client.get_transactions("1", 5, 10) + assert len(all_transactions) > 1 + assert some_transactions == all_transactions[0:5] + assert some_transactions_2 == all_transactions[5:10] + + transaction_count = await client.get_transaction_count("1") + assert transaction_count == len(all_transactions) + pks = await client.get_public_keys() assert len(pks) == 1 @@ -276,7 +534,7 @@ async def tx_in_mempool_2(): return tx.is_in_mempool() await time_out_assert(5, tx_in_mempool_2, True) - assert len(await wallet.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(1)) == 2 + assert len(await wallet.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(1)) == 1 await client.delete_unconfirmed_transactions("1") assert len(await wallet.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(1)) == 0 @@ -297,8 +555,6 @@ async def tx_in_mempool_2(): await client.log_in_and_skip(pks[1]) sk_dict = await client.get_private_key(pks[1]) assert sk_dict["fingerprint"] == pks[1] - fingerprint = await client.get_logged_in_fingerprint() - assert fingerprint == pks[1] # Add in reward addresses into farmer and pool for testing delete key checks # set farmer to first private key @@ -341,25 +597,22 @@ async def tx_in_mempool_2(): balance = await client.get_wallet_balance(wallets[0]["id"]) assert balance["unconfirmed_wallet_balance"] == 0 - test_wallet_backup_path = Path("test_wallet_backup_file") - await client.create_backup(test_wallet_backup_path) - assert test_wallet_backup_path.exists() - test_wallet_backup_path.unlink() - try: await client.send_transaction(wallets[0]["id"], 100, addr) raise Exception("Should not create tx if no balance") except ValueError: pass - + # Delete all keys await client.delete_all_keys() - assert len(await client.get_public_keys()) == 0 finally: # Checks that the RPC manages to stop the node client.close() + client_2.close() client_node.close() await client.await_closed() + await client_2.await_closed() await client_node.await_closed() await rpc_cleanup() + await rpc_cleanup_2() await rpc_cleanup_node() diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index e4e00fde9ede..b45ec1e136a8 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -25,7 +25,7 @@ from tests.connection_utils import add_dummy_connection from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt from tests.time_out_assert import time_out_assert -from tests.wallet.cc_wallet.test_cc_wallet import tx_in_pool +from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool from tests.wallet_tools import WalletTool @@ -176,10 +176,18 @@ async def test_subscribe_for_ph(self, wallet_node_simulator): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hash)) funds = sum( - [calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)] + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks + 1) + ] + ) + fn_amount = sum( + cr.coin.amount + for cr in await full_node_api.full_node.coin_store.get_coin_records_by_puzzle_hash(False, puzzle_hash) ) await time_out_assert(15, wallet.get_confirmed_balance, funds) + assert funds == fn_amount msg_1 = wallet_protocol.RegisterForPhUpdates([puzzle_hash], 0) msg_response_1 = await full_node_api.register_interest_in_puzzle_hash(msg_1, fake_wallet_peer) diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index ce5ae216d249..63f1611760c6 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -16,7 +16,7 @@ def wallet_height_at_least(wallet_node, h): - height = wallet_node.wallet_state_manager.blockchain._peak_height + height = wallet_node.wallet_state_manager.blockchain.get_peak_height() if height == h: return True return False @@ -47,14 +47,22 @@ async def wallet_node_starting_height(self): async for _ in setup_node_and_wallet(test_constants, starting_height=100): yield _ + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_basic_sync_wallet(self, wallet_node, default_400_blocks): + async def test_basic_sync_wallet(self, wallet_node, default_400_blocks, trusted): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node for block in default_400_blocks: await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id: full_node_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} await wallet_server.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) # The second node should eventually catch up to the first one, and have the @@ -73,27 +81,79 @@ async def test_basic_sync_wallet(self, wallet_node, default_400_blocks): 100, wallet_height_at_least, True, wallet_node, len(default_400_blocks) + num_blocks - 5 - 1 ) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_backtrack_sync_wallet(self, wallet_node, default_400_blocks): + async def test_almost_recent(self, wallet_node, default_1000_blocks, trusted): + # Tests the edge case of receiving funds right before the recent blocks in weight proof + full_node_api, wallet_node, full_node_server, wallet_server = wallet_node + + for block in default_1000_blocks: + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) + + wallet = wallet_node.wallet_state_manager.main_wallet + ph = await wallet.get_new_puzzlehash() + + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} + + # Tests a reorg with the wallet + num_blocks = 20 + new_blocks = bt.get_consecutive_blocks( + num_blocks, block_list_input=default_1000_blocks, pool_reward_puzzle_hash=ph + ) + for i in range(1000, len(new_blocks)): + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(new_blocks[i])) + new_blocks = bt.get_consecutive_blocks( + test_constants.WEIGHT_PROOF_RECENT_BLOCKS + 10, block_list_input=new_blocks + ) + for i in range(1020, len(new_blocks)): + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(new_blocks[i])) + + await wallet_server.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + + await time_out_assert(30, wallet.get_confirmed_balance, 20 * calculate_pool_reward(1000)) + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_backtrack_sync_wallet(self, wallet_node, default_400_blocks, trusted): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node for block in default_400_blocks[:20]: await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} await wallet_server.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) # The second node should eventually catch up to the first one, and have the # same tip at height num_blocks - 1. await time_out_assert(100, wallet_height_at_least, True, wallet_node, 19) - # Tests a reorg with the wallet + # Tests a reorg with the wallet + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_short_batch_sync_wallet(self, wallet_node, default_400_blocks): - + async def test_short_batch_sync_wallet(self, wallet_node, default_400_blocks, trusted): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node for block in default_400_blocks[:200]: await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} await wallet_server.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) @@ -102,13 +162,21 @@ async def test_short_batch_sync_wallet(self, wallet_node, default_400_blocks): await time_out_assert(100, wallet_height_at_least, True, wallet_node, 199) # Tests a reorg with the wallet + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_long_sync_wallet(self, wallet_node, default_1000_blocks, default_400_blocks): + async def test_long_sync_wallet(self, wallet_node, default_1000_blocks, default_400_blocks, trusted): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node for block in default_400_blocks: await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) + if trusted: + wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} await wallet_server.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) @@ -122,7 +190,7 @@ async def test_long_sync_wallet(self, wallet_node, default_1000_blocks, default_ for block in default_1000_blocks: await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) - log.info(f"wallet node height is {wallet_node.wallet_state_manager.blockchain._peak_height}") + log.info(f"wallet node height is {wallet_node.wallet_state_manager.blockchain.get_peak_height()}") await time_out_assert(600, wallet_height_at_least, True, wallet_node, len(default_1000_blocks) - 1) await disconnect_all_and_reconnect(wallet_server, full_node_server) @@ -138,8 +206,12 @@ async def test_long_sync_wallet(self, wallet_node, default_1000_blocks, default_ 600, wallet_height_at_least, True, wallet_node, len(default_1000_blocks) + num_blocks - 5 - 1 ) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_reorg_sync(self, wallet_node_simulator, default_400_blocks): + async def test_wallet_reorg_sync(self, wallet_node_simulator, default_400_blocks, trusted): num_blocks = 5 full_nodes, wallets = wallet_node_simulator full_node_api = full_nodes[0] @@ -149,6 +221,11 @@ async def test_wallet_reorg_sync(self, wallet_node_simulator, default_400_blocks wallet = wsm.main_wallet ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {fn_server.node_id.hex(): fn_server.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} + await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) # Insert 400 blocks @@ -182,8 +259,12 @@ async def get_tx_count(wallet_id): await time_out_assert(5, get_tx_count, 0, 1) await time_out_assert(5, wallet.get_confirmed_balance, 0) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_reorg_get_coinbase(self, wallet_node_simulator, default_400_blocks): + async def test_wallet_reorg_get_coinbase(self, wallet_node_simulator, default_400_blocks, trusted): full_nodes, wallets = wallet_node_simulator full_node_api = full_nodes[0] wallet_node, server_2 = wallets[0] @@ -192,6 +273,11 @@ async def test_wallet_reorg_get_coinbase(self, wallet_node_simulator, default_40 wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {fn_server.node_id.hex(): fn_server.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} + await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) # Insert 400 blocks diff --git a/tests/wallet/test_puzzle_store.py b/tests/wallet/test_puzzle_store.py index 976c4f147d53..94fa358c70e7 100644 --- a/tests/wallet/test_puzzle_store.py +++ b/tests/wallet/test_puzzle_store.py @@ -43,6 +43,7 @@ async def test_puzzle_store(self): AugSchemeMPL.key_gen(token_bytes(32)).get_g1(), WalletType.STANDARD_WALLET, uint32(1), + False, ) ) derivation_recs.append( @@ -52,6 +53,7 @@ async def test_puzzle_store(self): AugSchemeMPL.key_gen(token_bytes(32)).get_g1(), WalletType.RATE_LIMITED, uint32(2), + False, ) ) assert await db.puzzle_hash_exists(derivation_recs[0].puzzle_hash) is False @@ -61,7 +63,7 @@ async def test_puzzle_store(self): assert len((await db.get_all_puzzle_hashes())) == 0 assert await db.get_last_derivation_path() is None assert await db.get_unused_derivation_path() is None - assert await db.get_derivation_record(0, 2) is None + assert await db.get_derivation_record(0, 2, False) is None await db.add_derivation_paths(derivation_recs) @@ -87,7 +89,7 @@ async def test_puzzle_store(self): assert len((await db.get_all_puzzle_hashes())) == 2000 assert await db.get_last_derivation_path() == 999 assert await db.get_unused_derivation_path() == 0 - assert await db.get_derivation_record(0, 2) == derivation_recs[1] + assert await db.get_derivation_record(0, 2, False) == derivation_recs[1] # Indeces up to 250 await db.set_used_up_to(249) diff --git a/tests/wallet/test_singleton_lifecycle_fast.py b/tests/wallet/test_singleton_lifecycle_fast.py index 19e60f3de30b..b848416605a5 100644 --- a/tests/wallet/test_singleton_lifecycle_fast.py +++ b/tests/wallet/test_singleton_lifecycle_fast.py @@ -270,13 +270,7 @@ def launcher_conditions_and_spend_bundle( puzzle_db.add_puzzle(launcher_puzzle) launcher_puzzle_hash = launcher_puzzle.get_tree_hash() launcher_coin = Coin(parent_coin_id, launcher_puzzle_hash, launcher_amount) - # TODO: address hint error and remove ignore - # error: Argument 1 to "singleton_puzzle" has incompatible type "bytes32"; expected "Program" [arg-type] - singleton_full_puzzle = singleton_puzzle( - launcher_coin.name(), # type: ignore[arg-type] - launcher_puzzle_hash, - initial_singleton_inner_puzzle, - ) + singleton_full_puzzle = singleton_puzzle(launcher_coin.name(), launcher_puzzle_hash, initial_singleton_inner_puzzle) puzzle_db.add_puzzle(singleton_full_puzzle) singleton_full_puzzle_hash = singleton_full_puzzle.get_tree_hash() message_program = Program.to([singleton_full_puzzle_hash, launcher_amount, metadata]) @@ -304,11 +298,11 @@ def launcher_conditions_and_spend_bundle( return launcher_coin.name(), expected_conditions, spend_bundle -def singleton_puzzle(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> Program: +def singleton_puzzle(launcher_id: bytes32, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> Program: return SINGLETON_MOD.curry((SINGLETON_MOD_HASH, (launcher_id, launcher_puzzle_hash)), inner_puzzle) -def singleton_puzzle_hash(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> bytes32: +def singleton_puzzle_hash(launcher_id: bytes32, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> bytes32: return singleton_puzzle(launcher_id, launcher_puzzle_hash, inner_puzzle).get_tree_hash() @@ -433,13 +427,7 @@ def spend_coin_to_singleton( assert_coin_spent(coin_store, launcher_coin) assert_coin_spent(coin_store, farmed_coin) - # TODO: address hint error and remove ignore - # error: Argument 1 to "singleton_puzzle" has incompatible type "bytes32"; expected "Program" [arg-type] - singleton_expected_puzzle = singleton_puzzle( - launcher_id, # type: ignore[arg-type] - launcher_puzzle_hash, - initial_singleton_puzzle, - ) + singleton_expected_puzzle = singleton_puzzle(launcher_id, launcher_puzzle_hash, initial_singleton_puzzle) singleton_expected_puzzle_hash = singleton_expected_puzzle.get_tree_hash() expected_singleton_coin = Coin(launcher_coin.name(), singleton_expected_puzzle_hash, launcher_amount) assert_coin_spent(coin_store, expected_singleton_coin, is_spent=False) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 23a534c3c788..d398d00d76f3 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -16,7 +16,7 @@ from chia.wallet.wallet_state_manager import WalletStateManager from tests.setup_nodes import self_hostname, setup_simulators_and_wallets from tests.time_out_assert import time_out_assert, time_out_assert_not_none -from tests.wallet.cc_wallet.test_cc_wallet import tx_in_pool +from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool @pytest.fixture(scope="module") @@ -28,7 +28,7 @@ def event_loop(): class TestWalletSimulator: @pytest.fixture(scope="function") async def wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): + async for _ in setup_simulators_and_wallets(1, 1, {}, True): yield _ @pytest.fixture(scope="function") @@ -38,21 +38,25 @@ async def wallet_node_100_pk(self): @pytest.fixture(scope="function") async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): + async for _ in setup_simulators_and_wallets(1, 2, {}, True): yield _ @pytest.fixture(scope="function") async def two_wallet_nodes_five_freeze(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): + async for _ in setup_simulators_and_wallets(1, 2, {}, True): yield _ @pytest.fixture(scope="function") async def three_sim_two_wallets(self): - async for _ in setup_simulators_and_wallets(3, 2, {}): + async for _ in setup_simulators_and_wallets(3, 2, {}, True): yield _ + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_coinbase(self, wallet_node): + async def test_wallet_coinbase(self, wallet_node, trusted): num_blocks = 10 full_nodes, wallets = wallet_node full_node_api = full_nodes[0] @@ -61,6 +65,10 @@ async def test_wallet_coinbase(self, wallet_node): wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {server_1.node_id: server_1.node_id} + else: + wallet_node.config["trusted_peers"] = {} await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) for i in range(0, num_blocks): @@ -99,8 +107,12 @@ async def check_tx_are_pool_farm_rewards(): await time_out_assert(10, check_tx_are_pool_farm_rewards, True) await time_out_assert(5, wallet.get_confirmed_balance, funds) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_make_transaction(self, two_wallet_nodes): + async def test_wallet_make_transaction(self, two_wallet_nodes, trusted): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -109,6 +121,12 @@ async def test_wallet_make_transaction(self, two_wallet_nodes): wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {server_1.node_id: server_1.node_id} + wallet_node_2.config["trusted_peers"] = {server_1.node_id: server_1.node_id} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) @@ -146,8 +164,12 @@ async def test_wallet_make_transaction(self, two_wallet_nodes): await time_out_assert(5, wallet.get_confirmed_balance, new_funds - 10) await time_out_assert(5, wallet.get_unconfirmed_balance, new_funds - 10) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_coinbase_reorg(self, wallet_node): + async def test_wallet_coinbase_reorg(self, wallet_node, trusted): num_blocks = 5 full_nodes, wallets = wallet_node full_node_api = full_nodes[0] @@ -155,7 +177,10 @@ async def test_wallet_coinbase_reorg(self, wallet_node): wallet_node, server_2 = wallets[0] wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() - + if trusted: + wallet_node.config["trusted_peers"] = {fn_server.node_id: fn_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -177,8 +202,12 @@ async def test_wallet_coinbase_reorg(self, wallet_node): await time_out_assert(5, wallet.get_confirmed_balance, funds) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_send_to_three_peers(self, three_sim_two_wallets): + async def test_wallet_send_to_three_peers(self, three_sim_two_wallets, trusted): num_blocks = 10 full_nodes, wallets = three_sim_two_wallets @@ -196,6 +225,10 @@ async def test_wallet_send_to_three_peers(self, three_sim_two_wallets): server_2 = full_node_2.server ph = await wallet_0.wallet_state_manager.main_wallet.get_new_puzzlehash() + if trusted: + wallet_0.config["trusted_peers"] = {server_0.node_id: server_0.node_id} + else: + wallet_0.config["trusted_peers"] = {} # wallet0 <-> sever0 await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(server_0._port)), None) @@ -223,15 +256,19 @@ async def test_wallet_send_to_three_peers(self, three_sim_two_wallets): # wallet0 <-> sever1 await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(server_1._port)), wallet_0.on_connect) - await time_out_assert_not_none(5, full_node_1.mempool_manager.get_spendbundle, tx.spend_bundle.name()) + await time_out_assert_not_none(15, full_node_1.mempool_manager.get_spendbundle, tx.spend_bundle.name()) # wallet0 <-> sever2 await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(server_2._port)), wallet_0.on_connect) - await time_out_assert_not_none(5, full_node_2.mempool_manager.get_spendbundle, tx.spend_bundle.name()) + await time_out_assert_not_none(15, full_node_2.mempool_manager.get_spendbundle, tx.spend_bundle.name()) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze): + async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze, trusted): num_blocks = 10 full_nodes, wallets = two_wallet_nodes_five_freeze full_node_api_0 = full_nodes[0] @@ -243,7 +280,12 @@ async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze): wallet_0 = wallet_node_0.wallet_state_manager.main_wallet wallet_1 = wallet_node_1.wallet_state_manager.main_wallet ph = await wallet_0.get_new_puzzlehash() - + if trusted: + wallet_node_0.config["trusted_peers"] = {server_0.node_id: server_0.node_id} + wallet_node_1.config["trusted_peers"] = {server_0.node_id: server_0.node_id} + else: + wallet_node_0.config["trusted_peers"] = {} + wallet_node_1.config["trusted_peers"] = {} await wallet_0_server.start_client(PeerInfo(self_hostname, uint16(server_0._port)), None) await wallet_1_server.start_client(PeerInfo(self_hostname, uint16(server_0._port)), None) @@ -341,9 +383,12 @@ async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze): # True, # ) # await _teardown_nodes(node_iters) - + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_make_transaction_with_fee(self, two_wallet_nodes): + async def test_wallet_make_transaction_with_fee(self, two_wallet_nodes, trusted): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -351,7 +396,16 @@ async def test_wallet_make_transaction_with_fee(self, two_wallet_nodes): wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() - + if trusted: + wallet_node.config["trusted_peers"] = { + full_node_1.full_node.server.node_id: full_node_1.full_node.server.node_id + } + wallet_node_2.config["trusted_peers"] = { + full_node_1.full_node.server.node_id: full_node_1.full_node.server.node_id + } + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_1.full_node.server._port)), None) for i in range(0, num_blocks): @@ -396,8 +450,12 @@ async def test_wallet_make_transaction_with_fee(self, two_wallet_nodes): await time_out_assert(5, wallet.get_confirmed_balance, new_funds - tx_amount - tx_fee) await time_out_assert(5, wallet.get_unconfirmed_balance, new_funds - tx_amount - tx_fee) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes): + async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes, trusted): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -405,7 +463,16 @@ async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes): wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() - + if trusted: + wallet_node.config["trusted_peers"] = { + full_node_1.full_node.server.node_id: full_node_1.full_node.server.node_id + } + wallet_node_2.config["trusted_peers"] = { + full_node_1.full_node.server.node_id: full_node_1.full_node.server.node_id + } + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_1.full_node.server._port)), None) for i in range(0, num_blocks): @@ -479,8 +546,12 @@ async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes): assert above_limit_tx is None + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_prevent_fee_theft(self, two_wallet_nodes): + async def test_wallet_prevent_fee_theft(self, two_wallet_nodes, trusted): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -488,7 +559,16 @@ async def test_wallet_prevent_fee_theft(self, two_wallet_nodes): wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() - + if trusted: + wallet_node.config["trusted_peers"] = { + full_node_1.full_node.server.node_id: full_node_1.full_node.server.node_id + } + wallet_node_2.config["trusted_peers"] = { + full_node_1.full_node.server.node_id: full_node_1.full_node.server.node_id + } + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_1.full_node.server._port)), None) for i in range(0, num_blocks): @@ -537,6 +617,7 @@ async def test_wallet_prevent_fee_theft(self, two_wallet_nodes): trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=name, + memos=list(stolen_sb.get_memos().items()), ) await wallet.push_transaction(stolen_tx) @@ -548,11 +629,14 @@ async def test_wallet_prevent_fee_theft(self, two_wallet_nodes): # Funds have not decreased because stolen_tx was rejected outstanding_coinbase_rewards = 2000000000000 - await time_out_assert(5, wallet.get_confirmed_balance, funds + outstanding_coinbase_rewards) - await time_out_assert(5, wallet.get_confirmed_balance, funds + outstanding_coinbase_rewards) + await time_out_assert(20, wallet.get_confirmed_balance, funds + outstanding_coinbase_rewards) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_wallet_tx_reorg(self, two_wallet_nodes): + async def test_wallet_tx_reorg(self, two_wallet_nodes, trusted): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -565,6 +649,12 @@ async def test_wallet_tx_reorg(self, two_wallet_nodes): ph = await wallet.get_new_puzzlehash() ph2 = await wallet_2.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {fn_server.node_id: fn_server.node_id} + wallet_node_2.config["trusted_peers"] = {fn_server.node_id: fn_server.node_id} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) await server_3.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) @@ -635,13 +725,20 @@ async def test_wallet_tx_reorg(self, two_wallet_nodes): assert add_2_coin_record_full_node is not None assert add_2_coin_record_full_node.confirmed_block_index > 0 + @pytest.mark.parametrize( + "trusted", + [True, False], + ) @pytest.mark.asyncio - async def test_address_sliding_window(self, wallet_node_100_pk): + async def test_address_sliding_window(self, wallet_node_100_pk, trusted): full_nodes, wallets = wallet_node_100_pk full_node_api = full_nodes[0] server_1: ChiaServer = full_node_api.full_node.server wallet_node, server_2 = wallets[0] - + if trusted: + wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} wallet = wallet_node.wallet_state_manager.main_wallet await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) @@ -653,7 +750,6 @@ async def test_address_sliding_window(self, wallet_node_100_pk): puzzle_hash: bytes32 = puzzle.get_tree_hash() puzzle_hashes.append(puzzle_hash) - # TODO: fix after merging new wallet. These 210 and 114 should be found await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[0])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[210])) @@ -662,12 +758,19 @@ async def test_address_sliding_window(self, wallet_node_100_pk): await time_out_assert(5, wallet.get_confirmed_balance, 2 * 10 ** 12) + # TODO: fix after merging new wallet. These 210 and 114 should be found + # TODO: This is fixed in trusted mode, needs to work in untrusted too. await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[50])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - await time_out_assert(5, wallet.get_confirmed_balance, 4 * 10 ** 12) + if trusted: + await time_out_assert(15, wallet.get_confirmed_balance, 8 * 10 ** 12) + else: + await time_out_assert(15, wallet.get_confirmed_balance, 4 * 10 ** 12) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[113])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[209])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - - await time_out_assert(5, wallet.get_confirmed_balance, 8 * 10 ** 12) + if trusted: + await time_out_assert(15, wallet.get_confirmed_balance, 12 * 10 ** 12) + else: + await time_out_assert(15, wallet.get_confirmed_balance, 8 * 10 ** 12) diff --git a/tests/wallet/test_wallet_blockchain.py b/tests/wallet/test_wallet_blockchain.py new file mode 100644 index 000000000000..39cbaec54030 --- /dev/null +++ b/tests/wallet/test_wallet_blockchain.py @@ -0,0 +1,117 @@ +import asyncio +import dataclasses +from pathlib import Path + +import aiosqlite +import pytest + +from chia.consensus.blockchain import ReceiveBlockResult +from chia.protocols import full_node_protocol +from chia.types.blockchain_format.vdf import VDFProof +from chia.types.weight_proof import WeightProof +from chia.util.db_wrapper import DBWrapper +from chia.util.generator_tools import get_block_header +from chia.wallet.key_val_store import KeyValStore +from chia.wallet.wallet_blockchain import WalletBlockchain +from tests.setup_nodes import test_constants, setup_node_and_wallet + + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +class TestWalletBlockchain: + @pytest.fixture(scope="function") + async def wallet_node(self): + async for _ in setup_node_and_wallet(test_constants): + yield _ + + @pytest.mark.asyncio + async def test_wallet_blockchain(self, wallet_node, default_1000_blocks): + full_node_api, wallet_node, full_node_server, wallet_server = wallet_node + + for block in default_1000_blocks[:600]: + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) + + res = await full_node_api.request_proof_of_weight( + full_node_protocol.RequestProofOfWeight( + default_1000_blocks[499].height + 1, default_1000_blocks[499].header_hash + ) + ) + res_2 = await full_node_api.request_proof_of_weight( + full_node_protocol.RequestProofOfWeight( + default_1000_blocks[460].height + 1, default_1000_blocks[460].header_hash + ) + ) + + res_3 = await full_node_api.request_proof_of_weight( + full_node_protocol.RequestProofOfWeight( + default_1000_blocks[505].height + 1, default_1000_blocks[505].header_hash + ) + ) + weight_proof: WeightProof = full_node_protocol.RespondProofOfWeight.from_bytes(res.data).wp + weight_proof_short: WeightProof = full_node_protocol.RespondProofOfWeight.from_bytes(res_2.data).wp + weight_proof_long: WeightProof = full_node_protocol.RespondProofOfWeight.from_bytes(res_3.data).wp + + db_filename = Path("wallet_store_test.db") + + if db_filename.exists(): + db_filename.unlink() + + db_connection = await aiosqlite.connect(db_filename) + db_wrapper = DBWrapper(db_connection) + store = await KeyValStore.create(db_wrapper) + chain = await WalletBlockchain.create( + store, test_constants, wallet_node.wallet_state_manager.weight_proof_handler + ) + try: + assert (await chain.get_peak_block()) is None + assert chain.get_peak_height() == 0 + assert chain.get_latest_timestamp() == 0 + + await chain.new_weight_proof(weight_proof) + assert (await chain.get_peak_block()) is not None + assert chain.get_peak_height() == 499 + assert chain.get_latest_timestamp() > 0 + + await chain.new_weight_proof(weight_proof_short) + assert chain.get_peak_height() == 499 + + await chain.new_weight_proof(weight_proof_long) + assert chain.get_peak_height() == 505 + + header_blocks = [] + for block in default_1000_blocks: + header_block = get_block_header(block, [], []) + header_blocks.append(header_block) + + res, err = await chain.receive_block(header_blocks[50]) + print(res, err) + assert res == ReceiveBlockResult.DISCONNECTED_BLOCK + + res, err = await chain.receive_block(header_blocks[400]) + print(res, err) + assert res == ReceiveBlockResult.ALREADY_HAVE_BLOCK + + res, err = await chain.receive_block(header_blocks[507]) + print(res, err) + assert res == ReceiveBlockResult.DISCONNECTED_BLOCK + + res, err = await chain.receive_block( + dataclasses.replace(header_blocks[506], challenge_chain_ip_proof=VDFProof(2, b"123", True)) + ) + assert res == ReceiveBlockResult.INVALID_BLOCK + + assert chain.get_peak_height() == 505 + + for block in header_blocks[506:]: + res, err = await chain.receive_block(block) + assert res == ReceiveBlockResult.NEW_PEAK + assert chain.get_peak_height() == block.height + + assert chain.get_peak_height() == 999 + finally: + await db_connection.close() + db_filename.unlink() diff --git a/tests/wallet/test_wallet_key_val_store.py b/tests/wallet/test_wallet_key_val_store.py new file mode 100644 index 000000000000..b65309151164 --- /dev/null +++ b/tests/wallet/test_wallet_key_val_store.py @@ -0,0 +1,58 @@ +import asyncio +from pathlib import Path +import aiosqlite +import pytest + +from chia.types.full_block import FullBlock +from chia.types.header_block import HeaderBlock +from chia.util.db_wrapper import DBWrapper +from chia.wallet.key_val_store import KeyValStore +from tests.setup_nodes import bt + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +class TestWalletKeyValStore: + @pytest.mark.asyncio + async def test_store(self): + db_filename = Path("wallet_store_test.db") + + if db_filename.exists(): + db_filename.unlink() + + db_connection = await aiosqlite.connect(db_filename) + db_wrapper = DBWrapper(db_connection) + store = await KeyValStore.create(db_wrapper) + try: + blocks = bt.get_consecutive_blocks(20) + block: FullBlock = blocks[0] + block_2: FullBlock = blocks[1] + + assert (await store.get_object("a", FullBlock)) is None + await store.set_object("a", block) + assert await store.get_object("a", FullBlock) == block + await store.set_object("a", block) + assert await store.get_object("a", FullBlock) == block + await store.set_object("a", block_2) + await store.set_object("a", block_2) + assert await store.get_object("a", FullBlock) == block_2 + await store.remove_object("a") + assert (await store.get_object("a", FullBlock)) is None + + for block in blocks: + assert (await store.get_object(block.header_hash.hex(), FullBlock)) is None + await store.set_object(block.header_hash.hex(), block) + assert (await store.get_object(block.header_hash.hex(), FullBlock)) == block + + # Wrong type + await store.set_object("a", block_2) + with pytest.raises(Exception): + await store.get_object("a", HeaderBlock) + + finally: + await db_connection.close() + db_filename.unlink()