From 8a8f3f116597214ff5a62508ab2a05f1dd28bc75 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Mon, 12 Jun 2023 22:20:11 -0700 Subject: [PATCH 01/21] Double check coin state --- chia/wallet/wallet_state_manager.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 96477cfbba04..36fe8059da07 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1130,6 +1130,8 @@ async def handle_clawback( # Record metadata assert coin_state.created_height is not None is_recipient: Optional[bool] = None + # For resync correctly, we need to fetch the coin state again + latest_coin_state = (await self.wallet_node.get_coin_state([coin_state.coin.name()], peer=peer))[0] # Check if the wallet is the sender sender_derivation_record: Optional[ DerivationRecord @@ -1149,11 +1151,14 @@ async def handle_clawback( if is_recipient is not None: spend_bundle = SpendBundle([coin_spend], G2Element()) memos = compute_memos(spend_bundle) + spent_height: uint32 = uint32(0) + if latest_coin_state.spent_height is not None: + spent_height = uint32(latest_coin_state.spent_height) coin_record = WalletCoinRecord( coin_state.coin, uint32(coin_state.created_height), - uint32(0), - False, + spent_height, + False if latest_coin_state.spent_height is None else True, False, WalletType.STANDARD_WALLET, 1, @@ -1171,7 +1176,7 @@ async def handle_clawback( to_puzzle_hash=metadata.recipient_puzzle_hash, amount=uint64(coin_state.coin.amount), fee_amount=uint64(0), - confirmed=False, + confirmed=False if latest_coin_state.spent_height is None else True, sent=uint32(0), spend_bundle=None, additions=[coin_state.coin], From 9a2ed50a50e0724768cba358ad26c91c6902bdca Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Tue, 13 Jun 2023 16:01:57 -0700 Subject: [PATCH 02/21] Fix coverage --- chia/wallet/wallet_state_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 36fe8059da07..e75b1899f38c 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1151,9 +1151,9 @@ async def handle_clawback( if is_recipient is not None: spend_bundle = SpendBundle([coin_spend], G2Element()) memos = compute_memos(spend_bundle) - spent_height: uint32 = uint32(0) - if latest_coin_state.spent_height is not None: - spent_height = uint32(latest_coin_state.spent_height) + spent_height: uint32 = ( + uint32(0) if latest_coin_state.spent_height is None else uint32(latest_coin_state.spent_height) + ) coin_record = WalletCoinRecord( coin_state.coin, uint32(coin_state.created_height), From 2b56b2b1e9f8ab9b16ce690e6b1141590b81ed1c Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Wed, 14 Jun 2023 17:29:59 -0700 Subject: [PATCH 03/21] Resolve comments --- chia/wallet/wallet_state_manager.py | 13 +++++------ tests/wallet/rpc/test_wallet_rpc.py | 35 ++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index e75b1899f38c..6adb418615f2 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1130,8 +1130,6 @@ async def handle_clawback( # Record metadata assert coin_state.created_height is not None is_recipient: Optional[bool] = None - # For resync correctly, we need to fetch the coin state again - latest_coin_state = (await self.wallet_node.get_coin_state([coin_state.coin.name()], peer=peer))[0] # Check if the wallet is the sender sender_derivation_record: Optional[ DerivationRecord @@ -1151,14 +1149,14 @@ async def handle_clawback( if is_recipient is not None: spend_bundle = SpendBundle([coin_spend], G2Element()) memos = compute_memos(spend_bundle) - spent_height: uint32 = ( - uint32(0) if latest_coin_state.spent_height is None else uint32(latest_coin_state.spent_height) - ) + spent_height: uint32 = uint32(0) + if coin_state.spent_height is not None: # pragma: no cover + spent_height = uint32(coin_state.spent_height) coin_record = WalletCoinRecord( coin_state.coin, uint32(coin_state.created_height), spent_height, - False if latest_coin_state.spent_height is None else True, + False if spent_height == 0 else True, False, WalletType.STANDARD_WALLET, 1, @@ -1169,14 +1167,13 @@ async def handle_clawback( await self.coin_store.add_coin_record(coin_record) # Add tx record created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.created_height)) - spend_bundle = SpendBundle([coin_spend], G2Element()) tx_record = TransactionRecord( confirmed_at_height=uint32(coin_state.created_height), created_at_time=uint64(created_timestamp), to_puzzle_hash=metadata.recipient_puzzle_hash, amount=uint64(coin_state.coin.amount), fee_amount=uint64(0), - confirmed=False if latest_coin_state.spent_height is None else True, + confirmed=False if spent_height == 0 else True, sent=uint32(0), spend_bundle=None, additions=[coin_state.coin], diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index ac9c6b004520..dbd963d3b79b 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1911,6 +1911,8 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes env: WalletRpcTestEnvironment = wallet_rpc_environment full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client + wallet_node: WalletNode = env.wallet_1.node + wallet_node_2: WalletNode = env.wallet_2.node await generate_funds(full_node_api, env.wallet_1) wc = env.wallet_1.rpc_client await wc.create_new_did_wallet(1, 0) @@ -1927,17 +1929,35 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) await farm_transaction_block(full_node_api, env.wallet_1.node) await time_out_assert(20, wc.get_synced) - - wallet_node: WalletNode = env.wallet_1.node - wallet_node_2: WalletNode = env.wallet_2.node + # Test Clawback resync + tx = await wc.send_transaction( + wallet_id=1, + amount=uint64(500), + address=address, + fee=uint64(0), + puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + ) + clawback_coin_id = tx.additions[0].name() + assert tx.spend_bundle is not None + await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) + await time_out_assert(20, wc.get_synced) + await asyncio.sleep(10) + resp = await wc.spend_clawback_coins([clawback_coin_id], 0) + assert resp["success"] + assert len(resp["transaction_ids"]) == 1 + await time_out_assert_not_none( + 10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0]) + ) + await farm_transaction_block(full_node_api, wallet_node) + await time_out_assert(20, wc.get_synced) wallet_node_2._close() await wallet_node_2._await_closed() # set flag to reset wallet sync data on start await client.set_wallet_resync_on_startup() fingerprint = wallet_node.logged_in_fingerprint assert wallet_node._wallet_state_manager - # 2 reward coins, 1 DID, 1 NFT - assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 4 + # 2 reward coins, 1 DID, 1 NFT, 1 clawbacked coin + assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 5 assert await wallet_node._wallet_state_manager.nft_store.count() == 1 # standard wallet, did wallet, nft wallet, did nft wallet assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 4 @@ -1958,6 +1978,10 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions() # transactions should be the same assert after_txs == before_txs + # Check clawback + clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id) + assert clawback_tx is not None + assert clawback_tx.confirmed # only coin_store was populated in this case, but now should be empty assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 0 assert await wallet_node_2._wallet_state_manager.nft_store.count() == 0 @@ -1968,6 +1992,7 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes assert updated_config["wallet"].get("reset_sync_for_fingerprint") is None wallet_node_2._close() await wallet_node_2._await_closed() + assert False @pytest.mark.asyncio From ab4b6cd79484bd489d13859caa0b55bb0530b83d Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Wed, 14 Jun 2023 18:49:03 -0700 Subject: [PATCH 04/21] Fix unit test --- tests/wallet/rpc/test_wallet_rpc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index dbd963d3b79b..f8efb8a93f4c 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1992,7 +1992,6 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes assert updated_config["wallet"].get("reset_sync_for_fingerprint") is None wallet_node_2._close() await wallet_node_2._await_closed() - assert False @pytest.mark.asyncio From 810af8ce3596cc6f628d8019a7d7df47fbb8183b Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Sun, 18 Jun 2023 22:51:40 -0700 Subject: [PATCH 05/21] Fix redundant tx --- chia/wallet/wallet_state_manager.py | 35 +++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 6adb418615f2..4fed25a6992b 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1151,7 +1151,34 @@ async def handle_clawback( memos = compute_memos(spend_bundle) spent_height: uint32 = uint32(0) if coin_state.spent_height is not None: # pragma: no cover + self.log.debug("Resync clawback coin: %s", coin_state.coin.name().hex()) + # Resync case spent_height = uint32(coin_state.spent_height) + # Create Clawback outgoing transaction + created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.spent_height)) + clawback_coin_spend: CoinSpend = await fetch_coin_spend_for_coin_state(coin_state, peer) + clawback_spend_bundle: SpendBundle = SpendBundle([clawback_coin_spend], G2Element()) + tx_record = TransactionRecord( + confirmed_at_height=uint32(coin_state.spent_height), + created_at_time=created_timestamp, + to_puzzle_hash=metadata.sender_puzzle_hash + if clawback_spend_bundle.additions()[0].puzzle_hash == metadata.sender_puzzle_hash + else metadata.recipient_puzzle_hash, + amount=uint64(coin_state.coin.amount), + fee_amount=uint64(0), + confirmed=True, + sent=uint32(0), + spend_bundle=clawback_spend_bundle, + additions=clawback_spend_bundle.additions(), + removals=clawback_spend_bundle.removals(), + wallet_id=uint32(1), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_CLAWBACK), + name=clawback_spend_bundle.name(), + memos=list(compute_memos(clawback_spend_bundle).items()), + ) + await self.add_pending_transaction(tx_record) coin_record = WalletCoinRecord( coin_state.coin, uint32(coin_state.created_height), @@ -1350,6 +1377,7 @@ async def _add_coin_states( change = False if not change: + self.log.debug("Found spent incoming coin: %s", coin_state) created_timestamp = await self.wallet_node.get_timestamp_for_height( uint32(coin_state.created_height) ) @@ -1416,6 +1444,10 @@ async def _add_coin_states( tx_record.name, uint32(coin_state.spent_height) ) else: + tx_name = bytes(coin_state.coin.name()) + for added_coin in additions: + tx_name += bytes(added_coin.name()) + tx_name = std_hash(tx_name) tx_record = TransactionRecord( confirmed_at_height=uint32(coin_state.spent_height), created_at_time=uint64(spent_timestamp), @@ -1433,10 +1465,9 @@ async def _add_coin_states( sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=bytes32(token_bytes()), + name=tx_name, memos=[], ) - await self.tx_store.add_transaction_record(tx_record) else: await self.coin_store.set_spent(coin_name, uint32(coin_state.spent_height)) From 91ac9331814eb3216deaf48412199185843b4738 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Tue, 20 Jun 2023 11:44:34 -0700 Subject: [PATCH 06/21] Add unit test --- tests/wallet/rpc/test_wallet_rpc.py | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index f8efb8a93f4c..af0d524b429d 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1905,6 +1905,97 @@ async def test_get_auto_claim(wallet_rpc_environment: WalletRpcTestEnvironment): assert res["min_amount"] == 0 assert res["batch_size"] == 50 +@pytest.mark.asyncio +async def test_fully_resync(wallet_rpc_environment: WalletRpcTestEnvironment): + env: WalletRpcTestEnvironment = wallet_rpc_environment + full_node_api: FullNodeSimulator = env.full_node.api + client: WalletRpcClient = env.wallet_1.rpc_client + wallet_node: WalletNode = env.wallet_1.node + wallet_node_2: WalletNode = env.wallet_2.node + await generate_funds(full_node_api, env.wallet_1) + wc = env.wallet_1.rpc_client + wc2 = env.wallet_2.rpc_client + await wc.create_new_did_wallet(1, 0) + await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) + await farm_transaction_block(full_node_api, env.wallet_1.node) + await time_out_assert(20, wc.get_synced) + + nft_wallet = await wc.create_new_nft_wallet(None) + nft_wallet_id = nft_wallet["wallet_id"] + address = await wc.get_next_address(env.wallet_1.wallet.id(), True) + await wc.mint_nft( + nft_wallet_id, royalty_address=address, target_address=address, hash="deadbeef", uris=["http://test.nft"] + ) + await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) + await farm_transaction_block(full_node_api, env.wallet_1.node) + await time_out_assert(20, wc.get_synced) + # Test Clawback resync + tx = await wc.send_transaction( + wallet_id=1, + amount=uint64(500), + address=address, + fee=uint64(0), + puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + ) + clawback_coin_id = tx.additions[0].name() + assert tx.spend_bundle is not None + await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) + await time_out_assert(20, wc.get_synced) + await asyncio.sleep(10) + resp = await wc.spend_clawback_coins([clawback_coin_id], 0) + assert resp["success"] + assert len(resp["transaction_ids"]) == 1 + await time_out_assert_not_none( + 10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0]) + ) + await farm_transaction_block(full_node_api, wallet_node) + await time_out_assert(20, wc.get_synced) + wallet_node_2._close() + await wallet_node_2._await_closed() + # set flag to reset wallet sync data on start + await client.set_wallet_resync_on_startup() + fingerprint = wallet_node.logged_in_fingerprint + assert wallet_node._wallet_state_manager + # 2 reward coins, 1 DID, 1 NFT, 1 clawbacked coin + assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 5 + assert await wallet_node._wallet_state_manager.nft_store.count() == 1 + # standard wallet, did wallet, nft wallet, did nft wallet + assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 4 + before_txs = await wallet_node.wallet_state_manager.tx_store.get_all_transactions() + # Clean transaction records + await wallet_node.wallet_state_manager.tx_store.rollback_to_block(0) + wallet_node._close() + await wallet_node._await_closed() + config = load_config(wallet_node.root_path, "config.yaml") + # check that flag was set in config file + assert config["wallet"]["reset_sync_for_fingerprint"] == fingerprint + new_config = wallet_node.config.copy() + new_config["reset_sync_for_fingerprint"] = config["wallet"]["reset_sync_for_fingerprint"] + wallet_node_2.config = new_config + wallet_node_2.root_path = wallet_node.root_path + wallet_node_2.local_keychain = wallet_node.local_keychain + # use second node to start the same wallet, reusing config and db + await wallet_node_2._start_with_fingerprint(fingerprint) + assert wallet_node_2._wallet_state_manager + await time_out_assert(20, wc2.get_synced) + after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions() + # transactions should be the same + assert len(after_txs) == len(before_txs) + # Check clawback + clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id) + assert clawback_tx is not None + assert clawback_tx.confirmed + # only coin_store was populated in this case, but now should be empty + assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 0 + assert await wallet_node_2._wallet_state_manager.nft_store.count() == 0 + # we don't delete wallets + assert len(await wallet_node_2.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 4 + updated_config = load_config(wallet_node.root_path, "config.yaml") + # check that it's disabled after reset + assert updated_config["wallet"].get("reset_sync_for_fingerprint") is None + wallet_node_2._close() + await wallet_node_2._await_closed() + @pytest.mark.asyncio async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTestEnvironment): From 8c51f8cdf934ded495098a90aa7d7a02189c4ccc Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Tue, 20 Jun 2023 14:32:45 -0700 Subject: [PATCH 07/21] Fix unit test --- tests/wallet/rpc/test_wallet_rpc.py | 45 +++++++++-------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index af0d524b429d..32e09b19b47c 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1905,6 +1905,7 @@ async def test_get_auto_claim(wallet_rpc_environment: WalletRpcTestEnvironment): assert res["min_amount"] == 0 assert res["batch_size"] == 50 + @pytest.mark.asyncio async def test_fully_resync(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment @@ -1915,20 +1916,7 @@ async def test_fully_resync(wallet_rpc_environment: WalletRpcTestEnvironment): await generate_funds(full_node_api, env.wallet_1) wc = env.wallet_1.rpc_client wc2 = env.wallet_2.rpc_client - await wc.create_new_did_wallet(1, 0) - await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) - await farm_transaction_block(full_node_api, env.wallet_1.node) - await time_out_assert(20, wc.get_synced) - - nft_wallet = await wc.create_new_nft_wallet(None) - nft_wallet_id = nft_wallet["wallet_id"] address = await wc.get_next_address(env.wallet_1.wallet.id(), True) - await wc.mint_nft( - nft_wallet_id, royalty_address=address, target_address=address, hash="deadbeef", uris=["http://test.nft"] - ) - await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) - await farm_transaction_block(full_node_api, env.wallet_1.node) - await time_out_assert(20, wc.get_synced) # Test Clawback resync tx = await wc.send_transaction( wallet_id=1, @@ -1950,19 +1938,16 @@ async def test_fully_resync(wallet_rpc_environment: WalletRpcTestEnvironment): ) await farm_transaction_block(full_node_api, wallet_node) await time_out_assert(20, wc.get_synced) - wallet_node_2._close() - await wallet_node_2._await_closed() # set flag to reset wallet sync data on start await client.set_wallet_resync_on_startup() fingerprint = wallet_node.logged_in_fingerprint assert wallet_node._wallet_state_manager - # 2 reward coins, 1 DID, 1 NFT, 1 clawbacked coin - assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 5 - assert await wallet_node._wallet_state_manager.nft_store.count() == 1 - # standard wallet, did wallet, nft wallet, did nft wallet - assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 4 + # 2 reward coins, 1 clawbacked coin + assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 3 + # standard wallet + assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 1 before_txs = await wallet_node.wallet_state_manager.tx_store.get_all_transactions() - # Clean transaction records + # Delete tx records await wallet_node.wallet_state_manager.tx_store.rollback_to_block(0) wallet_node._close() await wallet_node._await_closed() @@ -1977,6 +1962,7 @@ async def test_fully_resync(wallet_rpc_environment: WalletRpcTestEnvironment): # use second node to start the same wallet, reusing config and db await wallet_node_2._start_with_fingerprint(fingerprint) assert wallet_node_2._wallet_state_manager + await farm_transaction_block(full_node_api, wallet_node_2) await time_out_assert(20, wc2.get_synced) after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions() # transactions should be the same @@ -1985,16 +1971,13 @@ async def test_fully_resync(wallet_rpc_environment: WalletRpcTestEnvironment): clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id) assert clawback_tx is not None assert clawback_tx.confirmed - # only coin_store was populated in this case, but now should be empty - assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 0 - assert await wallet_node_2._wallet_state_manager.nft_store.count() == 0 - # we don't delete wallets - assert len(await wallet_node_2.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 4 - updated_config = load_config(wallet_node.root_path, "config.yaml") - # check that it's disabled after reset - assert updated_config["wallet"].get("reset_sync_for_fingerprint") is None - wallet_node_2._close() - await wallet_node_2._await_closed() + outgoing_clawback_txs = await wallet_node_2.wallet_state_manager.tx_store.get_transactions_between( + 1, 0, 100, type_filter=TransactionTypeFilter.include([TransactionType.OUTGOING_CLAWBACK]) + ) + assert len(outgoing_clawback_txs) == 1 + assert outgoing_clawback_txs[0].confirmed + # Check unspent coins + assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 3 @pytest.mark.asyncio From 05b54c09c23ffc4a3b03ffc15c3ec4e5f37eda7a Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Tue, 20 Jun 2023 22:38:11 -0700 Subject: [PATCH 08/21] refine unit test --- tests/wallet/test_wallet.py | 107 +++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 8f236add4abc..70c123005515 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -14,18 +14,20 @@ from chia.server.server import ChiaServer from chia.simulator.block_tools import BlockTools from chia.simulator.full_node_simulator import FullNodeSimulator, wait_for_coins_in_wallet -from chia.simulator.simulator_protocol import ReorgProtocol +from chia.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol from chia.simulator.time_out_assert import time_out_assert, time_out_assert_not_none from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import compute_additions from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash +from chia.util.config import load_config from chia.util.ints import uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.query_filter import TransactionTypeFilter from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import CoinType from chia.wallet.wallet import CHIP_0002_SIGN_MESSAGE_PREFIX @@ -583,6 +585,109 @@ async def test_wallet_clawback_reorg( 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK ) + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_clawback_resync( + self, + two_wallet_nodes: Tuple[List[FullNodeSimulator], List[Tuple[WalletNode, ChiaServer]], BlockTools], + trusted: bool, + self_hostname: str, + ) -> None: + num_blocks = 1 + full_nodes, wallets, _ = two_wallet_nodes + full_node_api = full_nodes[0] + server_1 = full_node_api.full_node.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + api_0 = WalletRpcApi(wallet_node) + if trusted: + wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} + wallet_node_2.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} + 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) + await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + expected_confirmed_balance = await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) + normal_puzhash = await wallet.get_new_puzzlehash() + # Transfer to normal wallet + tx = await wallet.generate_signed_transaction( + uint64(500), + normal_puzhash, + uint64(0), + puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + ) + clawback_coin_id = tx.additions[0].name() + assert tx.spend_bundle is not None + await wallet.push_transaction(tx) + await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) + expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) + # Check merkle coins + await time_out_assert( + 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK + ) + assert await wallet.get_confirmed_balance() == 3999999999500 + await asyncio.sleep(10) + # clawback merkle coin + resp = await api_0.spend_clawback_coins( + dict({"coin_ids": [normal_puzhash.hex(), clawback_coin_id.hex()], "fee": 0}) + ) + json.dumps(resp) + assert resp["success"] + assert len(resp["transaction_ids"]) == 1 + expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) + await time_out_assert( + 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK + ) + wallet_node_2._close() + await wallet_node_2._await_closed() + # set flag to reset wallet sync data on start + await api_0.set_wallet_resync_on_startup({"enable": True}) + fingerprint = wallet_node.logged_in_fingerprint + assert wallet_node._wallet_state_manager + # 2 reward coins, 1 clawbacked coin + assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 7 + # standard wallet + assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 1 + before_txs = await wallet_node.wallet_state_manager.tx_store.get_all_transactions() + # Delete tx records + await wallet_node.wallet_state_manager.tx_store.rollback_to_block(0) + wallet_node._close() + await wallet_node._await_closed() + config = load_config(wallet_node.root_path, "config.yaml") + # check that flag was set in config file + assert config["wallet"]["reset_sync_for_fingerprint"] == fingerprint + new_config = wallet_node.config.copy() + new_config["reset_sync_for_fingerprint"] = config["wallet"]["reset_sync_for_fingerprint"] + wallet_node_2.config = new_config + wallet_node_2.root_path = wallet_node.root_path + wallet_node_2.local_keychain = wallet_node.local_keychain + # use second node to start the same wallet, reusing config and db + await wallet_node_2._start_with_fingerprint(fingerprint) + assert wallet_node_2._wallet_state_manager + await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32))) + await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_2, timeout=20) + after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions() + # transactions should be the same + assert len(after_txs) == len(before_txs) + # Check clawback + clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id) + assert clawback_tx is not None + assert clawback_tx.confirmed + outgoing_clawback_txs = await wallet_node_2.wallet_state_manager.tx_store.get_transactions_between( + 1, 0, 100, type_filter=TransactionTypeFilter.include([TransactionType.OUTGOING_CLAWBACK]) + ) + assert len(outgoing_clawback_txs) == 1 + assert outgoing_clawback_txs[0].confirmed + # Check unspent coins + assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 7 + @pytest.mark.parametrize( "trusted", [True, False], From bfa2df2792049c4e6efb17652891765e7dfaf0e9 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Tue, 20 Jun 2023 23:26:06 -0700 Subject: [PATCH 09/21] Remove duplicate test --- tests/wallet/rpc/test_wallet_rpc.py | 74 ----------------------------- 1 file changed, 74 deletions(-) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 32e09b19b47c..f8efb8a93f4c 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1906,80 +1906,6 @@ async def test_get_auto_claim(wallet_rpc_environment: WalletRpcTestEnvironment): assert res["batch_size"] == 50 -@pytest.mark.asyncio -async def test_fully_resync(wallet_rpc_environment: WalletRpcTestEnvironment): - env: WalletRpcTestEnvironment = wallet_rpc_environment - full_node_api: FullNodeSimulator = env.full_node.api - client: WalletRpcClient = env.wallet_1.rpc_client - wallet_node: WalletNode = env.wallet_1.node - wallet_node_2: WalletNode = env.wallet_2.node - await generate_funds(full_node_api, env.wallet_1) - wc = env.wallet_1.rpc_client - wc2 = env.wallet_2.rpc_client - address = await wc.get_next_address(env.wallet_1.wallet.id(), True) - # Test Clawback resync - tx = await wc.send_transaction( - wallet_id=1, - amount=uint64(500), - address=address, - fee=uint64(0), - puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], - ) - clawback_coin_id = tx.additions[0].name() - assert tx.spend_bundle is not None - await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) - await time_out_assert(20, wc.get_synced) - await asyncio.sleep(10) - resp = await wc.spend_clawback_coins([clawback_coin_id], 0) - assert resp["success"] - assert len(resp["transaction_ids"]) == 1 - await time_out_assert_not_none( - 10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0]) - ) - await farm_transaction_block(full_node_api, wallet_node) - await time_out_assert(20, wc.get_synced) - # set flag to reset wallet sync data on start - await client.set_wallet_resync_on_startup() - fingerprint = wallet_node.logged_in_fingerprint - assert wallet_node._wallet_state_manager - # 2 reward coins, 1 clawbacked coin - assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 3 - # standard wallet - assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 1 - before_txs = await wallet_node.wallet_state_manager.tx_store.get_all_transactions() - # Delete tx records - await wallet_node.wallet_state_manager.tx_store.rollback_to_block(0) - wallet_node._close() - await wallet_node._await_closed() - config = load_config(wallet_node.root_path, "config.yaml") - # check that flag was set in config file - assert config["wallet"]["reset_sync_for_fingerprint"] == fingerprint - new_config = wallet_node.config.copy() - new_config["reset_sync_for_fingerprint"] = config["wallet"]["reset_sync_for_fingerprint"] - wallet_node_2.config = new_config - wallet_node_2.root_path = wallet_node.root_path - wallet_node_2.local_keychain = wallet_node.local_keychain - # use second node to start the same wallet, reusing config and db - await wallet_node_2._start_with_fingerprint(fingerprint) - assert wallet_node_2._wallet_state_manager - await farm_transaction_block(full_node_api, wallet_node_2) - await time_out_assert(20, wc2.get_synced) - after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions() - # transactions should be the same - assert len(after_txs) == len(before_txs) - # Check clawback - clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id) - assert clawback_tx is not None - assert clawback_tx.confirmed - outgoing_clawback_txs = await wallet_node_2.wallet_state_manager.tx_store.get_transactions_between( - 1, 0, 100, type_filter=TransactionTypeFilter.include([TransactionType.OUTGOING_CLAWBACK]) - ) - assert len(outgoing_clawback_txs) == 1 - assert outgoing_clawback_txs[0].confirmed - # Check unspent coins - assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 3 - - @pytest.mark.asyncio async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment From 374f10c22323b5b36e24903b1910aedf616a28ee Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Wed, 21 Jun 2023 09:11:18 -0700 Subject: [PATCH 10/21] Fix test on Windows --- tests/wallet/test_wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 70c123005515..f29431048647 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -664,6 +664,7 @@ async def test_clawback_resync( assert config["wallet"]["reset_sync_for_fingerprint"] == fingerprint new_config = wallet_node.config.copy() new_config["reset_sync_for_fingerprint"] = config["wallet"]["reset_sync_for_fingerprint"] + new_config["database_path"] = "wallet/db/blockchain_wallet_v2_test_CHALLENGE_KEY.sqlite" wallet_node_2.config = new_config wallet_node_2.root_path = wallet_node.root_path wallet_node_2.local_keychain = wallet_node.local_keychain From 6d6dac2961159b6f8c224417b9246a37e7ff6302 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Thu, 22 Jun 2023 10:45:06 -0700 Subject: [PATCH 11/21] Fix clawback memo --- chia/wallet/wallet_state_manager.py | 67 +++------ tests/wallet/test_wallet.py | 205 ++++++++++++++-------------- 2 files changed, 119 insertions(+), 153 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 4fed25a6992b..51cb2f14cbed 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -82,7 +82,7 @@ from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager from chia.wallet.util.query_filter import HashFilter -from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType from chia.wallet.util.wallet_sync_utils import ( PeerRequestException, fetch_coin_spend_for_coin_state, @@ -754,15 +754,24 @@ async def spend_clawback_coins(self, clawback_coins: Dict[Coin, ClawbackMetadata if self.main_wallet.secret_key_store.secret_key_for_public_key(derivation_record.pubkey) is None: await self.main_wallet.hack_populate_secret_key_for_puzzle_hash(derivation_record.puzzle_hash) amount = uint64(amount + coin.amount) + memos: List[bytes] = [] if len(incoming_tx.memos) == 0 else incoming_tx.memos[0][1].copy() + if ( + len(memos) > 0 + and not is_recipient + and await self.puzzle_store.puzzle_hash_exists(recipient_puzhash) + ): + # Sender and recipient belong to the same wallet + # Clean hint in this case to prevent duplicate processing + memos[0] = b"" + if len(memos) == 1 and memos[0] == b"": + memos = [] inner_puzzle: Program = self.main_wallet.puzzle_for_pk(derivation_record.pubkey) inner_solution: Program = self.main_wallet.make_solution( primaries=[ Payment( derivation_record.puzzle_hash, uint64(coin.amount), - [] - if len(incoming_tx.memos) == 0 - else incoming_tx.memos[0][1], # Forward memo of the first coin + memos, # Forward memo of the first coin ) ], coin_announcements=None if len(coin_spends) > 0 or fee == 0 else {message}, @@ -1149,41 +1158,11 @@ async def handle_clawback( if is_recipient is not None: spend_bundle = SpendBundle([coin_spend], G2Element()) memos = compute_memos(spend_bundle) - spent_height: uint32 = uint32(0) - if coin_state.spent_height is not None: # pragma: no cover - self.log.debug("Resync clawback coin: %s", coin_state.coin.name().hex()) - # Resync case - spent_height = uint32(coin_state.spent_height) - # Create Clawback outgoing transaction - created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.spent_height)) - clawback_coin_spend: CoinSpend = await fetch_coin_spend_for_coin_state(coin_state, peer) - clawback_spend_bundle: SpendBundle = SpendBundle([clawback_coin_spend], G2Element()) - tx_record = TransactionRecord( - confirmed_at_height=uint32(coin_state.spent_height), - created_at_time=created_timestamp, - to_puzzle_hash=metadata.sender_puzzle_hash - if clawback_spend_bundle.additions()[0].puzzle_hash == metadata.sender_puzzle_hash - else metadata.recipient_puzzle_hash, - amount=uint64(coin_state.coin.amount), - fee_amount=uint64(0), - confirmed=True, - sent=uint32(0), - spend_bundle=clawback_spend_bundle, - additions=clawback_spend_bundle.additions(), - removals=clawback_spend_bundle.removals(), - wallet_id=uint32(1), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.OUTGOING_CLAWBACK), - name=clawback_spend_bundle.name(), - memos=list(compute_memos(clawback_spend_bundle).items()), - ) - await self.add_pending_transaction(tx_record) coin_record = WalletCoinRecord( coin_state.coin, uint32(coin_state.created_height), - spent_height, - False if spent_height == 0 else True, + uint32(0), + False, False, WalletType.STANDARD_WALLET, 1, @@ -1194,13 +1173,14 @@ async def handle_clawback( await self.coin_store.add_coin_record(coin_record) # Add tx record created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.created_height)) + spend_bundle = SpendBundle([coin_spend], G2Element()) tx_record = TransactionRecord( confirmed_at_height=uint32(coin_state.created_height), created_at_time=uint64(created_timestamp), to_puzzle_hash=metadata.recipient_puzzle_hash, amount=uint64(coin_state.coin.amount), fee_amount=uint64(0), - confirmed=False if spent_height == 0 else True, + confirmed=False, sent=uint32(0), spend_bundle=None, additions=[coin_state.coin], @@ -1377,7 +1357,6 @@ async def _add_coin_states( change = False if not change: - self.log.debug("Found spent incoming coin: %s", coin_state) created_timestamp = await self.wallet_node.get_timestamp_for_height( uint32(coin_state.created_height) ) @@ -1444,10 +1423,6 @@ async def _add_coin_states( tx_record.name, uint32(coin_state.spent_height) ) else: - tx_name = bytes(coin_state.coin.name()) - for added_coin in additions: - tx_name += bytes(added_coin.name()) - tx_name = std_hash(tx_name) tx_record = TransactionRecord( confirmed_at_height=uint32(coin_state.spent_height), created_at_time=uint64(spent_timestamp), @@ -1465,9 +1440,10 @@ async def _add_coin_states( sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=tx_name, + name=bytes32(token_bytes()), memos=[], ) + await self.tx_store.add_transaction_record(tx_record) else: await self.coin_store.set_spent(coin_name, uint32(coin_state.spent_height)) @@ -1475,10 +1451,7 @@ async def _add_coin_states( await self.interested_store.remove_interested_coin_id(coin_state.coin.name()) confirmed_tx_records: List[TransactionRecord] = [] for tx_record in all_unconfirmed: - if tx_record.type in { - TransactionType.INCOMING_CLAWBACK_SEND.value, - TransactionType.INCOMING_CLAWBACK_RECEIVE.value, - }: + if tx_record.type in CLAWBACK_TRANSACTION_TYPES: for add_coin in tx_record.additions: if add_coin == coin_state.coin: confirmed_tx_records.append(tx_record) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index f29431048647..1c658a2f0bbf 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -14,20 +14,18 @@ from chia.server.server import ChiaServer from chia.simulator.block_tools import BlockTools from chia.simulator.full_node_simulator import FullNodeSimulator, wait_for_coins_in_wallet -from chia.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol +from chia.simulator.simulator_protocol import ReorgProtocol from chia.simulator.time_out_assert import time_out_assert, time_out_assert_not_none from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import compute_additions from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash -from chia.util.config import load_config from chia.util.ints import uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_memos import compute_memos -from chia.wallet.util.query_filter import TransactionTypeFilter from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import CoinType from chia.wallet.wallet import CHIP_0002_SIGN_MESSAGE_PREFIX @@ -347,8 +345,6 @@ async def test_wallet_clawback_clawback( assert await wallet.get_confirmed_balance() == 3999999999500 # clawback merkle coin merkle_coin = tx.additions[0] if tx.additions[0].amount == 500 else tx.additions[1] - interested_coins = await wallet_node_2.wallet_state_manager.interested_store.get_interested_coin_ids() - assert merkle_coin.name() in set(interested_coins) assert len(txs["transactions"]) == 1 assert not txs["transactions"][0]["confirmed"] assert txs["transactions"][0]["metadata"]["recipient_puzzle_hash"][2:] == normal_puzhash.hex() @@ -415,7 +411,7 @@ async def test_wallet_clawback_clawback( [True, False], ) @pytest.mark.asyncio - async def test_wallet_clawback_claim_manual( + async def test_wallet_clawback_sent_self( self, two_wallet_nodes: Tuple[List[FullNodeSimulator], List[Tuple[WalletNode, ChiaServer]], BlockTools], trusted: bool, @@ -429,7 +425,7 @@ async def test_wallet_clawback_claim_manual( wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet wallet_1 = wallet_node_2.wallet_state_manager.main_wallet - api_1 = WalletRpcApi(wallet_node_2) + api_0 = WalletRpcApi(wallet_node) if trusted: wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} wallet_node_2.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} @@ -445,7 +441,7 @@ async def test_wallet_clawback_claim_manual( assert await wallet.get_confirmed_balance() == expected_confirmed_balance assert await wallet.get_unconfirmed_balance() == expected_confirmed_balance expected_confirmed_balance = await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) - normal_puzhash = await wallet_1.get_new_puzzlehash() + normal_puzhash = await wallet.get_new_puzzlehash() # Transfer to normal wallet tx = await wallet.generate_signed_transaction( uint64(500), @@ -461,14 +457,11 @@ async def test_wallet_clawback_claim_manual( await time_out_assert( 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK ) - await time_out_assert( - 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK - ) assert await wallet.get_confirmed_balance() == 3999999999500 # Claim merkle coin await asyncio.sleep(20) merkle_coin = tx.additions[0] if tx.additions[0].amount == 500 else tx.additions[1] - resp = await api_1.spend_clawback_coins( + resp = await api_0.spend_clawback_coins( dict({"coin_ids": [merkle_coin.name().hex(), normal_puzhash.hex()], "fee": 1000}) ) json.dumps(resp) @@ -478,22 +471,33 @@ async def test_wallet_clawback_claim_manual( await time_out_assert_not_none( 5, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0]) ) - expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) + expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) await time_out_assert( 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK ) - await time_out_assert( - 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK + await time_out_assert(10, wallet.get_confirmed_balance, 6000000000000) + + txs = await api_0.get_transactions( + dict( + type_filter={ + "values": [TransactionType.INCOMING_CLAWBACK_SEND.value, TransactionType.OUTGOING_CLAWBACK.value], + "mode": 1, + }, + wallet_id=1, + ) ) - await time_out_assert(10, wallet.get_confirmed_balance, 3999999999500) - await time_out_assert(10, wallet_1.get_confirmed_balance, 4000000000500) + assert len(txs["transactions"]) == 2 + assert txs["transactions"][0]["confirmed"] + assert txs["transactions"][1]["confirmed"] + assert txs["transactions"][0]["memos"] != txs["transactions"][1]["memos"] + assert txs["transactions"][0]["memos"] == {} @pytest.mark.parametrize( "trusted", [True, False], ) @pytest.mark.asyncio - async def test_wallet_clawback_reorg( + async def test_wallet_clawback_claim_manual( self, two_wallet_nodes: Tuple[List[FullNodeSimulator], List[Tuple[WalletNode, ChiaServer]], BlockTools], trusted: bool, @@ -507,6 +511,8 @@ async def test_wallet_clawback_reorg( wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet wallet_1 = wallet_node_2.wallet_state_manager.main_wallet + api_0 = WalletRpcApi(wallet_node) + api_1 = WalletRpcApi(wallet_node_2) if trusted: wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} wallet_node_2.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} @@ -542,27 +548,19 @@ async def test_wallet_clawback_reorg( 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK ) assert await wallet.get_confirmed_balance() == 3999999999500 - # Reorg before claim - # Test Reorg mint - height = full_node_api.full_node.blockchain.get_peak_height() - assert height is not None - await full_node_api.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 2), uint32(height + 1), normal_puzhash, None) - ) - await time_out_assert( - 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK - ) - await time_out_assert( - 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK - ) - # Claim merkle coin - wallet_node_2.config["auto_claim"]["enabled"] = True await asyncio.sleep(20) - # clawback merkle coin - expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) + merkle_coin = tx.additions[0] if tx.additions[0].amount == 500 else tx.additions[1] + resp = await api_1.spend_clawback_coins( + dict({"coin_ids": [merkle_coin.name().hex(), normal_puzhash.hex()], "fee": 1000}) + ) + json.dumps(resp) + assert resp["success"] + assert len(resp["transaction_ids"]) == 1 # Wait mempool update - await asyncio.sleep(5) + await time_out_assert_not_none( + 5, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0]) + ) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) await time_out_assert( 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK @@ -570,27 +568,29 @@ async def test_wallet_clawback_reorg( await time_out_assert( 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK ) - await time_out_assert(10, wallet.get_confirmed_balance, 1999999999500) - await time_out_assert(10, wallet_1.get_confirmed_balance, 12000000000500) - # Reorg after claim - height = full_node_api.full_node.blockchain.get_peak_height() - assert height is not None - await full_node_api.reorg_from_index_to_new_index( - ReorgProtocol(uint32(height - 3), uint32(height + 1), normal_puzhash, None) - ) - await time_out_assert( - 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK - ) - await time_out_assert( - 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK + await time_out_assert(10, wallet.get_confirmed_balance, 3999999999500) + await time_out_assert(10, wallet_1.get_confirmed_balance, 4000000000500) + + txs = await api_0.get_transactions( + dict( + type_filter={ + "values": [ + TransactionType.INCOMING_CLAWBACK_SEND.value, + ], + "mode": 1, + }, + wallet_id=1, + ) ) + assert len(txs["transactions"]) == 1 + assert txs["transactions"][0]["confirmed"] @pytest.mark.parametrize( "trusted", [True, False], ) @pytest.mark.asyncio - async def test_clawback_resync( + async def test_wallet_clawback_reorg( self, two_wallet_nodes: Tuple[List[FullNodeSimulator], List[Tuple[WalletNode, ChiaServer]], BlockTools], trusted: bool, @@ -603,18 +603,23 @@ async def test_clawback_resync( wallet_node, server_2 = wallets[0] wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet - api_0 = WalletRpcApi(wallet_node) + wallet_1 = wallet_node_2.wallet_state_manager.main_wallet if trusted: wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} wallet_node_2.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} else: wallet_node.config["trusted_peers"] = {} wallet_node_2.config["trusted_peers"] = {} + wallet_node_2.config["auto_claim"]["enabled"] = False await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) expected_confirmed_balance = await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) - normal_puzhash = await wallet.get_new_puzzlehash() + + assert await wallet.get_confirmed_balance() == expected_confirmed_balance + assert await wallet.get_unconfirmed_balance() == expected_confirmed_balance + expected_confirmed_balance = await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) + normal_puzhash = await wallet_1.get_new_puzzlehash() # Transfer to normal wallet tx = await wallet.generate_signed_transaction( uint64(500), @@ -622,8 +627,7 @@ async def test_clawback_resync( uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) - clawback_coin_id = tx.additions[0].name() - assert tx.spend_bundle is not None + await wallet.push_transaction(tx) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) @@ -631,63 +635,52 @@ async def test_clawback_resync( await time_out_assert( 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK ) + await time_out_assert( + 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK + ) assert await wallet.get_confirmed_balance() == 3999999999500 - await asyncio.sleep(10) - # clawback merkle coin - resp = await api_0.spend_clawback_coins( - dict({"coin_ids": [normal_puzhash.hex(), clawback_coin_id.hex()], "fee": 0}) + # Reorg before claim + # Test Reorg mint + height = full_node_api.full_node.blockchain.get_peak_height() + assert height is not None + await full_node_api.reorg_from_index_to_new_index( + ReorgProtocol(uint32(height - 2), uint32(height + 1), normal_puzhash, None) ) - json.dumps(resp) - assert resp["success"] - assert len(resp["transaction_ids"]) == 1 - expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) await time_out_assert( 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK ) - wallet_node_2._close() - await wallet_node_2._await_closed() - # set flag to reset wallet sync data on start - await api_0.set_wallet_resync_on_startup({"enable": True}) - fingerprint = wallet_node.logged_in_fingerprint - assert wallet_node._wallet_state_manager - # 2 reward coins, 1 clawbacked coin - assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 7 - # standard wallet - assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 1 - before_txs = await wallet_node.wallet_state_manager.tx_store.get_all_transactions() - # Delete tx records - await wallet_node.wallet_state_manager.tx_store.rollback_to_block(0) - wallet_node._close() - await wallet_node._await_closed() - config = load_config(wallet_node.root_path, "config.yaml") - # check that flag was set in config file - assert config["wallet"]["reset_sync_for_fingerprint"] == fingerprint - new_config = wallet_node.config.copy() - new_config["reset_sync_for_fingerprint"] = config["wallet"]["reset_sync_for_fingerprint"] - new_config["database_path"] = "wallet/db/blockchain_wallet_v2_test_CHALLENGE_KEY.sqlite" - wallet_node_2.config = new_config - wallet_node_2.root_path = wallet_node.root_path - wallet_node_2.local_keychain = wallet_node.local_keychain - # use second node to start the same wallet, reusing config and db - await wallet_node_2._start_with_fingerprint(fingerprint) - assert wallet_node_2._wallet_state_manager - await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32))) - await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_2, timeout=20) - after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions() - # transactions should be the same - assert len(after_txs) == len(before_txs) - # Check clawback - clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id) - assert clawback_tx is not None - assert clawback_tx.confirmed - outgoing_clawback_txs = await wallet_node_2.wallet_state_manager.tx_store.get_transactions_between( - 1, 0, 100, type_filter=TransactionTypeFilter.include([TransactionType.OUTGOING_CLAWBACK]) - ) - assert len(outgoing_clawback_txs) == 1 - assert outgoing_clawback_txs[0].confirmed - # Check unspent coins - assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 7 + await time_out_assert( + 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK + ) + + # Claim merkle coin + wallet_node_2.config["auto_claim"]["enabled"] = True + await asyncio.sleep(20) + # clawback merkle coin + expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) + # Wait mempool update + await asyncio.sleep(5) + expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) + await time_out_assert( + 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK + ) + await time_out_assert( + 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK + ) + await time_out_assert(10, wallet.get_confirmed_balance, 1999999999500) + await time_out_assert(10, wallet_1.get_confirmed_balance, 12000000000500) + # Reorg after claim + height = full_node_api.full_node.blockchain.get_peak_height() + assert height is not None + await full_node_api.reorg_from_index_to_new_index( + ReorgProtocol(uint32(height - 3), uint32(height + 1), normal_puzhash, None) + ) + await time_out_assert( + 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK + ) + await time_out_assert( + 20, wallet_node_2.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK + ) @pytest.mark.parametrize( "trusted", From 3e02b1970a73dc2bc58dc23559fe2fc9c7ab9037 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Thu, 22 Jun 2023 13:53:00 -0700 Subject: [PATCH 12/21] Fix revert --- chia/wallet/util/transaction_type.py | 6 ++++ chia/wallet/wallet_state_manager.py | 43 ++++++++++++++++++++++++---- tests/wallet/test_wallet.py | 2 ++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/chia/wallet/util/transaction_type.py b/chia/wallet/util/transaction_type.py index b5d1ae927906..eff5090c0c8f 100644 --- a/chia/wallet/util/transaction_type.py +++ b/chia/wallet/util/transaction_type.py @@ -13,3 +13,9 @@ class TransactionType(IntEnum): INCOMING_CLAWBACK_RECEIVE = 6 INCOMING_CLAWBACK_SEND = 7 OUTGOING_CLAWBACK = 8 + + +CLAWBACK_TRANSACTION_TYPES = { + TransactionType.INCOMING_CLAWBACK_SEND.value, + TransactionType.INCOMING_CLAWBACK_RECEIVE.value, +} diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 51cb2f14cbed..22ba1352a709 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1158,11 +1158,41 @@ async def handle_clawback( if is_recipient is not None: spend_bundle = SpendBundle([coin_spend], G2Element()) memos = compute_memos(spend_bundle) + spent_height: uint32 = uint32(0) + if coin_state.spent_height is not None: # pragma: no cover + self.log.debug("Resync clawback coin: %s", coin_state.coin.name().hex()) + # Resync case + spent_height = uint32(coin_state.spent_height) + # Create Clawback outgoing transaction + created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.spent_height)) + clawback_coin_spend: CoinSpend = await fetch_coin_spend_for_coin_state(coin_state, peer) + clawback_spend_bundle: SpendBundle = SpendBundle([clawback_coin_spend], G2Element()) + tx_record = TransactionRecord( + confirmed_at_height=uint32(coin_state.spent_height), + created_at_time=created_timestamp, + to_puzzle_hash=metadata.sender_puzzle_hash + if clawback_spend_bundle.additions()[0].puzzle_hash == metadata.sender_puzzle_hash + else metadata.recipient_puzzle_hash, + amount=uint64(coin_state.coin.amount), + fee_amount=uint64(0), + confirmed=True, + sent=uint32(0), + spend_bundle=clawback_spend_bundle, + additions=clawback_spend_bundle.additions(), + removals=clawback_spend_bundle.removals(), + wallet_id=uint32(1), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_CLAWBACK), + name=clawback_spend_bundle.name(), + memos=list(compute_memos(clawback_spend_bundle).items()), + ) + await self.add_pending_transaction(tx_record) coin_record = WalletCoinRecord( coin_state.coin, uint32(coin_state.created_height), - uint32(0), - False, + spent_height, + False if spent_height == 0 else True, False, WalletType.STANDARD_WALLET, 1, @@ -1173,14 +1203,13 @@ async def handle_clawback( await self.coin_store.add_coin_record(coin_record) # Add tx record created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.created_height)) - spend_bundle = SpendBundle([coin_spend], G2Element()) tx_record = TransactionRecord( confirmed_at_height=uint32(coin_state.created_height), created_at_time=uint64(created_timestamp), to_puzzle_hash=metadata.recipient_puzzle_hash, amount=uint64(coin_state.coin.amount), fee_amount=uint64(0), - confirmed=False, + confirmed=False if spent_height == 0 else True, sent=uint32(0), spend_bundle=None, additions=[coin_state.coin], @@ -1423,6 +1452,10 @@ async def _add_coin_states( tx_record.name, uint32(coin_state.spent_height) ) else: + tx_name = bytes(coin_state.coin.name()) + for added_coin in additions: + tx_name += bytes(added_coin.name()) + tx_name = std_hash(tx_name) tx_record = TransactionRecord( confirmed_at_height=uint32(coin_state.spent_height), created_at_time=uint64(spent_timestamp), @@ -1440,7 +1473,7 @@ async def _add_coin_states( sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=bytes32(token_bytes()), + name=tx_name, memos=[], ) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 1c658a2f0bbf..56862c8c0e6a 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -345,6 +345,8 @@ async def test_wallet_clawback_clawback( assert await wallet.get_confirmed_balance() == 3999999999500 # clawback merkle coin merkle_coin = tx.additions[0] if tx.additions[0].amount == 500 else tx.additions[1] + interested_coins = await wallet_node_2.wallet_state_manager.interested_store.get_interested_coin_ids() + assert merkle_coin.name() in set(interested_coins) assert len(txs["transactions"]) == 1 assert not txs["transactions"][0]["confirmed"] assert txs["transactions"][0]["metadata"]["recipient_puzzle_hash"][2:] == normal_puzhash.hex() From b24b2a0abef2368877982c14e262a42d9d3fa69e Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Thu, 22 Jun 2023 17:38:27 -0700 Subject: [PATCH 13/21] Improve clawback test --- tests/wallet/clawback/test_clawback_lifecycle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/wallet/clawback/test_clawback_lifecycle.py b/tests/wallet/clawback/test_clawback_lifecycle.py index 3e4160e7a6ed..d628d009acda 100644 --- a/tests/wallet/clawback/test_clawback_lifecycle.py +++ b/tests/wallet/clawback/test_clawback_lifecycle.py @@ -195,9 +195,10 @@ async def test_clawback_spends(self, cost_logger: CostLogger) -> None: cost_logger=cost_logger, cost_log_msg="Create Second Clawback", ) - - new_cb_coin = (await sim_client.get_coin_records_by_puzzle_hash(cb_puz_hash))[1].coin - + cb_coins = await sim_client.get_coin_records_by_puzzle_hash(cb_puz_hash) + new_cb_coin = cb_coins[0].coin + if cb_coins[0].spent: + new_cb_coin = cb_coins[1].coin sender_claw_sol = solution_for_conditions([[ConditionOpcode.CREATE_COIN, cold_ph, amount]]) claw_sol = create_merkle_solution(timelock, sender_ph, recipient_ph, sender_puz, sender_claw_sol) coin_spend = CoinSpend(new_cb_coin, cb_puzzle, claw_sol) From eefd4a7e8af7c41e1a6bfa250292df8f7b6808c6 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Thu, 22 Jun 2023 19:07:36 -0700 Subject: [PATCH 14/21] Remove pragma --- chia/wallet/wallet_state_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 22ba1352a709..2796096fda2d 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1159,7 +1159,7 @@ async def handle_clawback( spend_bundle = SpendBundle([coin_spend], G2Element()) memos = compute_memos(spend_bundle) spent_height: uint32 = uint32(0) - if coin_state.spent_height is not None: # pragma: no cover + if coin_state.spent_height is not None: self.log.debug("Resync clawback coin: %s", coin_state.coin.name().hex()) # Resync case spent_height = uint32(coin_state.spent_height) From 01f044038d0af1948f8246efd0a827f316006807 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Fri, 23 Jun 2023 11:36:52 -0700 Subject: [PATCH 15/21] Resolve comments --- chia/cmds/wallet_funcs.py | 4 +- chia/rpc/wallet_rpc_api.py | 4 +- chia/wallet/util/transaction_type.py | 2 +- chia/wallet/wallet_state_manager.py | 61 +++++++-------- tests/wallet/test_wallet.py | 108 ++++++++++++++++++++++++++- 5 files changed, 140 insertions(+), 39 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 9f50a7b7ba83..406fe56dab83 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -30,7 +30,7 @@ from chia.wallet.util.address_type import AddressType, ensure_valid_address from chia.wallet.util.puzzle_decorator_type import PuzzleDecoratorType from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter -from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType +from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType from chia.wallet.util.wallet_types import WalletType from chia.wallet.vc_wallet.vc_store import VCProofs from chia.wallet.wallet_coin_store import GetCoinRecords @@ -206,7 +206,7 @@ async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerpri if i + j + skipped >= len(txs): break coin_record: Optional[Dict[str, Any]] = None - if txs[i + j + skipped].type in CLAWBACK_TRANSACTION_TYPES: + if txs[i + j + skipped].type in CLAWBACK_INCOMING_TRANSACTION_TYPES: coin_records = await wallet_client.get_coin_records( GetCoinRecords(coin_id_filter=HashFilter.include([txs[i + j + skipped].additions[0].name()])) ) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index f920ae8332e1..4140f34fee07 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -75,7 +75,7 @@ from chia.wallet.util.compute_hints import compute_coin_hints from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter -from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType +from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import CoinType, WalletType from chia.wallet.vc_wallet.vc_store import VCProofs @@ -903,7 +903,7 @@ async def get_transactions(self, request: Dict) -> EndpointResult: try: tx = (await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config) tx_list.append(tx) - if tx["type"] not in CLAWBACK_TRANSACTION_TYPES: + if tx["type"] not in CLAWBACK_INCOMING_TRANSACTION_TYPES: continue coin: Coin = tr.additions[0] record: Optional[WalletCoinRecord] = await self.service.wallet_state_manager.coin_store.get_coin_record( diff --git a/chia/wallet/util/transaction_type.py b/chia/wallet/util/transaction_type.py index eff5090c0c8f..24e6493a8a4a 100644 --- a/chia/wallet/util/transaction_type.py +++ b/chia/wallet/util/transaction_type.py @@ -15,7 +15,7 @@ class TransactionType(IntEnum): OUTGOING_CLAWBACK = 8 -CLAWBACK_TRANSACTION_TYPES = { +CLAWBACK_INCOMING_TRANSACTION_TYPES = { TransactionType.INCOMING_CLAWBACK_SEND.value, TransactionType.INCOMING_CLAWBACK_RECEIVE.value, } diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 2796096fda2d..bf5e5cdca2f0 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -82,7 +82,7 @@ from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager from chia.wallet.util.query_filter import HashFilter -from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType +from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType from chia.wallet.util.wallet_sync_utils import ( PeerRequestException, fetch_coin_spend_for_coin_state, @@ -755,15 +755,9 @@ async def spend_clawback_coins(self, clawback_coins: Dict[Coin, ClawbackMetadata await self.main_wallet.hack_populate_secret_key_for_puzzle_hash(derivation_record.puzzle_hash) amount = uint64(amount + coin.amount) memos: List[bytes] = [] if len(incoming_tx.memos) == 0 else incoming_tx.memos[0][1].copy() - if ( - len(memos) > 0 - and not is_recipient - and await self.puzzle_store.puzzle_hash_exists(recipient_puzhash) - ): - # Sender and recipient belong to the same wallet - # Clean hint in this case to prevent duplicate processing + if len(memos) > 1: memos[0] = b"" - if len(memos) == 1 and memos[0] == b"": + else: memos = [] inner_puzzle: Program = self.main_wallet.puzzle_for_pk(derivation_record.pubkey) inner_solution: Program = self.main_wallet.make_solution( @@ -1167,32 +1161,33 @@ async def handle_clawback( created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.spent_height)) clawback_coin_spend: CoinSpend = await fetch_coin_spend_for_coin_state(coin_state, peer) clawback_spend_bundle: SpendBundle = SpendBundle([clawback_coin_spend], G2Element()) - tx_record = TransactionRecord( - confirmed_at_height=uint32(coin_state.spent_height), - created_at_time=created_timestamp, - to_puzzle_hash=metadata.sender_puzzle_hash - if clawback_spend_bundle.additions()[0].puzzle_hash == metadata.sender_puzzle_hash - else metadata.recipient_puzzle_hash, - amount=uint64(coin_state.coin.amount), - fee_amount=uint64(0), - confirmed=True, - sent=uint32(0), - spend_bundle=clawback_spend_bundle, - additions=clawback_spend_bundle.additions(), - removals=clawback_spend_bundle.removals(), - wallet_id=uint32(1), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.OUTGOING_CLAWBACK), - name=clawback_spend_bundle.name(), - memos=list(compute_memos(clawback_spend_bundle).items()), - ) - await self.add_pending_transaction(tx_record) + if await self.puzzle_store.puzzle_hash_exists(clawback_spend_bundle.additions()[0].puzzle_hash): + tx_record = TransactionRecord( + confirmed_at_height=uint32(coin_state.spent_height), + created_at_time=created_timestamp, + to_puzzle_hash=metadata.sender_puzzle_hash + if clawback_spend_bundle.additions()[0].puzzle_hash == metadata.sender_puzzle_hash + else metadata.recipient_puzzle_hash, + amount=uint64(coin_state.coin.amount), + fee_amount=uint64(0), + confirmed=True, + sent=uint32(0), + spend_bundle=clawback_spend_bundle, + additions=clawback_spend_bundle.additions(), + removals=clawback_spend_bundle.removals(), + wallet_id=uint32(1), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_CLAWBACK), + name=clawback_spend_bundle.name(), + memos=list(compute_memos(clawback_spend_bundle).items()), + ) + await self.tx_store.add_transaction_record(tx_record) coin_record = WalletCoinRecord( coin_state.coin, uint32(coin_state.created_height), spent_height, - False if spent_height == 0 else True, + spent_height != 0, False, WalletType.STANDARD_WALLET, 1, @@ -1209,7 +1204,7 @@ async def handle_clawback( to_puzzle_hash=metadata.recipient_puzzle_hash, amount=uint64(coin_state.coin.amount), fee_amount=uint64(0), - confirmed=False if spent_height == 0 else True, + confirmed=spent_height != 0, sent=uint32(0), spend_bundle=None, additions=[coin_state.coin], @@ -1484,7 +1479,7 @@ async def _add_coin_states( await self.interested_store.remove_interested_coin_id(coin_state.coin.name()) confirmed_tx_records: List[TransactionRecord] = [] for tx_record in all_unconfirmed: - if tx_record.type in CLAWBACK_TRANSACTION_TYPES: + if tx_record.type in CLAWBACK_INCOMING_TRANSACTION_TYPES: for add_coin in tx_record.additions: if add_coin == coin_state.coin: confirmed_tx_records.append(tx_record) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 56862c8c0e6a..e65a1ed35833 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -14,18 +14,20 @@ from chia.server.server import ChiaServer from chia.simulator.block_tools import BlockTools from chia.simulator.full_node_simulator import FullNodeSimulator, wait_for_coins_in_wallet -from chia.simulator.simulator_protocol import ReorgProtocol +from chia.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol from chia.simulator.time_out_assert import time_out_assert, time_out_assert_not_none from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import compute_additions from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash +from chia.util.config import load_config from chia.util.ints import uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.query_filter import TransactionTypeFilter from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import CoinType from chia.wallet.wallet import CHIP_0002_SIGN_MESSAGE_PREFIX @@ -745,6 +747,110 @@ async def test_get_clawback_coins( assert len(resp["coin_records"]) == 1 assert resp["coin_records"][0]["id"][2:] == merkle_coin.name().hex() + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_clawback_resync( + self, + two_wallet_nodes: Tuple[List[FullNodeSimulator], List[Tuple[WalletNode, ChiaServer]], BlockTools], + trusted: bool, + self_hostname: str, + ) -> None: + num_blocks = 1 + full_nodes, wallets, _ = two_wallet_nodes + full_node_api = full_nodes[0] + server_1 = full_node_api.full_node.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + api_0 = WalletRpcApi(wallet_node) + if trusted: + wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} + wallet_node_2.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} + 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) + await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + expected_confirmed_balance = await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) + normal_puzhash = await wallet.get_new_puzzlehash() + # Transfer to normal wallet + tx = await wallet.generate_signed_transaction( + uint64(500), + normal_puzhash, + uint64(0), + puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + ) + clawback_coin_id = tx.additions[0].name() + assert tx.spend_bundle is not None + await wallet.push_transaction(tx) + await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) + expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) + # Check merkle coins + await time_out_assert( + 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 1, 1000, CoinType.CLAWBACK + ) + assert await wallet.get_confirmed_balance() == 3999999999500 + await asyncio.sleep(10) + # clawback merkle coin + resp = await api_0.spend_clawback_coins( + dict({"coin_ids": [normal_puzhash.hex(), clawback_coin_id.hex()], "fee": 0}) + ) + json.dumps(resp) + assert resp["success"] + assert len(resp["transaction_ids"]) == 1 + expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) + await time_out_assert( + 20, wallet_node.wallet_state_manager.coin_store.count_small_unspent, 0, 1000, CoinType.CLAWBACK + ) + wallet_node_2._close() + await wallet_node_2._await_closed() + # set flag to reset wallet sync data on start + await api_0.set_wallet_resync_on_startup({"enable": True}) + fingerprint = wallet_node.logged_in_fingerprint + assert wallet_node._wallet_state_manager + # 2 reward coins, 1 clawbacked coin + assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 7 + # standard wallet + assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 1 + before_txs = await wallet_node.wallet_state_manager.tx_store.get_all_transactions() + # Delete tx records + await wallet_node.wallet_state_manager.tx_store.rollback_to_block(0) + wallet_node._close() + await wallet_node._await_closed() + config = load_config(wallet_node.root_path, "config.yaml") + # check that flag was set in config file + assert config["wallet"]["reset_sync_for_fingerprint"] == fingerprint + new_config = wallet_node.config.copy() + new_config["reset_sync_for_fingerprint"] = config["wallet"]["reset_sync_for_fingerprint"] + new_config["database_path"] = "wallet/db/blockchain_wallet_v2_test_CHALLENGE_KEY.sqlite" + wallet_node_2.config = new_config + wallet_node_2.root_path = wallet_node.root_path + wallet_node_2.local_keychain = wallet_node.local_keychain + # use second node to start the same wallet, reusing config and db + await wallet_node_2._start_with_fingerprint(fingerprint) + assert wallet_node_2._wallet_state_manager + await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32))) + await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_2, timeout=20) + after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions() + # transactions should be the same + assert len(after_txs) == len(before_txs) + # Check clawback + clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id) + assert clawback_tx is not None + assert clawback_tx.confirmed + outgoing_clawback_txs = await wallet_node_2.wallet_state_manager.tx_store.get_transactions_between( + 1, 0, 100, type_filter=TransactionTypeFilter.include([TransactionType.OUTGOING_CLAWBACK]) + ) + assert len(outgoing_clawback_txs) == 1 + assert outgoing_clawback_txs[0].confirmed + # Check unspent coins + assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 7 + @pytest.mark.parametrize( "trusted", [True, False], From 1ba1f2be36786ccdc1d1aa65f06bbf25275f7e85 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Mon, 26 Jun 2023 09:52:46 -0700 Subject: [PATCH 16/21] Revert clawback test fix --- tests/wallet/clawback/test_clawback_lifecycle.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/wallet/clawback/test_clawback_lifecycle.py b/tests/wallet/clawback/test_clawback_lifecycle.py index d628d009acda..3e4160e7a6ed 100644 --- a/tests/wallet/clawback/test_clawback_lifecycle.py +++ b/tests/wallet/clawback/test_clawback_lifecycle.py @@ -195,10 +195,9 @@ async def test_clawback_spends(self, cost_logger: CostLogger) -> None: cost_logger=cost_logger, cost_log_msg="Create Second Clawback", ) - cb_coins = await sim_client.get_coin_records_by_puzzle_hash(cb_puz_hash) - new_cb_coin = cb_coins[0].coin - if cb_coins[0].spent: - new_cb_coin = cb_coins[1].coin + + new_cb_coin = (await sim_client.get_coin_records_by_puzzle_hash(cb_puz_hash))[1].coin + sender_claw_sol = solution_for_conditions([[ConditionOpcode.CREATE_COIN, cold_ph, amount]]) claw_sol = create_merkle_solution(timelock, sender_ph, recipient_ph, sender_puz, sender_claw_sol) coin_spend = CoinSpend(new_cb_coin, cb_puzzle, claw_sol) From e3faa45b7ecde409756c0c7a20a4ccb91913e460 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Mon, 26 Jun 2023 21:49:10 -0700 Subject: [PATCH 17/21] Add comment for tx confirmed --- chia/wallet/wallet_state_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index bf5e5cdca2f0..f6bd1888872c 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1197,6 +1197,8 @@ async def handle_clawback( # Add merkle coin await self.coin_store.add_coin_record(coin_record) # Add tx record + # We use confirmed to indicate if a Clawback transaction is claimable + # If the Clawback coin is unspent, confirmed should be false created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.created_height)) tx_record = TransactionRecord( confirmed_at_height=uint32(coin_state.created_height), From c32d542a320d8041733cb167dff254efe1b7ecb7 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Tue, 27 Jun 2023 09:45:43 -0700 Subject: [PATCH 18/21] Resolve comments --- chia/wallet/wallet_state_manager.py | 7 ++----- tests/wallet/test_wallet.py | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index f6bd1888872c..342a86963e80 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -754,11 +754,8 @@ async def spend_clawback_coins(self, clawback_coins: Dict[Coin, ClawbackMetadata if self.main_wallet.secret_key_store.secret_key_for_public_key(derivation_record.pubkey) is None: await self.main_wallet.hack_populate_secret_key_for_puzzle_hash(derivation_record.puzzle_hash) amount = uint64(amount + coin.amount) - memos: List[bytes] = [] if len(incoming_tx.memos) == 0 else incoming_tx.memos[0][1].copy() - if len(memos) > 1: - memos[0] = b"" - else: - memos = [] + # Remove the clawback hint since it is unnecessary for the XCH coin + memos: List[bytes] = [] if len(incoming_tx.memos) == 0 else incoming_tx.memos[0][1][1:] inner_puzzle: Program = self.main_wallet.puzzle_for_pk(derivation_record.pubkey) inner_solution: Program = self.main_wallet.make_solution( primaries=[ diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index e65a1ed35833..72252c1aef88 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -452,6 +452,7 @@ async def test_wallet_clawback_sent_self( normal_puzhash, uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + memos=[b"Test"], ) await wallet.push_transaction(tx) @@ -494,7 +495,7 @@ async def test_wallet_clawback_sent_self( assert txs["transactions"][0]["confirmed"] assert txs["transactions"][1]["confirmed"] assert txs["transactions"][0]["memos"] != txs["transactions"][1]["memos"] - assert txs["transactions"][0]["memos"] == {} + assert list(txs["transactions"][0]["memos"].values())[0] == b"Test".hex() @pytest.mark.parametrize( "trusted", From f010eb5ec31dd85da393bfda7ae0964effad6d81 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Tue, 27 Jun 2023 15:31:43 -0700 Subject: [PATCH 19/21] Remove unrelated change --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1a66f9a9198d..f56885abd3b5 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ "twine", "isort", "flake8", - "mypy==1.3.0", + "mypy", "black==23.3.0", "aiohttp_cors", # For blackd "ipython", # For asyncio debugging From 5bf6b9d09f35664ab5f3d4e34c786ffbad6992e6 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Wed, 28 Jun 2023 16:20:48 -0700 Subject: [PATCH 20/21] Resolve comments --- .github/workflows/test.yml | 2 +- chia/wallet/wallet_state_manager.py | 2 +- tests/wallet/rpc/test_wallet_rpc.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30a9b4c46ee0..c170cbc8707b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: python tests/build-job-matrix.py --per directory --verbose > matrix.json cat matrix.json echo configuration=$(cat matrix.json) >> $GITHUB_OUTPUT - echo matrix_mode=${{ ( github.event_name == 'workflow_dispatch' ) && 'all' || ( github.repository_owner == 'Chia-Network' && github.repository != 'Chia-Network/chia-blockchain' ) && 'limited' || ( github.repository_owner == 'Chia-Network' && github.repository == 'Chia-Network/chia-blockchain' && github.ref == 'refs/heads/main' ) && 'main' || ( github.repository_owner == 'Chia-Network' && github.repository == 'Chia-Network/chia-blockchain' && startsWith(github.ref, 'refs/heads/release/') ) && 'all' || ( github.repository_owner == 'Chia-Network' && github.repository == 'Chia-Network/chia-blockchain' && startsWith(github.base_ref, 'release/') ) && 'all' || 'main' }} >> $GITHUB_OUTPUT + echo matrix_mode=${{ ( github.event_name == 'workflow_dispatch' ) && 'all' || ( github.repository_owner == 'Chia-Network' && github.repository != 'Chia-Network/chia-blockchain' ) && 'limited' || ( github.repository_owner == 'Chia-Network' && github.repository == 'Chia-Network/chia-blockchain' && github.ref == 'refs/heads/main' ) && 'main' || ( github.repository_owner == 'Chia-Network' && github.repository == 'Chia-Network/chia-blockchain' && startsWith(github.ref, 'refs/heads/release/') ) && 'all' || 'main' }} >> $GITHUB_OUTPUT outputs: configuration: ${{ steps.configure.outputs.configuration }} diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 3877d0f295f8..d57f2ab93e98 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1208,7 +1208,7 @@ async def handle_clawback( # Add merkle coin await self.coin_store.add_coin_record(coin_record) # Add tx record - # We use confirmed to indicate if a Clawback transaction is claimable + # We use TransactionRecord.confirmed to indicate if a Clawback transaction is claimable # If the Clawback coin is unspent, confirmed should be false created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.created_height)) tx_record = TransactionRecord( diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 3adf9c82a632..1f6a6a715629 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1994,8 +1994,6 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes env: WalletRpcTestEnvironment = wallet_rpc_environment full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client - wallet_node: WalletNode = env.wallet_1.node - wallet_node_2: WalletNode = env.wallet_2.node await generate_funds(full_node_api, env.wallet_1) wc = env.wallet_1.rpc_client await wc.create_new_did_wallet(1, 0) @@ -2012,6 +2010,9 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) await farm_transaction_block(full_node_api, env.wallet_1.node) await time_out_assert(20, wc.get_synced) + + wallet_node: WalletNode = env.wallet_1.node + wallet_node_2: WalletNode = env.wallet_2.node # Test Clawback resync tx = await wc.send_transaction( wallet_id=1, From 4ed1ef7556e1d775392bdbf6c333bef8d475d679 Mon Sep 17 00:00:00 2001 From: ytx1991 Date: Mon, 10 Jul 2023 14:16:10 -0700 Subject: [PATCH 21/21] Fix coverage --- chia/cmds/wallet_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index b8d749cd2aa5..93d66825e4b9 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -177,7 +177,7 @@ async def get_transactions( sort_key: SortKey, reverse: bool, clawback: bool, -) -> None: +) -> None: # pragma: no cover async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if wallet_client is None: return