diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 9e012a02c50d..08a704e8f3fa 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 9e012a02c50d6ddf0e96797d85eb183de91874e7 +Subproject commit 08a704e8f3faa41442da5199eee9aa329d3066c4 diff --git a/chia/_tests/build-init-files.py b/chia/_tests/build-init-files.py old mode 100755 new mode 100644 diff --git a/chia/_tests/check_pytest_monitor_output.py b/chia/_tests/check_pytest_monitor_output.py old mode 100755 new mode 100644 diff --git a/chia/_tests/check_sql_statements.py b/chia/_tests/check_sql_statements.py old mode 100755 new mode 100644 diff --git a/chia/_tests/chia-start-sim b/chia/_tests/chia-start-sim old mode 100755 new mode 100644 diff --git a/chia/_tests/clvm/test_p2_singletons.py b/chia/_tests/clvm/test_p2_singletons.py new file mode 100644 index 000000000000..40fb1b341098 --- /dev/null +++ b/chia/_tests/clvm/test_p2_singletons.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import pytest +from chia_rs import G2Element + +from chia._tests.util.spend_sim import CostLogger, sim_and_client +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_spend import make_spend +from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.types.spend_bundle import SpendBundle +from chia.util.errors import Err +from chia.util.ints import uint64 +from chia.wallet.conditions import CreateCoin +from chia.wallet.puzzles import p2_singleton_via_delegated_puzzle_safe as dp_safe +from chia.wallet.util.curry_and_treehash import shatree_atom + +ACS = Program.to(1) +ACS_HASH = ACS.get_tree_hash() + +MOCK_SINGLETON_MOD = Program.to([2, 5, 7]) # (a 5 11) - (mod (_ PUZZLE . solution) (a PUZZLE solution)) +MOCK_SINGLETON_MOD_HASH = MOCK_SINGLETON_MOD.get_tree_hash() +MOCK_SINGLETON_LAUNCHER_ID = bytes32([0] * 32) +MOCK_SINGLETON_LAUNCHER_HASH = bytes32([1] * 32) +MOCK_SINGLETON = MOCK_SINGLETON_MOD.curry( + Program.to((MOCK_SINGLETON_MOD_HASH, (MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_LAUNCHER_HASH))), + ACS, +) +MOCK_SINGLETON_HASH = MOCK_SINGLETON.get_tree_hash() +dp_safe.PRE_HASHED_HASHES[MOCK_SINGLETON_MOD_HASH] = shatree_atom(MOCK_SINGLETON_MOD_HASH) +dp_safe.PRE_HASHED_HASHES[MOCK_SINGLETON_LAUNCHER_HASH] = shatree_atom(MOCK_SINGLETON_LAUNCHER_HASH) + + +@pytest.mark.anyio +async def test_dp_safe_lifecycle(cost_logger: CostLogger) -> None: + P2_SINGLETON = dp_safe.construct(MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_MOD_HASH, MOCK_SINGLETON_LAUNCHER_HASH) + P2_SINGLETON_HASH = dp_safe.construct_hash( + MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_MOD_HASH, MOCK_SINGLETON_LAUNCHER_HASH + ) + assert dp_safe.match(P2_SINGLETON) is not None + assert dp_safe.match(ACS) is None + assert dp_safe.match(MOCK_SINGLETON) is None + + async with sim_and_client() as (sim, sim_client): + await sim.farm_block(P2_SINGLETON_HASH) + await sim.farm_block(MOCK_SINGLETON_HASH) + p2_singleton = (await sim_client.get_coin_records_by_puzzle_hash(P2_SINGLETON_HASH, include_spent_coins=False))[ + 0 + ].coin + singleton = (await sim_client.get_coin_records_by_puzzle_hash(MOCK_SINGLETON_HASH, include_spent_coins=False))[ + 0 + ].coin + + dp = Program.to(1) + dp_hash = dp.get_tree_hash() + bundle = cost_logger.add_cost( + "p2_singleton_w_mock_singleton", + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + p2_singleton.name(), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([dp_safe.required_announcement(dp_hash, p2_singleton.name()).to_program()]), + ), + ], + G2Element(), + ), + ) + result = await sim_client.push_tx(bundle) + assert result == (MempoolInclusionStatus.SUCCESS, None) + checkpoint = sim.block_height + await sim.farm_block() + + assert len(await sim_client.get_coin_records_by_puzzle_hash(bytes32([0] * 32), include_spent_coins=False)) == 1 + + await sim.rewind(checkpoint) + + result = await sim_client.push_tx( + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + bytes32([0] * 32), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([dp_safe.required_announcement(dp_hash, p2_singleton.name()).to_program()]), + ), + ], + G2Element(), + ) + ) + assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_MY_COIN_ID_FAILED) + + result = await sim_client.push_tx( + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + p2_singleton.name(), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([]), + ), + ], + G2Element(), + ) + ) + assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_ANNOUNCE_CONSUMED_FAILED) + + result = await sim_client.push_tx( + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + p2_singleton.name(), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([dp_safe.required_announcement(dp_hash, bytes32([0] * 32)).to_program()]), + ), + ], + G2Element(), + ) + ) + assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_ANNOUNCE_CONSUMED_FAILED) diff --git a/chia/_tests/cmds/wallet/test_vault.py b/chia/_tests/cmds/wallet/test_vault.py new file mode 100644 index 000000000000..6da6d63bea19 --- /dev/null +++ b/chia/_tests/cmds/wallet/test_vault.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from pathlib import Path + +from chia_rs import G1Element + +from chia._tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert +from chia._tests.cmds.wallet.test_consts import FINGERPRINT_ARG, STD_TX, STD_UTX, WALLET_ID_ARG +from chia.rpc.wallet_request_types import VaultCreate, VaultCreateResponse, VaultRecovery, VaultRecoveryResponse +from chia.util.ints import uint64 +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.util.tx_config import TXConfig + +test_condition_valid_times: ConditionValidTimes = ConditionValidTimes(min_time=uint64(100), max_time=uint64(150)) + + +def test_vault_create(capsys: object, get_test_cli_clients: tuple[TestRpcClients, Path]) -> None: + test_rpc_clients, root_dir = get_test_cli_clients + + # set RPC clients + class CreateVaultRpcClient(TestWalletRpcClient): + async def vault_create( + self, + args: VaultCreate, + tx_config: TXConfig, + timelock_info: ConditionValidTimes, + ) -> VaultCreateResponse: + return VaultCreateResponse([STD_UTX], [STD_TX]) + + inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter + test_rpc_clients.wallet_rpc_client = inst_rpc_client + pk = bytes(G1Element()).hex() + recovery_pk = bytes(G1Element()).hex() + timelock = "100" + hidden_puzzle_index = "10" + fee = "0.1" + command_args = [ + "vault", + "create", + FINGERPRINT_ARG, + "-pk", + pk, + "-rk", + recovery_pk, + "-rt", + timelock, + "-i", + hidden_puzzle_index, + "-m", + fee, + "--valid-at", + "100", + "--expires-at", + "150", + ] + assert_list = ["Successfully created a Vault wallet"] + run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) + + +def test_vault_recovery(capsys: object, get_test_cli_clients: tuple[TestRpcClients, Path], tmp_path: Path) -> None: + test_rpc_clients, root_dir = get_test_cli_clients + + # set RPC clients + class CreateVaultRpcClient(TestWalletRpcClient): + async def vault_recovery( + self, + args: VaultRecovery, + tx_config: TXConfig, + timelock_info: ConditionValidTimes, + ) -> VaultRecoveryResponse: + return VaultRecoveryResponse([STD_UTX, STD_UTX], [STD_TX, STD_TX], STD_TX.name, STD_TX.name) + + inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter + test_rpc_clients.wallet_rpc_client = inst_rpc_client + pk = bytes(G1Element()).hex() + recovery_pk = bytes(G1Element()).hex() + timelock = "100" + hidden_puzzle_index = "10" + command_args = [ + "vault", + "recover", + "-pk", + pk, + "-rk", + recovery_pk, + "-rt", + timelock, + "-i", + hidden_puzzle_index, + "-ri", + str(tmp_path / "recovery_init.json"), + "-rf", + str(tmp_path / "recovery_finish.json"), + "--valid-at", + "100", + "--expires-at", + "150", + ] + assert_list = [ + "Writing transactions to file ", + "recovery_init.json", + "recovery_finish.json", + ] + run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG, WALLET_ID_ARG], assert_list) diff --git a/chia/_tests/core/daemon/test_daemon.py b/chia/_tests/core/daemon/test_daemon.py index 78cdda2afc2b..98e80b995830 100644 --- a/chia/_tests/core/daemon/test_daemon.py +++ b/chia/_tests/core/daemon/test_daemon.py @@ -10,7 +10,7 @@ import pytest from aiohttp import WSMessage from aiohttp.web_ws import WebSocketResponse -from chia_rs import G1Element +from chia_rs import G1Element, PrivateKey from pytest_mock import MockerFixture from chia._tests.util.misc import Marks, datacases @@ -32,7 +32,7 @@ from chia.simulator.setup_services import setup_full_node from chia.util.config import load_config from chia.util.json_util import dict_to_json_str -from chia.util.keychain import Keychain, KeyData, supports_os_passphrase_storage +from chia.util.keychain import Keychain, KeyData, KeyTypes, supports_os_passphrase_storage from chia.util.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, KeyringWrapper from chia.util.ws_message import create_payload, create_payload_dict from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_pool_sk @@ -205,6 +205,8 @@ async def get_keys_for_plotting(self, request: dict[str, Any]) -> dict[str, Any] "hammer stable page grunt venture purse canyon discover " "egg vivid spare immune awake code announce message" ) +assert isinstance(test_key_data.private_key, PrivateKey) +assert isinstance(test_key_data_2.private_key, PrivateKey) success_response_data = { "success": True, @@ -237,6 +239,7 @@ async def get_keys_for_plotting(self, request: dict[str, Any]) -> dict[str, Any] def add_private_key_response_data(fingerprint: int) -> dict[str, object]: return { "success": True, + "key_type": KeyTypes.G1_ELEMENT.value, "fingerprint": fingerprint, } diff --git a/chia/_tests/core/daemon/test_keychain_proxy.py b/chia/_tests/core/daemon/test_keychain_proxy.py index a07c45f99fb1..6389138cba89 100644 --- a/chia/_tests/core/daemon/test_keychain_proxy.py +++ b/chia/_tests/core/daemon/test_keychain_proxy.py @@ -6,6 +6,7 @@ from typing import Any import pytest +from chia_rs import G1Element from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate from chia.simulator.block_tools import BlockTools @@ -48,21 +49,22 @@ async def test_add_private_key(keychain_proxy: KeychainProxy) -> None: @pytest.mark.anyio async def test_add_public_key(keychain_proxy: KeychainProxy) -> None: keychain = keychain_proxy - await keychain.add_key(bytes(TEST_KEY_3.public_key).hex(), TEST_KEY_3.label, private=False) + assert isinstance(TEST_KEY_3.observation_root, G1Element) + await keychain.add_key(TEST_KEY_3.public_key.hex(), TEST_KEY_3.label, private=False) with pytest.raises(Exception, match="already exists"): - await keychain.add_key(bytes(TEST_KEY_3.public_key).hex(), "", private=False) + await keychain.add_key(TEST_KEY_3.public_key.hex(), "", private=False) key = await keychain.get_key(TEST_KEY_3.fingerprint, include_secrets=False) assert key is not None - assert key.public_key == TEST_KEY_3.public_key + assert key.observation_root == TEST_KEY_3.observation_root assert key.secrets is None pk = await keychain.get_key_for_fingerprint(TEST_KEY_3.fingerprint, private=False) assert pk is not None - assert pk == TEST_KEY_3.public_key + assert pk == TEST_KEY_3.observation_root pk = await keychain.get_key_for_fingerprint(None, private=False) assert pk is not None - assert pk == TEST_KEY_3.public_key + assert pk == TEST_KEY_3.observation_root with pytest.raises(KeychainKeyNotFound): pk = await keychain.get_key_for_fingerprint(1234567890, private=False) @@ -83,8 +85,8 @@ async def test_get_key_for_fingerprint(keychain_proxy: KeychainProxy) -> None: with pytest.raises(KeychainIsEmpty): await keychain.get_key_for_fingerprint(None, private=False) await keychain_proxy.add_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) - assert await keychain.get_key_for_fingerprint(TEST_KEY_1.fingerprint, private=False) == TEST_KEY_1.public_key - assert await keychain.get_key_for_fingerprint(None, private=False) == TEST_KEY_1.public_key + assert await keychain.get_key_for_fingerprint(TEST_KEY_1.fingerprint, private=False) == TEST_KEY_1.observation_root + assert await keychain.get_key_for_fingerprint(None, private=False) == TEST_KEY_1.observation_root with pytest.raises(KeychainKeyNotFound): await keychain.get_key_for_fingerprint(1234567890, private=False) @@ -99,3 +101,15 @@ async def test_get_keys(keychain_proxy_with_keys: KeychainProxy, include_secrets else: expected_keys = [replace(TEST_KEY_1, secrets=None), replace(TEST_KEY_2, secrets=None)] assert keys == expected_keys + + +@pytest.mark.anyio +async def test_get_first_private_key(keychain_proxy_with_keys: KeychainProxy) -> None: + assert TEST_KEY_1.private_key == await keychain_proxy_with_keys.get_first_private_key() + + +@pytest.mark.anyio +async def test_get_all_private_keys(keychain_proxy_with_keys: KeychainProxy) -> None: + assert [TEST_KEY_1.private_key, TEST_KEY_2.private_key] == [ + k for k, e in await keychain_proxy_with_keys.get_all_private_keys() + ] diff --git a/chia/_tests/core/data_layer/test_data_rpc.py b/chia/_tests/core/data_layer/test_data_rpc.py index 2b08621692b9..a79c2e483571 100644 --- a/chia/_tests/core/data_layer/test_data_rpc.py +++ b/chia/_tests/core/data_layer/test_data_rpc.py @@ -70,8 +70,8 @@ from chia.util.timing import adjusted_timeout, backoff_times from chia.wallet.trading.offer import Offer as TradingOffer from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol pytestmark = pytest.mark.data_layer nodes = tuple[WalletNode, FullNodeSimulator] @@ -825,7 +825,7 @@ async def offer_setup_fixture( [full_node_service], wallet_services, bt = two_wallet_nodes_services enable_batch_autoinsertion_settings = getattr(request, "param", (True, True)) full_node_api = full_node_service._api - wallets: list[Wallet] = [] + wallets: list[MainWalletProtocol] = [] for wallet_service in wallet_services: wallet_node = wallet_service._node assert wallet_node.server is not None @@ -2373,8 +2373,8 @@ async def test_wallet_log_in_changes_active_fingerprint( mnemonic = create_mnemonic() assert wallet_rpc_api.service.local_keychain is not None - private_key = wallet_rpc_api.service.local_keychain.add_key(mnemonic_or_pk=mnemonic) - secondary_fingerprint: int = private_key.get_g1().get_fingerprint() + private_key, _ = wallet_rpc_api.service.local_keychain.add_key(mnemonic_or_pk=mnemonic) + secondary_fingerprint: int = private_key.public_key().get_fingerprint() await wallet_rpc_api.log_in(request={"fingerprint": primary_fingerprint}) diff --git a/chia/_tests/core/mempool/test_mempool_manager.py b/chia/_tests/core/mempool/test_mempool_manager.py index 45c80258a2a0..53052293d16b 100644 --- a/chia/_tests/core/mempool/test_mempool_manager.py +++ b/chia/_tests/core/mempool/test_mempool_manager.py @@ -57,9 +57,9 @@ from chia.wallet.conditions import AssertCoinAnnouncement from chia.wallet.payment import Payment from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol IDENTITY_PUZZLE = SerializedProgram.to(1) IDENTITY_PUZZLE_HASH = IDENTITY_PUZZLE.get_tree_hash() @@ -1646,7 +1646,7 @@ async def farm_a_block(full_node_api: FullNodeSimulator, wallet_node: WalletNode async def make_setup_and_coins( full_node_api: FullNodeSimulator, wallet_node: WalletNode - ) -> tuple[Wallet, list[WalletCoinRecord], bytes32]: + ) -> tuple[MainWalletProtocol, list[WalletCoinRecord], bytes32]: wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() phs = [await wallet.get_new_puzzlehash() for _ in range(3)] diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index ba21e90eedcc..b8cccf859ff7 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -3,7 +3,7 @@ import json import random from dataclasses import dataclass, replace -from typing import Callable, Optional +from typing import Any, Callable, Optional import importlib_resources import pytest @@ -21,16 +21,21 @@ KeychainSecretsMissing, ) from chia.util.ints import uint32 +from chia.util.key_types import Secp256r1PrivateKey from chia.util.keychain import ( Keychain, KeyData, KeyDataSecrets, + KeyTypes, bytes_from_mnemonic, bytes_to_mnemonic, generate_mnemonic, mnemonic_from_short_words, mnemonic_to_seed, ) +from chia.util.observation_root import ObservationRoot +from chia.util.secret_info import SecretInfo +from chia.wallet.vault.vault_root import VaultRoot @dataclass @@ -93,7 +98,7 @@ def test_basic_add_delete( entropy = bytes_from_mnemonic(mnemonic) assert bytes_to_mnemonic(entropy) == mnemonic mnemonic_2 = generate_mnemonic() - fingerprint_2 = AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic_2)).get_g1().get_fingerprint() + fingerprint_2 = AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic_2)).public_key().get_fingerprint() # misspelled words in the mnemonic bad_mnemonic = mnemonic.split(" ") @@ -115,6 +120,9 @@ def test_basic_add_delete( assert kc._get_free_private_key_index() == 2 assert len(kc.get_all_private_keys()) == 2 assert len(kc.get_all_public_keys()) == 2 + all_pks: list[G1Element] = kc.get_all_public_keys_of_type(G1Element) + assert len(all_pks) == 2 + assert kc.get_all_private_keys()[0] == kc.get_first_private_key() assert kc.get_all_public_keys()[0] == kc.get_first_public_key() @@ -122,7 +130,7 @@ def test_basic_add_delete( seed_2 = mnemonic_to_seed(mnemonic) seed_key_2 = AugSchemeMPL.key_gen(seed_2) - kc.delete_key_by_fingerprint(seed_key_2.get_g1().get_fingerprint()) + kc.delete_key_by_fingerprint(seed_key_2.public_key().get_fingerprint()) assert kc._get_free_private_key_index() == 0 assert len(kc.get_all_private_keys()) == 1 @@ -131,7 +139,7 @@ def test_basic_add_delete( assert len(kc.get_all_private_keys()) == 0 kc.add_key(key_info.bech32, label=None, private=False) - all_pks = kc.get_all_public_keys() + all_pks = kc.get_all_public_keys_of_type(G1Element) assert len(all_pks) == 1 assert all_pks[0] == key_info.public_key kc.delete_all_keys() @@ -189,11 +197,11 @@ def test_bip39_eip2333_test_vector(self, empty_temp_file_keyring: TempKeyring): mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" print("entropy to seed:", mnemonic_to_seed(mnemonic).hex()) - master_sk = kc.add_key(mnemonic) + master_sk, _ = kc.add_key(mnemonic) tv_master_int = 8075452428075949470768183878078858156044736575259233735633523546099624838313 tv_child_int = 18507161868329770878190303689452715596635858303241878571348190917018711023613 assert master_sk == PrivateKey.from_bytes(tv_master_int.to_bytes(32, "big")) - child_sk = AugSchemeMPL.derive_child_sk(master_sk, 0) + child_sk = master_sk.derive_hardened(0) assert child_sk == PrivateKey.from_bytes(tv_child_int.to_bytes(32, "big")) def test_bip39_test_vectors(self): @@ -271,8 +279,8 @@ def test_key_data_generate(label: Optional[str]) -> None: key_data = KeyData.generate(label) assert key_data.private_key == AugSchemeMPL.key_gen(mnemonic_to_seed(key_data.mnemonic_str())) assert key_data.entropy == bytes_from_mnemonic(key_data.mnemonic_str()) - assert key_data.public_key == key_data.private_key.get_g1() - assert key_data.fingerprint == key_data.private_key.get_g1().get_fingerprint() + assert key_data.observation_root == key_data.private_key.public_key() + assert key_data.fingerprint == key_data.private_key.public_key().get_fingerprint() assert key_data.label == label @@ -284,7 +292,7 @@ def test_key_data_generate(label: Optional[str]) -> None: def test_key_data_creation(label: str, key_info: KeyInfo, get_item: str, from_method: Callable[..., KeyData]) -> None: key_data = from_method(getattr(key_info, get_item), label) assert key_data.fingerprint == key_info.fingerprint - assert key_data.public_key == key_info.public_key + assert key_data.public_key == bytes(key_info.public_key) assert key_data.mnemonic == key_info.mnemonic.split() assert key_data.mnemonic_str() == key_info.mnemonic assert key_data.entropy == key_info.entropy @@ -294,7 +302,7 @@ def test_key_data_creation(label: str, key_info: KeyInfo, get_item: str, from_me @pytest.mark.parametrize("key_info", [_24keyinfo, _12keyinfo]) def test_key_data_without_secrets(key_info: KeyInfo) -> None: - key_data = KeyData(key_info.fingerprint, key_info.public_key, None, None) + key_data = KeyData(key_info.fingerprint, bytes(key_info.public_key), None, None, KeyTypes.G1_ELEMENT.value) assert key_data.secrets is None with pytest.raises(KeychainSecretsMissing): @@ -315,10 +323,10 @@ def test_key_data_without_secrets(key_info: KeyInfo) -> None: [ ((_24keyinfo.mnemonic.split()[:-1], _24keyinfo.entropy, _24keyinfo.private_key), "mnemonic"), ((_24keyinfo.mnemonic.split(), KeyDataSecrets.generate().entropy, _24keyinfo.private_key), "entropy"), - ((_24keyinfo.mnemonic.split(), _24keyinfo.entropy, KeyDataSecrets.generate().private_key), "private_key"), + ((_24keyinfo.mnemonic.split(), _24keyinfo.entropy, KeyDataSecrets.generate().secret_info_bytes), "private_key"), ], ) -def test_key_data_secrets_post_init(input_data: tuple[list[str], bytes, PrivateKey], data_type: str) -> None: +def test_key_data_secrets_post_init(input_data: tuple[list[str], bytes, bytes], data_type: str) -> None: with pytest.raises(KeychainKeyDataMismatch, match=data_type): KeyDataSecrets(*input_data) @@ -329,17 +337,18 @@ def test_key_data_secrets_post_init(input_data: tuple[list[str], bytes, PrivateK ( ( _24keyinfo.fingerprint, - G1Element(), + bytes(G1Element()), None, - KeyDataSecrets(_24keyinfo.mnemonic.split(), _24keyinfo.entropy, _24keyinfo.private_key), + KeyDataSecrets(_24keyinfo.mnemonic.split(), _24keyinfo.entropy, bytes(_24keyinfo.private_key)), + KeyTypes.G1_ELEMENT.value, ), "public_key", ), - ((_24keyinfo.fingerprint, G1Element(), None, None), "fingerprint"), + ((_24keyinfo.fingerprint, bytes(G1Element()), None, None, KeyTypes.G1_ELEMENT.value), "fingerprint"), ], ) def test_key_data_post_init( - input_data: tuple[uint32, G1Element, Optional[str], Optional[KeyDataSecrets]], data_type: str + input_data: tuple[uint32, bytes, Optional[str], Optional[KeyDataSecrets], str], data_type: str ) -> None: with pytest.raises(KeychainKeyDataMismatch, match=data_type): KeyData(*input_data) @@ -512,3 +521,33 @@ async def test_delete_drops_labels(get_temp_keyring: Keychain, delete_all: bool) for key_data in keys: keychain.delete_key_by_fingerprint(key_data.fingerprint) assert keychain.keyring_wrapper.keyring.get_label(key_data.fingerprint) is None + + +@pytest.mark.parametrize("key_type", [e.value for e in KeyTypes]) +@pytest.mark.parametrize("key_info", [_24keyinfo, _12keyinfo]) +def test_key_type_support(key_type: str, key_info: KeyInfo) -> None: + """ + The purpose of this test is to make sure that whenever KeyTypes is updated, all relevant functionality is + also updated with it. + """ + launcher_id = bytes32(b"1" * 32) + vault_root = VaultRoot(launcher_id) + secp_sk = Secp256r1PrivateKey.from_seed(mnemonic_to_seed(key_info.mnemonic)) + secp_pk = secp_sk.public_key() + generate_test_key_for_key_type: dict[str, tuple[int, ObservationRoot, Optional[SecretInfo[Any]]]] = { + KeyTypes.G1_ELEMENT.value: ( + G1Element().get_fingerprint(), + G1Element(), + key_info.private_key, + ), + KeyTypes.SECP_256_R1.value: (secp_pk.get_fingerprint(), secp_pk, secp_sk), + KeyTypes.VAULT_LAUNCHER.value: (vault_root.get_fingerprint(), vault_root, None), + } + obr_fingerprint, obr, secret_info = generate_test_key_for_key_type[key_type] + assert KeyData(uint32(obr_fingerprint), bytes(obr), None, None, key_type).observation_root == obr + assert KeyTypes.parse_observation_root(bytes(obr), KeyTypes(key_type)) == obr + if secret_info is not None: + assert KeyTypes.parse_secret_info(bytes(secret_info), KeyTypes(key_type)) == secret_info + assert ( + KeyTypes.parse_secret_info_from_seed(mnemonic_to_seed(key_info.mnemonic), KeyTypes(key_type)) == secret_info + ) diff --git a/chia/_tests/environments/wallet.py b/chia/_tests/environments/wallet.py index d1ca5d166cf6..b05b591bcbe3 100644 --- a/chia/_tests/environments/wallet.py +++ b/chia/_tests/environments/wallet.py @@ -8,6 +8,7 @@ from chia._tests.environments.common import ServiceEnvironment from chia.rpc.full_node_rpc_client import FullNodeRpcClient from chia.rpc.rpc_server import RpcServer +from chia.rpc.wallet_request_types import LogIn from chia.rpc.wallet_rpc_api import WalletRpcApi from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.server.server import ChiaServer @@ -19,9 +20,9 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import Balance, WalletNode from chia.wallet.wallet_node_api import WalletNodeAPI +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_state_manager import WalletStateManager OPP_DICT = {"<": operator.lt, ">": operator.gt, "<=": operator.le, ">=": operator.ge} @@ -96,9 +97,21 @@ def wallet_state_manager(self) -> WalletStateManager: return self.service._node.wallet_state_manager @property - def xch_wallet(self) -> Wallet: + def xch_wallet(self) -> MainWalletProtocol: return self.service._node.wallet_state_manager.main_wallet + async def restart(self, new_fingerprint: Optional[int]) -> None: + old_peer_info = next(v for v in self.node.server.all_connections.values()).peer_info + await self.rpc_client.log_in( + LogIn(uint32(new_fingerprint)) + if new_fingerprint is not None + else LogIn(uint32(self.wallet_state_manager.observation_root.get_fingerprint())) + ) + + await self.node.server.start_client(old_peer_info, None) + + self.wallet_states = {} + def dealias_wallet_id(self, wallet_id_or_alias: Union[int, str]) -> uint32: """ This function turns something that is either a wallet id or a wallet alias into a wallet id. diff --git a/chia/_tests/util/test_secp256r1.py b/chia/_tests/util/test_secp256r1.py new file mode 100644 index 000000000000..1beae0961056 --- /dev/null +++ b/chia/_tests/util/test_secp256r1.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import pytest + +from chia.util.ints import uint32 +from chia.util.key_types import Secp256r1PrivateKey, Secp256r1PublicKey, Secp256r1Signature +from chia.util.keychain import generate_mnemonic, mnemonic_to_seed + + +def test_key_drivers() -> None: + """ + This tests that the chia.util.key_types drivers for these keys works properly, it does not test the sanity of the + underlying library. + """ + mnemonic = generate_mnemonic() + sk = Secp256r1PrivateKey.from_seed(mnemonic_to_seed(mnemonic)) + assert Secp256r1PrivateKey.from_bytes(bytes(sk)) == sk + with pytest.raises(NotImplementedError): + sk.derive_hardened(1) + with pytest.raises(NotImplementedError): + sk.derive_unhardened(1) + + pk = sk.public_key() + assert Secp256r1PublicKey.from_bytes(bytes(pk)) == pk + assert pk.get_fingerprint() < uint32.MAXIMUM + with pytest.raises(NotImplementedError): + pk.derive_unhardened(1) + + sig = sk.sign(b"foo") + assert Secp256r1Signature.from_bytes(bytes(sig)) == sig + with pytest.raises(NotImplementedError): + sk.sign(b"foo", final_pk=pk) diff --git a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py index ea22288f84bd..85e72df9cc2f 100644 --- a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py +++ b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +from chia_rs import G1Element from chia._tests.conftest import ConsensusMode from chia._tests.environments.wallet import WalletEnvironment, WalletStateTransition, WalletTestFramework @@ -1458,7 +1459,10 @@ async def test_cat_change_detection(wallet_environments: WalletTestFramework) -> # Mint CAT to ourselves, immediately spend it to an unhinted puzzle hash that we have manually added to the DB # We should pick up this coin as balance even though it is unhinted because it is "change" - pubkey_unhardened = master_pk_to_wallet_pk_unhardened(wsm.root_pubkey, uint32(100000000)) + assert isinstance(env.node.wallet_state_manager.observation_root, G1Element) + pubkey_unhardened = master_pk_to_wallet_pk_unhardened( + env.node.wallet_state_manager.observation_root, uint32(100000000) + ) inner_puzhash = puzzle_hash_for_pk(pubkey_unhardened) puzzlehash_unhardened = construct_cat_puzzle( CAT_MOD, Program.to(None).get_tree_hash(), inner_puzhash @@ -1687,46 +1691,48 @@ async def test_cat_melt_balance(wallet_environments: WalletTestFramework) -> Non assert isinstance(cat_wallet, CATWallet) # Let's test that continuing to melt this CAT results in the correct balance changes - for _ in range(0, 5): - tx_amount -= 1 - new_coin = (await cat_wallet.get_cat_spendable_coins())[0].coin - new_spend = unsigned_spend_bundle_for_spendable_cats( - CAT_MOD, - [ - SpendableCAT( - coin=new_coin, - limitations_program_hash=ACS_TAIL_HASH, - inner_puzzle=await cat_wallet.inner_puzzle_for_cat_puzhash(new_coin.puzzle_hash), - inner_solution=wallet.make_solution( - primaries=[Payment(wallet_ph, uint64(tx_amount), [wallet_ph])], - conditions=( - UnknownCondition( - opcode=Program.to(51), - args=[Program.to(None), Program.to(-113), Program.to(ACS_TAIL), Program.to(None)], + async with wallet.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=False) as action_scope: + for _ in range(0, 5): + tx_amount -= 1 + new_coin = (await cat_wallet.get_cat_spendable_coins())[0].coin + new_spend = unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + [ + SpendableCAT( + coin=new_coin, + limitations_program_hash=ACS_TAIL_HASH, + inner_puzzle=await cat_wallet.inner_puzzle_for_cat_puzhash(new_coin.puzzle_hash), + inner_solution=await wallet.make_solution( + primaries=[Payment(wallet_ph, uint64(tx_amount), [wallet_ph])], + action_scope=action_scope, + conditions=( + UnknownCondition( + opcode=Program.to(51), + args=[Program.to(None), Program.to(-113), Program.to(ACS_TAIL), Program.to(None)], + ), ), ), - ), - extra_delta=-1, - ) - ], - ) - signed_spend, _ = await env.wallet_state_manager.sign_bundle(new_spend.coin_spends) - await env.rpc_client.push_tx(PushTX(signed_spend)) - await time_out_assert(10, simulator.tx_id_in_mempool, True, signed_spend.name()) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={}, - post_block_balance_updates={ - "xch": {}, - "cat": { - "confirmed_wallet_balance": -1, - "unconfirmed_wallet_balance": -1, - "spendable_balance": -1, - "max_send_amount": -1, + extra_delta=-1, + ) + ], + ) + signed_spend, _ = await env.wallet_state_manager.sign_bundle(new_spend.coin_spends) + await env.rpc_client.push_tx(PushTX(signed_spend)) + await time_out_assert(10, simulator.tx_id_in_mempool, True, signed_spend.name()) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={}, + post_block_balance_updates={ + "xch": {}, + "cat": { + "confirmed_wallet_balance": -1, + "unconfirmed_wallet_balance": -1, + "spendable_balance": -1, + "max_send_amount": -1, + }, }, - }, - ) - ] - ) + ) + ] + ) diff --git a/chia/_tests/wallet/dao_wallet/test_dao_clvm.py b/chia/_tests/wallet/dao_wallet/test_dao_clvm.py index 0072467edbc9..117a625d76ca 100644 --- a/chia/_tests/wallet/dao_wallet/test_dao_clvm.py +++ b/chia/_tests/wallet/dao_wallet/test_dao_clvm.py @@ -47,7 +47,7 @@ "genesis_by_coin_id_or_singleton.clsp", package_or_requirement="chia.wallet.cat_wallet.puzzles" ) DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash() -P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp") +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle_w_aggregator.clsp") P2_SINGLETON_MOD_HASH: bytes32 = P2_SINGLETON_MOD.get_tree_hash() P2_SINGLETON_AGGREGATOR_MOD: Program = load_clvm("p2_singleton_aggregator.clsp") P2_SINGLETON_AGGREGATOR_MOD_HASH: bytes32 = P2_SINGLETON_AGGREGATOR_MOD.get_tree_hash() diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index c10fd55ba089..502257669b06 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -10,7 +10,7 @@ import aiosqlite import pytest -from chia_rs import G1Element, G2Element +from chia_rs import G1Element, G2Element, PrivateKey from chia._tests.conftest import ConsensusMode from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework @@ -111,11 +111,10 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.util.wallet_types import CoinType, WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_coin_store import GetCoinRecords from chia.wallet.wallet_node import WalletNode -from chia.wallet.wallet_protocol import WalletProtocol +from chia.wallet.wallet_protocol import MainWalletProtocol, WalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle log = logging.getLogger(__name__) @@ -126,7 +125,7 @@ class WalletBundle: service: Service node: WalletNode rpc_client: WalletRpcClient - wallet: Wallet + wallet: MainWalletProtocol @dataclasses.dataclass @@ -232,7 +231,9 @@ async def wallet_rpc_environment(two_wallet_nodes_services, request, self_hostna yield WalletRpcTestEnvironment(wallet_bundle_1, wallet_bundle_2, node_bundle) -async def create_tx_outputs(wallet: Wallet, output_args: list[tuple[int, Optional[list[str]]]]) -> list[dict[str, Any]]: +async def create_tx_outputs( + wallet: MainWalletProtocol, output_args: list[tuple[int, Optional[list[str]]]] +) -> list[dict[str, Any]]: outputs = [] for args in output_args: output = {"amount": uint64(args[0]), "puzzle_hash": await wallet.get_new_puzzlehash()} @@ -316,7 +317,7 @@ async def get_unconfirmed_balance(client: WalletRpcClient, wallet_id: int): async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -396,7 +397,7 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment async def test_push_transactions(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -442,7 +443,7 @@ async def test_push_transactions(wallet_rpc_environment: WalletRpcTestEnvironmen @pytest.mark.anyio async def test_get_balance(wallet_rpc_environment: WalletRpcTestEnvironment): env = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api wallet_rpc_client = env.wallet_1.rpc_client @@ -464,7 +465,7 @@ async def test_get_balance(wallet_rpc_environment: WalletRpcTestEnvironment): @pytest.mark.anyio async def test_get_farmed_amount(wallet_rpc_environment: WalletRpcTestEnvironment): env = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet full_node_api: FullNodeSimulator = env.full_node.api wallet_rpc_client = env.wallet_1.rpc_client await full_node_api.farm_blocks_to_wallet(2, wallet) @@ -490,7 +491,7 @@ async def test_get_farmed_amount(wallet_rpc_environment: WalletRpcTestEnvironmen @pytest.mark.anyio async def test_get_farmed_amount_with_fee(wallet_rpc_environment: WalletRpcTestEnvironment): env = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet full_node_api: FullNodeSimulator = env.full_node.api wallet_rpc_client = env.wallet_1.rpc_client wallet_node: WalletNode = env.wallet_1.node @@ -554,7 +555,7 @@ async def test_create_signed_transaction( ): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_1_node: WalletNode = env.wallet_1.node wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client full_node_api: FullNodeSimulator = env.full_node.api @@ -652,7 +653,7 @@ async def test_create_signed_transaction( async def test_create_signed_transaction_with_coin_announcement(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client client_node: FullNodeRpcClient = env.full_node.rpc_client @@ -684,7 +685,7 @@ async def test_create_signed_transaction_with_coin_announcement(wallet_rpc_envir async def test_create_signed_transaction_with_puzzle_announcement(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client client_node: FullNodeRpcClient = env.full_node.rpc_client @@ -715,7 +716,7 @@ async def test_create_signed_transaction_with_puzzle_announcement(wallet_rpc_env @pytest.mark.anyio async def test_create_signed_transaction_with_excluded_coins(wallet_rpc_environment: WalletRpcTestEnvironment) -> None: env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_1: Wallet = env.wallet_1.wallet + wallet_1: MainWalletProtocol = env.wallet_1.wallet wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client full_node_api: FullNodeSimulator = env.full_node.api full_node_rpc: FullNodeRpcClient = env.full_node.rpc_client @@ -884,7 +885,7 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -935,7 +936,7 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir async def test_get_transactions(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -1495,8 +1496,8 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_1: Wallet = env.wallet_1.wallet - wallet_2: Wallet = env.wallet_2.wallet + wallet_1: MainWalletProtocol = env.wallet_1.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_1_node: WalletNode = env.wallet_1.node wallet_2_node: WalletNode = env.wallet_2.node wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client @@ -1619,7 +1620,7 @@ async def test_nft_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment wallet_1_node: WalletNode = env.wallet_1.node wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_2_node: WalletNode = env.wallet_2.node wallet_2_rpc: WalletRpcClient = env.wallet_2.rpc_client full_node_api: FullNodeSimulator = env.full_node.api @@ -1713,10 +1714,12 @@ async def _check_delete_key( sk = await wallet_node.get_key_for_fingerprint(farmer_fp, private=True) assert sk is not None + assert isinstance(sk, PrivateKey) farmer_ph = create_puzzlehash_for_pk(create_sk(sk, uint32(0)).get_g1()) sk = await wallet_node.get_key_for_fingerprint(pool_fp, private=True) assert sk is not None + assert isinstance(sk, PrivateKey) pool_ph = create_puzzlehash_for_pk(create_sk(sk, uint32(0)).get_g1()) with lock_and_load_config(wallet_node.root_path, "config.yaml") as test_config: @@ -1747,7 +1750,7 @@ async def _check_delete_key( async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node client: WalletRpcClient = env.wallet_1.rpc_client @@ -1839,7 +1842,7 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -2126,7 +2129,7 @@ async def test_get_coin_records_rpc_failures( async def test_notification_rpcs(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client diff --git a/chia/_tests/wallet/test_main_wallet_protocol.py b/chia/_tests/wallet/test_main_wallet_protocol.py new file mode 100644 index 000000000000..2f81b4803e09 --- /dev/null +++ b/chia/_tests/wallet/test_main_wallet_protocol.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import logging +import time +import types +from collections.abc import Awaitable +from typing import Any, Callable, Optional + +import pytest +from chia_rs import G1Element, G2Element, PrivateKey +from typing_extensions import Unpack + +from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework +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 make_spend +from chia.types.signing_mode import SigningMode +from chia.util.ints import uint32, uint64 +from chia.util.observation_root import ObservationRoot +from chia.wallet.conditions import Condition, CreateCoin, ReserveFee, parse_timelock_info +from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.payment import Payment +from chia.wallet.signer_protocol import ( + PathHint, + SignedTransaction, + SigningInstructions, + SigningResponse, + Spend, + SumHint, + TransactionInfo, +) +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.wallet import Wallet +from chia.wallet.wallet_action_scope import WalletActionScope +from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol +from chia.wallet.wallet_spend_bundle import WalletSpendBundle +from chia.wallet.wallet_state_manager import WalletStateManager + +ACS: Program = Program.to(1) +ACS_PH: bytes32 = ACS.get_tree_hash() + + +class AnyoneCanSpend(Wallet): + @staticmethod + async def create( + wallet_state_manager: Any, + info: WalletInfo, + name: str = __name__, + ) -> AnyoneCanSpend: + self = AnyoneCanSpend() + self.wallet_state_manager = wallet_state_manager + self.wallet_info = info + self.wallet_id = info.id + self.log = logging.getLogger(name) + return self + + async def get_new_puzzle(self) -> Program: # pragma: no cover + return ACS + + async def get_new_puzzlehash(self) -> bytes32: # pragma: no cover + return ACS_PH + + async def generate_signed_transaction( + self, + amount: uint64, + puzzle_hash: bytes32, + action_scope: WalletActionScope, + fee: uint64 = uint64(0), + coins: Optional[set[Coin]] = None, + primaries: Optional[list[Payment]] = None, + memos: Optional[list[bytes]] = None, + puzzle_decorator_override: Optional[list[dict[str, Any]]] = None, + extra_conditions: tuple[Condition, ...] = tuple(), + **kwargs: Unpack[GSTOptionalArgs], + ) -> None: + condition_list: list[Payment] = [] if primaries is None else primaries + condition_list.append(Payment(puzzle_hash, amount, [] if memos is None else memos)) + non_change_amount: int = ( + sum(c.amount for c in condition_list) + + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + + fee + ) + + coins = await self.select_coins(uint64(non_change_amount), action_scope) + total_amount = sum(c.amount for c in coins) + + condition_list.append(Payment(ACS_PH, uint64(total_amount - non_change_amount))) + + spend_bundle = WalletSpendBundle( + [ + make_spend( + coin, + ACS, + ( + await self.make_solution(condition_list, action_scope, extra_conditions, fee) + if i == 0 + else Program.to([]) + ), + ) + for i, coin in enumerate(coins) + ], + G2Element(), + ) + + now = uint64(int(time.time())) + async with action_scope.use() as interface: + interface.side_effects.transactions.append( + TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=now, + to_puzzle_hash=puzzle_hash, + amount=uint64(non_change_amount), + fee_amount=uint64(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(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), + ) + ) + + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: # pragma: no cover + raise ValueError("This won't work") + + async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: + if puzzle_hash == ACS_PH: + return ACS + else: + raise ValueError("puzzle hash was not ACS_PH") # pragma: no cover + + async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> tuple[G1Element, G2Element]: + raise ValueError("This won't work") # pragma: no cover + + async def get_puzzle_hash(self, new: bool) -> bytes32: + return ACS_PH + + async def apply_signatures( + self, spends: list[Spend], signing_responses: list[SigningResponse] + ) -> SignedTransaction: + return SignedTransaction( + TransactionInfo(spends), + [], + ) + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> list[SigningResponse]: + if len(signing_instructions.targets) > 0: + raise ValueError("This won't work") # pragma: no cover + else: + return [] + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: # pragma: no cover + return None + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: # pragma: no cover + return None + + async def make_solution( + self, + primaries: list[Payment], + action_scope: WalletActionScope, + conditions: tuple[Condition, ...] = tuple(), + fee: uint64 = uint64(0), + **kwargs: Any, + ) -> Program: + condition_list: list[Condition] = [CreateCoin(p.puzzle_hash, p.amount, p.memos) for p in primaries] + condition_list.append(ReserveFee(fee)) + condition_list.extend(conditions) + prog: Program = Program.to([c.to_program() for c in condition_list]) + return prog + + async def get_puzzle(self, new: bool) -> Program: # pragma: no cover + return ACS + + def puzzle_hash_for_pk(self, pubkey: ObservationRoot) -> bytes32: # pragma: no cover + raise ValueError("This won't work") + + def require_derivation_paths(self) -> bool: + return True + + async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: # pragma: no cover + if coin.puzzle_hash == ACS_PH or hint == ACS_PH: + return True + else: + return False + + def handle_own_derivation(self) -> bool: + return True + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: + return [ + DerivationRecord( + uint32(index), + ACS_PH, + G1Element(), + self.type(), + uint32(self.id()), + False, + ) + ] + + +async def acs_setup(wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch) -> None: + def get_main_wallet_driver(self: WalletStateManager, observation_root: ObservationRoot) -> type[MainWalletProtocol]: + return AnyoneCanSpend + + monkeypatch.setattr( + WalletStateManager, + "get_main_wallet_driver", + types.MethodType(get_main_wallet_driver, WalletStateManager), + ) + + for env in wallet_environments.environments: + pk = PrivateKey.from_bytes( + bytes.fromhex("548dd25590a19f0a6a294560fc36f2900575fb9d1b2650e6fe80ad9abc1c4a60") + ).get_g1() + await env.node.keychain_proxy.add_key(bytes(pk).hex(), None, private=False) + await env.restart(pk.get_fingerprint()) + + +async def bls_got_setup(wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch) -> None: + return None + + +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [0], + } + ], + indirect=True, +) +@pytest.mark.parametrize("setup_function", [acs_setup, bls_got_setup]) +@pytest.mark.anyio +async def test_main_wallet( + setup_function: Callable[[WalletTestFramework, pytest.MonkeyPatch], Awaitable[None]], + wallet_environments: WalletTestFramework, + monkeypatch: pytest.MonkeyPatch, +) -> None: + await setup_function(wallet_environments, monkeypatch) + main_wallet: MainWalletProtocol = wallet_environments.environments[0].xch_wallet + ph: bytes32 = await main_wallet.get_puzzle_hash(False) + await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, ph, guarantee_transaction_blocks=True) + await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, guarantee_transaction_blocks=True) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "confirmed_wallet_balance": 2_000_000_000_000, + "unconfirmed_wallet_balance": 2_000_000_000_000, + "max_send_amount": 2_000_000_000_000, + "spendable_balance": 2_000_000_000_000, + "unspent_coin_count": 2, + } + } + ) + ] + ) + async with main_wallet.wallet_state_manager.new_action_scope( + wallet_environments.tx_config, push=True, sign=True + ) as action_scope: + await main_wallet.generate_signed_transaction( + uint64(1_750_000_000_001), + ph, + action_scope, + fee=uint64(2), + primaries=[Payment(ph, uint64(3))], + extra_conditions=(CreateCoin(ph, uint64(4)),), + ) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "unconfirmed_wallet_balance": -2, # Only thing that actually went out was fee + "max_send_amount": -2_000_000_000_000, # All coins are now pending + "spendable_balance": -2_000_000_000_000, # All coins are now pending + "pending_change": 1_999_999_999_998, + "pending_coin_removal_count": 2, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -2, + "max_send_amount": 1_999_999_999_998, + "spendable_balance": 1_999_999_999_998, + "pending_change": -1_999_999_999_998, + "pending_coin_removal_count": -2, + # Minus: both farmed coins. Plus: Output, change, primary, extra_condition + "unspent_coin_count": -2 + 4, + } + }, + ) + ] + ) + + # Miscellaneous checks + assert [coin.puzzle_hash for tx in action_scope.side_effects.transactions for coin in tx.removals] == [ + (await main_wallet.puzzle_for_puzzle_hash(coin.puzzle_hash)).get_tree_hash() + for tx in action_scope.side_effects.transactions + for coin in tx.removals + ] diff --git a/chia/_tests/wallet/test_sign_coin_spends.py b/chia/_tests/wallet/test_sign_coin_spends.py index de4905fa42db..94f4d3ec16f5 100644 --- a/chia/_tests/wallet/test_sign_coin_spends.py +++ b/chia/_tests/wallet/test_sign_coin_spends.py @@ -75,7 +75,7 @@ async def test_wsm_sign_transaction() -> None: wsm.puzzle_store = await WalletPuzzleStore.create(db) wsm.constants = DEFAULT_CONSTANTS wsm.private_key = top_sk - wsm.root_pubkey = top_sk.get_g1() + wsm.observation_root = top_sk.get_g1() wsm.user_store = await WalletUserStore.create(db) wallet_info = await wsm.user_store.get_wallet_by_id(1) assert wallet_info is not None diff --git a/chia/_tests/wallet/test_signer_protocol.py b/chia/_tests/wallet/test_signer_protocol.py index c288be7c7503..f10a0b5d3830 100644 --- a/chia/_tests/wallet/test_signer_protocol.py +++ b/chia/_tests/wallet/test_signer_protocol.py @@ -78,7 +78,7 @@ json_serialize_with_clvm_streamable, ) from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG -from chia.wallet.wallet import Wallet +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle from chia.wallet.wallet_state_manager import WalletStateManager @@ -129,7 +129,7 @@ def test_unsigned_transaction_type() -> None: ) @pytest.mark.anyio async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFramework) -> None: - wallet: Wallet = wallet_environments.environments[0].xch_wallet + wallet: MainWalletProtocol = wallet_environments.environments[0].xch_wallet wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client @@ -172,7 +172,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ] assert utx.signing_instructions.key_hints.path_hints == [ PathHint( - wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big"), + wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big"), [uint64(12381), uint64(8444), uint64(2), uint64(derivation_record.index)], ) ] @@ -314,8 +314,9 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ) @pytest.mark.anyio async def test_p2blsdohp_execute_signing_instructions(wallet_environments: WalletTestFramework) -> None: - wallet: Wallet = wallet_environments.environments[0].xch_wallet - root_sk: PrivateKey = wallet.wallet_state_manager.get_master_private_key() + wallet: MainWalletProtocol = wallet_environments.environments[0].xch_wallet + root_sk = wallet.wallet_state_manager.get_master_private_key() + assert isinstance(root_sk, PrivateKey) root_pk: G1Element = root_sk.get_g1() root_fingerprint: bytes = root_pk.get_fingerprint().to_bytes(4, "big") @@ -605,12 +606,12 @@ def test_blind_signer_translation_layer() -> None: ) @pytest.mark.anyio async def test_signer_commands(wallet_environments: WalletTestFramework) -> None: - wallet: Wallet = wallet_environments.environments[0].xch_wallet + wallet: MainWalletProtocol = wallet_environments.environments[0].xch_wallet wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client client_info: WalletClientInfo = WalletClientInfo( wallet_rpc, - wallet_state_manager.root_pubkey.get_fingerprint(), + wallet_state_manager.observation_root.get_fingerprint(), wallet_state_manager.config, ) diff --git a/chia/_tests/wallet/test_wallet.py b/chia/_tests/wallet/test_wallet.py index d5ad285d514c..1176b10ae456 100644 --- a/chia/_tests/wallet/test_wallet.py +++ b/chia/_tests/wallet/test_wallet.py @@ -5,7 +5,7 @@ from typing import Any, Optional import pytest -from chia_rs import AugSchemeMPL, G1Element, G2Element +from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia._tests.util.time_out_assert import time_out_assert @@ -1884,9 +1884,11 @@ async def test_address_sliding_window(self, wallet_environments: WalletTestFrame peak = full_node_api.full_node.blockchain.get_peak_height() assert peak is not None - puzzle_hashes = [] + puzzle_hashes: list[bytes32] = [] for i in range(211): - pubkey = master_sk_to_wallet_sk(wallet.wallet_state_manager.get_master_private_key(), uint32(i)).get_g1() + sk = wallet.wallet_state_manager.get_master_private_key() + assert isinstance(sk, PrivateKey) + pubkey = master_sk_to_wallet_sk(sk, uint32(i)).public_key() puzzle: Program = wallet.puzzle_for_pk(pubkey) puzzle_hash: bytes32 = puzzle.get_tree_hash() puzzle_hashes.append(puzzle_hash) diff --git a/chia/_tests/wallet/test_wallet_node.py b/chia/_tests/wallet/test_wallet_node.py index 44cbdd4cbc53..086532e4ce65 100644 --- a/chia/_tests/wallet/test_wallet_node.py +++ b/chia/_tests/wallet/test_wallet_node.py @@ -28,7 +28,8 @@ from chia.util.config import load_config from chia.util.errors import Err from chia.util.ints import uint8, uint32, uint64, uint128 -from chia.util.keychain import Keychain, KeyData, generate_mnemonic +from chia.util.keychain import Keychain, KeyData, KeyTypes, generate_mnemonic +from chia.util.observation_root import ObservationRoot from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.util.wallet_sync_utils import PeerRequestException from chia.wallet.wallet_node import Balance, WalletNode @@ -40,8 +41,8 @@ async def test_get_private_key(root_path_populated_with_config: Path, get_temp_k keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants, keychain) - sk = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + sk, _ = keychain.add_key(generate_mnemonic()) + fingerprint = sk.public_key().get_fingerprint() key = await node.get_key(fingerprint) @@ -56,8 +57,8 @@ async def test_get_private_key_default_key(root_path_populated_with_config: Path keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants, keychain) - sk = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + sk, _ = keychain.add_key(generate_mnemonic()) + fingerprint = sk.public_key().get_fingerprint() # Add a couple more keys keychain.add_key(generate_mnemonic()) @@ -70,6 +71,20 @@ async def test_get_private_key_default_key(root_path_populated_with_config: Path assert isinstance(key, PrivateKey) assert key.get_g1().get_fingerprint() == fingerprint + # We should get the same result with a bogus fingerprint + key = await node.get_key(123456789) + + assert key is not None + assert isinstance(key, PrivateKey) + assert key.get_g1().get_fingerprint() == fingerprint + + # Test coverage + key = await node.get_key(123456789, private=False) + + assert key is not None + assert isinstance(key, G1Element) + assert key.get_fingerprint() == fingerprint + @pytest.mark.anyio @pytest.mark.parametrize("fingerprint", [None, 1234567890]) @@ -87,35 +102,13 @@ async def test_get_private_key_missing_key( assert key is None -@pytest.mark.anyio -async def test_get_private_key_missing_key_use_default( - root_path_populated_with_config: Path, get_temp_keyring: Keychain -) -> None: - root_path = root_path_populated_with_config - keychain = get_temp_keyring - config = load_config(root_path, "config.yaml", "wallet") - node = WalletNode(config, root_path, test_constants, keychain) - sk = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() - - # Stupid sanity check that the fingerprint we're going to use isn't actually in the keychain - assert fingerprint != 1234567890 - - # When fingerprint is provided and the key is missing, we should get the default (first) key - key = await node.get_key(1234567890) - - assert key is not None - assert isinstance(key, PrivateKey) - assert key.get_g1().get_fingerprint() == fingerprint - - @pytest.mark.anyio async def test_get_public_key(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: root_path: Path = root_path_populated_with_config keychain: Keychain = get_temp_keyring config: dict[str, Any] = load_config(root_path, "config.yaml", "wallet") node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - pk: G1Element = keychain.add_key( + pk, key_type = keychain.add_key( "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", None, private=False, @@ -127,6 +120,7 @@ async def test_get_public_key(root_path_populated_with_config: Path, get_temp_ke assert key is not None assert isinstance(key, G1Element) assert key.get_fingerprint() == fingerprint + assert key_type == KeyTypes.G1_ELEMENT @pytest.mark.anyio @@ -135,7 +129,7 @@ async def test_get_public_key_default_key(root_path_populated_with_config: Path, keychain: Keychain = get_temp_keyring config: dict[str, Any] = load_config(root_path, "config.yaml", "wallet") node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - pk: G1Element = keychain.add_key( + pk, key_type = keychain.add_key( "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", None, private=False, @@ -160,6 +154,7 @@ async def test_get_public_key_default_key(root_path_populated_with_config: Path, assert key is not None assert isinstance(key, G1Element) assert key.get_fingerprint() == fingerprint + assert key_type == KeyTypes.G1_ELEMENT @pytest.mark.anyio @@ -178,39 +173,13 @@ async def test_get_public_key_missing_key( assert key is None -@pytest.mark.anyio -async def test_get_public_key_missing_key_use_default( - root_path_populated_with_config: Path, get_temp_keyring: Keychain -) -> None: - root_path: Path = root_path_populated_with_config - keychain: Keychain = get_temp_keyring - config: dict[str, Any] = load_config(root_path, "config.yaml", "wallet") - node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - pk: G1Element = keychain.add_key( - "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - None, - private=False, - ) - fingerprint: int = pk.get_fingerprint() - - # Stupid sanity check that the fingerprint we're going to use isn't actually in the keychain - assert fingerprint != 1234567890 - - # When fingerprint is provided and the key is missing, we should get the default (first) key - key = await node.get_key(1234567890, private=False) - - assert key is not None - assert isinstance(key, G1Element) - assert key.get_fingerprint() == fingerprint - - def test_log_in(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: root_path = root_path_populated_with_config keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) - sk = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + sk, _ = keychain.add_key(generate_mnemonic()) + fingerprint = sk.public_key().get_fingerprint() node.log_in(fingerprint) @@ -235,8 +204,8 @@ def patched_update_last_used_fingerprint(self: Self) -> None: keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) - sk = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + sk, _ = keychain.add_key(generate_mnemonic()) + fingerprint = sk.public_key().get_fingerprint() # Expect log_in to succeed, even though we can't write the last used fingerprint node.log_in(fingerprint) @@ -252,8 +221,8 @@ def test_log_out(root_path_populated_with_config: Path, get_temp_keyring: Keycha keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) - sk = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + sk, _ = keychain.add_key(generate_mnemonic()) + fingerprint = sk.public_key().get_fingerprint() node.log_in(fingerprint) @@ -677,6 +646,32 @@ def check_wallet_cache_empty() -> bool: await time_out_assert(5, check_wallet_cache_empty, True) +@pytest.mark.anyio +async def test_get_last_used_fingerprint_if_exists( + self_hostname: str, simulator_and_wallet: OldSimulatorsAndWallets +) -> None: + [full_node_api], [(node, wallet_server)], _ = simulator_and_wallet + + await wallet_server.start_client(PeerInfo(self_hostname, full_node_api.server.get_port()), None) + + node.update_last_used_fingerprint() + assert node.wallet_state_manager.private_key is not None + assert ( + await node.get_last_used_fingerprint_if_exists() + == node.wallet_state_manager.private_key.public_key().get_fingerprint() + ) + await node.keychain_proxy.delete_all_keys() + assert await node.get_last_used_fingerprint_if_exists() is None + + sk_2, _ = await node.keychain_proxy.add_key(generate_mnemonic()) + fingerprint_2: int = sk_2.public_key().get_fingerprint() + + node._close() + await node._await_closed(shutting_down=False) + await node._start_with_fingerprint() + assert node.logged_in_fingerprint == fingerprint_2 + + @pytest.mark.limit_consensus_modes(reason="consensus rules irrelevant") @pytest.mark.anyio async def test_wallet_node_bad_coin_state_ignore( @@ -728,16 +723,18 @@ async def restart_with_fingerprint(fingerprint: Optional[int]) -> None: initial_sk = wallet_node.wallet_state_manager.private_key - pk: G1Element = await wallet_node.keychain_proxy.add_key( - "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - None, - private=False, - ) + pk: ObservationRoot = ( + await wallet_node.keychain_proxy.add_key( + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + None, + private=False, + ) + )[0] fingerprint_pk: int = pk.get_fingerprint() await restart_with_fingerprint(fingerprint_pk) assert wallet_node.wallet_state_manager.private_key is None - assert wallet_node.wallet_state_manager.root_pubkey == G1Element() + assert wallet_node.wallet_state_manager.observation_root == G1Element() await wallet_node.keychain_proxy.delete_key_by_fingerprint(fingerprint_pk) @@ -759,15 +756,17 @@ async def restart_with_fingerprint(fingerprint: Optional[int]) -> None: initial_sk = wallet_node.wallet_state_manager.private_key - sk_2: PrivateKey = await wallet_node.keychain_proxy.add_key( - ( - "cup smoke miss park baby say island tomorrow segment lava bitter easily settle gift " - "renew arrive kangaroo dilemma organ skin design salt history awesome" - ), - None, - private=True, - ) - fingerprint_2: int = sk_2.get_g1().get_fingerprint() + sk_2 = ( + await wallet_node.keychain_proxy.add_key( + ( + "cup smoke miss park baby say island tomorrow segment lava bitter easily settle gift " + "renew arrive kangaroo dilemma organ skin design salt history awesome" + ), + None, + private=True, + ) + )[0] + fingerprint_2: int = sk_2.public_key().get_fingerprint() await restart_with_fingerprint(fingerprint_2) assert wallet_node.wallet_state_manager.private_key == sk_2 diff --git a/chia/_tests/wallet/test_wallet_state_manager.py b/chia/_tests/wallet/test_wallet_state_manager.py index 06332c1f0834..c2cff1ef54b6 100644 --- a/chia/_tests/wallet/test_wallet_state_manager.py +++ b/chia/_tests/wallet/test_wallet_state_manager.py @@ -4,7 +4,7 @@ from contextlib import asynccontextmanager import pytest -from chia_rs import G2Element +from chia_rs import G2Element, PrivateKey from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia._tests.util.setup_nodes import OldSimulatorsAndWallets @@ -69,11 +69,13 @@ async def test_get_private_key(simulator_and_wallet: OldSimulatorsAndWallets, ha wallet_state_manager: WalletStateManager = wallet_node.wallet_state_manager derivation_index = uint32(10000) conversion_method = master_sk_to_wallet_sk if hardened else master_sk_to_wallet_sk_unhardened - expected_private_key = conversion_method(wallet_state_manager.get_master_private_key(), derivation_index) + sk = wallet_state_manager.get_master_private_key() + assert isinstance(sk, PrivateKey) + expected_private_key = conversion_method(sk, derivation_index) record = DerivationRecord( derivation_index, bytes32(b"0" * 32), - expected_private_key.get_g1(), + bytes(expected_private_key.public_key()), WalletType.STANDARD_WALLET, uint32(1), hardened, diff --git a/chia/_tests/wallet/vault/__init__.py b/chia/_tests/wallet/vault/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/_tests/wallet/vault/config.py b/chia/_tests/wallet/vault/config.py new file mode 100644 index 000000000000..e46b82aa493b --- /dev/null +++ b/chia/_tests/wallet/vault/config.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +job_timeout = 90 +checkout_blocks_and_plots = True diff --git a/chia/_tests/wallet/vault/test_vault_clsp.py b/chia/_tests/wallet/vault/test_vault_clsp.py new file mode 100644 index 000000000000..4cf6c8fa12fa --- /dev/null +++ b/chia/_tests/wallet/vault/test_vault_clsp.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +from typing import Optional + +import pytest +from chia_rs import G1Element, PrivateKey +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from chia._tests.clvm.test_puzzles import secret_exponent_for_index +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.types.blockchain_format.program import INFINITE_COST, Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.condition_opcodes import ConditionOpcode +from chia.util.condition_tools import conditions_dict_for_solution +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE_HASH +from chia.wallet.util.merkle_tree import MerkleTree + +P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_or_hidden_secp.clsp") +P2_1_OF_N_MOD: Program = load_clvm("p2_1_of_n.clsp") +P2_1_OF_N_MOD_HASH: bytes32 = P2_1_OF_N_MOD.get_tree_hash() +P2_RECOVERY_MOD: Program = load_clvm("vault_p2_recovery.clsp") +P2_RECOVERY_MOD_HASH: bytes32 = P2_RECOVERY_MOD.get_tree_hash() +RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") +RECOVERY_FINISH_MOD_HASH: bytes32 = RECOVERY_FINISH_MOD.get_tree_hash() +ACS: Program = Program.to(1) +ACS_PH: bytes32 = ACS.get_tree_hash() + +# setup keys +seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 +secp_sk = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) +secp_pk = secp_sk.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + + +def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + + +def test_secp_hidden() -> None: + HIDDEN_PUZZLE: Program = Program.to(1) + HIDDEN_PUZZLE_HASH: bytes32 = HIDDEN_PUZZLE.get_tree_hash() + escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, HIDDEN_PUZZLE_HASH) + coin_id = Program.to("coin_id").get_tree_hash() + conditions = Program.to([[51, ACS_PH, 100]]) + hidden_escape_solution = Program.to([HIDDEN_PUZZLE, conditions, 0, coin_id]) + hidden_result = escape_puzzle.run(hidden_escape_solution) + assert hidden_result == Program.to(conditions) + + +def test_recovery_puzzles() -> None: + bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) + bls_pk: Optional[G1Element] = bls_sk.get_g1() + p2_puzzlehash = ACS_PH + amount = 10000 + timelock = 5000 + coin_id = Program.to("coin_id").get_tree_hash() + recovery_conditions = Program.to([[51, p2_puzzlehash, amount]]) + + escape_puzzle = P2_DELEGATED_SECP_MOD.curry( + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH + ) + escape_puzzlehash = escape_puzzle.get_tree_hash() + finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + finish_puzzlehash = finish_puzzle.get_tree_hash() + + curried_recovery_puzzle = P2_RECOVERY_MOD.curry( + P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, escape_puzzlehash, bls_pk, timelock + ) + + recovery_solution = Program.to([amount, recovery_conditions]) + + conds = conditions_dict_for_solution(curried_recovery_puzzle, recovery_solution, INFINITE_COST) + + # Calculate the merkle root and expected recovery puzzle + merkle_tree = MerkleTree([escape_puzzlehash, finish_puzzlehash]) + merkle_root = merkle_tree.calculate_root() + recovery_puzzle = P2_1_OF_N_MOD.curry(merkle_root) + recovery_puzzlehash = recovery_puzzle.get_tree_hash() + + # check for correct puzhash in conditions + assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == recovery_puzzlehash + + # Spend the recovery puzzle + # 1. Finish Recovery (after timelock) + proof = merkle_tree.generate_proof(finish_puzzlehash) + finish_proof = Program.to((proof[0], proof[1][0])) + inner_solution = Program.to([]) + finish_solution = Program.to([finish_proof, finish_puzzle, inner_solution]) + finish_conds = conditions_dict_for_solution(recovery_puzzle, finish_solution, INFINITE_COST) + assert finish_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == p2_puzzlehash + + # 2. Escape Recovery + proof = merkle_tree.generate_proof(escape_puzzlehash) + escape_proof = Program.to((proof[0], proof[1][0])) + delegated_puzzle = ACS + delegated_solution = Program.to([[51, ACS_PH, amount]]) + sig = sign_message( + secp_sk, + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH, + ) + secp_solution = Program.to( + [delegated_puzzle, delegated_solution, sig, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + ) + escape_solution = Program.to([escape_proof, escape_puzzle, secp_solution]) + escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) + assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH + + +def test_p2_delegated_secp() -> None: + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH) + + coin_id = Program.to("coin_id").get_tree_hash() + delegated_puzzle = ACS + delegated_solution = Program.to([[51, ACS_PH, 1000]]) + sig = sign_message( + secp_sk, + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH, + ) + + secp_solution = Program.to([delegated_puzzle, delegated_solution, sig, coin_id]) + conds = secp_puzzle.run(secp_solution) + + assert conds.at("rfrf").as_atom() == ACS_PH + + # test that a bad secp sig fails + sig_bytes = bytearray(sig) + sig_bytes[0] ^= (sig_bytes[0] + 1) % 256 + bad_signature = bytes(sig_bytes) + + bad_solution = Program.to( + [delegated_puzzle, delegated_solution, bad_signature, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + ) + with pytest.raises(ValueError, match="secp256r1_verify failed"): + secp_puzzle.run(bad_solution) + + +def test_vault_root_puzzle() -> None: + # create the secp and recovery puzzles + # secp puzzle + bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) + bls_pk: Optional[G1Element] = bls_sk.get_g1() + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH) + secp_puzzlehash = secp_puzzle.get_tree_hash() + + timelock = 5000 + amount = 10000 + coin_id = Program.to("coin_id").get_tree_hash() + + recovery_puzzle = P2_RECOVERY_MOD.curry( + P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzlehash, bls_pk, timelock + ) + recovery_puzzlehash = recovery_puzzle.get_tree_hash() + + # create the vault root puzzle + vault_merkle_tree = MerkleTree([secp_puzzlehash, recovery_puzzlehash]) + vault_merkle_root = vault_merkle_tree.calculate_root() + vault_puzzle = P2_1_OF_N_MOD.curry(vault_merkle_root) + + # secp spend path + delegated_puzzle = ACS + delegated_solution = Program.to([[51, ACS_PH, amount]]) + sig = sign_message( + secp_sk, + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH, + ) + secp_solution = Program.to([delegated_puzzle, delegated_solution, sig, coin_id]) + proof = vault_merkle_tree.generate_proof(secp_puzzlehash) + secp_proof = Program.to((proof[0], proof[1][0])) + vault_solution = Program.to([secp_proof, secp_puzzle, secp_solution]) + secp_conds = conditions_dict_for_solution(vault_puzzle, vault_solution, INFINITE_COST) + assert secp_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH + + # recovery spend path + recovery_conditions = Program.to([[51, ACS_PH, amount]]) + curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry( + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH + ) + curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + recovery_merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) + recovery_merkle_root = recovery_merkle_tree.calculate_root() + recovery_merkle_puzzle = P2_1_OF_N_MOD.curry(recovery_merkle_root) + recovery_merkle_puzzlehash = recovery_merkle_puzzle.get_tree_hash() + recovery_solution = Program.to([amount, recovery_conditions]) + + proof = vault_merkle_tree.generate_proof(recovery_puzzlehash) + recovery_proof = Program.to((proof[0], proof[1][0])) + vault_solution = Program.to([recovery_proof, recovery_puzzle, recovery_solution]) + recovery_conds = conditions_dict_for_solution(vault_puzzle, vault_solution, INFINITE_COST) + assert recovery_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == recovery_merkle_puzzlehash diff --git a/chia/_tests/wallet/vault/test_vault_lifecycle.py b/chia/_tests/wallet/vault/test_vault_lifecycle.py new file mode 100644 index 000000000000..e68a1cdbf74e --- /dev/null +++ b/chia/_tests/wallet/vault/test_vault_lifecycle.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +from typing import Optional + +import pytest +from chia_rs import AugSchemeMPL, G2Element, PrivateKey +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from chia._tests.clvm.test_puzzles import secret_exponent_for_index +from chia._tests.util.spend_sim import CostLogger, sim_and_client +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program +from chia.types.coin_spend import make_spend +from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.types.spend_bundle import SpendBundle +from chia.util.errors import Err +from chia.util.ints import uint64 +from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions +from chia.wallet.vault.vault_drivers import ( + construct_p2_delegated_secp, + construct_recovery_finish, + construct_secp_message, + construct_vault_merkle_tree, + construct_vault_puzzle, + get_recovery_puzzle, + get_vault_proof, +) + +seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 +SECP_SK = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) +SECP_PK = SECP_SK.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + +BLS_SK = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) +BLS_PK = BLS_SK.get_g1() + +TIMELOCK = uint64(1000) +ACS = Program.to(0) +ACS_PH = ACS.get_tree_hash() +HIDDEN_PUZZLE_HASH = Program.to("hph").get_tree_hash() + + +def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + + +@pytest.mark.anyio +async def test_vault_inner(cost_logger: CostLogger) -> None: + async with sim_and_client() as (sim, client): + # Setup puzzles + secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, HIDDEN_PUZZLE_HASH) + secp_puzzlehash = secp_puzzle.get_tree_hash() + p2_recovery_puzzle = get_recovery_puzzle(secp_puzzlehash, BLS_PK, TIMELOCK) + p2_recovery_puzzlehash = p2_recovery_puzzle.get_tree_hash() + vault_puzzle = construct_vault_puzzle(secp_puzzlehash, p2_recovery_puzzlehash) + vault_puzzlehash = vault_puzzle.get_tree_hash() + vault_merkle_tree = construct_vault_merkle_tree(secp_puzzlehash, p2_recovery_puzzlehash) + + await sim.farm_block(vault_puzzlehash) + + vault_coin: Coin = ( + await client.get_coin_records_by_puzzle_hashes([vault_puzzlehash], include_spent_coins=False) + )[0].coin + + # SECP SPEND + amount = 10000 + secp_conditions = Program.to([[51, ACS_PH, amount], [51, vault_puzzlehash, vault_coin.amount - amount]]) + secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) + secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) + secp_signature = sign_message( + SECP_SK, + construct_secp_message( + secp_delegated_puzzle.get_tree_hash(), + vault_coin.name(), + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + HIDDEN_PUZZLE_HASH, + ), + ) + + secp_solution = Program.to( + [ + secp_delegated_puzzle, + secp_delegated_solution, + secp_signature, + vault_coin.name(), + ] + ) + + proof = get_vault_proof(vault_merkle_tree, secp_puzzlehash) + vault_solution_secp = Program.to([proof, secp_puzzle, secp_solution]) + vault_spendbundle = cost_logger.add_cost( + "Standard spend w/ 1 extra CC", + SpendBundle([make_spend(vault_coin, vault_puzzle, vault_solution_secp)], G2Element()), + ) + + result: tuple[MempoolInclusionStatus, Optional[Err]] = await client.push_tx(vault_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS + await sim.farm_block() + + # RECOVERY SPEND + vault_coin = (await client.get_coin_records_by_puzzle_hashes([vault_puzzlehash], include_spent_coins=False))[ + 0 + ].coin + + recovery_conditions = Program.to([[51, ACS_PH, vault_coin.amount]]) + recovery_solution = Program.to([vault_coin.amount, recovery_conditions]) + recovery_proof = get_vault_proof(vault_merkle_tree, p2_recovery_puzzlehash) + vault_solution_recovery = Program.to([recovery_proof, p2_recovery_puzzle, recovery_solution]) + vault_spendbundle = cost_logger.add_cost( + "Recovery initiation", + SpendBundle( + [make_spend(vault_coin, vault_puzzle, vault_solution_recovery)], + AugSchemeMPL.sign( + BLS_SK, + ( + recovery_conditions.get_tree_hash() + + vault_coin.name() + + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA + ), + ), + ), + ) + + result = await client.push_tx(vault_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS + await sim.farm_block() + + recovery_finish_puzzle = construct_recovery_finish(TIMELOCK, recovery_conditions) + recovery_finish_puzzlehash = recovery_finish_puzzle.get_tree_hash() + recovery_puzzle = construct_vault_puzzle(secp_puzzlehash, recovery_finish_puzzlehash) + recovery_puzzlehash = recovery_puzzle.get_tree_hash() + recovery_merkle_tree = construct_vault_merkle_tree(secp_puzzlehash, recovery_finish_puzzlehash) + + recovery_coin: Coin = ( + await client.get_coin_records_by_puzzle_hashes([recovery_puzzlehash], include_spent_coins=False) + )[0].coin + + # Finish recovery + proof = get_vault_proof(recovery_merkle_tree, recovery_finish_puzzlehash) + recovery_finish_solution = Program.to([]) + recovery_solution = Program.to([proof, recovery_finish_puzzle, recovery_finish_solution]) + finish_spendbundle = cost_logger.add_cost( + "Finish recovery", SpendBundle([make_spend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + ) + + result = await client.push_tx(finish_spendbundle) + assert result[1] == Err.ASSERT_SECONDS_RELATIVE_FAILED + + # Skip time + sim.pass_time(TIMELOCK) + await sim.farm_block() + + result = await client.push_tx(finish_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS + + # Escape recovery + # just farm a coin to the recovery puzhash + await sim.farm_block(recovery_puzzlehash) + recovery_coin = ( + await client.get_coin_records_by_puzzle_hashes([recovery_puzzlehash], include_spent_coins=False) + )[0].coin + + proof = get_vault_proof(recovery_merkle_tree, secp_puzzlehash) + secp_conditions = Program.to([[51, ACS_PH, recovery_coin.amount]]) + secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) + secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) + secp_signature = sign_message( + SECP_SK, + construct_secp_message( + secp_delegated_puzzle.get_tree_hash(), + recovery_coin.name(), + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + HIDDEN_PUZZLE_HASH, + ), + ) + secp_solution = Program.to( + [ + secp_delegated_puzzle, + secp_delegated_solution, + secp_signature, + recovery_coin.name(), + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + ] + ) + + recovery_solution = Program.to([proof, secp_puzzle, secp_solution]) + escape_spendbundle = cost_logger.add_cost( + "Escape recovery", SpendBundle([make_spend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + ) + result = await client.push_tx(escape_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py new file mode 100644 index 000000000000..5ba507923ea5 --- /dev/null +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -0,0 +1,407 @@ +from __future__ import annotations + +from collections.abc import Awaitable +from typing import Callable + +import pytest +from chia_rs import G1Element +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from chia._tests.conftest import ConsensusMode +from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework +from chia.rpc.wallet_request_types import GetPrivateKey, PushTransactions, VaultCreate, VaultRecovery +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint32, uint64 +from chia.util.keychain import KeyTypes +from chia.wallet.payment import Payment +from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG +from chia.wallet.vault.vault_info import VaultInfo +from chia.wallet.vault.vault_root import VaultRoot +from chia.wallet.vault.vault_wallet import Vault + + +async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: bool) -> None: + env = wallet_environments.environments[0] + seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 + SECP_SK = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) + SECP_PK = SECP_SK.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + + # Temporary hack so execute_signing_instructions can access the key + env.wallet_state_manager.config["test_sk"] = SECP_SK + client = wallet_environments.environments[1].rpc_client + fingerprint = (await client.get_public_keys()).pk_fingerprints[0] + bls_pk = None + timelock = None + if with_recovery: + bls_pk = (await client.get_private_key(GetPrivateKey(fingerprint))).private_key.observation_root() + timelock = uint64(10) + if bls_pk is not None: + assert isinstance(bls_pk, G1Element) + hidden_puzzle_index = uint32(0) + res = await client.vault_create( + VaultCreate( + secp_pk=SECP_PK, + hp_index=hidden_puzzle_index, + bls_pk=bls_pk, + timelock=timelock, + push=True, + ), + tx_config=wallet_environments.tx_config, + ) + + all_removals = [coin for tx in res.transactions for coin in tx.removals] + eve_coin = [ + item for tx in res.transactions for item in tx.additions if item not in all_removals and item.amount == 1 + ][0] + launcher_id = eve_coin.parent_coin_info + vault_root = VaultRoot.from_bytes(launcher_id) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + # "confirmed_wallet_balance": -1, + "set_remainder": True, + } + }, + ), + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + # "confirmed_wallet_balance": -1, + "set_remainder": True, + } + }, + ), + ] + ) + await env.node.keychain_proxy.add_key( + launcher_id.hex(), label="vault", private=False, key_type=KeyTypes.VAULT_LAUNCHER + ) + await env.restart(vault_root.get_fingerprint()) + await wallet_environments.full_node.wait_for_wallet_synced(env.node, 20) + + +@pytest.mark.parametrize( + "wallet_environments", + [{"num_environments": 2, "blocks_needed": [1, 1]}], + indirect=True, +) +@pytest.mark.parametrize("setup_function", [vault_setup]) +@pytest.mark.parametrize("with_recovery", [True, False]) +@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") +@pytest.mark.anyio +async def test_vault_creation( + wallet_environments: WalletTestFramework, + setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], + with_recovery: bool, +) -> None: + await setup_function(wallet_environments, with_recovery) + env = wallet_environments.environments[0] + assert isinstance(env.xch_wallet, Vault) + + wallet: Vault = env.xch_wallet + await wallet.sync_vault_launcher() + assert wallet.vault_info + + # get a p2_singleton + p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() + + coins_to_create = 2 + funding_amount = uint64(1000000000) + funding_wallet = wallet_environments.environments[1].xch_wallet + for _ in range(coins_to_create): + async with funding_wallet.wallet_state_manager.new_action_scope( + DEFAULT_TX_CONFIG, push=True, sign=True + ) as action_scope: + await funding_wallet.generate_signed_transaction( + funding_amount, + p2_singleton_puzzle_hash, + action_scope, + memos=[wallet.vault_info.pubkey], + ) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": funding_amount * 2, + "set_remainder": True, + } + }, + ), + ], + ) + + recipient_ph = await funding_wallet.get_new_puzzlehash() + + primaries = [ + Payment(recipient_ph, uint64(500000000), memos=[recipient_ph]), + Payment(recipient_ph, uint64(510000000), memos=[recipient_ph]), + ] + amount = uint64(1000000) + fee = uint64(100) + balance_delta = 1011000099 + + async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True, sign=True) as action_scope: + await wallet.generate_signed_transaction( + amount, recipient_ph, action_scope, primaries=primaries, fee=fee, memos=[recipient_ph] + ) + + vault_eve_id = wallet.vault_info.coin.name() + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -balance_delta, + "set_remainder": True, + } + }, + ), + ], + ) + + # check the wallet and singleton store have the latest vault coin + assert wallet.vault_info.coin.parent_coin_info == vault_eve_id + record = (await wallet.wallet_state_manager.singleton_store.get_records_by_coin_id(wallet.vault_info.coin.name()))[ + 0 + ] + assert record is not None + + assert isinstance(record.custom_data, bytes) + custom_data = record.custom_data + vault_info = VaultInfo.from_bytes(custom_data) + assert vault_info == wallet.vault_info + assert vault_info.recovery_info == wallet.vault_info.recovery_info + + +@pytest.mark.parametrize( + "wallet_environments", + [{"num_environments": 2, "blocks_needed": [1, 1]}], + indirect=True, +) +@pytest.mark.parametrize("setup_function", [vault_setup]) +@pytest.mark.parametrize("with_recovery", [True]) +@pytest.mark.parametrize("spent_recovery", [True, False]) +@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") +@pytest.mark.anyio +async def test_vault_recovery( + wallet_environments: WalletTestFramework, + setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], + with_recovery: bool, + spent_recovery: bool, +) -> None: + await setup_function(wallet_environments, with_recovery) + env = wallet_environments.environments[0] + assert isinstance(env.xch_wallet, Vault) + recovery_seed = 0x6D836489B057E59FF0E16CE2D8F876C454697B76549E11D93F8102C4140B2DC5 + RECOVERY_SECP_SK = ec.derive_private_key(recovery_seed, ec.SECP256R1(), default_backend()) + RECOVERY_SECP_PK = RECOVERY_SECP_SK.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + client = wallet_environments.environments[1].rpc_client + fingerprint = (await client.get_public_keys()).pk_fingerprints[0] + bls_pk = (await client.get_private_key(GetPrivateKey(fingerprint))).private_key.observation_root() + assert isinstance(bls_pk, G1Element) + timelock = uint64(10) + + wallet: Vault = env.xch_wallet + await wallet.sync_vault_launcher() + assert wallet.vault_info + + p2_addr = await wallet_environments.environments[0].rpc_client.get_next_address(wallet.id(), False) + + funding_amount = uint64(1000000000) + funding_wallet = wallet_environments.environments[1].xch_wallet + await wallet_environments.environments[1].rpc_client.send_transaction( + funding_wallet.id(), funding_amount, p2_addr, DEFAULT_TX_CONFIG + ) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": funding_amount, + "set_remainder": True, + } + }, + ), + ], + ) + + # make a spend before recovery + if spent_recovery: + amount = uint64(10000) + recipient_ph = await funding_wallet.get_new_puzzlehash() + async with wallet.wallet_state_manager.new_action_scope( + DEFAULT_TX_CONFIG, push=False, sign=False + ) as action_scope: + await wallet.generate_signed_transaction(amount, recipient_ph, action_scope, memos=[recipient_ph]) + + await wallet_environments.environments[0].rpc_client.push_transactions( + PushTransactions( # pylint: disable=unexpected-keyword-arg + transactions=action_scope.side_effects.transactions, + sign=True, + ), + tx_config=wallet_environments.tx_config, + ) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -amount + 1, + "set_remainder": True, + } + }, + ), + ], + ) + + [initiate_tx, finish_tx] = ( + await env.rpc_client.vault_recovery( + VaultRecovery( + wallet_id=wallet.id(), + secp_pk=RECOVERY_SECP_PK, + hp_index=uint32(0), + bls_pk=bls_pk, + timelock=timelock, + sign=False, + ), + tx_config=wallet_environments.tx_config, + ) + ).transactions + + await wallet_environments.environments[1].rpc_client.push_transactions( + PushTransactions( # pylint: disable=unexpected-keyword-arg + transactions=[initiate_tx], + sign=True, + ), + tx_config=wallet_environments.tx_config, + ) + + vault_coin = wallet.vault_info.coin + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "<=#confirmed_wallet_balance": 1, + "set_remainder": True, + } + }, + ), + ], + ) + + recovery_coin = wallet.vault_info.coin + assert recovery_coin.parent_coin_info == vault_coin.name() + + wallet_environments.full_node.time_per_block = 100 + await wallet_environments.full_node.farm_blocks_to_puzzlehash( + count=2, guarantee_transaction_blocks=True, farm_to=bytes32(b"1" * 32) + ) + + await wallet_environments.environments[1].rpc_client.push_transactions( + PushTransactions(transactions=[finish_tx]), tx_config=wallet_environments.tx_config + ) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "<=#confirmed_wallet_balance": 1, + "set_remainder": True, + } + }, + ), + ], + ) + + recovered_coin = wallet.vault_info.coin + assert recovered_coin.parent_coin_info == recovery_coin.name() + + # spend recovery balance + env.wallet_state_manager.config["test_sk"] = RECOVERY_SECP_SK + recipient_ph = await funding_wallet.get_new_puzzlehash() + amount = uint64(200) + + async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=False, sign=False) as action_scope: + await wallet.generate_signed_transaction(amount, recipient_ph, action_scope, memos=[recipient_ph]) + + # Test we can push the transaction separately + await wallet_environments.environments[0].rpc_client.push_transactions( + PushTransactions( # pylint: disable=unexpected-keyword-arg + transactions=action_scope.side_effects.transactions, + sign=True, + ), + tx_config=wallet_environments.tx_config, + ) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -amount - 1, + "set_remainder": True, + } + }, + ), + ], + ) diff --git a/chia/_tests/wallet/vc_wallet/test_vc_wallet.py b/chia/_tests/wallet/vc_wallet/test_vc_wallet.py index 701d44be501f..d1f7f3734564 100644 --- a/chia/_tests/wallet/vc_wallet/test_vc_wallet.py +++ b/chia/_tests/wallet/vc_wallet/test_vc_wallet.py @@ -29,14 +29,14 @@ from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker, construct_cr_layer from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle async def mint_cr_cat( num_blocks: int, - wallet_0: Wallet, + wallet_0: MainWalletProtocol, wallet_node_0: WalletNode, client_0: WalletRpcClient, full_node_api: FullNodeSimulator, diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 3c7291a72de1..94c790aefddd 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -26,6 +26,7 @@ from chia.cmds.show import show_cmd from chia.cmds.start import start_cmd from chia.cmds.stop import stop_cmd +from chia.cmds.vault import vault_cmd from chia.cmds.wallet import wallet_cmd from chia.util.default_root import DEFAULT_KEYS_ROOT_PATH, DEFAULT_ROOT_PATH from chia.util.errors import KeychainCurrentPassphraseIsInvalid @@ -131,6 +132,7 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: cli.add_command(completion) cli.add_command(dao_cmd) cli.add_command(dev_cmd) +cli.add_command(vault_cmd) def main() -> None: diff --git a/chia/cmds/cmds_util.py b/chia/cmds/cmds_util.py index 57f4a6b0f989..441495424fe8 100644 --- a/chia/cmds/cmds_util.py +++ b/chia/cmds/cmds_util.py @@ -355,6 +355,12 @@ class TransactionBundle(Streamable): txs: list[TransactionRecord] +def write_transactions_to_file(txs: list[TransactionRecord], transaction_file: str) -> None: + print(f"Writing transactions to file {transaction_file}:") + with open(Path(transaction_file), "wb") as file: + file.write(bytes(TransactionBundle(txs))) + + def tx_out_cmd( enable_timelock_args: Optional[bool] = None, ) -> Callable[[Callable[..., list[TransactionRecord]]], Callable[..., None]]: @@ -363,9 +369,7 @@ def _tx_out_cmd(func: Callable[..., list[TransactionRecord]]) -> Callable[..., N def original_cmd(transaction_file: Optional[str] = None, **kwargs: Any) -> None: txs: list[TransactionRecord] = func(**kwargs) if transaction_file is not None: - print(f"Writing transactions to file {transaction_file}:") - with open(Path(transaction_file), "wb") as file: - file.write(bytes(TransactionBundle(txs))) + write_transactions_to_file(txs, transaction_file) return click.option( "--push/--no-push", help="Push the transaction to the network", type=bool, is_flag=True, default=True diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index e6a4794e4fca..ad81207c4a2f 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -7,6 +7,7 @@ from typing import Any, Optional import yaml +from chia_rs import PrivateKey from chia.cmds.configure import configure from chia.consensus.coinbase import create_puzzlehash_for_pk @@ -64,13 +65,13 @@ def dict_add_new_default(updated: dict[str, Any], default: dict[str, Any], do_no def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: if keychain is None: keychain = Keychain() - all_sks = keychain.get_all_private_keys() + all_sks: list[PrivateKey] = [sk for sk, _ in keychain.get_all_private_keys() if isinstance(sk, PrivateKey)] if len(all_sks) == 0: print("No keys are present in the keychain. Generate them with 'chia keys generate'") return None with lock_and_load_config(new_root, "config.yaml") as config: - pool_child_pubkeys = [master_sk_to_pool_sk(sk).get_g1() for sk, _ in all_sks] + pool_child_pubkeys = [master_sk_to_pool_sk(sk).get_g1() for sk in all_sks] all_targets = [] stop_searching_for_farmer = "xch_target_address" not in config["farmer"] stop_searching_for_pool = "xch_target_address" not in config["pool"] @@ -79,7 +80,7 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: prefix = config["network_overrides"]["config"][selected]["address_prefix"] intermediates = {} - for sk, _ in all_sks: + for sk in all_sks: intermediates[bytes(sk)] = { "observer": master_sk_to_wallet_sk_unhardened_intermediate(sk), "non-observer": master_sk_to_wallet_sk_intermediate(sk), @@ -88,7 +89,7 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: for i in range(number_of_ph_to_search): if stop_searching_for_farmer and stop_searching_for_pool and i > 0: break - for sk, _ in all_sks: + for sk in all_sks: intermediate_n = intermediates[bytes(sk)]["non-observer"] intermediate_o = intermediates[bytes(sk)]["observer"] diff --git a/chia/cmds/keys.py b/chia/cmds/keys.py index 1a7194beed3f..b8d7495ff32f 100644 --- a/chia/cmds/keys.py +++ b/chia/cmds/keys.py @@ -1,11 +1,12 @@ from __future__ import annotations -from typing import Optional +from typing import Any, Optional import click from chia_rs import PrivateKey from chia.cmds import options +from chia.util.secret_info import SecretInfo @click.group("keys", help="Manage your keys") @@ -342,6 +343,10 @@ def search_cmd( if resolved_sk is None: print("Could not resolve private key from fingerprint/mnemonic file") + if resolved_sk is not None and not isinstance(resolved_sk, PrivateKey): + print("Cannot derive from non-BLS keys") + return + found: bool = search_derive( ctx.obj["root_path"], fingerprint, @@ -364,7 +369,7 @@ class ResolutionError(Exception): def _resolve_fingerprint_and_sk( filename: Optional[str], fingerprint: Optional[int], non_observer_derivation: bool -) -> tuple[Optional[int], Optional[PrivateKey]]: +) -> tuple[Optional[int], Optional[SecretInfo[Any]]]: from .keys_funcs import resolve_derivation_master_key reolved_fp, resolved_sk = resolve_derivation_master_key(filename if filename is not None else fingerprint) @@ -418,6 +423,10 @@ def wallet_address_cmd( except ResolutionError: return + if sk is not None and not isinstance(sk, PrivateKey): + print("Cannot derive from non-BLS keys") + return + derive_wallet_address( ctx.obj["root_path"], fingerprint, index, count, prefix, non_observer_derivation, show_hd_path, sk ) @@ -496,6 +505,10 @@ def child_key_cmd( except ResolutionError: return + if sk is not None and not isinstance(sk, PrivateKey): + print("Cannot derive from non-BLS keys") + return + derive_child_key( fingerprint, key_type, diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 10975d374460..da66020ae565 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -25,6 +25,7 @@ mnemonic_to_seed, ) from chia.util.keyring_wrapper import KeyringWrapper, obtain_current_passphrase +from chia.util.secret_info import SecretInfo from chia.wallet.derive_keys import ( master_pk_to_wallet_pk_unhardened, master_sk_to_farmer_sk, @@ -85,11 +86,11 @@ def add_key_info(mnemonic_or_pk: str, label: Optional[str]) -> None: unlock_keyring() try: if check_mnemonic_validity(mnemonic_or_pk): - sk = Keychain().add_key(mnemonic_or_pk, label, private=True) - fingerprint = sk.get_g1().get_fingerprint() + sk, _ = Keychain().add_key(mnemonic_or_pk, label, private=True) + fingerprint = sk.public_key().get_fingerprint() print(f"Added private key with public key fingerprint {fingerprint}") else: - pk = Keychain().add_key(mnemonic_or_pk, label, private=False) + pk, _ = Keychain().add_key(mnemonic_or_pk, label, private=False) fingerprint = pk.get_fingerprint() print(f"Added public key with fingerprint {fingerprint}") @@ -180,39 +181,47 @@ def process_key_data(key_data: KeyData) -> dict[str, Any]: key["label"] = key_data.label key["fingerprint"] = key_data.fingerprint - key["master_pk"] = bytes(key_data.public_key).hex() - if sk is not None: + if isinstance(key_data.observation_root, G1Element): + key["master_pk"] = key_data.public_key.hex() + else: # pragma: no cover + # TODO: Add test coverage once vault wallet exists + key["observation_root"] = key_data.public_key.hex() + if sk is not None and isinstance(sk, PrivateKey): key["farmer_pk"] = bytes(master_sk_to_farmer_sk(sk).get_g1()).hex() key["pool_pk"] = bytes(master_sk_to_pool_sk(sk).get_g1()).hex() else: key["farmer_pk"] = None key["pool_pk"] = None - if non_observer_derivation: - if sk is None: - first_wallet_pk: Optional[G1Element] = None + if isinstance(key_data.observation_root, G1Element): + if non_observer_derivation: + if sk is None: + first_wallet_pk: Optional[G1Element] = None + else: + assert isinstance(sk, PrivateKey) + first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).public_key() else: - first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).get_g1() - else: - first_wallet_pk = master_pk_to_wallet_pk_unhardened(key_data.public_key, uint32(0)) + first_wallet_pk = master_pk_to_wallet_pk_unhardened(key_data.observation_root, uint32(0)) - if first_wallet_pk is not None: - wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_pk), prefix) - key["wallet_address"] = wallet_address - else: - key["wallet_address"] = None + if first_wallet_pk is not None: + wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_pk), prefix) + key["wallet_address"] = wallet_address + else: + key["wallet_address"] = None key["non_observer"] = non_observer_derivation if show_mnemonic and sk is not None: key["master_sk"] = bytes(sk).hex() - key["farmer_sk"] = bytes(master_sk_to_farmer_sk(sk)).hex() - key["wallet_sk"] = bytes(master_sk_to_wallet_sk(sk, uint32(0))).hex() + if isinstance(sk, PrivateKey): + key["farmer_sk"] = bytes(master_sk_to_farmer_sk(sk)).hex() + key["wallet_sk"] = bytes(master_sk_to_wallet_sk(sk, uint32(0))).hex() key["mnemonic"] = bytes_to_mnemonic(key_data.entropy) else: key["master_sk"] = None - key["farmer_sk"] = None - key["wallet_sk"] = None + if isinstance(key_data.observation_root, G1Element): + key["farmer_sk"] = None + key["wallet_sk"] = None key["mnemonic"] = None return key @@ -309,15 +318,23 @@ class DerivationType(Enum): return (current_pk, current_sk, "m/" + "/".join(path) + "/") -def sign(message: str, private_key: PrivateKey, hd_path: str, as_bytes: bool, json_output: bool) -> None: - sk = derive_pk_and_sk_from_hd_path(private_key.get_g1(), hd_path, master_sk=private_key)[1] +def sign(message: str, private_key: SecretInfo[Any], hd_path: str, as_bytes: bool, json_output: bool) -> None: + if hd_path != "m": + if not isinstance(private_key, PrivateKey): + print("Cannot derive non-BLS keys") + return + sk: Optional[SecretInfo[Any]] = derive_pk_and_sk_from_hd_path( + private_key.public_key(), hd_path, master_sk=private_key + )[1] + else: + sk = private_key assert sk is not None data = bytes.fromhex(message) if as_bytes else bytes(message, "utf-8") signing_mode: SigningMode = ( SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT if as_bytes else SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT ) - pubkey_hex: str = bytes(sk.get_g1()).hex() - signature_hex: str = bytes(AugSchemeMPL.sign(sk, data)).hex() + pubkey_hex: str = bytes(sk.public_key()).hex() + signature_hex: str = bytes(sk.sign(data)).hex() if json_output: print( json.dumps( @@ -512,9 +529,10 @@ def search_derive( search_private_key = True if fingerprint is None and private_key is None: - public_keys: list[G1Element] = Keychain().get_all_public_keys() + public_keys: list[G1Element] = Keychain().get_all_public_keys_of_type(G1Element) private_keys: list[Optional[PrivateKey]] = [ - data.private_key if data.secrets is not None else None for data in Keychain().get_keys(include_secrets=True) + data.private_key if data.secrets is not None and isinstance(data.private_key, PrivateKey) else None + for data in Keychain().get_keys(include_secrets=True) ] elif fingerprint is None: assert private_key is not None @@ -522,8 +540,19 @@ def search_derive( private_keys = [private_key] else: master_key_data = Keychain().get_key(fingerprint, include_secrets=True) - public_keys = [master_key_data.public_key] - private_keys = [master_key_data.private_key if master_key_data.secrets is not None else None] + if isinstance(master_key_data.observation_root, G1Element): + public_keys = [master_key_data.observation_root] + private_keys = [ + ( + master_key_data.private_key + if master_key_data.secrets is not None and isinstance(master_key_data.private_key, PrivateKey) + else None + ) + ] + else: # pragma: no cover + # TODO: Add test coverage once vault wallet exists + print("Cannot currently derive paths from non-BLS keys") + return True for pk, sk in zip(public_keys, private_keys): if sk is None and non_observer_derivation: @@ -656,11 +685,19 @@ def derive_wallet_address( """ if fingerprint is not None: key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=non_observer_derivation) - if non_observer_derivation: + if not isinstance(key_data.observation_root, G1Element): # pragma: no cover + # TODO: Add test coverage once vault wallet exists + print("Cannot currently derive from non-BLS keys") + return + if non_observer_derivation and key_data.secrets is None: + print("Need a private key for non observer derivation of wallet addresses") + return + elif non_observer_derivation: + assert isinstance(key_data.private_key, PrivateKey) sk = key_data.private_key else: sk = None - pk = key_data.public_key + pk: G1Element = key_data.observation_root else: assert private_key is not None sk = private_key @@ -718,8 +755,17 @@ def derive_child_key( if fingerprint is not None: key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=True) - current_pk: G1Element = key_data.public_key - current_sk: Optional[PrivateKey] = key_data.private_key if key_data.secrets is not None else None + if not isinstance(key_data.observation_root, G1Element): # pragma: no cover + # TODO: Add coverage when vault wallet exists + print("Cannot currently derive from non-BLS keys") + return + if key_data.secrets is not None: + assert isinstance(key_data.private_key, PrivateKey) + current_pk: G1Element = key_data.observation_root + # mypy can't figure out the semantics here + current_sk: Optional[PrivateKey] = ( + key_data.private_key if key_data.secrets is not None else None # type: ignore[assignment] + ) else: assert private_key is not None current_pk = private_key.get_g1() @@ -790,12 +836,12 @@ def derive_child_key( print(f"{key_type_str} private key {i}{hd_path}: {private_key_string_repr(sk)}") -def private_key_for_fingerprint(fingerprint: int) -> Optional[PrivateKey]: +def private_key_for_fingerprint(fingerprint: int) -> Optional[SecretInfo[Any]]: unlock_keyring() private_keys = Keychain().get_all_private_keys() for sk, _ in private_keys: - if sk.get_g1().get_fingerprint() == fingerprint: + if sk.public_key().get_fingerprint() == fingerprint: return sk return None @@ -825,7 +871,7 @@ def prompt_for_fingerprint() -> Optional[int]: def get_private_key_with_fingerprint_or_prompt( fingerprint: Optional[int], -) -> tuple[Optional[int], Optional[PrivateKey]]: +) -> tuple[Optional[int], Optional[SecretInfo[Any]]]: """ Get a private key with the specified fingerprint. If fingerprint is not specified, prompt the user to select a key. @@ -851,7 +897,7 @@ def private_key_from_mnemonic_seed_file(filename: Path) -> PrivateKey: def resolve_derivation_master_key( fingerprint_or_filename: Optional[Union[int, str, Path]], -) -> tuple[Optional[int], Optional[PrivateKey]]: +) -> tuple[Optional[int], Optional[SecretInfo[Any]]]: """ Given a key fingerprint of file containing a mnemonic seed, return the private key. """ diff --git a/chia/cmds/sim_funcs.py b/chia/cmds/sim_funcs.py index 91567f65698b..96ec3f3ca5b8 100644 --- a/chia/cmds/sim_funcs.py +++ b/chia/cmds/sim_funcs.py @@ -35,6 +35,8 @@ def get_ph_from_fingerprint(fingerprint: int, key_id: int = 1) -> bytes32: if priv_key_and_entropy is None: raise Exception("Fingerprint not found") private_key = priv_key_and_entropy[0] + if not isinstance(private_key, PrivateKey): + raise ValueError("Can only use BLS keys for the simulator") sk_for_wallet_id: PrivateKey = master_sk_to_wallet_sk(private_key, uint32(key_id)) puzzle_hash: bytes32 = create_puzzlehash_for_pk(sk_for_wallet_id.get_g1()) return puzzle_hash @@ -150,6 +152,9 @@ def display_key_info(fingerprint: int, prefix: str) -> None: print(f"Fingerprint {fingerprint} not found") return sk, seed = private_key_and_seed + if not isinstance(sk, PrivateKey): + print("Can only use BLS keys for the simulator") + return print("\nFingerprint:", sk.get_g1().get_fingerprint()) print("Master public key (m):", sk.get_g1()) print("Farmer public key (m/12381/8444/0/0):", master_sk_to_farmer_sk(sk).get_g1()) @@ -175,8 +180,8 @@ def generate_and_return_fingerprint(mnemonic: Optional[str] = None) -> int: print("Generating private key") mnemonic = generate_mnemonic() try: - sk = Keychain().add_key(mnemonic, None) - fingerprint: int = sk.get_g1().get_fingerprint() + sk, _ = Keychain().add_key(mnemonic, None) + fingerprint: int = sk.public_key().get_fingerprint() except KeychainFingerprintExists as e: fingerprint = e.fingerprint print(f"Fingerprint: {fingerprint} for provided private key already exists.") diff --git a/chia/cmds/vault.py b/chia/cmds/vault.py new file mode 100644 index 000000000000..521ff0ab7926 --- /dev/null +++ b/chia/cmds/vault.py @@ -0,0 +1,256 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Sequence +from typing import Optional + +import click + +from chia.cmds import options +from chia.cmds.cmds_util import timelock_args, tx_out_cmd +from chia.cmds.param_types import AmountParamType, Bytes32ParamType, CliAmount, cli_amount_none +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.transaction_record import TransactionRecord + + +@click.group("vault", help="Manage your vault") +@click.pass_context +def vault_cmd(ctx: click.Context) -> None: + pass + + +@vault_cmd.command("create", help="Create a new vault") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@options.create_fingerprint() +@click.option( + "-pk", + "--public-key", + help="SECP public key", + type=str, + required=True, +) +@click.option( + "-rk", + "--recovery-public-key", + help="BLS public key for vault recovery", + type=str, + required=False, + default=None, +) +@click.option( + "-rt", + "--recovery-timelock", + help="Timelock for vault recovery (in seconds)", + type=int, + required=False, + default=None, +) +@click.option( + "-i", + "--hidden-puzzle-index", + help="Starting index for hidden puzzle", + type=int, + required=False, + default=0, +) +@options.create_fee() +@click.option("-n", "--name", help="Set the vault name", type=str) +@click.option( + "-ma", + "--min-coin-amount", + help="Ignore coins worth less then this much XCH or CAT units", + type=AmountParamType(), + required=False, + default=cli_amount_none, +) +@click.option( + "-l", + "--max-coin-amount", + help="Ignore coins worth more then this much XCH or CAT units", + type=AmountParamType(), + required=False, + default=cli_amount_none, +) +@click.option( + "--exclude-coin", + "coins_to_exclude", + multiple=True, + type=Bytes32ParamType(), + help="Exclude this coin from being spent.", +) +@click.option( + "--reuse", + help="Reuse existing address for the change.", + is_flag=True, + default=False, +) +@tx_out_cmd() +def vault_create_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + public_key: str, + recovery_public_key: Optional[str], + recovery_timelock: Optional[int], + hidden_puzzle_index: int, + fee: uint64, + name: Optional[str], + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + coins_to_exclude: Sequence[bytes32], + reuse: bool, + push: bool, + condition_valid_times: ConditionValidTimes, +) -> list[TransactionRecord]: + from .vault_funcs import create_vault + + return asyncio.run( + create_vault( + wallet_rpc_port, + fingerprint, + public_key, + recovery_public_key, + recovery_timelock, + hidden_puzzle_index, + fee, + name, + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=coins_to_exclude, + reuse_puzhash=True if reuse else None, + push=push, + condition_valid_times=condition_valid_times, + ) + ) + + +@vault_cmd.command("recover", help="Generate transactions for vault recovery") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@options.create_fingerprint() +@click.option("-i", "--wallet-id", help="Vault Wallet ID", type=int, required=True, default=1) +@click.option( + "-pk", + "--public-key", + help="SECP public key", + type=str, + required=True, +) +@click.option( + "-i", + "--hidden-puzzle-index", + help="Starting index for hidden puzzle", + type=int, + required=False, + default=0, +) +@click.option( + "-rk", + "--recovery-public-key", + help="BLS public key for vault recovery", + type=str, + required=False, + default=None, +) +@click.option( + "-rt", + "--recovery-timelock", + help="Timelock for vault recovery (in seconds)", + type=int, + required=False, + default=None, +) +@click.option( + "-ri", + "--recovery-initiate-file", + help="Provide a filename to store the recovery transactions", + type=str, + required=True, + default="initiate_recovery.json", +) +@click.option( + "-rf", + "--recovery-finish-file", + help="Provide a filename to store the recovery transactions", + type=str, + required=True, + default="finish_recovery.json", +) +@click.option( + "-ma", + "--min-coin-amount", + help="Ignore coins worth less then this much XCH or CAT units", + type=AmountParamType(), + required=False, + default=cli_amount_none, +) +@click.option( + "-l", + "--max-coin-amount", + help="Ignore coins worth more then this much XCH or CAT units", + type=AmountParamType(), + required=False, + default=cli_amount_none, +) +@click.option( + "--exclude-coin", + "coins_to_exclude", + multiple=True, + type=Bytes32ParamType(), + help="Exclude this coin from being spent.", +) +@click.option( + "--reuse", + help="Reuse existing address for the change.", + is_flag=True, + default=False, +) +@timelock_args() +def vault_recover_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + wallet_id: int, + public_key: str, + hidden_puzzle_index: int, + recovery_public_key: Optional[str], + recovery_timelock: Optional[int], + recovery_initiate_file: str, + recovery_finish_file: str, + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + coins_to_exclude: Sequence[bytes32], + reuse: bool, + condition_valid_times: ConditionValidTimes, +) -> None: + from .vault_funcs import recover_vault + + asyncio.run( + recover_vault( + wallet_rpc_port, + fingerprint, + wallet_id, + public_key, + hidden_puzzle_index, + recovery_public_key, + recovery_timelock, + recovery_initiate_file, + recovery_finish_file, + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=coins_to_exclude, + reuse_puzhash=True if reuse else None, + condition_valid_times=condition_valid_times, + ) + ) diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py new file mode 100644 index 000000000000..484e6f8444fb --- /dev/null +++ b/chia/cmds/vault_funcs.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import Optional + +from chia_rs import G1Element + +from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client, write_transactions_to_file +from chia.cmds.param_types import CliAmount +from chia.cmds.units import units +from chia.rpc.wallet_request_types import VaultCreate, VaultRecovery +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint32, uint64 +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.transaction_record import TransactionRecord + + +async def create_vault( + wallet_rpc_port: Optional[int], + fingerprint: Optional[int], + public_key: str, + recovery_public_key: Optional[str], + timelock: Optional[int], + hidden_puzzle_index: int, + fee: uint64, + name: Optional[str], + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + excluded_coin_ids: Sequence[bytes32], + reuse_puzhash: Optional[bool], + push: bool, + condition_valid_times: ConditionValidTimes, +) -> list[TransactionRecord]: + async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): + assert hidden_puzzle_index >= 0 + tx_config = CMDTXConfigLoader( + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=list(excluded_coin_ids), + reuse_puzhash=reuse_puzhash, + ).to_tx_config(units["chia"], config, fingerprint) + if timelock is not None: + assert timelock > 0 + try: + res = await wallet_client.vault_create( + VaultCreate( + secp_pk=bytes.fromhex(public_key), + hp_index=uint32(hidden_puzzle_index), + bls_pk=G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, + timelock=uint64(timelock) if timelock else None, + fee=fee, + push=push, + ), + tx_config=tx_config, + timelock_info=condition_valid_times, + ) + print("Successfully created a Vault wallet") + return res.transactions + except Exception as e: + print(f"Failed to create a new Vault: {e}") + return [] + + +async def recover_vault( + wallet_rpc_port: Optional[int], + fingerprint: Optional[int], + wallet_id: int, + public_key: str, + hidden_puzzle_index: int, + recovery_public_key: Optional[str], + timelock: Optional[int], + initiate_file: str, + finish_file: str, + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + excluded_coin_ids: Sequence[bytes32], + reuse_puzhash: Optional[bool], + condition_valid_times: ConditionValidTimes, +) -> None: + async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): + assert hidden_puzzle_index >= 0 + if timelock is not None: + assert timelock > 0 + tx_config = CMDTXConfigLoader( + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=list(excluded_coin_ids), + reuse_puzhash=reuse_puzhash, + ).to_tx_config(units["chia"], config, fingerprint) + try: + response = await wallet_client.vault_recovery( + VaultRecovery( + wallet_id=uint32(wallet_id), + secp_pk=bytes.fromhex(public_key), + hp_index=uint32(hidden_puzzle_index), + bls_pk=G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, + timelock=uint64(timelock) if timelock else None, + ), + tx_config=tx_config, + timelock_info=condition_valid_times, + ) + write_transactions_to_file([response.recovery_tx], initiate_file) + write_transactions_to_file([response.finish_tx], finish_file) + except Exception as e: + print(f"Error creating recovery transactions: {e}") diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 2bbdbc89ded1..fae3c5f2a602 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -8,7 +8,7 @@ from typing import Any, Literal, Optional, Union, overload from aiohttp import ClientConnectorError, ClientSession -from chia_rs import AugSchemeMPL, G1Element, PrivateKey +from chia_rs import AugSchemeMPL from chia.cmds.init_funcs import check_keys from chia.daemon.client import DaemonProxy @@ -30,7 +30,9 @@ KeychainMalformedResponse, KeychainProxyConnectionTimeout, ) -from chia.util.keychain import Keychain, KeyData, bytes_to_mnemonic, mnemonic_to_seed +from chia.util.keychain import Keychain, KeyData, KeyTypes, bytes_to_mnemonic, mnemonic_to_seed +from chia.util.observation_root import ObservationRoot +from chia.util.secret_info import SecretInfo from chia.util.ws_message import WsRpcMessage @@ -171,41 +173,57 @@ def handle_error(self, response: WsRpcMessage) -> None: raise Exception(f"{error}") @overload - async def add_key(self, mnemonic_or_pk: str) -> PrivateKey: ... + async def add_key(self, mnemonic_or_pk: str) -> tuple[SecretInfo[Any], KeyTypes]: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: ... + async def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> tuple[SecretInfo[Any], KeyTypes]: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: ... + async def add_key(self, mnemonic_or_pk: str, *, key_type: KeyTypes) -> tuple[SecretInfo[Any], KeyTypes]: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: ... + async def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True] + ) -> tuple[SecretInfo[Any], KeyTypes]: ... @overload async def add_key( - self, mnemonic_or_pk: str, label: Optional[str], private: bool - ) -> Union[PrivateKey, G1Element]: ... + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True], key_type: KeyTypes + ) -> tuple[SecretInfo[Any], KeyTypes]: ... + @overload + async def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False] + ) -> tuple[ObservationRoot, KeyTypes]: ... + + @overload async def add_key( - self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True - ) -> Union[PrivateKey, G1Element]: + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False], key_type: KeyTypes + ) -> tuple[ObservationRoot, KeyTypes]: ... + + async def add_key( + self, + mnemonic_or_pk: str, + label: Optional[str] = None, + private: bool = True, + key_type: KeyTypes = KeyTypes.G1_ELEMENT, + ) -> tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: """ Forwards to Keychain.add_key() """ - key: Union[PrivateKey, G1Element] if self.use_local_keychain(): - key = self.keychain.add_key(mnemonic_or_pk, label, private) + key, key_type = self.keychain.add_key(mnemonic_or_pk, label, private, key_type) else: response, success = await self.get_response_for_request( - "add_key", {"mnemonic_or_pk": mnemonic_or_pk, "label": label, "private": private} + "add_key", {"mnemonic_or_pk": mnemonic_or_pk, "label": label, "private": private, "key_type": key_type} ) if success: + key_type = KeyTypes(response["data"]["key_type"]) if private: seed = mnemonic_to_seed(mnemonic_or_pk) - key = AugSchemeMPL.key_gen(seed) + key = KeyTypes.parse_secret_info_from_seed(seed, key_type) else: - key = G1Element.from_bytes(hexstr_to_bytes(mnemonic_or_pk)) + key = KeyTypes.parse_observation_root(hexstr_to_bytes(mnemonic_or_pk), key_type) else: error = response["data"].get("error", None) if error == KEYCHAIN_ERR_KEYERROR: @@ -214,8 +232,9 @@ async def add_key( raise KeyError(word) else: self.handle_error(response) + raise RuntimeError("This should be impossible to reach") # pragma: no cover - return key + return key, key_type async def check_keys(self, root_path: Path) -> None: """ @@ -252,11 +271,11 @@ async def delete_key_by_fingerprint(self, fingerprint: int) -> None: if not success: self.handle_error(response) - async def get_all_private_keys(self) -> list[tuple[PrivateKey, bytes]]: + async def get_all_private_keys(self) -> list[tuple[SecretInfo[Any], bytes]]: """ Forwards to Keychain.get_all_private_keys() """ - keys: list[tuple[PrivateKey, bytes]] = [] + keys: list[tuple[SecretInfo[Any], bytes]] = [] if self.use_local_keychain(): keys = self.keychain.get_all_private_keys() else: @@ -289,17 +308,17 @@ async def get_all_private_keys(self) -> list[tuple[PrivateKey, bytes]]: return keys - async def get_first_private_key(self) -> Optional[PrivateKey]: + async def get_first_private_key(self, key_type: Optional[KeyTypes] = None) -> Optional[SecretInfo[Any]]: """ Forwards to Keychain.get_first_private_key() """ - key: Optional[PrivateKey] = None + key: Optional[SecretInfo[Any]] = None if self.use_local_keychain(): - sk_ent = self.keychain.get_first_private_key() + sk_ent = self.keychain.get_first_private_key(key_type=key_type) if sk_ent: key = sk_ent[0] else: - response, success = await self.get_response_for_request("get_first_private_key", {}) + response, success = await self.get_response_for_request("get_first_private_key", {"type": key_type}) if success: private_key = response["data"].get("private_key", None) if private_key is None: @@ -328,30 +347,30 @@ async def get_first_private_key(self) -> Optional[PrivateKey]: return key @overload - async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: ... + async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[SecretInfo[Any]]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: Literal[True] - ) -> Optional[PrivateKey]: ... + ) -> Optional[SecretInfo[Any]]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: Literal[False] - ) -> Optional[G1Element]: ... + ) -> Optional[ObservationRoot]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool - ) -> Optional[Union[PrivateKey, G1Element]]: ... + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool = True - ) -> Optional[Union[PrivateKey, G1Element]]: + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: """ Locates and returns a private key matching the provided fingerprint """ - key: Optional[Union[PrivateKey, G1Element]] = None + key: Optional[Union[SecretInfo[Any], ObservationRoot]] = None if self.use_local_keychain(): keys = self.keychain.get_keys(include_secrets=private) if len(keys) == 0: @@ -360,7 +379,7 @@ async def get_key_for_fingerprint( selected_key = keys[0] if fingerprint is not None: for key_data in keys: - if key_data.public_key.get_fingerprint() == fingerprint: + if key_data.observation_root.get_fingerprint() == fingerprint: selected_key = key_data break else: @@ -368,7 +387,7 @@ async def get_key_for_fingerprint( if private and selected_key.secrets is not None: key = selected_key.private_key elif not private: - key = selected_key.public_key + key = selected_key.observation_root else: return None else: @@ -392,7 +411,7 @@ async def get_key_for_fingerprint( err = "G1Elements don't match" self.log.error(f"{err}") else: - key = G1Element.from_bytes(bytes.fromhex(pk)) + key = KeyTypes.parse_observation_root(bytes.fromhex(pk), KeyTypes(response["data"]["key_type"])) else: self.handle_error(response) diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index 8c3cfafd8145..737d28dba714 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -224,7 +224,7 @@ async def add_key(self, request: dict[str, Any]) -> dict[str, Any]: } try: - key = self.get_keychain_for_request(request).add_key(mnemonic_or_pk, label, private) + key, key_type = self.get_keychain_for_request(request).add_key(mnemonic_or_pk, label, private) except KeyError as e: return { "success": False, @@ -243,7 +243,7 @@ async def add_key(self, request: dict[str, Any]) -> dict[str, Any]: else: fingerprint = key.get_fingerprint() - return {"success": True, "fingerprint": fingerprint} + return {"success": True, "fingerprint": fingerprint, "key_type": key_type} async def check_keys(self, request: dict[str, Any]) -> dict[str, Any]: if self.get_keychain_for_request(request).is_keyring_locked(): @@ -321,7 +321,7 @@ async def get_all_private_keys(self, request: dict[str, Any]) -> dict[str, Any]: private_keys = self.get_keychain_for_request(request).get_all_private_keys() for sk, entropy in private_keys: - all_keys.append({"pk": bytes(sk.get_g1()).hex(), "entropy": entropy.hex()}) + all_keys.append({"pk": bytes(sk.public_key()).hex(), "entropy": entropy.hex()}) return {"success": True, "private_keys": all_keys} @@ -334,7 +334,7 @@ async def get_first_private_key(self, request: dict[str, Any]) -> dict[str, Any] if sk_ent is None: return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS} - pk_str = bytes(sk_ent[0].get_g1()).hex() + pk_str = bytes(sk_ent[0].public_key()).hex() ent_str = sk_ent[1].hex() key = {"pk": pk_str, "entropy": ent_str} @@ -361,5 +361,6 @@ async def get_key_for_fingerprint(self, request: dict[str, Any]) -> dict[str, An return { "success": True, "pk": bytes(key_data.public_key).hex(), + "key_type": key_data.key_type, "entropy": key_data.entropy.hex() if private else None, } diff --git a/chia/daemon/server.py b/chia/daemon/server.py index c59fd47480ec..d04d37a00f6c 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -21,7 +21,7 @@ from types import FrameType from typing import Any, Optional, TextIO -from chia_rs import G1Element +from chia_rs import G1Element, PrivateKey from typing_extensions import Protocol from chia import __version__ @@ -657,6 +657,10 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: dict wallet_addresses_by_fingerprint = {} for key in keys: + if not isinstance(key.observation_root, G1Element) or ( + key.secrets is not None and not isinstance(key.private_key, PrivateKey) + ): + continue # pragma: no cover address_entries = [] # we require access to the private key to generate wallet addresses for non observer @@ -665,10 +669,11 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: dict for i in range(index, index + count): if non_observer_derivation: - sk = master_sk_to_wallet_sk(key.private_key, uint32(i)) + # This ifs above are too complex for mypy but do rule out the possibility of a non-PrivateKey here + sk = master_sk_to_wallet_sk(key.private_key, uint32(i)) # type: ignore[arg-type] pk = sk.get_g1() else: - pk = master_pk_to_wallet_pk_unhardened(key.public_key, uint32(i)) + pk = master_pk_to_wallet_pk_unhardened(key.observation_root, uint32(i)) wallet_address = encode_puzzle_hash(create_puzzlehash_for_pk(pk), prefix) if non_observer_derivation: hd_path = f"m/12381n/8444n/2n/{i}n" @@ -690,7 +695,7 @@ async def get_keys_for_plotting(self, websocket: WebSocketResponse, request: dic keys_for_plot: dict[uint32, Any] = {} for key in keys: - if key.secrets is None: + if key.secrets is None or not isinstance(key.private_key, PrivateKey): continue sk = key.private_key farmer_public_key: G1Element = master_sk_to_farmer_sk(sk).get_g1() diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index ddba6886894e..c1dcbe3b94f1 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -58,11 +58,10 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_sync_utils import fetch_coin_spend, fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle if TYPE_CHECKING: @@ -124,7 +123,7 @@ class DataLayerWallet: log: logging.Logger wallet_info: WalletInfo wallet_id: uint8 - standard_wallet: Wallet + standard_wallet: MainWalletProtocol """ interface used by datalayer for interacting with the chain """ @@ -540,8 +539,9 @@ async def create_update_state_spend( ], ), ) - inner_sol: Program = self.standard_wallet.make_solution( + inner_sol: Program = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(b"$")) if fee > 0 else extra_conditions, ) db_layer_sol = Program.to([inner_sol]) @@ -747,8 +747,9 @@ async def delete_mirror( new=not action_scope.config.tx_config.reuse_puzhash ) excess_fee: int = fee - mirror_coin.amount - inner_sol: Program = self.standard_wallet.make_solution( + inner_sol: Program = await self.standard_wallet.make_solution( primaries=[Payment(new_puzhash, uint64(mirror_coin.amount - fee))] if excess_fee < 0 else [], + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(b"$")) if excess_fee > 0 else extra_conditions, ) mirror_spend = CoinSpend( @@ -1234,6 +1235,12 @@ async def select_coins( async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return coin.amount % 2 == 1 and await self.wallet_state_manager.dl_store.get_launcher(hint) is not None + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() + def verify_offer( maker: tuple[StoreProofs, ...], diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 98cdbae588c2..ce398f50a559 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -232,7 +232,12 @@ async def ensure_keychain_proxy(self) -> KeychainProxy: async def get_all_private_keys(self) -> list[tuple[PrivateKey, bytes]]: keychain_proxy = await self.ensure_keychain_proxy() - return await keychain_proxy.get_all_private_keys() + all_keys = [] + for key, entropy in await keychain_proxy.get_all_private_keys(): + if not isinstance(key, PrivateKey): + continue + all_keys.append((key, entropy)) + return all_keys async def setup_keys(self) -> bool: no_keys_error_str = "No keys exist. Please run 'chia keys generate' or open the UI." diff --git a/chia/legacy/keyring.py b/chia/legacy/keyring.py index 20823e2eed6d..cf1b41a68df4 100644 --- a/chia/legacy/keyring.py +++ b/chia/legacy/keyring.py @@ -24,7 +24,7 @@ from chia.util.errors import KeychainUserNotFound -from chia.util.keychain import MAX_KEYS, KeyData, KeyDataSecrets, get_private_key_user +from chia.util.keychain import MAX_KEYS, KeyData, KeyDataSecrets, KeyTypes, get_private_key_user LegacyKeyring = Union[MacKeyring, WinKeyring, CryptFileKeyring] @@ -60,7 +60,7 @@ def generate_and_add(keyring: LegacyKeyring) -> KeyData: keyring.set_password( DEFAULT_SERVICE, get_private_key_user(DEFAULT_USER, index), - bytes(key.public_key).hex() + key.entropy.hex(), + bytes(key.observation_root).hex() + key.entropy.hex(), ) return key @@ -72,15 +72,17 @@ def get_key_data(keyring: LegacyKeyring, index: int) -> KeyData: raise KeychainUserNotFound(DEFAULT_SERVICE, user) str_bytes = bytes.fromhex(read_str) - public_key = G1Element.from_bytes(str_bytes[: G1Element.SIZE]) - fingerprint = public_key.get_fingerprint() + pk_bytes = str_bytes[: G1Element.SIZE] + observation_root = G1Element.from_bytes(pk_bytes) + fingerprint = observation_root.get_fingerprint() entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] return KeyData( fingerprint=uint32(fingerprint), - public_key=public_key, + public_key=pk_bytes, label=None, secrets=KeyDataSecrets.from_entropy(entropy), + key_type=KeyTypes.G1_ELEMENT, ) diff --git a/chia/plotting/check_plots.py b/chia/plotting/check_plots.py index 8633ad2d4781..4f999a691e2d 100644 --- a/chia/plotting/check_plots.py +++ b/chia/plotting/check_plots.py @@ -8,7 +8,7 @@ from time import sleep, time from typing import Optional -from chia_rs import G1Element +from chia_rs import G1Element, PrivateKey from chiapos import Verifier from chia.plotting.manager import PlotManager @@ -119,7 +119,7 @@ def check_plots( # for keychain access, KeychainProxy/connect_to_keychain should be used instead of Keychain. kc: Keychain = Keychain() plot_manager.set_public_keys( - [master_sk_to_farmer_sk(sk).get_g1() for sk, _ in kc.get_all_private_keys()], + [master_sk_to_farmer_sk(sk).get_g1() for sk, _ in kc.get_all_private_keys() if isinstance(sk, PrivateKey)], [G1Element.from_bytes(bytes.fromhex(pk)) for pk in config["farmer"]["pool_public_keys"]], ) plot_manager.start_refreshing() diff --git a/chia/plotting/create_plots.py b/chia/plotting/create_plots.py index 09208740da41..550c499ef101 100644 --- a/chia/plotting/create_plots.py +++ b/chia/plotting/create_plots.py @@ -3,7 +3,7 @@ import logging from datetime import datetime from pathlib import Path -from typing import Optional +from typing import Any, Optional from chia_rs import AugSchemeMPL, G1Element, PrivateKey from chiapos import DiskPlotter @@ -17,7 +17,8 @@ ) from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash -from chia.util.keychain import Keychain +from chia.util.keychain import Keychain, KeyTypes +from chia.util.secret_info import SecretInfo from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_local_sk, master_sk_to_pool_sk log = logging.getLogger(__name__) @@ -95,25 +96,27 @@ async def resolve(self) -> PlotKeys: return self.resolved_keys async def get_sk(self, keychain_proxy: Optional[KeychainProxy] = None) -> Optional[PrivateKey]: - sk: Optional[PrivateKey] = None + sk: Optional[SecretInfo[Any]] = None if keychain_proxy: try: if self.alt_fingerprint is not None: sk = await keychain_proxy.get_key_for_fingerprint(self.alt_fingerprint) else: - sk = await keychain_proxy.get_first_private_key() + sk = await keychain_proxy.get_first_private_key(KeyTypes.G1_ELEMENT) except Exception as e: log.error(f"Keychain proxy failed with error: {e}") else: - sk_ent: Optional[tuple[PrivateKey, bytes]] = None + sk_ent: Optional[tuple[SecretInfo[Any], bytes]] = None keychain: Keychain = Keychain() if self.alt_fingerprint is not None: sk_ent = keychain.get_private_key_by_fingerprint(self.alt_fingerprint) else: - sk_ent = keychain.get_first_private_key() + sk_ent = keychain.get_first_private_key(KeyTypes.G1_ELEMENT) if sk_ent: sk = sk_ent[0] + if not isinstance(sk, PrivateKey): + raise ValueError("Cannot create plots with non-BLS key") return sk async def get_farmer_public_key(self, keychain_proxy: Optional[KeychainProxy] = None) -> G1Element: diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 7b1c1025f50c..95cedd0a3719 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -44,15 +44,16 @@ from chia.types.coin_spend import CoinSpend, compute_additions from chia.util.ints import uint32, uint64, uint128 from chia.wallet.conditions import AssertCoinAnnouncement, Condition, ConditionValidTimes +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import find_owner_sk from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle if TYPE_CHECKING: @@ -75,7 +76,7 @@ class PoolWallet: wallet_state_manager: WalletStateManager log: logging.Logger wallet_info: WalletInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_id: int next_transaction_fee: uint64 = uint64(0) next_tx_config: TXConfig = DEFAULT_TX_CONFIG @@ -324,7 +325,7 @@ async def rewind(self, block_height: int) -> bool: async def create( cls, wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, launcher_coin_id: bytes32, block_spends: list[CoinSpend], block_height: uint32, @@ -365,7 +366,7 @@ async def create( async def create_from_db( cls, wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> PoolWallet: @@ -385,7 +386,7 @@ async def create_from_db( @staticmethod async def create_new_pool_wallet_transaction( wallet_state_manager: Any, - main_wallet: Wallet, + main_wallet: MainWalletProtocol, initial_target_state: PoolState, action_scope: WalletActionScope, fee: uint64 = uint64(0), @@ -409,7 +410,7 @@ async def create_new_pool_wallet_transaction( if p2_singleton_delay_time is None: p2_singleton_delay_time = uint64(604800) - unspent_records = await wallet_state_manager.coin_store.get_unspent_coins_for_wallet(standard_wallet.wallet_id) + unspent_records = await wallet_state_manager.coin_store.get_unspent_coins_for_wallet(standard_wallet.id()) balance = await standard_wallet.get_confirmed_balance(unspent_records) if balance < PoolWallet.MINIMUM_INITIAL_BALANCE: raise ValueError("Not enough balance in main wallet to create a managed plotting pool.") @@ -438,9 +439,11 @@ async def create_new_pool_wallet_transaction( return p2_singleton_puzzle_hash, launcher_coin_id async def _get_owner_key_cache(self) -> tuple[PrivateKey, uint32]: + private_key = self.wallet_state_manager.get_master_private_key() + assert isinstance(private_key, PrivateKey) if self._owner_sk_and_index is None: self._owner_sk_and_index = find_owner_sk( - [self.wallet_state_manager.get_master_private_key()], + [private_key], (await self.get_current_state()).current.owner_pubkey, ) assert self._owner_sk_and_index is not None @@ -562,7 +565,7 @@ async def generate_travel_transactions(self, fee: uint64, action_scope: WalletAc @staticmethod async def generate_launcher_spend( - standard_wallet: Wallet, + standard_wallet: MainWalletProtocol, amount: uint64, fee: uint64, initial_target_state: PoolState, @@ -924,3 +927,9 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: # pragma: no cover return False # PoolWallet pre-dates hints + + def handle_own_derivation(self) -> bool: # pragma: no cover + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() diff --git a/chia/rpc/util.py b/chia/rpc/util.py index af31786fe09a..c2210bd5aeae 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -113,6 +113,7 @@ async def inner(request: aiohttp.web.Request) -> aiohttp.web.StreamResponse: def tx_endpoint( push: bool = False, merge_spends: bool = True, + sign: Optional[bool] = None, ) -> Callable[[RpcEndpoint], RpcEndpoint]: def _inner(func: RpcEndpoint) -> RpcEndpoint: async def rpc_endpoint( @@ -163,7 +164,7 @@ async def rpc_endpoint( tx_config, push=request.get("push", push), merge_spends=request.get("merge_spends", merge_spends), - sign=request.get("sign", self.service.config.get("auto_sign_txs", True)), + sign=request.get("sign", self.service.config.get("auto_sign_txs", True) if sign is None else sign), ) as action_scope: response: EndpointResult = await func( self, diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index dd4fc8ea076f..29c44c29d22b 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -4,12 +4,15 @@ from dataclasses import dataclass, field from typing import Any, Optional, TypeVar -from chia_rs import G1Element, G2Element, PrivateKey +from chia_rs import G1Element, G2Element from typing_extensions import dataclass_transform from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.byte_types import hexstr_to_bytes from chia.util.ints import uint16, uint32, uint64 +from chia.util.keychain import KeyTypes +from chia.util.observation_root import ObservationRoot +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import Condition, ConditionValidTimes from chia.wallet.notification_store import Notification @@ -29,6 +32,7 @@ from chia.wallet.wallet_spend_bundle import WalletSpendBundle _T_OfferEndpointResponse = TypeVar("_T_OfferEndpointResponse", bound="_OfferEndpointResponse") +_T_KW_Dataclass = TypeVar("_T_KW_Dataclass") @dataclass_transform(frozen_default=True, kw_only_default=True) @@ -93,12 +97,18 @@ class GetPrivateKey(Streamable): @dataclass(frozen=True) class GetPrivateKeyFormat(Streamable): fingerprint: uint32 - sk: PrivateKey - pk: G1Element - farmer_pk: G1Element - pool_pk: G1Element + sk: bytes + pk: bytes + farmer_pk: Optional[G1Element] + pool_pk: Optional[G1Element] seed: Optional[str] + def secret_info(self, key_type: KeyTypes = KeyTypes.G1_ELEMENT) -> SecretInfo[Any]: + return KeyTypes.parse_secret_info(self.sk, key_type) + + def observation_root(self, key_type: KeyTypes = KeyTypes.G1_ELEMENT) -> ObservationRoot: + return KeyTypes.parse_observation_root(self.pk, key_type) + @streamable @dataclass(frozen=True) @@ -116,6 +126,7 @@ class GenerateMnemonicResponse(Streamable): @dataclass(frozen=True) class AddKey(Streamable): mnemonic: list[str] + key_type: Optional[str] = None @streamable @@ -500,8 +511,6 @@ class SplitCoins(TransactionEndpointRequest): target_coin_id: bytes32 = field(default_factory=default_raise) -@streamable -@dataclass(frozen=True) class SplitCoinsResponse(TransactionEndpointResponse): pass @@ -517,8 +526,6 @@ class CombineCoins(TransactionEndpointRequest): coin_num_limit: uint16 = uint16(500) -@streamable -@dataclass(frozen=True) class CombineCoinsResponse(TransactionEndpointResponse): pass @@ -553,6 +560,46 @@ class NFTTransferBulkResponse(TransactionEndpointResponse): spend_bundle: WalletSpendBundle +@streamable +@kw_only_dataclass +class VaultCreate(TransactionEndpointRequest): + secp_pk: bytes = field(default_factory=default_raise) + hp_index: uint32 = uint32(0) + bls_pk: Optional[G1Element] = None + timelock: Optional[uint64] = None + + +@streamable +@dataclass(frozen=True) +class VaultCreateResponse(TransactionEndpointResponse): + pass + + +@streamable +@kw_only_dataclass +class VaultRecovery(TransactionEndpointRequest): + wallet_id: uint32 = field(default_factory=default_raise) + secp_pk: bytes = field(default_factory=default_raise) + hp_index: uint32 = uint32(0) + bls_pk: Optional[G1Element] = None + timelock: Optional[uint64] = None + + +@streamable +@dataclass(frozen=True) +class VaultRecoveryResponse(TransactionEndpointResponse): + recovery_tx_id: bytes32 + finish_tx_id: bytes32 + + @property + def recovery_tx(self) -> TransactionRecord: + return next(tx for tx in self.transactions if tx.name == self.recovery_tx_id) + + @property + def finish_tx(self) -> TransactionRecord: + return next(tx for tx in self.transactions if tx.name == self.finish_tx_id) + + # TODO: The section below needs corresponding request types # TODO: The section below should be added to the API (currently only for client) @streamable diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 9da56239cec0..8bee288b7afb 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -56,6 +56,10 @@ SplitCoinsResponse, SubmitTransactions, SubmitTransactionsResponse, + VaultCreate, + VaultCreateResponse, + VaultRecovery, + VaultRecoveryResponse, ) from chia.server.outbound_message import NodeType from chia.server.ws_connection import WSChiaConnection @@ -71,8 +75,9 @@ from chia.util.errors import KeychainIsLocked from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 -from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic +from chia.util.keychain import KeyTypes, bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, UInt32Range, streamable from chia.util.ws_message import WsRpcMessage, create_payload_dict from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS @@ -143,6 +148,8 @@ from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig, TXConfigLoader 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.vault.vault_drivers import get_vault_hidden_puzzle_with_index +from chia.wallet.vault.vault_wallet import Vault from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet from chia.wallet.vc_wallet.vc_store import VCProofs @@ -331,6 +338,9 @@ def get_routes(self) -> dict[str, Endpoint]: "/submit_transactions": self.submit_transactions, # Not technically Signer Protocol but related "/execute_signing_instructions": self.execute_signing_instructions, + # VAULT + "/vault_create": self.vault_create, + "/vault_recovery": self.vault_recovery, } def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[str, Any]]: @@ -429,7 +439,7 @@ async def get_logged_in_fingerprint(self, request: Empty) -> GetLoggedInFingerpr async def get_public_keys(self, request: Empty) -> GetPublicKeysResponse: try: fingerprints = [ - uint32(sk.get_g1().get_fingerprint()) + uint32(sk.public_key().get_fingerprint()) for (sk, seed) in await self.service.keychain_proxy.get_all_private_keys() ] except KeychainIsLocked: @@ -442,11 +452,11 @@ async def get_public_keys(self, request: Empty) -> GetPublicKeysResponse: else: return GetPublicKeysResponse(keyring_is_locked=False, public_key_fingerprints=fingerprints) - async def _get_private_key(self, fingerprint: int) -> tuple[Optional[PrivateKey], Optional[bytes]]: + async def _get_private_key(self, fingerprint: int) -> tuple[Optional[SecretInfo[Any]], Optional[bytes]]: try: all_keys = await self.service.keychain_proxy.get_all_private_keys() for sk, seed in all_keys: - if sk.get_g1().get_fingerprint() == fingerprint: + if sk.public_key().get_fingerprint() == fingerprint: return sk, seed except Exception as e: log.error(f"Failed to get private key by fingerprint: {e}") @@ -460,10 +470,10 @@ async def get_private_key(self, request: GetPrivateKey) -> GetPrivateKeyResponse return GetPrivateKeyResponse( private_key=GetPrivateKeyFormat( fingerprint=request.fingerprint, - sk=sk, - pk=sk.get_g1(), - farmer_pk=master_sk_to_farmer_sk(sk).get_g1(), - pool_pk=master_sk_to_pool_sk(sk).get_g1(), + sk=bytes(sk), + pk=bytes(sk.public_key()), + farmer_pk=master_sk_to_farmer_sk(sk).get_g1() if isinstance(sk, PrivateKey) else None, + pool_pk=master_sk_to_pool_sk(sk).get_g1() if isinstance(sk, PrivateKey) else None, seed=s, ) ) @@ -478,11 +488,13 @@ async def generate_mnemonic(self, request: Empty) -> GenerateMnemonicResponse: async def add_key(self, request: AddKey) -> AddKeyResponse: # Adding a key from 24 word mnemonic try: - sk = await self.service.keychain_proxy.add_key(" ".join(request.mnemonic)) + sk, _ = await self.service.keychain_proxy.add_key( + " ".join(request.mnemonic), KeyTypes(request.key_type) if request.key_type is not None else None + ) except KeyError as e: raise ValueError(f"The word '{e.args[0]}' is incorrect.") - fingerprint = uint32(sk.get_g1().get_fingerprint()) + fingerprint = uint32(sk.public_key().get_fingerprint()) await self._stop_wallet() # Makes sure the new key is added to config properly @@ -565,9 +577,10 @@ async def check_delete_key(self, request: CheckDeleteKey) -> CheckDeleteKeyRespo sk, _ = await self._get_private_key(request.fingerprint) if sk is not None: - used_for_farmer, used_for_pool = await self._check_key_used_for_rewards( - self.service.root_path, sk, request.max_ph_to_search - ) + if isinstance(sk, PrivateKey): + used_for_farmer, used_for_pool = await self._check_key_used_for_rewards( + self.service.root_path, sk, request.max_ph_to_search + ) if self.service.logged_in_fingerprint != request.fingerprint: await self._stop_wallet() @@ -986,9 +999,9 @@ async def create_new_wallet( if max_pwi + 1 >= (MAX_POOL_WALLETS - 1): raise ValueError(f"Too many pool wallets ({max_pwi}), cannot create any more on this key.") - owner_sk: PrivateKey = master_sk_to_singleton_owner_sk( - self.service.wallet_state_manager.get_master_private_key(), uint32(max_pwi + 1) - ) + master_sk = self.service.wallet_state_manager.get_master_private_key() + assert isinstance(master_sk, PrivateKey), "Pooling only works with BLS keys at this time" + owner_sk: PrivateKey = master_sk_to_singleton_owner_sk(master_sk, uint32(max_pwi + 1)) owner_pk: G1Element = owner_sk.get_g1() initial_target_state = initial_pool_state_from_dict( @@ -4784,3 +4797,65 @@ async def execute_signing_instructions( request.signing_instructions, request.partial_allowed ) ) + + ########################################################################################## + # VAULT + ########################################################################################## + @tx_endpoint(push=False) + @marshal + async def vault_create( + self, + request: VaultCreate, + action_scope: WalletActionScope, + extra_conditions: tuple[Condition, ...] = tuple(), + ) -> VaultCreateResponse: + """ + Create a new vault + """ + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(request.hp_index).get_tree_hash() + genesis_challenge = self.service.wallet_state_manager.constants.GENESIS_CHALLENGE + + await self.service.wallet_state_manager.create_vault_wallet( + request.secp_pk, + hidden_puzzle_hash, + genesis_challenge, + action_scope, + bls_pk=request.bls_pk, + timelock=request.timelock, + fee=request.fee, + ) + return VaultCreateResponse([], []) # tx_endpoint will take care of filling this out + + @tx_endpoint(push=False, merge_spends=False, sign=False) + @marshal + async def vault_recovery( + self, + request: VaultRecovery, + action_scope: WalletActionScope, + extra_conditions: tuple[Condition, ...] = tuple(), + ) -> VaultRecoveryResponse: + """ + Initiate Vault Recovery + """ + if request.fee != 0: + raise ValueError("Recovery endpoint cannot add fees because it assumes your vault is currently inacessible") + if action_scope.config.push or action_scope.config.sign: + raise ValueError( + "Cannot push or sign from this endpoint because the vault is assumed to be inaccessible by this wallet." + " Please push the individual transactions to the /push_transactions endpoint on a wallet " + " with the correct keyset." + ) + wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Vault) + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(request.hp_index).get_tree_hash() + genesis_challenge = self.service.wallet_state_manager.constants.GENESIS_CHALLENGE + recovery_tx_id, finish_tx_id = await wallet.create_recovery_spends( + request.secp_pk, + hidden_puzzle_hash, + genesis_challenge, + action_scope, + bls_pk=request.bls_pk, + timelock=request.timelock, + ) + + # tx_endpoint will take care of filling the empty lists out + return VaultRecoveryResponse([], [], recovery_tx_id, finish_tx_id) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 02fc08284162..afe0d965aa1b 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -85,6 +85,10 @@ SubmitTransactions, SubmitTransactionsResponse, TakeOfferResponse, + VaultCreate, + VaultCreateResponse, + VaultRecovery, + VaultRecoveryResponse, VCMintResponse, VCRevokeResponse, VCSpendResponse, @@ -1842,3 +1846,29 @@ async def combine_coins( "combine_coins", args.json_serialize_for_transport(tx_config, extra_conditions, timelock_info) ) ) + + async def vault_create( + self, + args: VaultCreate, + tx_config: TXConfig, + extra_conditions: tuple[Condition, ...] = tuple(), + timelock_info: ConditionValidTimes = ConditionValidTimes(), + ) -> VaultCreateResponse: + return VaultCreateResponse.from_json_dict( + await self.fetch( + "vault_create", args.json_serialize_for_transport(tx_config, extra_conditions, timelock_info) + ) + ) + + async def vault_recovery( + self, + args: VaultRecovery, + tx_config: TXConfig, + extra_conditions: tuple[Condition, ...] = tuple(), + timelock_info: ConditionValidTimes = ConditionValidTimes(), + ) -> VaultRecoveryResponse: + return VaultRecoveryResponse.from_json_dict( + await self.fetch( + "vault_recovery", args.json_serialize_for_transport(tx_config, extra_conditions, timelock_info) + ) + ) diff --git a/chia/simulator/block_tools.py b/chia/simulator/block_tools.py index 23726b989d16..fbfcfd3f5d47 100644 --- a/chia/simulator/block_tools.py +++ b/chia/simulator/block_tools.py @@ -317,16 +317,22 @@ async def setup_keys(self, fingerprint: Optional[int] = None, reward_ph: Optiona await keychain_proxy.delete_all_keys() self.farmer_master_sk_entropy = std_hash(b"block_tools farmer key") # both entropies are only used here self.pool_master_sk_entropy = std_hash(b"block_tools pool key") - self.farmer_master_sk = await keychain_proxy.add_key(bytes_to_mnemonic(self.farmer_master_sk_entropy)) - self.pool_master_sk = await keychain_proxy.add_key( - bytes_to_mnemonic(self.pool_master_sk_entropy), - ) + farmer_master_sk = (await keychain_proxy.add_key(bytes_to_mnemonic(self.farmer_master_sk_entropy)))[0] + assert isinstance(farmer_master_sk, PrivateKey) + self.farmer_master_sk = farmer_master_sk + pool_master_sk = ( + await keychain_proxy.add_key( + bytes_to_mnemonic(self.pool_master_sk_entropy), + ) + )[0] + assert isinstance(pool_master_sk, PrivateKey) + self.pool_master_sk = pool_master_sk else: sk = await keychain_proxy.get_key_for_fingerprint(fingerprint) - assert sk is not None + assert sk is not None and isinstance(sk, PrivateKey) self.farmer_master_sk = sk sk = await keychain_proxy.get_key_for_fingerprint(fingerprint) - assert sk is not None + assert sk is not None and isinstance(sk, PrivateKey) self.pool_master_sk = sk self.farmer_pk = master_sk_to_farmer_sk(self.farmer_master_sk).get_g1() @@ -343,7 +349,9 @@ async def setup_keys(self, fingerprint: Optional[int] = None, reward_ph: Optiona self.farmer_ph = reward_ph self.pool_ph = reward_ph if self.automated_testing: - self.all_sks: list[PrivateKey] = [sk for sk, _ in await keychain_proxy.get_all_private_keys()] + self.all_sks: list[PrivateKey] = [ + sk for sk, _ in await keychain_proxy.get_all_private_keys() if isinstance(sk, PrivateKey) + ] else: self.all_sks = [self.farmer_master_sk] # we only want to include plots under the same fingerprint self.pool_pubkeys: list[G1Element] = [master_sk_to_pool_sk(sk).get_g1() for sk in self.all_sks] diff --git a/chia/simulator/full_node_simulator.py b/chia/simulator/full_node_simulator.py index 72e9c9b0da32..d05d2bc283d1 100644 --- a/chia/simulator/full_node_simulator.py +++ b/chia/simulator/full_node_simulator.py @@ -33,8 +33,8 @@ from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_state_manager import WalletStateManager @@ -47,7 +47,7 @@ class _Default: timeout_per_block = 5 -async def wait_for_coins_in_wallet(coins: set[Coin], wallet: Wallet, timeout: Optional[float] = 5): +async def wait_for_coins_in_wallet(coins: set[Coin], wallet: MainWalletProtocol, timeout: Optional[float] = 5): """Wait until all of the specified coins are simultaneously reported as spendable by the wallet. @@ -352,7 +352,7 @@ async def farm_blocks_to_puzzlehash( async def farm_blocks_to_wallet( self, count: int, - wallet: Wallet, + wallet: MainWalletProtocol, timeout: Union[None, _Default, float] = default, _wait_for_synced: bool = True, ) -> int: @@ -424,7 +424,7 @@ async def farm_blocks_to_wallet( async def farm_rewards_to_wallet( self, amount: int, - wallet: Wallet, + wallet: MainWalletProtocol, timeout: Union[None, _Default, float] = default, ) -> int: """Farm at least the requested amount of mojos to the passed wallet. Extra @@ -620,7 +620,7 @@ async def process_coin_spends( if len(coin_set) == 0: return - async def process_all_wallet_transactions(self, wallet: Wallet, timeout: Optional[float] = 5) -> None: + async def process_all_wallet_transactions(self, wallet: MainWalletProtocol, timeout: Optional[float] = 5) -> None: # TODO: Maybe something could be done around waiting for the tx to enter the # mempool. Maybe not, might be too many races or such. wallet_state_manager: Optional[WalletStateManager] = wallet.wallet_state_manager @@ -663,7 +663,7 @@ async def check_transactions_confirmed( async def create_coins_with_amounts( self, amounts: list[uint64], - wallet: Wallet, + wallet: MainWalletProtocol, per_transaction_record_group: int = 50, timeout: Union[None, float] = 15, ) -> set[Coin]: diff --git a/chia/simulator/simulator_test_tools.py b/chia/simulator/simulator_test_tools.py index 552175ee2403..c3d87ed4fec9 100644 --- a/chia/simulator/simulator_test_tools.py +++ b/chia/simulator/simulator_test_tools.py @@ -43,10 +43,10 @@ def mnemonic_fingerprint(keychain: Keychain) -> tuple[str, int]: ) # add key to keychain try: - sk = keychain.add_key(mnemonic) + sk, _ = keychain.add_key(mnemonic) except KeychainFingerprintExists: pass - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() return mnemonic, fingerprint @@ -55,6 +55,7 @@ def get_puzzle_hash_from_key(keychain: Keychain, fingerprint: int, key_id: int = if priv_key_and_entropy is None: raise Exception("Fingerprint not found") private_key = priv_key_and_entropy[0] + assert isinstance(private_key, PrivateKey) sk_for_wallet_id: PrivateKey = master_sk_to_wallet_sk(private_key, uint32(key_id)) puzzle_hash: bytes32 = create_puzzlehash_for_pk(sk_for_wallet_id.get_g1()) return puzzle_hash diff --git a/chia/ssl/create_ssl.py b/chia/ssl/create_ssl.py index 29bde0956901..bbdb72de3742 100644 --- a/chia/ssl/create_ssl.py +++ b/chia/ssl/create_ssl.py @@ -68,6 +68,7 @@ def generate_ca_signed_cert(ca_crt: bytes, ca_key: bytes, cert_out: Path, key_ou one_day = datetime.timedelta(1, 0, 0) root_cert = x509.load_pem_x509_certificate(ca_crt, default_backend()) root_key = load_pem_private_key(ca_key, None, default_backend()) + assert isinstance(root_key, rsa.RSAPrivateKey) cert_key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) new_subject = x509.Name( diff --git a/chia/util/key_types.py b/chia/util/key_types.py new file mode 100644 index 000000000000..ea4f6dabafae --- /dev/null +++ b/chia/util/key_types.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + +from chia.util.hash import std_hash + + +# A wrapper for VerifyingKey that conforms to the ObservationRoot protocol +@dataclass(frozen=True) +class Secp256r1PublicKey: + _public_key: ec.EllipticCurvePublicKey + + def get_fingerprint(self) -> int: + hash_bytes = std_hash(bytes(self)) + return int.from_bytes(hash_bytes[0:4], "big") + + def __bytes__(self) -> bytes: + return self._public_key.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + @classmethod + def from_bytes(cls, blob: bytes) -> Secp256r1PublicKey: + pk = serialization.load_der_public_key(blob) + if isinstance(pk, ec.EllipticCurvePublicKey): + return Secp256r1PublicKey(pk) + else: # pragma: no cover + # Not sure how to test this, it's really just for mypy sake + raise ValueError("Could not load EllipticCurvePublicKey provided blob") + + def derive_unhardened(self, index: int) -> Secp256r1PublicKey: + raise NotImplementedError("SECP keys do not support derivation") + + +@dataclass(frozen=True) +class Secp256r1Signature: + _buf: bytes + + def __bytes__(self) -> bytes: + return self._buf + + @classmethod + def from_bytes(cls, blob: bytes) -> Secp256r1Signature: + return cls(blob) + + +# A wrapper for SigningKey that conforms to the SecretInfo protocol +@dataclass(frozen=True) +class Secp256r1PrivateKey: + _private_key: ec.EllipticCurvePrivateKey + + def __eq__(self, other: object) -> bool: + return isinstance(other, Secp256r1PrivateKey) and self.public_key() == other.public_key() + + def __bytes__(self) -> bytes: + return self._private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + @classmethod + def from_bytes(cls, blob: bytes) -> Secp256r1PrivateKey: + sk = serialization.load_der_private_key(blob, password=None) + if isinstance(sk, ec.EllipticCurvePrivateKey): + return Secp256r1PrivateKey(sk) + else: # pragma: no cover + # Not sure how to test this, it's really just for mypy sake + raise ValueError("Could not load EllipticCurvePrivateKey provided blob") + + def public_key(self) -> Secp256r1PublicKey: + return Secp256r1PublicKey(self._private_key.public_key()) + + @classmethod + def from_seed(cls, seed: bytes) -> Secp256r1PrivateKey: + return Secp256r1PrivateKey( + ec.derive_private_key(int.from_bytes(std_hash(seed), "big"), ec.SECP256R1(), default_backend()) + ) + + def sign(self, msg: bytes, final_pk: Optional[Secp256r1PublicKey] = None) -> Secp256r1Signature: + if final_pk is not None: + raise NotImplementedError("SECP256r1 does not support signature aggregation") + der_sig = self._private_key.sign(msg, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + return Secp256r1Signature(sig) + + def derive_hardened(self, index: int) -> Secp256r1PrivateKey: + raise NotImplementedError("SECP keys do not support derivation") + + def derive_unhardened(self, index: int) -> Secp256r1PrivateKey: + raise NotImplementedError("SECP keys do not support derivation") diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 0b835731dd10..12f0cf3809d5 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -1,18 +1,18 @@ -# Package: utils - from __future__ import annotations import sys import unicodedata from collections.abc import Iterator from dataclasses import dataclass +from enum import Enum +from functools import cached_property from hashlib import pbkdf2_hmac from pathlib import Path -from typing import Any, Literal, Optional, Union, overload +from typing import Any, Literal, Optional, TypeVar, Union, overload import importlib_resources -from bitstring import BitArray # pyright: reportMissingImports=false -from chia_rs import AugSchemeMPL, G1Element, PrivateKey # pyright: reportMissingImports=false +from bitstring import BitArray +from chia_rs import AugSchemeMPL, G1Element, PrivateKey from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint32 from typing_extensions import final @@ -30,8 +30,12 @@ ) from chia.util.file_keyring import Key from chia.util.hash import std_hash +from chia.util.key_types import Secp256r1PrivateKey, Secp256r1PublicKey from chia.util.keyring_wrapper import KeyringWrapper +from chia.util.observation_root import ObservationRoot +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, streamable +from chia.wallet.vault.vault_root import VaultRoot CURRENT_KEY_VERSION = "1.8" DEFAULT_USER = f"user-chia-{CURRENT_KEY_VERSION}" # e.g. user-chia-1.8 @@ -40,6 +44,9 @@ MIN_PASSPHRASE_LEN = 8 +_T_ObservationRoot = TypeVar("_T_ObservationRoot", bound=ObservationRoot) + + def supports_os_passphrase_storage() -> bool: return sys.platform in ["darwin", "win32", "cygwin"] @@ -184,13 +191,58 @@ def get_private_key_user(user: str, index: int) -> str: return f"wallet-{user}-{index}" +class KeyTypes(str, Enum): + G1_ELEMENT = "G1 Element" + VAULT_LAUNCHER = "Vault Launcher" + SECP_256_R1 = "SECP256r1" + + @classmethod + def parse_observation_root(cls: type[KeyTypes], pk_bytes: bytes, key_type: KeyTypes) -> ObservationRoot: + if key_type == cls.G1_ELEMENT: + return G1Element.from_bytes(pk_bytes) + if key_type == cls.VAULT_LAUNCHER: + return VaultRoot(pk_bytes) + elif key_type == cls.SECP_256_R1: + return Secp256r1PublicKey.from_bytes(pk_bytes) + else: # pragma: no cover + # mypy should prevent this from ever running + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") + + @classmethod + def parse_secret_info(cls: type[KeyTypes], sk_bytes: bytes, key_type: KeyTypes) -> SecretInfo[Any]: + if key_type == cls.G1_ELEMENT: + return PrivateKey.from_bytes(sk_bytes) + elif key_type == cls.SECP_256_R1: + return Secp256r1PrivateKey.from_bytes(sk_bytes) + else: # pragma: no cover + # mypy should prevent this from ever running + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_secret_info") + + @classmethod + def parse_secret_info_from_seed(cls: type[KeyTypes], seed: bytes, key_type: KeyTypes) -> SecretInfo[Any]: + if key_type == cls.G1_ELEMENT: + return PrivateKey.from_seed(seed) + elif key_type == cls.SECP_256_R1: + return Secp256r1PrivateKey.from_seed(seed) + else: # pragma: no cover + # mypy should prevent this from ever running + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_secret_info_from_seed") + + @final @streamable @dataclass(frozen=True) class KeyDataSecrets(Streamable): mnemonic: list[str] entropy: bytes - private_key: PrivateKey + secret_info_bytes: bytes + key_type: str = KeyTypes.G1_ELEMENT.value + + @property + def private_key(self) -> SecretInfo[Any]: + return PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[KeyTypes(self.key_type)]].from_bytes( + self.secret_info_bytes + ) def __post_init__(self) -> None: # This is redundant if `from_*` methods are used but its to make sure there can't be an `KeyDataSecrets` @@ -203,15 +255,23 @@ def __post_init__(self) -> None: raise KeychainKeyDataMismatch("mnemonic") from e if bytes_from_mnemonic(mnemonic_str) != self.entropy: raise KeychainKeyDataMismatch("entropy") - if AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic_str)) != self.private_key: + if ( + PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[KeyTypes(self.key_type)]].from_seed( + mnemonic_to_seed(mnemonic_str) + ) + != self.private_key + ): raise KeychainKeyDataMismatch("private_key") @classmethod - def from_mnemonic(cls, mnemonic: str) -> KeyDataSecrets: + def from_mnemonic(cls, mnemonic: str, key_type: KeyTypes = KeyTypes.G1_ELEMENT) -> KeyDataSecrets: return cls( mnemonic=mnemonic.split(), entropy=bytes_from_mnemonic(mnemonic), - private_key=AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic)), + secret_info_bytes=bytes( + PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[key_type]].from_seed(mnemonic_to_seed(mnemonic)) + ), + key_type=key_type.value, ) @classmethod @@ -226,21 +286,38 @@ def mnemonic_str(self) -> str: return " ".join(self.mnemonic) +TYPES_TO_KEY_TYPES: dict[type[ObservationRoot], KeyTypes] = { + G1Element: KeyTypes.G1_ELEMENT, + VaultRoot: KeyTypes.VAULT_LAUNCHER, + Secp256r1PublicKey: KeyTypes.SECP_256_R1, +} +KEY_TYPES_TO_TYPES: dict[KeyTypes, type[ObservationRoot]] = {v: k for k, v in TYPES_TO_KEY_TYPES.items()} +PUBLIC_TYPES_TO_PRIVATE_TYPES: dict[type[ObservationRoot], type[SecretInfo[Any]]] = { + G1Element: PrivateKey, + Secp256r1PublicKey: Secp256r1PrivateKey, +} + + @final @streamable @dataclass(frozen=True) class KeyData(Streamable): fingerprint: uint32 - public_key: G1Element + public_key: bytes label: Optional[str] secrets: Optional[KeyDataSecrets] + key_type: str + + @cached_property + def observation_root(self) -> ObservationRoot: + return KeyTypes.parse_observation_root(self.public_key, KeyTypes(self.key_type)) def __post_init__(self) -> None: # This is redundant if `from_*` methods are used but its to make sure there can't be an `KeyData` instance with # an attribute mismatch for calculated cached values. Should be ok since we don't handle a lot of keys here. - if self.secrets is not None and self.public_key != self.private_key.get_g1(): + if self.secrets is not None and self.observation_root != self.private_key.public_key(): raise KeychainKeyDataMismatch("public_key") - if uint32(self.public_key.get_fingerprint()) != self.fingerprint: + if uint32(self.observation_root.get_fingerprint()) != self.fingerprint: raise KeychainKeyDataMismatch("fingerprint") @classmethod @@ -248,9 +325,10 @@ def from_mnemonic(cls, mnemonic: str, label: Optional[str] = None) -> KeyData: private_key = AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic)) return cls( fingerprint=uint32(private_key.get_g1().get_fingerprint()), - public_key=private_key.get_g1(), + public_key=bytes(private_key.get_g1()), label=label, secrets=KeyDataSecrets.from_mnemonic(mnemonic), + key_type=KeyTypes.G1_ELEMENT.value, ) @classmethod @@ -279,7 +357,7 @@ def entropy(self) -> bytes: return self.secrets.entropy @property - def private_key(self) -> PrivateKey: + def private_key(self) -> SecretInfo[Any]: if self.secrets is None: raise KeychainSecretsMissing() return self.secrets.private_key @@ -287,9 +365,9 @@ def private_key(self) -> PrivateKey: class Keychain: """ - The keychain stores two types of keys: private keys, which are PrivateKeys from blspy, + The keychain stores two types of keys: private keys, which are SecretInfos, and private key seeds, which are bytes objects that are used as a seed to construct - PrivateKeys. Private key seeds are converted to mnemonics when shown to users. + SecretInfos. Private key seeds are converted to mnemonics when shown to users. Both types of keys are stored as hex strings in the python keyring, and the implementation of the keyring depends on OS. Both types of keys can be added, and get_private_keys returns a @@ -307,7 +385,7 @@ def __init__(self, user: Optional[str] = None, service: Optional[str] = None): self.keyring_wrapper = keyring_wrapper - def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: + def _get_key_data(self, index: int, include_secrets: bool = True) -> Optional[KeyData]: """ Returns the parsed keychain contents for a specific 'user' (key index). The content is represented by the class `KeyData`. @@ -318,19 +396,34 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: raise KeychainUserNotFound(self.service, user) str_bytes = key.secret - public_key = G1Element.from_bytes(str_bytes[: G1Element.SIZE]) - fingerprint = public_key.get_fingerprint() - if len(str_bytes) > G1Element.SIZE: - entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] + if key.metadata is None or key.metadata.get("type", KeyTypes.G1_ELEMENT.value) == KeyTypes.G1_ELEMENT.value: + pk_bytes: bytes = str_bytes[: G1Element.SIZE] + observation_root: ObservationRoot = G1Element.from_bytes(pk_bytes) + fingerprint = observation_root.get_fingerprint() + if len(str_bytes) > G1Element.SIZE: + entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] + else: + entropy = None + + return KeyData( + fingerprint=uint32(fingerprint), + public_key=pk_bytes, + label=self.keyring_wrapper.keyring.get_label(fingerprint), + secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, + key_type=KeyTypes.G1_ELEMENT.value, + ) + elif key.metadata.get("type", KeyTypes.G1_ELEMENT.value) == KeyTypes.VAULT_LAUNCHER.value: + observation_root = VaultRoot.from_bytes(str_bytes) + fingerprint = observation_root.get_fingerprint() + return KeyData( + fingerprint=uint32(fingerprint), + public_key=str_bytes, + label=self.keyring_wrapper.keyring.get_label(fingerprint), + secrets=None, + key_type=KeyTypes.VAULT_LAUNCHER.value, + ) else: - entropy = None - - return KeyData( - fingerprint=uint32(fingerprint), - public_key=public_key, - label=self.keyring_wrapper.keyring.get_label(fingerprint), - secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, - ) + return None def _get_free_private_key_index(self) -> int: """ @@ -344,37 +437,74 @@ def _get_free_private_key_index(self) -> int: except KeychainUserNotFound: return index + # pylint requires these NotImplementedErrors for some reason + @overload + def add_key(self, mnemonic_or_pk: str) -> tuple[SecretInfo[Any], KeyTypes]: + raise NotImplementedError() # pragma: no cover + @overload - def add_key(self, mnemonic_or_pk: str) -> PrivateKey: ... + def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> tuple[SecretInfo[Any], KeyTypes]: + raise NotImplementedError() # pragma: no cover + + @overload + def add_key(self, mnemonic_or_pk: str, *, key_type: KeyTypes) -> tuple[SecretInfo[Any], KeyTypes]: + raise NotImplementedError() # pragma: no cover + + @overload + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True] + ) -> tuple[SecretInfo[Any], KeyTypes]: + raise NotImplementedError() # pragma: no cover + + @overload + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False] + ) -> tuple[ObservationRoot, KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: ... + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: bool + ) -> tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: ... + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True], key_type: KeyTypes + ) -> tuple[SecretInfo[Any], KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: ... + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False], key_type: KeyTypes + ) -> tuple[ObservationRoot, KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: bool) -> Union[PrivateKey, G1Element]: ... + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: bool, key_type: KeyTypes + ) -> tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: + raise NotImplementedError() # pragma: no cover def add_key( - self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True - ) -> Union[PrivateKey, G1Element]: + self, + mnemonic_or_pk: str, + label: Optional[str] = None, + private: bool = True, + key_type: KeyTypes = KeyTypes.G1_ELEMENT, + ) -> tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: """ Adds a key to the keychain. The keychain itself will store the public key, and the entropy bytes (if given), but not the passphrase. """ - key: Union[PrivateKey, G1Element] + key: Union[SecretInfo[Any], ObservationRoot] if private: seed = mnemonic_to_seed(mnemonic_or_pk) entropy = bytes_from_mnemonic(mnemonic_or_pk) index = self._get_free_private_key_index() - key = AugSchemeMPL.key_gen(seed) - assert isinstance(key, PrivateKey) - pk = key.get_g1() - key_data = Key(bytes(pk) + entropy) + key = KeyTypes.parse_secret_info_from_seed(seed, key_type) + pk = key.public_key() + key_data = Key(bytes(pk) + entropy, metadata={"type": key_type.value}) fingerprint = pk.get_fingerprint() else: index = self._get_free_private_key_index() @@ -384,9 +514,8 @@ def add_key( pk_bytes = bytes(convertbits(data, 5, 8, False)) else: pk_bytes = hexstr_to_bytes(mnemonic_or_pk) - key = G1Element.from_bytes(pk_bytes) - assert isinstance(key, G1Element) - key_data = Key(pk_bytes) + key = KeyTypes.parse_observation_root(pk_bytes, key_type) + key_data = Key(pk_bytes, metadata={"type": key_type.value}) fingerprint = key.get_fingerprint() if fingerprint in [pk.get_fingerprint() for pk in self.get_all_public_keys()]: @@ -409,7 +538,7 @@ def add_key( self.keyring_wrapper.keyring.delete_label(fingerprint) raise - return key + return key, key_type def set_label(self, fingerprint: int, label: str) -> None: """ @@ -437,15 +566,17 @@ def _iterate_through_key_datas( pass return None - def get_first_private_key(self) -> Optional[tuple[PrivateKey, bytes]]: + def get_first_private_key(self, key_type: Optional[KeyTypes] = None) -> Optional[tuple[SecretInfo[Any], bytes]]: """ Returns the first key in the keychain that has one of the passed in passphrases. """ for key_data in self._iterate_through_key_datas(skip_public_only=True): + if key_type is not None and key_data.key_type != key_type.value: + continue return key_data.private_key, key_data.entropy return None - def get_private_key_by_fingerprint(self, fingerprint: int) -> Optional[tuple[PrivateKey, bytes]]: + def get_private_key_by_fingerprint(self, fingerprint: int) -> Optional[tuple[SecretInfo[Any], bytes]]: """ Return first private key which have the given public key fingerprint. """ @@ -454,12 +585,12 @@ def get_private_key_by_fingerprint(self, fingerprint: int) -> Optional[tuple[Pri return key_data.private_key, key_data.entropy return None - def get_all_private_keys(self) -> list[tuple[PrivateKey, bytes]]: + def get_all_private_keys(self) -> list[tuple[SecretInfo[Any], bytes]]: """ Returns all private keys which can be retrieved, with the given passphrases. A tuple of key, and entropy bytes (i.e. mnemonic) is returned for each key. """ - all_keys: list[tuple[PrivateKey, bytes]] = [] + all_keys: list[tuple[SecretInfo[Any], bytes]] = [] for key_data in self._iterate_through_key_datas(skip_public_only=True): all_keys.append((key_data.private_key, key_data.entropy)) return all_keys @@ -469,7 +600,7 @@ def get_key(self, fingerprint: int, include_secrets: bool = False) -> KeyData: Return the KeyData of the first key which has the given public key fingerprint. """ for key_data in self._iterate_through_key_datas(include_secrets=include_secrets, skip_public_only=False): - if key_data.public_key.get_fingerprint() == fingerprint: + if key_data.observation_root.get_fingerprint() == fingerprint: return key_data raise KeychainFingerprintNotFound(fingerprint) @@ -483,13 +614,22 @@ def get_keys(self, include_secrets: bool = False) -> list[KeyData]: return all_keys - def get_all_public_keys(self) -> list[G1Element]: + def get_all_public_keys(self) -> list[ObservationRoot]: """ Returns all public keys. """ - all_keys: list[G1Element] = [] + all_keys: list[ObservationRoot] = [] for key_data in self._iterate_through_key_datas(skip_public_only=False): - all_keys.append(key_data.public_key) + all_keys.append(key_data.observation_root) + + return all_keys + + def get_all_public_keys_of_type(self, key_type: type[_T_ObservationRoot]) -> list[_T_ObservationRoot]: + all_keys: list[_T_ObservationRoot] = [] + for key_data in self._iterate_through_key_datas(skip_public_only=False): + if key_data.key_type == TYPES_TO_KEY_TYPES[key_type]: + assert isinstance(key_data.observation_root, key_type) + all_keys.append(key_data.observation_root) return all_keys @@ -498,7 +638,7 @@ def get_first_public_key(self) -> Optional[G1Element]: Returns the first public key. """ key_data = self.get_first_private_key() - return None if key_data is None else key_data[0].get_g1() + return None if key_data is None else key_data[0].public_key() def delete_key_by_fingerprint(self, fingerprint: int) -> int: """ @@ -524,19 +664,6 @@ def delete_key_by_fingerprint(self, fingerprint: int) -> int: pass return removed - def delete_keys(self, keys_to_delete: list[tuple[PrivateKey, bytes]]) -> None: - """ - Deletes all keys in the list. - """ - remaining_fingerprints = {x[0].get_g1().get_fingerprint() for x in keys_to_delete} - remaining_removals = len(remaining_fingerprints) - while len(remaining_fingerprints): - key_to_delete = remaining_fingerprints.pop() - if self.delete_key_by_fingerprint(key_to_delete) > 0: - remaining_removals -= 1 - if remaining_removals > 0: - raise ValueError(f"{remaining_removals} keys could not be found for deletion") - def delete_all_keys(self) -> None: """ Deletes all keys from the keychain. diff --git a/chia/util/observation_root.py b/chia/util/observation_root.py new file mode 100644 index 000000000000..57bc96ebd1f7 --- /dev/null +++ b/chia/util/observation_root.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class ObservationRoot(Protocol): + def get_fingerprint(self) -> int: ... + + def __bytes__(self) -> bytes: ... + + @classmethod + def from_bytes(cls, blob: bytes) -> ObservationRoot: ... + + +class Signature(Protocol): + def __bytes__(self) -> bytes: ... diff --git a/chia/util/secret_info.py b/chia/util/secret_info.py new file mode 100644 index 000000000000..01f15a2d05a4 --- /dev/null +++ b/chia/util/secret_info.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import Optional, Protocol, TypeVar + +from chia.util.observation_root import ObservationRoot, Signature + +_T_ObservationRoot = TypeVar("_T_ObservationRoot", bound=ObservationRoot) + + +class SecretInfo(Protocol[_T_ObservationRoot]): + def __bytes__(self) -> bytes: ... + @classmethod + def from_bytes(cls: type[_T_SecretInfo], blob: bytes) -> _T_SecretInfo: ... + def public_key(self) -> _T_ObservationRoot: ... + def derive_hardened(self: _T_SecretInfo, index: int) -> _T_SecretInfo: ... + def derive_unhardened(self: _T_SecretInfo, index: int) -> _T_SecretInfo: ... + @classmethod + def from_seed(cls: type[_T_SecretInfo], seed: bytes) -> _T_SecretInfo: ... + def sign(self, msg: bytes, final_pk: Optional[_T_ObservationRoot] = None) -> Signature: ... + + +_T_SecretInfo = TypeVar("_T_SecretInfo", bound=SecretInfo[ObservationRoot]) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index d281a871b219..281da27ab564 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -52,11 +52,10 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle if TYPE_CHECKING: @@ -95,7 +94,7 @@ class CATWallet: log: logging.Logger wallet_info: WalletInfo cat_info: CATInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol lineage_store: CATLineageStore @staticmethod @@ -105,7 +104,7 @@ def default_wallet_name_for_unknown_cat(limitations_program_hash_hex: str) -> st @staticmethod async def create_new_cat_wallet( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, cat_tail_info: dict[str, Any], amount: uint64, action_scope: WalletActionScope, @@ -116,7 +115,7 @@ async def create_new_cat_wallet( self = CATWallet() self.standard_wallet = wallet self.log = logging.getLogger(__name__) - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") @@ -200,7 +199,7 @@ async def create_new_cat_wallet( @staticmethod async def get_or_create_wallet_for_cat( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, limitations_program_hash_hex: str, name: Optional[str] = None, ) -> CATWallet: @@ -253,7 +252,7 @@ async def get_or_create_wallet_for_cat( async def create_from_puzzle_info( cls, wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, puzzle_driver: PuzzleInfo, name: Optional[str] = None, # We're hinting this as Any for mypy by should explore adding this to the wallet protocol and hinting properly @@ -279,7 +278,7 @@ async def create_from_puzzle_info( @staticmethod async def create( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, ) -> CATWallet: self = CATWallet() @@ -708,8 +707,9 @@ async def generate_unsigned_spendbundle( action_scope, extra_conditions=(announcement.corresponding_assertion(),), ) - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) elif regular_chia_to_claim > fee: # pragma: no cover @@ -719,21 +719,23 @@ async def generate_unsigned_spendbundle( action_scope, ) assert xch_announcement is not None - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, xch_announcement, announcement), ) else: # TODO: what about when they are equal? raise Exception("Equality not handled") else: - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) else: - innersol = self.standard_wallet.make_solution( - primaries=[], conditions=(announcement.corresponding_assertion(),) + innersol = await self.standard_wallet.make_solution( + primaries=[], action_scope=action_scope, conditions=(announcement.corresponding_assertion(),) ) inner_puzzle = await self.inner_puzzle_for_cat_puzhash(coin.puzzle_hash) lineage_proof = await self.get_lineage_proof_for_coin(coin) @@ -867,3 +869,9 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: construct_cat_puzzle(CAT_MOD, self.cat_info.limitations_program_hash, hint).get_tree_hash_precalc(hint) == coin.puzzle_hash ) + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() diff --git a/chia/wallet/cat_wallet/dao_cat_wallet.py b/chia/wallet/cat_wallet/dao_cat_wallet.py index c931cc2c38fc..76b6e5a546c0 100644 --- a/chia/wallet/cat_wallet/dao_cat_wallet.py +++ b/chia/wallet/cat_wallet/dao_cat_wallet.py @@ -29,6 +29,7 @@ get_innerpuz_from_lockup_puzzle, get_lockup_puzzle, ) +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord @@ -37,10 +38,10 @@ from chia.wallet.util.tx_config import TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle if TYPE_CHECKING: @@ -61,7 +62,7 @@ class DAOCATWallet: log: logging.Logger wallet_info: WalletInfo dao_cat_info: DAOCATInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol cost_of_single_tx: Optional[int] lineage_store: CATLineageStore @@ -72,7 +73,7 @@ def type(cls) -> WalletType: @staticmethod async def create( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, ) -> DAOCATWallet: self = DAOCATWallet() @@ -93,7 +94,7 @@ async def create( @staticmethod async def get_or_create_wallet_for_cat( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, limitations_program_hash_hex: str, name: Optional[str] = None, ) -> DAOCATWallet: @@ -256,6 +257,7 @@ async def create_vote_spend( amount: uint64, proposal_id: bytes32, is_yes_vote: bool, + action_scope: WalletActionScope, proposal_puzzle: Optional[Program] = None, ) -> WalletSpendBundle: coins: list[LockedCoinInfo] = await self.advanced_select_coins(amount, proposal_id) @@ -297,8 +299,9 @@ async def create_vote_spend( ) ] message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash() - inner_solution = self.standard_wallet.make_solution( + inner_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(CreatePuzzleAnnouncement(message),), ) else: @@ -320,8 +323,9 @@ async def create_vote_spend( ) ) message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash() - inner_solution = self.standard_wallet.make_solution( + inner_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(CreatePuzzleAnnouncement(message),), ) if is_yes_vote: @@ -419,8 +423,9 @@ async def exit_vote_state( ), ] total_amt += coin.amount - inner_solution = self.standard_wallet.make_solution( + inner_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, ) # Create the solution using only the values needed for exiting the lockup mode (my_id = 0) solution = Program.to( @@ -667,3 +672,9 @@ async def save_info(self, dao_cat_info: DAOCATInfo) -> None: def get_name(self) -> str: return self.wallet_info.name + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() diff --git a/chia/wallet/dao_wallet/dao_utils.py b/chia/wallet/dao_wallet/dao_utils.py index 6618d48c0a50..ddd7e127f3c4 100644 --- a/chia/wallet/dao_wallet/dao_utils.py +++ b/chia/wallet/dao_wallet/dao_utils.py @@ -41,7 +41,7 @@ ) DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash() DAO_CAT_LAUNCHER: Program = load_clvm("dao_cat_launcher.clsp") -P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp") +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle_w_aggregator.clsp") P2_SINGLETON_MOD_HASH: bytes32 = P2_SINGLETON_MOD.get_tree_hash() DAO_UPDATE_PROPOSAL_MOD: Program = load_clvm("dao_update_proposal.clsp") DAO_UPDATE_PROPOSAL_MOD_HASH: bytes32 = DAO_UPDATE_PROPOSAL_MOD.get_tree_hash() diff --git a/chia/wallet/dao_wallet/dao_wallet.py b/chia/wallet/dao_wallet/dao_wallet.py index a73d8085cc2d..a07b5fb50683 100644 --- a/chia/wallet/dao_wallet/dao_wallet.py +++ b/chia/wallet/dao_wallet/dao_wallet.py @@ -62,6 +62,7 @@ uncurry_proposal, uncurry_treasury, ) +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.singleton import ( get_inner_puzzle_from_singleton, @@ -74,10 +75,10 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle @@ -114,13 +115,13 @@ class DAOWallet: wallet_info: WalletInfo dao_info: DAOInfo dao_rules: DAORules - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_id: uint32 @staticmethod async def create_new_dao_and_wallet( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, amount_of_cats: uint64, dao_rules: DAORules, action_scope: WalletActionScope, @@ -150,7 +151,7 @@ async def create_new_dao_and_wallet( self.standard_wallet = wallet self.log = logging.getLogger(name if name else __name__) - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount_of_cats > bal: raise ValueError(f"Your balance of {bal} mojos is not enough to create {amount_of_cats} CATs") @@ -174,7 +175,7 @@ async def create_new_dao_and_wallet( name, WalletType.DAO.value, info_as_string ) self.wallet_id = self.wallet_info.id - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() try: await self.generate_new_dao( @@ -207,7 +208,7 @@ async def create_new_dao_and_wallet( @staticmethod async def create_new_dao_wallet_for_existing_dao( wallet_state_manager: Any, - main_wallet: Wallet, + main_wallet: MainWalletProtocol, treasury_id: bytes32, filter_amount: uint64 = uint64(1), name: Optional[str] = None, @@ -273,7 +274,7 @@ async def create_new_dao_wallet_for_existing_dao( @staticmethod async def create( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> DAOWallet: @@ -932,6 +933,7 @@ async def generate_new_proposal( proposed_puzzle_reveal=proposed_puzzle, launcher_coin=launcher_coin, vote_amount=vote_amount, + action_scope=action_scope, ) full_spend = WalletSpendBundle.aggregate([eve_spend, launcher_sb]) @@ -968,6 +970,7 @@ async def generate_proposal_eve_spend( proposed_puzzle_reveal: Program, launcher_coin: Coin, vote_amount: uint64, + action_scope: WalletActionScope, ) -> WalletSpendBundle: cat_wallet: CATWallet = self.wallet_state_manager.wallets[self.dao_info.cat_wallet_id] cat_tail = cat_wallet.cat_info.limitations_program_hash @@ -977,7 +980,7 @@ async def generate_proposal_eve_spend( assert dao_cat_wallet is not None dao_cat_spend = await dao_cat_wallet.create_vote_spend( - vote_amount, launcher_coin.name(), True, proposal_puzzle=dao_proposal_puzzle + vote_amount, launcher_coin.name(), True, action_scope, proposal_puzzle=dao_proposal_puzzle ) vote_amounts = [] vote_coins = [] @@ -1057,7 +1060,11 @@ async def generate_proposal_vote_spend( vote_amount = await dao_cat_wallet.get_votable_balance(proposal_id) assert vote_amount is not None dao_cat_spend = await dao_cat_wallet.create_vote_spend( - vote_amount, proposal_id, is_yes_vote, proposal_puzzle=proposal_info.current_innerpuz + vote_amount, + proposal_id, + is_yes_vote, + action_scope=action_scope, + proposal_puzzle=proposal_info.current_innerpuz, ) vote_amounts = [] vote_coins = [] @@ -1543,7 +1550,7 @@ async def _create_treasury_fund_transaction( ) -> None: if funding_wallet.type() == WalletType.STANDARD_WALLET.value: p2_singleton_puzhash = get_p2_singleton_puzhash(self.dao_info.treasury_id, asset_id=None) - wallet: Wallet = funding_wallet # type: ignore[assignment] + wallet: MainWalletProtocol = funding_wallet # type: ignore[assignment] await wallet.generate_signed_transaction( amount, p2_singleton_puzhash, @@ -2122,3 +2129,9 @@ async def apply_state_transition(self, new_state: CoinSpend, block_height: uint3 raise ValueError(f"Unsupported spend in DAO Wallet: {self.id()}") return True + + def handle_own_derivation(self) -> bool: # pragma: no cover + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() diff --git a/chia/wallet/derivation_record.py b/chia/wallet/derivation_record.py index eec3776ff1ec..e470b386de69 100644 --- a/chia/wallet/derivation_record.py +++ b/chia/wallet/derivation_record.py @@ -28,3 +28,10 @@ class DerivationRecord: def pubkey(self) -> G1Element: assert isinstance(self._pubkey, G1Element) return self._pubkey + + @property + def pubkey_bytes(self) -> bytes: + if isinstance(self._pubkey, G1Element): + return bytes(self._pubkey) + else: + return self._pubkey diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 1d6afd4c0f69..c7937f324cdc 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -49,11 +49,10 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_sync_utils import fetch_coin_spend, fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import WalletProtocol +from chia.wallet.wallet_protocol import MainWalletProtocol, WalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle @@ -66,7 +65,7 @@ class DIDWallet: log: logging.Logger wallet_info: WalletInfo did_info: DIDInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol base_puzzle_program: Optional[bytes] base_inner_puzzle_hash: Optional[bytes32] wallet_id: int @@ -74,7 +73,7 @@ class DIDWallet: @staticmethod async def create_new_did_wallet( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, amount: uint64, action_scope: WalletActionScope, backups_ids: list[bytes32] = [], @@ -106,7 +105,7 @@ async def create_new_did_wallet( self.base_inner_puzzle_hash = None self.standard_wallet = wallet self.log = logging.getLogger(name if name else __name__) - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") @@ -134,7 +133,7 @@ async def create_new_did_wallet( name=name, wallet_type=WalletType.DECENTRALIZED_ID.value, data=info_as_string ) self.wallet_id = self.wallet_info.id - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") @@ -152,7 +151,7 @@ async def create_new_did_wallet( @staticmethod async def create_new_did_wallet_from_recovery( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, backup_data: str, name: Optional[str] = None, ): @@ -192,7 +191,7 @@ async def create_new_did_wallet_from_recovery( @staticmethod async def create_new_did_wallet_from_coin_spend( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, launch_coin: Coin, inner_puzzle: Program, coin_spend: CoinSpend, @@ -267,7 +266,7 @@ async def create_new_did_wallet_from_coin_spend( @staticmethod async def create( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: str = None, ): @@ -581,7 +580,7 @@ async def create_update_spend( assert uncurried is not None p2_puzzle = uncurried[0] # innerpuz solution is (mode, p2_solution) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[ Payment( puzzle_hash=new_inner_puzzle.get_tree_hash(), @@ -589,6 +588,7 @@ async def create_update_spend( memos=[p2_puzzle.get_tree_hash()], ) ], + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(coin.name())), ) innersol: Program = Program.to([1, p2_solution]) @@ -694,8 +694,9 @@ async def transfer_did( launcher_id=self.did_info.origin_coin.name(), metadata=did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), ) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(new_did_puzhash, uint64(coin.amount), [new_puzhash])], + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(coin.name())), ) # Need to include backup list reveal here, even we are don't recover @@ -780,8 +781,9 @@ async def create_message_spend( launcher_id=self.did_info.origin_coin.name(), metadata=did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), ) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(puzzle_hash=new_innerpuzzle_hash, amount=uint64(coin.amount), memos=[p2_ph])], + action_scope=action_scope, conditions=extra_conditions, ) # innerpuz solution is (mode p2_solution) @@ -914,11 +916,12 @@ async def create_attestment( assert uncurried is not None p2_puzzle = uncurried[0] # innerpuz solution is (mode, p2_solution) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[ Payment(innerpuz.get_tree_hash(), uint64(coin.amount), [p2_puzzle.get_tree_hash()]), Payment(innermessage, uint64(0)), ], + action_scope=action_scope, conditions=extra_conditions, ) innersol = Program.to([1, p2_solution]) @@ -1268,7 +1271,12 @@ async def generate_new_decentralised_id( metadata=self.did_info.metadata, ) await self.save_info(did_info) - eve_spend = await self.generate_eve_spend(eve_coin, did_full_puz, did_inner) + eve_spend = await self.generate_eve_spend( + eve_coin, + did_full_puz, + did_inner, + action_scope, + ) full_spend = WalletSpendBundle.aggregate([eve_spend, launcher_sb]) assert self.did_info.origin_coin is not None assert self.did_info.current_inner is not None @@ -1300,6 +1308,7 @@ async def generate_eve_spend( coin: Coin, full_puzzle: Program, innerpuz: Program, + action_scope: WalletActionScope, extra_conditions: tuple[Condition, ...] = tuple(), ): assert self.did_info.origin_coin is not None @@ -1307,8 +1316,9 @@ async def generate_eve_spend( assert uncurried is not None p2_puzzle = uncurried[0] # innerpuz solution is (mode p2_solution) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(innerpuz.get_tree_hash(), uint64(coin.amount), [p2_puzzle.get_tree_hash()])], + action_scope=action_scope, conditions=extra_conditions, ) innersol = Program.to([1, p2_solution]) @@ -1491,3 +1501,9 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: ).get_tree_hash_precalc(hint) == coin.puzzle_hash ) + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 53f18f24a6d3..0ef3d7f68df8 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -53,12 +53,11 @@ from chia.wallet.util.compute_memos import compute_memos 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_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_nft_store import WalletNftStore -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle _T_NFTWallet = TypeVar("_T_NFTWallet", bound="NFTWallet") @@ -72,7 +71,7 @@ class NFTWallet: log: logging.Logger wallet_info: WalletInfo nft_wallet_info: NFTWalletInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_id: int nft_store: WalletNftStore @@ -84,7 +83,7 @@ def did_id(self) -> Optional[bytes32]: async def create_new_nft_wallet( cls: type[_T_NFTWallet], wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, did_id: Optional[bytes32] = None, name: Optional[str] = None, ) -> _T_NFTWallet: @@ -104,7 +103,7 @@ async def create_new_nft_wallet( ) self.wallet_id = self.wallet_info.id self.nft_store = wallet_state_manager.nft_store - self.log.debug("NFT wallet id: %r and standard wallet id: %r", self.wallet_id, self.standard_wallet.wallet_id) + self.log.debug("NFT wallet id: %r and standard wallet id: %r", self.wallet_id, self.standard_wallet.id()) await self.wallet_state_manager.add_new_wallet(self) self.log.debug("Generated a new NFT wallet: %s", self.__dict__) @@ -114,7 +113,7 @@ async def create_new_nft_wallet( async def create( cls: type[_T_NFTWallet], wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> _T_NFTWallet: @@ -547,7 +546,7 @@ async def match_puzzle_info(self, puzzle_driver: PuzzleInfo) -> bool: async def create_from_puzzle_info( cls: Any, wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, puzzle_driver: PuzzleInfo, name: Optional[str] = None, ) -> Any: @@ -702,8 +701,9 @@ async def generate_unsigned_spendbundle( ), ) - innersol: Program = self.standard_wallet.make_solution( + innersol: Program = await self.standard_wallet.make_solution( primaries=payments, + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(coin_name)) if fee > 0 else extra_conditions, ) @@ -1379,8 +1379,9 @@ async def mint_from_did( if len(xch_coins) > 1: xch_extra_conditions += (CreateCoinAnnouncement(message),) - solution: Program = self.standard_wallet.make_solution( + solution: Program = await self.standard_wallet.make_solution( primaries=[xch_payment], + action_scope=action_scope, fee=fee, conditions=xch_extra_conditions, ) @@ -1392,15 +1393,16 @@ async def mint_from_did( for xch_coin in xch_coins_iter: puzzle = await self.standard_wallet.puzzle_for_puzzle_hash(xch_coin.puzzle_hash) - solution = self.standard_wallet.make_solution( - primaries=[], conditions=(AssertCoinAnnouncement(primary_announcement_hash),) + solution = await self.standard_wallet.make_solution( + primaries=[], action_scope=action_scope, conditions=(AssertCoinAnnouncement(primary_announcement_hash),) ) xch_spends.append(make_spend(xch_coin, puzzle, solution)) xch_spend = WalletSpendBundle(xch_spends, G2Element()) # Create the DID spend using the announcements collected when making the intermediate launcher coins - did_p2_solution = self.standard_wallet.make_solution( + did_p2_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=( *extra_conditions, did_coin_announcement, @@ -1637,15 +1639,18 @@ async def mint_from_xch( extra_conditions += tuple(AssertCoinAnnouncement(ann) for ann in coin_announcements) extra_conditions += tuple(AssertPuzzleAnnouncement(ann) for ann in puzzle_assertions) - solution: Program = self.standard_wallet.make_solution( + solution: Program = await self.standard_wallet.make_solution( primaries=[xch_payment] + primaries, + action_scope=action_scope, fee=fee, conditions=extra_conditions, ) primary_announcement = AssertCoinAnnouncement(asserted_id=xch_coin.name(), asserted_msg=message) first = False else: - solution = self.standard_wallet.make_solution(primaries=[], conditions=(primary_announcement,)) + solution = await self.standard_wallet.make_solution( + primaries=[], action_scope=action_scope, conditions=(primary_announcement,) + ) xch_spends.append(make_spend(xch_coin, puzzle, solution)) # Collect up all the coin spends and sign them @@ -1685,3 +1690,9 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return False + + def handle_own_derivation(self) -> bool: # pragma: no cover + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 80d3ef4450d9..6942bf536a10 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -43,6 +43,7 @@ "p2_announced_delegated_puzzle": "c4d24c3c5349376f3e8f3aba202972091713b4ec4915f0f26192ae4ace0bd04d", "p2_conditions": "1c77d7d5efde60a7a1d2d27db6d746bc8e568aea1ef8586ca967a0d60b83cc36", "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", + "p2_delegated_or_hidden_secp": "007b927c693b5594c777ce0a433d5bce0c1c3a101140c1897301f74b0ae103f5", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", @@ -51,7 +52,8 @@ "p2_singleton": "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834", "p2_singleton_aggregator": "f79a31fcfe3736cc75720617b4cdcb4376b4b8f8f71108617710612b909a4924", "p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b", - "p2_singleton_via_delegated_puzzle": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", + "p2_singleton_via_delegated_puzzle_safe": "20f2e0c7e3ccc1dc035f23fb70d4cbeb32b232615a916d56bc7d04d1d15b3121", + "p2_singleton_via_delegated_puzzle_w_aggregator": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", "pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494", "pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307", "rom_bootstrap_generator": "161bade1f822dcd62ab712ebaf30f3922a301e48a639e4295c5685f8bece7bd9", @@ -63,5 +65,7 @@ "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", "test_generator_deserialize": "52add794fc76e89512e4a063c383418bda084c8a78c74055abe80179e4a7832c", "test_multiple_generator_input_arguments": "156dafbddc3e1d3bfe1f2a84e48e5e46b287b8358bf65c3c091c93e855fbfc5b", + "vault_p2_recovery": "751f07d363747c2d1c9fd8a2ac7ee3aca0c03a806f3f7553259642365ec8e56c", + "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp new file mode 100644 index 000000000000..c5b66d264e33 --- /dev/null +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp @@ -0,0 +1,21 @@ +; p2_delegated with SECP256-R1 signature +; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) + +(mod (GENESIS_CHALLENGE SECP_PK HIDDEN_PUZZLE_HASH delegated_puzzle delegated_solution signature coin_id) + (include *standard-cl-21*) + (include condition_codes.clib) + (include sha256tree.clib) + + (let ((delegated_puzzle_hash (sha256tree delegated_puzzle))) + (if (= delegated_puzzle_hash HIDDEN_PUZZLE_HASH) + (a delegated_puzzle delegated_solution) + (if (secp256r1_verify SECP_PK (sha256 delegated_puzzle_hash coin_id GENESIS_CHALLENGE HIDDEN_PUZZLE_HASH) signature) + (x) ; this doesn't actually run because secp256_verify will raise on failure + (c + (list ASSERT_MY_COIN_ID coin_id) + (a delegated_puzzle delegated_solution) + ) + ) + ) + ) +) diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex new file mode 100644 index 000000000000..d89e050ccc47 --- /dev/null +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ffff09ffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ff018080808080ffff01ff02ffff01ff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff0180808080808080ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ffff05ffff06ffff06ff01808080ffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff05ffff06ff018080ffff05ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01808080ffff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff018080808080808080ff018080ff0180ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/puzzles/p2_singleton_aggregator.clsp b/chia/wallet/puzzles/p2_singleton_aggregator.clsp index 17d03dd596e6..99c32565d965 100644 --- a/chia/wallet/puzzles/p2_singleton_aggregator.clsp +++ b/chia/wallet/puzzles/p2_singleton_aggregator.clsp @@ -1,4 +1,4 @@ -;; Works with p2_singleton_via_delegated_puzzle +;; Works with p2_singleton_via_delegated_puzzle_w_aggregator ;; When we have many p2_singleton coins and want to aggregate them together ;; all coins make announcements of their puzhash, amount, and ID diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp new file mode 100644 index 000000000000..759d4af25427 --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp @@ -0,0 +1,36 @@ +(mod ( + SINGLETON_MOD_HASH + SINGLETON_STRUCT_HASH + singleton_inner_puzhash + delegated_puzzle + delegated_solution + my_id + ) + + (include condition_codes.clib) + (include curry-and-treehash.clib) + + (defun-inline calculate_full_puzzle_hash (SINGLETON_MOD_HASH SINGLETON_STRUCT_HASH singleton_inner_puzhash) + (puzzle-hash-of-curried-function SINGLETON_MOD_HASH + singleton_inner_puzhash + SINGLETON_STRUCT_HASH + ) + ) + + (c + (list + ASSERT_PUZZLE_ANNOUNCEMENT + (sha256 + (calculate_full_puzzle_hash SINGLETON_MOD_HASH SINGLETON_STRUCT_HASH singleton_inner_puzhash) + (sha256tree (list my_id (sha256tree delegated_puzzle))) + ) + ) + (c + (list ASSERT_MY_COIN_ID my_id) + (c + (list CREATE_COIN_ANNOUNCEMENT ()) + (a delegated_puzzle delegated_solution) + ) + ) + ) +) diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex new file mode 100644 index 000000000000..0eaf83e76dae --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff17ffff04ff0bff808080808080ffff02ff3effff04ff02ffff04ffff04ff81bfffff04ffff02ff3effff04ff02ffff04ff2fff80808080ff808080ff8080808080ff808080ffff04ffff04ff10ffff04ff81bfff808080ffff04ffff04ff2cffff01ff808080ffff02ff2fff5f80808080ffff04ffff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py new file mode 100644 index 000000000000..1bf467665a9a --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import Optional + +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.wallet.conditions import CreatePuzzleAnnouncement +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.singleton_top_layer_v1_1 import SINGLETON_LAUNCHER_HASH, SINGLETON_MOD_HASH +from chia.wallet.util.curry_and_treehash import curry_and_treehash, shatree_atom, shatree_pair + +PUZZLE = load_clvm("p2_singleton_via_delegated_puzzle_safe.clsp") +PUZZLE_HASH = PUZZLE.get_tree_hash() +QUOTED_PUZZLE = Program.to((1, PUZZLE)) +QUOTED_PUZZLE_HASH = QUOTED_PUZZLE.get_tree_hash() +PRE_HASHED_HASHES: dict[bytes32, bytes32] = { + SINGLETON_MOD_HASH: shatree_atom(SINGLETON_MOD_HASH), + SINGLETON_LAUNCHER_HASH: shatree_atom(SINGLETON_LAUNCHER_HASH), +} + + +def _treehash_hash(atom_hash: bytes32) -> bytes32: + if atom_hash in PRE_HASHED_HASHES: + return PRE_HASHED_HASHES[atom_hash] + else: + return shatree_atom(atom_hash) + + +def _struct_hash(singleton_mod_hash: bytes32, launcher_id: bytes32, singleton_launcher_hash: bytes32) -> bytes32: + return shatree_pair( + _treehash_hash(singleton_mod_hash), + shatree_pair(_treehash_hash(launcher_id), _treehash_hash(singleton_launcher_hash)), + ) + + +def match(potential_match: Program) -> Optional[Program]: + mod, args = potential_match.uncurry() + if mod == PUZZLE: + return args + else: + return None + + +def construct( + launcher_id: bytes32, + singleton_mod_hash: bytes32 = SINGLETON_MOD_HASH, + singleton_launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH, +) -> Program: + return PUZZLE.curry( + singleton_mod_hash, + _struct_hash(singleton_mod_hash, launcher_id, singleton_launcher_hash), + ) + + +def construct_hash( + launcher_id: bytes32, + singleton_mod_hash: bytes32 = SINGLETON_MOD_HASH, + singleton_launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH, +) -> bytes32: + return curry_and_treehash( + QUOTED_PUZZLE_HASH, + _treehash_hash(singleton_mod_hash), + shatree_atom(_struct_hash(singleton_mod_hash, launcher_id, singleton_launcher_hash)), + ) + + +def solve( + singleton_inner_puzhash: bytes32, delegated_puzzle: Program, delegated_solution: Program, my_id: bytes32 +) -> Program: + return Program.to([singleton_inner_puzhash, delegated_puzzle, delegated_solution, my_id]) + + +def required_announcement(delegated_puzzle_hash: bytes32, my_id: bytes32) -> CreatePuzzleAnnouncement: + return CreatePuzzleAnnouncement(Program.to([my_id, delegated_puzzle_hash]).get_tree_hash()) diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp similarity index 100% rename from chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp rename to chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp.hex b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp.hex similarity index 100% rename from chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp.hex rename to chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp.hex diff --git a/chia/wallet/puzzles/singleton_top_layer_v1_1.py b/chia/wallet/puzzles/singleton_top_layer_v1_1.py index bdf9c845a560..262e16215d9b 100644 --- a/chia/wallet/puzzles/singleton_top_layer_v1_1.py +++ b/chia/wallet/puzzles/singleton_top_layer_v1_1.py @@ -13,6 +13,7 @@ from chia.wallet.lineage_proof import LineageProof from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile from chia.wallet.uncurried_puzzle import UncurriedPuzzle +from chia.wallet.util.curry_and_treehash import calculate_hash_of_quoted_mod_hash, curry_and_treehash SINGLETON_MOD = load_clvm_maybe_recompile("singleton_top_layer_v1_1.clsp") SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() @@ -253,6 +254,15 @@ def puzzle_for_singleton( ) +# Convenience to calculate the singleton puzzle hash with just inner puzhash and launcher id +def puzzle_hash_for_singleton( + launcher_id: bytes32, inner_puzzle_hash: bytes32, launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH +) -> bytes32: + hash_of_quoted_mod_hash = calculate_hash_of_quoted_mod_hash(SINGLETON_MOD_HASH) + hashed_args = [Program.to((SINGLETON_MOD_HASH, (launcher_id, launcher_hash))).get_tree_hash(), inner_puzzle_hash] + return curry_and_treehash(hash_of_quoted_mod_hash, *hashed_args) + + # Return a solution to spend a singleton def solution_for_singleton( lineage_proof: LineageProof, diff --git a/chia/wallet/puzzles/tails.py b/chia/wallet/puzzles/tails.py index e335c116dbcf..3cce29eb20e6 100644 --- a/chia/wallet/puzzles/tails.py +++ b/chia/wallet/puzzles/tails.py @@ -120,7 +120,12 @@ async def generate_issuance_bundle( inner_tree_hash = cat_inner.get_tree_hash() inner_solution = wallet.standard_wallet.add_condition_to_solution( Program.to([51, 0, -113, tail, []]), - wallet.standard_wallet.make_solution(primaries=[Payment(inner_tree_hash, amount, [inner_tree_hash])]), + ( + await wallet.standard_wallet.make_solution( + primaries=[Payment(inner_tree_hash, amount, [inner_tree_hash])], + action_scope=action_scope, + ) + ), ) eve_spend = unsigned_spend_bundle_for_spendable_cats( CAT_MOD, @@ -301,8 +306,11 @@ async def generate_issuance_bundle( payment = Payment(cat_inner.get_tree_hash(), amount) inner_solution = wallet.standard_wallet.add_condition_to_solution( Program.to([51, 0, -113, tail, []]), - wallet.standard_wallet.make_solution( - primaries=[payment], + ( + await wallet.standard_wallet.make_solution( + primaries=[payment], + action_scope=action_scope, + ) ), ) eve_spend = unsigned_spend_bundle_for_spendable_cats( diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp b/chia/wallet/puzzles/vault_p2_recovery.clsp new file mode 100644 index 000000000000..2041f6c1ee23 --- /dev/null +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp @@ -0,0 +1,66 @@ +; Recovery spend path for vault +; This puzzle is included in the vault puzzle (p2_1_of_n) merkle root +; when executed it creates a new p2_1_of_n of the timelocked p2_conditions and the escape puzzle +; the escape puzzle sends funds back to the vault (my_puzzlehash) + +(mod + ( + P2_1_OF_N_MOD_HASH + FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH + BLS_PK + TIMELOCK + my_amount + recovery_conditions + ) + + (include *standard-cl-21*) + (include condition_codes.clib) + (include sha256tree.clib) + (include curry.clib) + + (defconstant ONE 1) + + (defun create_recovery_puzzlehash + ( + P2_1_OF_N_MOD_HASH + FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH + TIMELOCK + recovery_conditions + ) + (curry_hashes P2_1_OF_N_MOD_HASH + (sha256 ONE + ; calculate the merkle root of the two puzzles + (sha256 + TWO + (sha256 ONE + P2_SECP_PUZZLEHASH + ) + (sha256 ONE + (curry_hashes FINISH_RECOVERY_MOD_HASH + (sha256 ONE TIMELOCK) (sha256tree recovery_conditions) + ) + ) + ) + ) + ) + ) + + (assign + recovery_puzzlehash + (create_recovery_puzzlehash + P2_1_OF_N_MOD_HASH + FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH + TIMELOCK + recovery_conditions + ) + (list + (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) + (list CREATE_COIN recovery_puzzlehash my_amount (list recovery_puzzlehash)) + (list ASSERT_MY_AMOUNT my_amount) + ) + ) + +) diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex new file mode 100644 index 000000000000..56f61f6a7b41 --- /dev/null +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff02ff1effff04ff02ffff04ffff06ff0180ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ff8080808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff14ffff04ff02ffff04ffff05ff0580ffff04ffff02ff1cffff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff14ffff04ff02ffff04ff05ffff04ffff02ff1cffff04ff02ffff04ff07ff80808080ff808080808080ffff02ff0affff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff0affff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff04ffff04ffff0132ffff04ff5dffff04ffff02ff08ffff04ff02ffff04ff8202fdff80808080ff80808080ffff04ffff04ffff0133ffff04ff0bffff04ff82017dffff04ffff04ff0bff8080ff8080808080ffff04ffff04ffff0149ffff04ff82017dff808080ff80808080ff018080 diff --git a/chia/wallet/puzzles/vault_recovery_finish.clsp b/chia/wallet/puzzles/vault_recovery_finish.clsp new file mode 100644 index 000000000000..05bb5eb4338c --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery_finish.clsp @@ -0,0 +1,13 @@ +; p2_conditions for vault +; TIMELOCK is curried into the vault_recovery puzzle +; CONDITIONS are curried in when a recovery is initiated + +(mod (TIMELOCK CONDITIONS) + (include *standard-cl-21*) + (include condition_codes.clib) + + (c + (list ASSERT_SECONDS_RELATIVE TIMELOCK) + CONDITIONS + ) +) diff --git a/chia/wallet/puzzles/vault_recovery_finish.clsp.hex b/chia/wallet/puzzles/vault_recovery_finish.clsp.hex new file mode 100644 index 000000000000..0f3a14d66f08 --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery_finish.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ffff0150ffff04ff05ffff01808080ff0b80ffff04ff80ff018080 diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 1f64f406f422..37957c1ec39e 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -48,7 +48,7 @@ from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord -from chia.wallet.wallet_protocol import WalletProtocol +from chia.wallet.wallet_protocol import MainWalletProtocol, WalletProtocol if TYPE_CHECKING: from chia.wallet.wallet_state_manager import WalletStateManager @@ -316,7 +316,7 @@ async def cancel_pending_offers( interface.side_effects.selected_coins.append(coin) # This should probably not switch on whether or not we're spending a XCH but it has to for now if wallet.type() == WalletType.STANDARD_WALLET: - assert isinstance(wallet, Wallet) + assert isinstance(wallet, MainWalletProtocol) if fee_to_pay > coin.amount: selected_coins: set[Coin] = await wallet.select_coins( uint64(fee_to_pay - coin.amount), @@ -505,7 +505,7 @@ async def _create_offer_for_ids( if isinstance(id, int): wallet_id = uint32(id) wallet = self.wallet_state_manager.wallets.get(wallet_id) - assert isinstance(wallet, (CATWallet, Wallet)) + assert isinstance(wallet, (CATWallet, MainWalletProtocol)) p2_ph: bytes32 = await wallet.get_puzzle_hash( new=not action_scope.config.tx_config.reuse_puzhash ) @@ -549,7 +549,7 @@ async def _create_offer_for_ids( amount_to_select = abs(amount) if wallet.type() == WalletType.STANDARD_WALLET: amount_to_select += fee - assert isinstance(wallet, (CATWallet, DataLayerWallet, NFTWallet, Wallet)) + assert isinstance(wallet, (CATWallet, DataLayerWallet, NFTWallet, MainWalletProtocol)) if isinstance(wallet, DataLayerWallet): assert asset_id is not None coins_to_offer[id] = await wallet.get_coins_to_offer(launcher_id=asset_id) @@ -684,7 +684,7 @@ async def maybe_create_wallets_for_offer(self, offer: Offer) -> None: if key is None: continue # ATTENTION: new_wallets - exists = await wsm.get_wallet_for_puzzle_info(offer.driver_dict[key]) + exists: Optional[WalletProtocol[Any]] = await wsm.get_wallet_for_puzzle_info(offer.driver_dict[key]) if exists is None: await wsm.create_wallet_for_puzzle_info(offer.driver_dict[key]) @@ -742,7 +742,7 @@ async def calculate_tx_records_for_offer(self, offer: Offer, validate: bool) -> if wallet_identifier is not None: if addition.parent_coin_info in settlement_coin_ids: wallet = self.wallet_state_manager.wallets[wallet_identifier.id] - assert isinstance(wallet, (CATWallet, NFTWallet, Wallet)) + assert isinstance(wallet, (CATWallet, NFTWallet, MainWalletProtocol)) to_puzzle_hash = await wallet.convert_puzzle_hash(addition.puzzle_hash) # ATTENTION: new wallets txs.append( TransactionRecord( diff --git a/chia/wallet/vault/__init__.py b/chia/wallet/vault/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py new file mode 100644 index 000000000000..287d2a5b1863 --- /dev/null +++ b/chia/wallet/vault/vault_drivers.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +from typing import Optional, Union + +from chia_rs import G1Element + +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.util.ints import uint32, uint64 +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE +from chia.wallet.puzzles.singleton_top_layer_v1_1 import ( + SINGLETON_LAUNCHER_HASH, + SINGLETON_MOD, + SINGLETON_MOD_HASH, + puzzle_for_singleton, + puzzle_hash_for_singleton, + solution_for_singleton, +) +from chia.wallet.util.merkle_tree import MerkleTree + +# MODS +P2_CONDITIONS_MOD: Program = load_clvm("p2_conditions.clsp") +P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_or_hidden_secp.clsp") +P2_1_OF_N_MOD: Program = load_clvm("p2_1_of_n.clsp") +P2_1_OF_N_MOD_HASH = P2_1_OF_N_MOD.get_tree_hash() +P2_RECOVERY_MOD: Program = load_clvm("vault_p2_recovery.clsp") +P2_RECOVERY_MOD_HASH = P2_RECOVERY_MOD.get_tree_hash() +RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") +RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle_safe.clsp") +P2_SINGLETON_MOD_HASH = P2_SINGLETON_MOD.get_tree_hash() + + +# PUZZLES +def construct_p2_delegated_secp(secp_pk: bytes, genesis_challenge: bytes32, hidden_puzzle_hash: bytes) -> Program: + return P2_DELEGATED_SECP_MOD.curry(genesis_challenge, secp_pk, hidden_puzzle_hash) + + +def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> Program: + return RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + + +def construct_vault_puzzle(secp_puzzle_hash: bytes32, recovery_puzzle_hash: Optional[bytes32]) -> Program: + if recovery_puzzle_hash: + merkle_root = MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]).calculate_root() + else: + merkle_root = MerkleTree([secp_puzzle_hash]).calculate_root() + return P2_1_OF_N_MOD.curry(merkle_root) + + +def get_recovery_puzzle(secp_puzzle_hash: bytes32, bls_pk: Optional[G1Element], timelock: Optional[uint64]) -> Program: + return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzle_hash, bls_pk, timelock) + + +def get_vault_hidden_puzzle_with_index(index: uint32, hidden_puzzle: Program = DEFAULT_HIDDEN_PUZZLE) -> Program: + hidden_puzzle_with_index: Program = Program.to([6, (index, hidden_puzzle)]) + return hidden_puzzle_with_index + + +def get_vault_inner_puzzle( + secp_pk: bytes, + genesis_challenge: bytes32, + hidden_puzzle_hash: bytes, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, +) -> Program: + secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, hidden_puzzle_hash).get_tree_hash() + recovery_puzzle_hash = get_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() if bls_pk else None + vault_inner = construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash) + return vault_inner + + +def get_vault_inner_puzzle_hash( + secp_pk: bytes, + genesis_challenge: bytes32, + hidden_puzzle_hash: bytes32, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, +) -> bytes32: + vault_puzzle = get_vault_inner_puzzle(secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock) + vault_puzzle_hash: bytes32 = vault_puzzle.get_tree_hash() + return vault_puzzle_hash + + +def get_recovery_inner_puzzle(secp_puzzle_hash: bytes32, recovery_finish_hash: bytes32) -> Program: + puzzle = construct_vault_puzzle(secp_puzzle_hash, recovery_finish_hash) + return puzzle + + +def get_vault_full_puzzle(launcher_id: bytes32, inner_puzzle: Program) -> Program: + full_puzzle = puzzle_for_singleton(launcher_id, inner_puzzle) + return full_puzzle + + +def get_vault_full_puzzle_hash(launcher_id: bytes32, inner_puzzle_hash: bytes32) -> bytes32: + puzzle_hash = puzzle_hash_for_singleton(launcher_id, inner_puzzle_hash) + return puzzle_hash + + +def get_recovery_finish_puzzle( + new_vault_inner_puzhash: bytes32, timelock: uint64, amount: uint64, memos: Union[list[bytes], Program] +) -> Program: + recovery_condition = Program.to([[51, new_vault_inner_puzhash, amount, memos]]) + return RECOVERY_FINISH_MOD.curry(timelock, recovery_condition) + + +def get_p2_singleton_puzzle(launcher_id: bytes32) -> Program: + singleton_struct = Program.to((SINGLETON_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_HASH))) + puzzle = P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, singleton_struct.get_tree_hash()) + return puzzle + + +def get_p2_singleton_puzzle_hash(launcher_id: bytes32) -> bytes32: + return get_p2_singleton_puzzle(launcher_id).get_tree_hash() + + +def match_vault_puzzle(mod: Program, curried_args: Program) -> bool: + try: + if mod == SINGLETON_MOD: + if curried_args.at("rf").uncurry()[0] == P2_1_OF_N_MOD: + return True + except ValueError: + # We just pass here to prevent spamming logs with error messages when WSM checks incoming coins + pass + return False + + +def match_p2_delegated_secp(mod: Program, curried_args: Program) -> bool: + if mod == P2_DELEGATED_SECP_MOD: + return True + else: + return False + + +def match_recovery_puzzle(mod: Program, curried_args: Program, solution: Program) -> bool: + if match_vault_puzzle(mod, curried_args): + try: + delegated_puz = solution.at("rrfrf") + if delegated_puz.uncurry()[0] == P2_RECOVERY_MOD: + return True + except ValueError: + pass + return False + + +def get_recovery_puzzle_from_spend(spend: CoinSpend) -> Program: + solution = spend.solution.to_program() + delegated_puz = solution.at("rrfrf") + recovery_args = delegated_puz.uncurry()[1] + secp_puzzle_hash = bytes32(recovery_args.at("rrf").as_atom()) + timelock = uint64(recovery_args.at("rrrrf").as_int()) + new_vault_condition = solution.at("rrfrrfrff") + new_vault_inner_puzhash = bytes32(new_vault_condition.at("rf").as_atom()) + memos = new_vault_condition.at("rrrf") + recovery_finish_puzzle = get_recovery_finish_puzzle(new_vault_inner_puzhash, timelock, spend.coin.amount, memos) + recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) + return recovery_inner_puzzle + + +def get_new_vault_info_from_spend(spend: CoinSpend) -> tuple[bytes, bytes32, Optional[G1Element], Optional[uint64]]: + solution = spend.solution.to_program() + delegated_puz = solution.at("rrfrf") + conds = delegated_puz.at("rrfrrfrfr") + for cond in conds.as_iter(): + if (cond.at("f").as_int() == 51) and (cond.at("rrf").as_int() == 1): + memos = cond.at("rrrf") if cond.list_len() == 4 else None + assert memos is not None + secp_pk = memos.at("f").as_atom() + hidden_puzzle_hash = bytes32(memos.at("rf").as_atom()) + bls_pk = None + timelock = None + if memos.list_len() > 2: + bls_pk = G1Element.from_bytes(memos.at("rrf").as_atom()) + timelock = uint64(memos.at("rrrf").as_int()) + break + return secp_pk, hidden_puzzle_hash, bls_pk, timelock + + +def match_finish_spend(spend: CoinSpend) -> bool: + solution = spend.solution.to_program() + delegated_puz = solution.at("rrfrf") + return delegated_puz.uncurry()[0] == RECOVERY_FINISH_MOD + + +# SOLUTIONS +def get_recovery_solution(new_vault_inner_puzhash: bytes32, amount: uint64, memos: list[bytes]) -> Program: + recovery_condition = Program.to([[51, new_vault_inner_puzhash, amount, memos]]) + recovery_solution: Program = Program.to([amount, recovery_condition]) + return recovery_solution + + +def get_vault_inner_solution(puzzle_to_run: Program, solution: Program, proof: Program) -> Program: + inner_solution: Program = Program.to([proof, puzzle_to_run, solution]) + return inner_solution + + +def get_vault_full_solution(lineage_proof: LineageProof, amount: uint64, inner_solution: Program) -> Program: + full_solution: Program = solution_for_singleton(lineage_proof, amount, inner_solution) + return full_solution + + +# MERKLE +def construct_vault_merkle_tree( + secp_puzzle_hash: bytes32, recovery_puzzle_hash: Optional[bytes32] = None +) -> MerkleTree: + if recovery_puzzle_hash: + return MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]) + return MerkleTree([secp_puzzle_hash]) + + +def get_vault_proof(merkle_tree: MerkleTree, puzzle_hash: bytes32) -> Program: + proof = merkle_tree.generate_proof(puzzle_hash) + vault_proof: Program = Program.to((proof[0], proof[1][0])) + return vault_proof + + +# SECP SIGNATURE +def construct_secp_message( + delegated_puzzle_hash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, hidden_puzzle_hash: bytes +) -> bytes: + return delegated_puzzle_hash + coin_id + genesis_challenge + hidden_puzzle_hash diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py new file mode 100644 index 000000000000..65d63e78c57a --- /dev/null +++ b/chia/wallet/vault/vault_info.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from chia_rs import G1Element + +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 +from chia.util.streamable import Streamable, streamable +from chia.wallet.lineage_proof import LineageProof + + +@streamable +@dataclass(frozen=True) +class RecoveryInfo(Streamable): + bls_pk: Optional[G1Element] = None + timelock: Optional[uint64] = None + + +@streamable +@dataclass(frozen=True) +class VaultInfo(Streamable): + coin: Coin + pubkey: bytes + hidden_puzzle_hash: bytes32 + inner_puzzle_hash: bytes32 + lineage_proof: LineageProof + is_recoverable: bool + recovery_info: RecoveryInfo diff --git a/chia/wallet/vault/vault_root.py b/chia/wallet/vault/vault_root.py new file mode 100644 index 000000000000..ea4d60fd727c --- /dev/null +++ b/chia/wallet/vault/vault_root.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class VaultRoot: + launcher_id: bytes + + def get_fingerprint(self) -> int: + # Convert the first four bytes of PK into an integer + return int.from_bytes(self.launcher_id[:4], byteorder="big") + + def __bytes__(self) -> bytes: + return self.launcher_id + + @classmethod + def from_bytes(cls, blob: bytes) -> VaultRoot: + return cls(blob) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py new file mode 100644 index 000000000000..1031b3cae48e --- /dev/null +++ b/chia/wallet/vault/vault_wallet.py @@ -0,0 +1,783 @@ +from __future__ import annotations + +import logging +import time +from dataclasses import dataclass +from typing import Any, Optional + +from chia_rs import G1Element, G2Element +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from typing_extensions import Unpack + +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.coin_spend import CoinSpend, make_spend +from chia.types.signing_mode import SigningMode +from chia.util.hash import std_hash +from chia.util.ints import uint32, uint64, uint128 +from chia.util.observation_root import ObservationRoot +from chia.wallet.coin_selection import select_coins +from chia.wallet.conditions import ( + AssertCoinAnnouncement, + Condition, + CreateCoin, + CreatePuzzleAnnouncement, + parse_timelock_info, +) +from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.payment import Payment +from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions +from chia.wallet.signer_protocol import ( + PathHint, + SignedTransaction, + SigningInstructions, + SigningResponse, + SigningTarget, + Spend, + SumHint, + TransactionInfo, +) +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.compute_hints import compute_spend_hints_and_additions +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.wallet_sync_utils import fetch_coin_spend +from chia.wallet.util.wallet_types import WalletIdentifier +from chia.wallet.vault.vault_drivers import ( + construct_p2_delegated_secp, + construct_vault_merkle_tree, + get_new_vault_info_from_spend, + get_p2_singleton_puzzle, + get_p2_singleton_puzzle_hash, + get_recovery_finish_puzzle, + get_recovery_inner_puzzle, + get_recovery_puzzle, + get_recovery_puzzle_from_spend, + get_recovery_solution, + get_vault_full_puzzle, + get_vault_full_solution, + get_vault_hidden_puzzle_with_index, + get_vault_inner_puzzle, + get_vault_inner_puzzle_hash, + get_vault_inner_solution, + get_vault_proof, + match_finish_spend, + match_p2_delegated_secp, + match_recovery_puzzle, + match_vault_puzzle, +) +from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo +from chia.wallet.vault.vault_root import VaultRoot +from chia.wallet.wallet import Wallet +from chia.wallet.wallet_action_scope import WalletActionScope +from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import GSTOptionalArgs +from chia.wallet.wallet_spend_bundle import WalletSpendBundle + + +@dataclass +class Vault(Wallet): + _vault_info: Optional[VaultInfo] = None + + @property + def vault_info(self) -> VaultInfo: + if self._vault_info is None: + raise ValueError("VaultInfo is not set") + return self._vault_info + + @property + def launcher_id(self) -> bytes32: + assert isinstance(self.wallet_state_manager.observation_root, VaultRoot) + return bytes32(self.wallet_state_manager.observation_root.launcher_id) + + @staticmethod + async def create( + wallet_state_manager: Any, + info: WalletInfo, + name: str = __name__, + ) -> Vault: + self = Vault() + self.wallet_state_manager = wallet_state_manager + self.wallet_info = info + self.wallet_id = info.id + self.log = logging.getLogger(name) + return self + + async def get_new_puzzle(self) -> Program: + dr = await self.wallet_state_manager.get_unused_derivation_record(self.id()) + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(dr.index).get_tree_hash() + puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, + ) + return puzzle + + async def get_new_vault_puzzlehash(self) -> bytes32: + puzzle = await self.get_new_puzzle() + return puzzle.get_tree_hash() + + async def generate_signed_transaction( + self, + amount: uint64, + puzzle_hash: bytes32, + action_scope: WalletActionScope, + fee: uint64 = uint64(0), + coins: Optional[set[Coin]] = None, + primaries: Optional[list[Payment]] = None, + memos: Optional[list[bytes]] = None, + puzzle_decorator_override: Optional[list[dict[str, Any]]] = None, + extra_conditions: tuple[Condition, ...] = tuple(), + **kwargs: Unpack[GSTOptionalArgs], + ) -> None: + """ + Creates Un-signed transactions to be passed into signer. + """ + if primaries is None: + non_change_amount: int = amount + else: + non_change_amount = amount + sum(p.amount for p in primaries) + + non_change_amount += sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + coin_spends = await self._generate_unsigned_transaction( + amount, + puzzle_hash, + action_scope, + fee=fee, + coins=coins, + primaries_input=primaries, + memos=memos, + puzzle_decorator_override=puzzle_decorator_override, + extra_conditions=extra_conditions, + ) + spend_bundle = WalletSpendBundle(coin_spends, G2Element()) + + async with action_scope.use() as interface: + interface.side_effects.transactions.append( + TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=puzzle_hash, + amount=uint64(non_change_amount), + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=[], + removals=spend_bundle.removals(), + wallet_id=self.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=spend_bundle.name(), + valid_times=parse_timelock_info(tuple()), + ) + ) + + async def _generate_unsigned_transaction( + self, + amount: uint64, + newpuzzlehash: bytes32, + action_scope: WalletActionScope, + fee: uint64 = uint64(0), + origin_id: Optional[bytes32] = None, + coins: Optional[set[Coin]] = None, + primaries_input: Optional[list[Payment]] = None, + memos: Optional[list[bytes]] = None, + negative_change_allowed: bool = False, + puzzle_decorator_override: Optional[list[dict[str, Any]]] = None, + extra_conditions: tuple[Condition, ...] = tuple(), + ) -> list[CoinSpend]: + primaries = [] + if primaries_input is not None: + primaries.extend(primaries_input) + total_amount = ( + amount + + sum(primary.amount for primary in primaries) + + fee + + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + ) + + # Select p2_singleton coins to spend + if coins is None: + total_balance = await self.get_spendable_balance() + if total_amount > total_balance: + raise ValueError( + f"Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {amount} mojos" + ) + coins = await self.select_coins( + uint64(total_amount), + action_scope, + ) + assert len(coins) > 0 + selected_amount = sum(coin.amount for coin in coins) + assert selected_amount >= amount + + conditions = [primary.as_condition() for primary in primaries] + conditions.append(Payment(newpuzzlehash, amount, memos if memos else []).as_condition()) + p2_singleton_puzzle = get_p2_singleton_puzzle(self.launcher_id) + p2_singleton_puzhash = p2_singleton_puzzle.get_tree_hash() + # add the change condition + if selected_amount > amount: + conditions.append( + Payment( + p2_singleton_puzhash, uint64(selected_amount - total_amount), memos=[p2_singleton_puzhash] + ).as_condition() + ) + # create the p2_singleton spends + delegated_puzzle = puzzle_for_conditions(conditions) + delegated_solution = Program.to(None) + + p2_singleton_spends: list[CoinSpend] = [] + for coin in coins: + if not p2_singleton_spends: + p2_solution = Program.to( + [self.vault_info.inner_puzzle_hash, delegated_puzzle, delegated_solution, coin.name()] + ) + else: + p2_solution = Program.to([self.vault_info.inner_puzzle_hash, 0, 0, coin.name()]) + + p2_singleton_spends.append(make_spend(coin, p2_singleton_puzzle, p2_solution)) + + next_puzzle_hash = ( + self.vault_info.coin.puzzle_hash + if action_scope.config.tx_config.reuse_puzhash + else (await self.get_new_vault_puzzlehash()) + ) + vault_conditions: list[Program] = [] + recreate_vault_condition = CreateCoin( + next_puzzle_hash, uint64(self.vault_info.coin.amount), memos=[next_puzzle_hash] + ).to_program() + vault_conditions.append(recreate_vault_condition) + for i, spend in enumerate(p2_singleton_spends): + puzzle_to_assert = delegated_puzzle if i == 0 else Program.to(0) + vault_conditions.extend( + [ + CreatePuzzleAnnouncement( + Program.to([spend.coin.name(), puzzle_to_assert.get_tree_hash()]).get_tree_hash(), + ).to_program(), + AssertCoinAnnouncement(asserted_id=spend.coin.name(), asserted_msg=b"").to_program(), + ] + ) + + vault_delegated_puzzle = puzzle_for_conditions(vault_conditions) + vault_delegated_solution = solution_for_conditions(vault_conditions) + + secp_puzzle = construct_p2_delegated_secp( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + ) + vault_inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, + ) + + secp_solution = Program.to( + [ + vault_delegated_puzzle, + vault_delegated_solution, + None, # Slot for signed message + self.vault_info.coin.name(), + ] + ) + if self.vault_info.is_recoverable: + recovery_puzzle_hash = get_recovery_puzzle( + secp_puzzle.get_tree_hash(), + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, + ).get_tree_hash() + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash(), recovery_puzzle_hash) + else: + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash()) + proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) + vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) + + full_puzzle = get_vault_full_puzzle(self.launcher_id, vault_inner_puzzle) + full_solution = get_vault_full_solution( + self.vault_info.lineage_proof, + uint64(self.vault_info.coin.amount), + vault_inner_solution, + ) + + vault_spend = make_spend(self.vault_info.coin, full_puzzle, full_solution) + all_spends = [*p2_singleton_spends, vault_spend] + + return all_spends + + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: + raise NotImplementedError("vault wallet") + + async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: + raise NotImplementedError("vault wallet") + + async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> tuple[G1Element, G2Element]: + raise NotImplementedError("vault wallet") + + async def get_puzzle_hash(self, new: bool) -> bytes32: + if new: + return self.get_p2_singleton_puzzle_hash() + else: + record: Optional[ + DerivationRecord + ] = await self.wallet_state_manager.get_current_derivation_record_for_wallet(self.id()) + if record is None: + return self.get_p2_singleton_puzzle_hash() + return record.puzzle_hash + + async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: + pk = self.vault_info.pubkey + + targets = [] + for spend in coin_spends: + mod, curried_args = spend.puzzle.uncurry() + # match the vault puzzle + if match_vault_puzzle(mod, curried_args) and match_p2_delegated_secp(*spend.solution.at("rrfrf").uncurry()): + vault_spend = spend + inner_sol = vault_spend.solution.at("rrf") + secp_puz = inner_sol.at("rf") + secp_sol = inner_sol.at("rrf") + _, secp_args = secp_puz.uncurry() + genesis_challenge = secp_args.at("f").as_atom() + hidden_puzzle_hash = secp_args.at("rrf").as_atom() + delegated_puzzle_hash = secp_sol.at("f").get_tree_hash() + coin_id = secp_sol.at("rrrf").as_atom() + message = delegated_puzzle_hash + coin_id + genesis_challenge + hidden_puzzle_hash + fingerprint = self.wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big") + targets.append(SigningTarget(fingerprint, message, std_hash(pk + message))) + + sig_info = SigningInstructions( + await self.wallet_state_manager.key_hints_for_pubkeys([pk]), + targets, + ) + return sig_info + + async def apply_signatures( + self, spends: list[Spend], signing_responses: list[SigningResponse] + ) -> SignedTransaction: + signed_spends = [] + for spend in spends: + mod, curried_args = spend.puzzle.uncurry() + if match_vault_puzzle(mod, curried_args): + new_sol = spend.solution.replace(rrfrrfrrf=signing_responses[0].signature) + signed_spends.append(Spend(spend.coin, spend.puzzle, new_sol)) + else: + signed_spends.append(spend) + return SignedTransaction( + TransactionInfo(signed_spends), + [], + ) + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> list[SigningResponse]: + root_pubkey = self.wallet_state_manager.observation_root + # Temporary access to private key + sk: ec.EllipticCurvePrivateKey = self.wallet_state_manager.config["test_sk"] + sk_lookup: dict[int, ec.EllipticCurvePrivateKey] = {root_pubkey.get_fingerprint(): sk} + responses: list[SigningResponse] = [] + + # We don't need to expand path and sum hints since vault signer always uses the same keys + # so just sign the targets + for target in signing_instructions.targets: + fingerprint: int = int.from_bytes(target.fingerprint, "big") + if fingerprint not in sk_lookup: + raise ValueError(f"Pubkey {fingerprint} not found") + der_sig = sk_lookup[fingerprint].sign(target.message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + responses.append( + SigningResponse( + sig, + target.hook, + ) + ) + + return responses + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: + return None + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: + return None + + async def make_solution( + self, + primaries: list[Payment], + action_scope: WalletActionScope, + conditions: tuple[Condition, ...] = tuple(), + fee: uint64 = uint64(0), + **kwargs: Any, + ) -> Program: + assert fee >= 0 + coin_id = kwargs.get("coin_id") + if coin_id is None: + raise ValueError("Vault p2_singleton solutions require a coin id") + p2_singleton_solution: Program = Program.to([self.vault_info.inner_puzzle_hash, coin_id]) + return p2_singleton_solution + + async def get_puzzle(self, new: bool) -> Program: + if new: + return await self.get_new_puzzle() + else: + record: Optional[ + DerivationRecord + ] = await self.wallet_state_manager.get_current_derivation_record_for_wallet(self.id()) + if record is None: + return await self.get_new_puzzle() + assert isinstance(record._pubkey, bytes) + puzzle = construct_p2_delegated_secp( + record._pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, record.puzzle_hash + ) + return puzzle + + def puzzle_hash_for_pk(self, pubkey: ObservationRoot) -> bytes32: + raise ValueError("This won't work") + + def require_derivation_paths(self) -> bool: + if getattr(self, "_vault_info", None): + return True + return False + + async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: + wallet_identifier: Optional[ + WalletIdentifier + ] = await self.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash(hint) + if wallet_identifier: + return True + return False + + def handle_own_derivation(self) -> bool: + return True + + def get_p2_singleton_puzzle_hash(self) -> bytes32: + return get_p2_singleton_puzzle_hash(self.launcher_id) + + async def select_coins(self, amount: uint64, action_scope: WalletActionScope) -> set[Coin]: + unconfirmed_removals: dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet( + self.id() + ) + puzhash = self.get_p2_singleton_puzzle_hash() + records = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash(puzhash) + assert records + spendable_amount = uint128(sum(rec.coin.amount for rec in records)) + async with action_scope.use() as interface: + coins = await select_coins( + spendable_amount, + action_scope.config.adjust_for_side_effects(interface.side_effects).tx_config.coin_selection_config, + records, + unconfirmed_removals, + self.log, + uint128(amount), + ) + interface.side_effects.selected_coins.extend([*coins]) + return coins + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: + hidden_puzzle = get_vault_hidden_puzzle_with_index(uint32(index)) + hidden_puzzle_hash = hidden_puzzle.get_tree_hash() + inner_puzzle_hash = get_vault_inner_puzzle_hash( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, + ) + record = DerivationRecord( + uint32(index), inner_puzzle_hash, self.vault_info.pubkey, self.type(), self.id(), False + ) + return [record] + + async def create_recovery_spends( + self, + secp_pk: bytes, + hidden_puzzle_hash: bytes32, + genesis_challenge: bytes32, + action_scope: WalletActionScope, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, + ) -> tuple[bytes32, bytes32]: + """ + Returns two tx IDs + 1. Recover the vault which can be taken to the appropriate BLS wallet for signing + 2. Complete the recovery after the timelock has elapsed + """ + assert self.vault_info.recovery_info is not None + wallet_node: Any = self.wallet_state_manager.wallet_node + peer = wallet_node.get_full_node_peer() + assert peer is not None + # 1. Generate the recovery spend + # Get the current vault coin, ensure it's unspent + vault_coin = self.vault_info.coin + amount = uint64(self.vault_info.coin.amount) + vault_coin_state = (await wallet_node.get_coin_state([vault_coin.name()], peer))[0] + assert vault_coin_state.spent_height is None + + # get the new vault puzhash we'll recover to + new_vault_inner_puzhash = get_vault_inner_puzzle_hash( + secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock + ) + memos = [secp_pk, hidden_puzzle_hash] + if bls_pk: + memos.extend([bls_pk.to_bytes(), Program.to(timelock).as_atom()]) + + # Generate the current inner puzzle + inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, + ) + assert inner_puzzle.get_tree_hash() == self.vault_info.inner_puzzle_hash + + secp_puzzle_hash = construct_p2_delegated_secp( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + ).get_tree_hash() + + recovery_puzzle = get_recovery_puzzle( + secp_puzzle_hash, self.vault_info.recovery_info.bls_pk, self.vault_info.recovery_info.timelock + ) + recovery_puzzle_hash = recovery_puzzle.get_tree_hash() + assert isinstance(self.vault_info.recovery_info.bls_pk, G1Element) + recovery_solution = get_recovery_solution(new_vault_inner_puzhash, amount, memos) + + merkle_tree = construct_vault_merkle_tree(secp_puzzle_hash, recovery_puzzle_hash) + proof = get_vault_proof(merkle_tree, recovery_puzzle_hash) + inner_solution = get_vault_inner_solution(recovery_puzzle, recovery_solution, proof) + + full_puzzle = get_vault_full_puzzle(self.launcher_id, inner_puzzle) + assert full_puzzle.get_tree_hash() == vault_coin.puzzle_hash + + full_solution = get_vault_full_solution(self.vault_info.lineage_proof, amount, inner_solution) + recovery_spend = WalletSpendBundle([make_spend(vault_coin, full_puzzle, full_solution)], G2Element()) + + # 2. Generate the Finish Recovery Spend + assert isinstance(self.vault_info.recovery_info.bls_pk, G1Element) + assert isinstance(self.vault_info.recovery_info.timelock, uint64) + + recovery_finish_puzzle = get_recovery_finish_puzzle( + new_vault_inner_puzhash, self.vault_info.recovery_info.timelock, amount, memos + ) + recovery_finish_solution = Program.to([]) + recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) + full_recovery_puzzle = get_vault_full_puzzle(self.launcher_id, recovery_inner_puzzle) + recovery_coin = Coin(vault_coin.name(), full_recovery_puzzle.get_tree_hash(), amount) + recovery_solution = get_vault_inner_solution(recovery_finish_puzzle, recovery_finish_solution, proof) + lineage = LineageProof(vault_coin.parent_coin_info, inner_puzzle.get_tree_hash(), amount) + full_recovery_solution = get_vault_full_solution(lineage, amount, recovery_solution) + finish_spend = WalletSpendBundle( + [make_spend(recovery_coin, full_recovery_puzzle, full_recovery_solution)], G2Element() + ) + new_vault_coin_id = finish_spend.additions()[0].name() + await self.wallet_state_manager.add_interested_coin_ids( + [recovery_coin.name(), new_vault_coin_id], [self.id(), self.id()] + ) + + recovery_tx = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=full_puzzle.get_tree_hash(), + amount=amount, + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=recovery_spend, + additions=recovery_spend.additions(), + removals=recovery_spend.removals(), + wallet_id=self.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=recovery_spend.name(), + valid_times=parse_timelock_info(tuple()), + ) + + finish_tx = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=full_puzzle.get_tree_hash(), + amount=amount, + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=finish_spend, + additions=finish_spend.additions(), + removals=finish_spend.removals(), + wallet_id=self.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=finish_spend.name(), + valid_times=parse_timelock_info(tuple()), + ) + + async with action_scope.use() as interface: + interface.side_effects.transactions.extend([recovery_tx, finish_tx]) + + return (recovery_tx.name, finish_tx.name) + + async def sync_vault_launcher(self) -> None: + wallet_node: Any = self.wallet_state_manager.wallet_node + peer = wallet_node.get_full_node_peer() + assert peer is not None + + assert isinstance(self.wallet_state_manager.observation_root, VaultRoot) + + coin_states = await wallet_node.get_coin_state([self.launcher_id], peer) + if not coin_states: + raise ValueError(f"No coin found for launcher id: {self.launcher_id}.") + coin_state: CoinState = coin_states[0] + + assert coin_state.spent_height is not None + launcher_spend = await fetch_coin_spend(uint32(coin_state.spent_height), coin_state.coin, peer) + launcher_solution = launcher_spend.solution.to_program() + + is_recoverable = False + bls_pk = None + timelock = None + memos = launcher_solution.at("rrf") + secp_pk = memos.at("f").as_atom() + hidden_puzzle_hash = bytes32(memos.at("rf").as_atom()) + if memos.list_len() == 4: + bls_pk = G1Element.from_bytes(memos.at("rrf").as_atom()) + timelock = uint64(memos.at("rrrf").as_int()) + recovery_info = RecoveryInfo(bls_pk, timelock) + is_recoverable = True + else: + recovery_info = RecoveryInfo(None, None) + is_recoverable = False + inner_puzzle = get_vault_inner_puzzle( + secp_pk, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock + ) + inner_puzzle_hash = inner_puzzle.get_tree_hash() + lineage_proof = LineageProof(coin_state.coin.parent_coin_info, None, uint64(coin_state.coin.amount)) + vault_puzzle_hash = get_vault_full_puzzle(coin_state.coin.name(), inner_puzzle).get_tree_hash() + vault_coin = Coin(self.launcher_id, vault_puzzle_hash, uint64(coin_state.coin.amount)) + vault_info = VaultInfo( + vault_coin, + secp_pk, + hidden_puzzle_hash, + inner_puzzle_hash, + lineage_proof, + is_recoverable, + recovery_info, + ) + await self.save_info(vault_info) + await self.wallet_state_manager.create_more_puzzle_hashes() + + # subscribe to p2_singleton puzzle hash + p2_puzzle_hash = self.get_p2_singleton_puzzle_hash() + await self.wallet_state_manager.add_interested_puzzle_hashes([p2_puzzle_hash], [self.id()]) + + # add the singleton record to store + await self.wallet_state_manager.singleton_store.add_eve_record( + self.id(), + coin_state.coin, + launcher_spend, + inner_puzzle_hash, + lineage_proof, + uint32(coin_state.spent_height) if coin_state.spent_height else uint32(0), + pending=False, + custom_data=vault_info.stream_to_bytes(), + ) + + async def update_vault_singleton( + self, next_inner_puzzle: Program, coin_spend: CoinSpend, coin_state: CoinState + ) -> None: + hints, _ = compute_spend_hints_and_additions(coin_spend) + inner_puzzle_hash = hints[coin_state.coin.name()].hint + dr = None + new_recovery_info = None + replace_key = False + replace_recovery = False + if inner_puzzle_hash is not None: + dr = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(inner_puzzle_hash) + + if dr is None: + # We're in recovery mode + puzzle, curried_args = coin_spend.puzzle_reveal.to_program().uncurry() + solution = coin_spend.solution.to_program() + if match_recovery_puzzle(puzzle, curried_args, solution): + # recreate the vault inner puz with the recovery settings + # hidden_puzzle_hash = None + next_inner_puzzle = get_recovery_puzzle_from_spend(coin_spend) + elif match_finish_spend(coin_spend): + # We've finished the recovery and have a new key + ( + new_secp_pk, + hidden_puzzle_hash, + new_bls_pk, + new_timelock, + ) = get_new_vault_info_from_spend(coin_spend) + next_inner_puzzle = get_vault_inner_puzzle( + new_secp_pk, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + new_bls_pk, + new_timelock, + ) + if new_bls_pk: + new_recovery_info = RecoveryInfo(bls_pk=new_bls_pk, timelock=new_timelock) + replace_recovery = True + replace_key = True + else: + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(dr.index).get_tree_hash() + next_inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, + ) + + assert get_vault_full_puzzle(self.launcher_id, next_inner_puzzle).get_tree_hash() == coin_state.coin.puzzle_hash + # get the parent state to create lineage proof + wallet_node: Any = self.wallet_state_manager.wallet_node + peer = wallet_node.get_full_node_peer() + assert peer is not None + parent_state = (await wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer))[0] + parent_spend = await fetch_coin_spend(uint32(parent_state.spent_height), parent_state.coin, peer) + parent_puzzle = parent_spend.puzzle_reveal.to_program() + parent_inner_puzzle_hash = parent_puzzle.uncurry()[1].at("rf").get_tree_hash() + lineage_proof = LineageProof( + parent_state.coin.parent_coin_info, parent_inner_puzzle_hash, parent_state.coin.amount + ) + if replace_recovery: + assert isinstance(new_recovery_info, RecoveryInfo) + recovery: RecoveryInfo = new_recovery_info + else: + recovery = self.vault_info.recovery_info + new_vault_info = VaultInfo( + coin_state.coin, + self.vault_info.pubkey if not replace_key else new_secp_pk, + hidden_puzzle_hash if dr else self.vault_info.hidden_puzzle_hash, + next_inner_puzzle.get_tree_hash(), + lineage_proof, + self.vault_info.is_recoverable if not replace_key else replace_recovery, + recovery, + ) + + await self.update_vault_store(new_vault_info, coin_spend) + await self.save_info(new_vault_info) + + async def save_info(self, vault_info: VaultInfo) -> None: + self._vault_info = vault_info + + async def update_vault_store(self, vault_info: VaultInfo, coin_spend: CoinSpend) -> None: + custom_data = vault_info.stream_to_bytes() + await self.wallet_state_manager.singleton_store.add_spend(self.id(), coin_spend, custom_data=custom_data) diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index 2df1ce6efb4b..4e8fd1c03245 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -52,11 +52,10 @@ ) from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential from chia.wallet.vc_wallet.vc_wallet import VCWallet -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import MetadataTypes, WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle if TYPE_CHECKING: @@ -68,7 +67,7 @@ class CRCATWallet(CATWallet): log: logging.Logger wallet_info: WalletInfo info: CRCATInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol @staticmethod def default_wallet_name_for_unknown_cat(limitations_program_hash_hex: str) -> str: @@ -81,7 +80,7 @@ def cost_of_single_tx(self) -> int: @staticmethod async def create_new_cat_wallet( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, cat_tail_info: dict[str, Any], amount: uint64, action_scope: WalletActionScope, @@ -94,7 +93,7 @@ async def create_new_cat_wallet( @staticmethod async def get_or_create_wallet_for_cat( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, limitations_program_hash_hex: str, name: Optional[str] = None, authorized_providers: Optional[list[bytes32]] = None, @@ -130,7 +129,7 @@ async def get_or_create_wallet_for_cat( async def create_from_puzzle_info( cls, wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, puzzle_driver: PuzzleInfo, name: Optional[str] = None, # We're hinting this as Any for mypy by should explore adding this to the wallet protocol and hinting properly @@ -151,7 +150,7 @@ async def create_from_puzzle_info( @staticmethod async def create( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, ) -> CRCATWallet: self = CRCATWallet() @@ -516,8 +515,9 @@ async def _generate_unsigned_spendbundle( action_scope, extra_conditions=(announcement.corresponding_assertion(),), ) - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) elif regular_chia_to_claim > fee: @@ -527,21 +527,24 @@ async def _generate_unsigned_spendbundle( action_scope, ) assert xch_announcement is not None - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, xch_announcement, announcement), ) else: # TODO: what about when they are equal? raise Exception("Equality not handled") else: - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) else: - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=[], + action_scope=action_scope, conditions=(announcement.corresponding_assertion(),), ) inner_derivation_record = ( diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index d21f26f0557e..04c4fe043cd5 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -27,6 +27,7 @@ UnknownCondition, parse_timelock_info, ) +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.payment import Payment from chia.wallet.puzzle_drivers import Solver @@ -41,11 +42,10 @@ from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, CRCATSpend, ProofsChecker, construct_pending_approval_state from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord, VCStore -from chia.wallet.wallet import Wallet from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol from chia.wallet.wallet_spend_bundle import WalletSpendBundle if TYPE_CHECKING: @@ -57,7 +57,7 @@ class VCWallet: wallet_state_manager: WalletStateManager log: logging.Logger - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_info: WalletInfo store: VCStore @@ -65,7 +65,7 @@ class VCWallet: async def create_new_vc_wallet( cls: type[_T_VCWallet], wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, name: Optional[str] = None, ) -> _T_VCWallet: name = "VCWallet" if name is None else name @@ -82,7 +82,7 @@ async def create_new_vc_wallet( async def create( cls: type[_T_VCWallet], wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> _T_VCWallet: @@ -297,8 +297,9 @@ async def generate_signed_transaction( else: magic_condition = vc_record.vc.standard_magic_condition() extra_conditions = (*extra_conditions, UnknownCondition.from_program(magic_condition)) - innersol: Program = self.standard_wallet.make_solution( + innersol: Program = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=extra_conditions, ) did_announcement, coin_spend, _vc = vc_record.vc.do_spend(inner_puzzle, innersol, new_proof_hash) @@ -633,6 +634,12 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return False + def handle_own_derivation(self) -> bool: # pragma: no cover + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() + if TYPE_CHECKING: _dummy: WalletProtocol[VerifiedCredential] = VCWallet() # pragma: no cover diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index ac61c0cad843..9cbb7755a740 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -13,11 +13,19 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend, make_spend from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode +from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 +from chia.util.observation_root import ObservationRoot from chia.util.streamable import Streamable from chia.wallet.coin_selection import select_coins -from chia.wallet.conditions import AssertCoinAnnouncement, Condition, CreateCoinAnnouncement, parse_timelock_info +from chia.wallet.conditions import ( + AssertCoinAnnouncement, + Condition, + CreateCoin, + CreateCoinAnnouncement, + parse_timelock_info, +) from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import ( MAX_POOL_WALLETS, @@ -43,6 +51,7 @@ SignedTransaction, SigningInstructions, SigningResponse, + SigningTarget, Spend, SumHint, TransactionInfo, @@ -81,6 +90,7 @@ async def create( self = Wallet() self.log = logging.getLogger(name) self.wallet_state_manager = wallet_state_manager + self.wallet_info = info self.wallet_id = info.id return self @@ -159,10 +169,12 @@ async def get_pending_change_balance(self) -> uint64: def require_derivation_paths(self) -> bool: return True - def puzzle_for_pk(self, pubkey: G1Element) -> Program: + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: + assert isinstance(pubkey, G1Element), "Standard wallet cannot support non-BLS keys yet" return puzzle_for_pk(pubkey) - def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: + def puzzle_hash_for_pk(self, pubkey: ObservationRoot) -> bytes32: + assert isinstance(pubkey, G1Element), "Standard wallet cannot support non-BLS keys yet" return puzzle_hash_for_pk(pubkey) async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: @@ -204,11 +216,13 @@ async def get_new_puzzlehash(self) -> bytes32: puzhash = (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash return puzhash - def make_solution( + async def make_solution( self, primaries: list[Payment], + action_scope: WalletActionScope, conditions: tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + **kwargs: Any, ) -> Program: assert fee >= 0 condition_list: list[Any] = [condition.to_program() for condition in conditions] @@ -281,7 +295,12 @@ async def _generate_unsigned_transaction( if primaries_input is not None: primaries.extend(primaries_input) - total_amount = amount + sum(primary.amount for primary in primaries) + fee + total_amount = ( + amount + + sum(primary.amount for primary in primaries) + + fee + + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + ) total_balance = await self.get_spendable_balance() if coins is None: if total_amount > total_balance: @@ -342,8 +361,9 @@ async def _generate_unsigned_transaction( message_list.append(Coin(coin.name(), primary.puzzle_hash, primary.amount).name()) message: bytes32 = std_hash(b"".join(message_list)) puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) - solution: Program = self.make_solution( + solution: Program = await self.make_solution( primaries=primaries, + action_scope=action_scope, fee=fee, conditions=(*extra_conditions, CreateCoinAnnouncement(message)), ) @@ -364,7 +384,9 @@ async def _generate_unsigned_transaction( if coin.name() == origin_id: continue puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) - solution = self.make_solution(primaries=[], conditions=(primary_announcement,)) + solution = await self.make_solution( + primaries=[], action_scope=action_scope, conditions=(primary_announcement,) + ) solution = decorator_manager.solve(puzzle, [], solution) spends.append( make_spend( @@ -379,6 +401,7 @@ async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMo # CHIP-0002 message signing as documented at: # https://github.com/Chia-Network/chips/blob/80e4611fe52b174bf1a0382b9dff73805b18b8c6/CHIPs/chip-0002.md#signmessage private = await self.wallet_state_manager.get_private_key(puzzle_hash) + assert isinstance(private, PrivateKey) synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) synthetic_pk = synthetic_secret_key.get_g1() if mode == SigningMode.CHIP_0002_HEX_INPUT: @@ -412,9 +435,11 @@ async def generate_signed_transaction( 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 + non_change_amount: int = amount else: - non_change_amount = uint64(amount + sum(p.amount for p in primaries)) + non_change_amount = amount + sum(p.amount for p in primaries) + + non_change_amount += sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) self.log.debug("Generating transaction for: %s %s %s", puzzle_hash, amount, repr(coins)) transaction = await self._generate_unsigned_transaction( @@ -536,43 +561,46 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash( puzzle_hash_for_synthetic_public_key(pk_parsed) ) - root_pubkey: bytes = self.wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big") + root_fingerprint: bytes = self.wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big") if index is None: # Pool wallet may have a secret key here - if self.wallet_state_manager.private_key is not None: + if self.wallet_state_manager.private_key is not None and isinstance( + self.wallet_state_manager.private_key, PrivateKey + ): for pool_wallet_index in range(MAX_POOL_WALLETS): try_owner_sk = master_sk_to_singleton_owner_sk( self.wallet_state_manager.private_key, uint32(pool_wallet_index) ) if try_owner_sk.get_g1() == pk_parsed: return PathHint( - root_pubkey, + root_fingerprint, [uint64(12381), uint64(8444), uint64(5), uint64(pool_wallet_index)], ) return None return PathHint( - root_pubkey, + root_fingerprint, [uint64(12381), uint64(8444), uint64(2), uint64(index)], ) async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> list[SigningResponse]: - root_pubkey: G1Element = self.wallet_state_manager.root_pubkey - pk_lookup: dict[int, G1Element] = ( - {root_pubkey.get_fingerprint(): root_pubkey} if self.wallet_state_manager.private_key is not None else {} - ) - sk_lookup: dict[int, PrivateKey] = ( - {root_pubkey.get_fingerprint(): self.wallet_state_manager.get_master_private_key()} - if self.wallet_state_manager.private_key is not None - else {} - ) + assert isinstance(self.wallet_state_manager.observation_root, G1Element) + root_pubkey: G1Element = self.wallet_state_manager.observation_root + pk_lookup: dict[int, G1Element] = {} + sk_lookup: dict[int, PrivateKey] = {} aggregate_responses_at_end: bool = True responses: list[SigningResponse] = [] # TODO: expand path hints and sum hints recursively (a sum hint can give a new key to path hint) # Next, expand our pubkey set with path hints if self.wallet_state_manager.private_key is not None: + root_secret_key = self.wallet_state_manager.get_master_private_key() + assert isinstance(root_secret_key, PrivateKey) + root_fingerprint = root_pubkey.get_fingerprint() + pk_lookup[root_fingerprint] = root_pubkey + sk_lookup[root_fingerprint] = root_secret_key + for path_hint in signing_instructions.key_hints.path_hints: if int.from_bytes(path_hint.root_fingerprint, "big") != root_pubkey.get_fingerprint(): if not partial_allowed: @@ -581,12 +609,10 @@ async def execute_signing_instructions( continue else: path = [int(step) for step in path_hint.path] - derive_child_sk = _derive_path(self.wallet_state_manager.get_master_private_key(), path) - derive_child_sk_unhardened = _derive_path_unhardened( - self.wallet_state_manager.get_master_private_key(), path - ) - derive_child_pk = derive_child_sk.get_g1() - derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1() + derive_child_sk = _derive_path(root_secret_key, path) + derive_child_sk_unhardened = _derive_path_unhardened(root_secret_key, path) + derive_child_pk = derive_child_sk.public_key() + derive_child_pk_unhardened = derive_child_sk_unhardened.public_key() pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk pk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_pk_unhardened sk_lookup[derive_child_pk.get_fingerprint()] = derive_child_sk @@ -696,3 +722,34 @@ async def apply_signatures( ) ], ) + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: # pragma: no cover + raise NotImplementedError() + + async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: + pks: list[bytes] = [] + signing_targets: list[SigningTarget] = [] + for coin_spend in coin_spends: + _coin_spend = coin_spend.as_coin_spend() + # Get AGG_SIG conditions + conditions_dict = conditions_dict_for_solution( + _coin_spend.puzzle_reveal.to_program(), + _coin_spend.solution.to_program(), + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + ) + # Create signature + for pk, msg in pkm_pairs_for_conditions_dict( + conditions_dict, _coin_spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ): + pk_bytes = pk.to_bytes() + pks.append(pk_bytes) + fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") + signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) + + return SigningInstructions( + await self.wallet_state_manager.key_hints_for_pubkeys(pks), + signing_targets, + ) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 6a220e009e06..751baac578b7 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -52,8 +52,10 @@ from chia.util.hash import std_hash from chia.util.ints import uint16, uint32, uint64, uint128 from chia.util.keychain import Keychain +from chia.util.observation_root import ObservationRoot from chia.util.path import path_from_root from chia.util.profiler import mem_profile_task, profile_task +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, streamable from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings from chia.wallet.transaction_record import TransactionRecord @@ -216,30 +218,30 @@ def rollback_request_caches(self, reorg_height: int) -> None: cache.clear_after_height(reorg_height) @overload - async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[G1Element]: ... + async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ObservationRoot]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: Literal[True] - ) -> Optional[PrivateKey]: ... + ) -> Optional[SecretInfo[Any]]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: Literal[False] - ) -> Optional[G1Element]: ... + ) -> Optional[ObservationRoot]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool - ) -> Optional[Union[PrivateKey, G1Element]]: ... + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool = False - ) -> Optional[Union[PrivateKey, G1Element]]: + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: try: keychain_proxy = await self.ensure_keychain_proxy() # Returns first key if fingerprint is None - key: Optional[Union[PrivateKey, G1Element]] = await keychain_proxy.get_key_for_fingerprint( + key: Optional[Union[SecretInfo[Any], ObservationRoot]] = await keychain_proxy.get_key_for_fingerprint( fingerprint, private=private ) except KeychainIsEmpty: @@ -258,23 +260,46 @@ async def get_key_for_fingerprint( return key + @overload + async def get_key(self, fingerprint: Optional[int]) -> Optional[ObservationRoot]: ... + + @overload + async def get_key(self, fingerprint: Optional[int], private: Literal[True]) -> Optional[SecretInfo[Any]]: ... + + @overload + async def get_key(self, fingerprint: Optional[int], private: Literal[False]) -> Optional[ObservationRoot]: ... + + @overload + async def get_key( + self, fingerprint: Optional[int], private: bool + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... + + @overload + async def get_key( + self, fingerprint: Optional[int], private: bool, find_a_default: bool + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... + async def get_key( self, fingerprint: Optional[int], private: bool = True, find_a_default: bool = True - ) -> Optional[Union[PrivateKey, G1Element]]: + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: """ Attempt to get the private key for the given fingerprint. If the fingerprint is None, get_key_for_fingerprint() will return the first private key. Similarly, if a key isn't returned for the provided fingerprint, the first key will be returned. """ - key: Optional[Union[PrivateKey, G1Element]] = await self.get_key_for_fingerprint(fingerprint, private=private) + key: Optional[Union[SecretInfo[Any], ObservationRoot]] = await self.get_key_for_fingerprint( + fingerprint, private=private + ) if key is None and fingerprint is not None and find_a_default: key = await self.get_key_for_fingerprint(None, private=private) if key is not None: - if isinstance(key, PrivateKey): - fp = key.get_g1().get_fingerprint() + if private: + # Mypy can't understand that private being True both ensures SecretInfo[Any] and this branch + fp = key.public_key().get_fingerprint() # type: ignore[union-attr] else: - fp = key.get_fingerprint() + # Mypy can't understand that private being False both ensures ObservationRoot and this branch + fp = key.get_fingerprint() # type: ignore[union-attr] self.log.info(f"Using first key found (fingerprint: {fp})") return key @@ -324,6 +349,7 @@ async def reset_sync_db(self, db_path: Union[Path, str], fingerprint: int) -> bo "tx_times", "pool_state_transitions", "singleton_records", + "singletons", "mirrors", "mirror_confirmations", "launchers", @@ -397,31 +423,34 @@ async def _start_with_fingerprint( # got Future attached to a different loop self._new_peak_queue = NewPeakQueue(inner_queue=asyncio.PriorityQueue()) if not fingerprint: - fingerprint = self.get_last_used_fingerprint() + fingerprint = await self.get_last_used_fingerprint_if_exists() multiprocessing_start_method = process_config_start_method(config=self.config, log=self.log) multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) self._weight_proof_handler = WalletWeightProofHandler(self.constants, multiprocessing_context) self.synced_peers = set() - public_key = None + observation_root = None private_key = await self.get_key(fingerprint, private=True, find_a_default=False) if private_key is None: - public_key = await self.get_key(fingerprint, private=False, find_a_default=False) + observation_root = await self.get_key(fingerprint, private=False, find_a_default=False) else: - assert isinstance(private_key, PrivateKey) - public_key = private_key.get_g1() - - if public_key is None: + if not isinstance(private_key, PrivateKey): + raise ValueError( + "Cannot start wallet with non-BLS public key." + "Try starting in observer mode first and adding private key by some other means." + ) + observation_root = private_key.public_key() + if observation_root is None: private_key = await self.get_key(None, private=True, find_a_default=True) if private_key is not None: assert isinstance(private_key, PrivateKey) - public_key = private_key.get_g1() + observation_root = private_key.get_g1() else: self.log_out() return False - assert isinstance(public_key, G1Element) # override with private key fetched in case it's different from what was passed + assert isinstance(observation_root, ObservationRoot) if fingerprint is None: - fingerprint = public_key.get_fingerprint() + fingerprint = observation_root.get_fingerprint() if self.config.get("enable_profiler", False): if sys.getprofile() is not None: self.log.warning("not enabling profiler, getprofile() is already set") @@ -445,7 +474,7 @@ async def _start_with_fingerprint( self.server, self.root_path, self, - public_key, + observation_root, ) if self.state_changed_callback is not None: @@ -687,6 +716,12 @@ def get_last_used_fingerprint(self) -> Optional[int]: self.log.exception("Non-fatal: Unable to read last used fingerprint.") return fingerprint + async def get_last_used_fingerprint_if_exists(self) -> Optional[int]: + fingerprint = self.get_last_used_fingerprint() + if fingerprint is not None and await self.get_key_for_fingerprint(fingerprint) is None: + fingerprint = None + return fingerprint + def get_last_used_fingerprint_path(self) -> Path: db_path: Path = path_from_root(self.root_path, self.config["database_path"]) fingerprint_path = db_path.parent / "last_used_fingerprint" diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index e80427d3573a..4d6c4e1a20c1 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -1,16 +1,30 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Optional, TypeVar, runtime_checkable -from chia_rs import G1Element -from typing_extensions import NotRequired, Protocol, TypedDict +from chia_rs import G1Element, G2Element +from typing_extensions import NotRequired, Protocol, TypedDict, Unpack from chia.server.ws_connection import WSChiaConnection 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.signing_mode import SigningMode from chia.util.ints import uint32, uint64, uint128 +from chia.util.observation_root import ObservationRoot +from chia.wallet.conditions import Condition +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.nft_wallet.nft_info import NFTCoinInfo +from chia.wallet.payment import Payment +from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata +from chia.wallet.signer_protocol import ( + PathHint, + SignedTransaction, + SigningInstructions, + SigningResponse, + Spend, + SumHint, +) from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_action_scope import WalletActionScope from chia.wallet.wallet_coin_record import WalletCoinRecord @@ -23,6 +37,7 @@ T = TypeVar("T", contravariant=True) +@runtime_checkable class WalletProtocol(Protocol[T]): @classmethod def type(cls) -> WalletType: ... @@ -57,10 +72,97 @@ def get_name(self) -> str: ... async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: ... + def handle_own_derivation(self) -> bool: ... + + def derivation_for_index(self, index: int) -> list[DerivationRecord]: ... + wallet_info: WalletInfo wallet_state_manager: WalletStateManager +@runtime_checkable +class MainWalletProtocol(WalletProtocol[ClawbackMetadata], Protocol): + @property + def max_send_quantity(self) -> int: ... + + @staticmethod + async def create( + wallet_state_manager: Any, + info: WalletInfo, + name: str = ..., + ) -> MainWalletProtocol: ... + + async def get_new_puzzle(self) -> Program: ... + + async def get_new_puzzlehash(self) -> bytes32: ... + + # This isn't part of the WalletProtocol but it should be + # Also this doesn't likely conform to the eventual one that ends up in WalletProtocol + async def generate_signed_transaction( + self, + amount: uint64, + puzzle_hash: bytes32, + action_scope: WalletActionScope, + fee: uint64 = uint64(0), + coins: Optional[set[Coin]] = None, + primaries: Optional[list[Payment]] = None, + memos: Optional[list[bytes]] = None, + puzzle_decorator_override: Optional[list[dict[str, Any]]] = None, + extra_conditions: tuple[Condition, ...] = tuple(), + **kwargs: Unpack[GSTOptionalArgs], + ) -> None: ... + + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: ... + + async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: ... + + async def sign_message( + self, message: str, puzzle_hash: bytes32, mode: SigningMode + ) -> tuple[G1Element, G2Element]: ... + + async def get_puzzle_hash(self, new: bool) -> bytes32: ... + + async def apply_signatures( + self, spends: list[Spend], signing_responses: list[SigningResponse] + ) -> SignedTransaction: ... + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> list[SigningResponse]: ... + + async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: ... + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: ... + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: ... + + async def create_tandem_xch_tx( + self, + fee: uint64, + action_scope: WalletActionScope, + extra_conditions: tuple[Condition, ...] = tuple(), + ) -> None: ... + + async def make_solution( + self, + primaries: list[Payment], + action_scope: WalletActionScope, + conditions: tuple[Condition, ...] = tuple(), + fee: uint64 = uint64(0), + ) -> Program: ... + + async def get_puzzle(self, new: bool) -> Program: ... + + async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: ... + + async def get_coins_to_offer( + self, + asset_id: Optional[bytes32], + amount: uint64, + action_scope: WalletActionScope, + ) -> set[Coin]: ... + + class GSTOptionalArgs(TypedDict): # DataLayerWallet launcher_id: NotRequired[Optional[bytes32]] diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index f4e3d3cbdad1..76c6a9990c2f 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -84,7 +84,7 @@ async def add_derivation_paths(self, records: list[DerivationRecord]) -> None: sql_records.append( ( record.index, - bytes(record.pubkey).hex(), + record.pubkey_bytes.hex(), record.puzzle_hash.hex(), record.wallet_type, record.wallet_id, @@ -174,10 +174,11 @@ async def puzzle_hash_exists(self, puzzle_hash: bytes32) -> bool: return row is not None def row_to_record(self, row) -> DerivationRecord: + pk_bytes = bytes.fromhex(row[1]) return DerivationRecord( uint32(row[0]), bytes32.fromhex(row[2]), - G1Element.from_bytes(bytes.fromhex(row[1])), + G1Element.from_bytes(pk_bytes) if len(pk_bytes) == 48 else pk_bytes, WalletType(row[3]), uint32(row[4]), bool(row[5]), diff --git a/chia/wallet/wallet_singleton_store.py b/chia/wallet/wallet_singleton_store.py index 325aaec0be83..82980cdc5479 100644 --- a/chia/wallet/wallet_singleton_store.py +++ b/chia/wallet/wallet_singleton_store.py @@ -81,35 +81,69 @@ async def save_singleton(self, record: SingletonRecord) -> None: ), ) + async def add_eve_record( + self, + wallet_id: uint32, + eve_coin: Coin, + parent_coin_spend: CoinSpend, + inner_puzzle_hash: bytes32, + lineage_proof: LineageProof, + removed_height: uint32 = uint32(0), + pending: bool = False, + custom_data: Optional[bytes] = None, + ) -> None: + pending_int = 1 if pending else False + async with self.db_wrapper.writer_maybe_transaction() as conn: + columns = ( + "coin_id, coin, singleton_id, wallet_id, parent_coin_spend, inner_puzzle_hash, " + "pending, removed_height, lineage_proof, custom_data" + ) + await conn.execute( + f"INSERT or REPLACE INTO singletons ({columns}) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + eve_coin.name().hex(), + json.dumps(eve_coin.to_json_dict()), + eve_coin.name().hex(), + wallet_id, + bytes(parent_coin_spend), + inner_puzzle_hash, + pending_int, + removed_height, + bytes(lineage_proof), + custom_data, + ), + ) + async def add_spend( self, wallet_id: uint32, - coin_state: CoinSpend, + coin_spend: CoinSpend, block_height: uint32 = uint32(0), pending: bool = True, + custom_data: Optional[bytes] = None, ) -> None: """Given a coin spend of a singleton, attempt to calculate the child coin and details for the new singleton record. Add the new record to the store and remove the old record if it exists """ # get singleton_id from puzzle_reveal - singleton_id = get_singleton_id_from_puzzle(coin_state.puzzle_reveal) + singleton_id = get_singleton_id_from_puzzle(coin_spend.puzzle_reveal) if not singleton_id: raise RuntimeError("Coin to add is not a valid singleton") # get details for singleton record conditions = conditions_dict_for_solution( - coin_state.puzzle_reveal, coin_state.solution, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM + coin_spend.puzzle_reveal, coin_spend.solution, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM ) cc_cond = [cond for cond in conditions[ConditionOpcode.CREATE_COIN] if int_from_bytes(cond.vars[1]) % 2 == 1][0] - coin = Coin(coin_state.coin.name(), cc_cond.vars[0], uint64(int_from_bytes(cc_cond.vars[1]))) - inner_puz = get_inner_puzzle_from_singleton(coin_state.puzzle_reveal) + coin = Coin(coin_spend.coin.name(), cc_cond.vars[0], uint64(int_from_bytes(cc_cond.vars[1]))) + inner_puz = get_inner_puzzle_from_singleton(coin_spend.puzzle_reveal) if inner_puz is None: # pragma: no cover - raise RuntimeError("Could not get inner puzzle from puzzle reveal in coin spend %s", coin_state) + raise RuntimeError("Could not get inner puzzle from puzzle reveal in coin spend %s", coin_spend) - lineage_bytes = [x.as_atom() for x in coin_state.solution.to_program().first().as_iter()] + lineage_bytes = [x.as_atom() for x in coin_spend.solution.to_program().first().as_iter()] if len(lineage_bytes) == 2: lineage_proof = LineageProof(bytes32(lineage_bytes[0]), None, uint64(int_from_bytes(lineage_bytes[1]))) else: @@ -118,13 +152,13 @@ async def add_spend( ) # Create and save the new singleton record new_record = SingletonRecord( - coin, singleton_id, wallet_id, coin_state, inner_puz.get_tree_hash(), pending, 0, lineage_proof, None + coin, singleton_id, wallet_id, coin_spend, inner_puz.get_tree_hash(), pending, 0, lineage_proof, custom_data ) await self.save_singleton(new_record) # check if coin is in DB and mark deleted if found - current_records = await self.get_records_by_coin_id(coin_state.coin.name()) + current_records = await self.get_records_by_coin_id(coin_spend.coin.name()) if len(current_records) > 0: - await self.delete_singleton_by_coin_id(coin_state.coin.name(), block_height) + await self.delete_singleton_by_coin_id(coin_spend.coin.name(), block_height) return def _to_singleton_record(self, row: Row) -> SingletonRecord: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 1cef6dee295e..7205e9e38ecf 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -35,17 +35,18 @@ from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord -from chia.types.coin_spend import CoinSpend, compute_additions +from chia.types.coin_spend import CoinSpend, compute_additions, make_spend from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.bech32m import encode_puzzle_hash -from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.db_synchronous import db_synchronous_on from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import Err from chia.util.hash import std_hash from chia.util.ints import uint16, uint32, uint64, uint128 from chia.util.lru_cache import LRUCache +from chia.util.observation_root import ObservationRoot from chia.util.path import path_from_root +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, UInt32Range, UInt64Range, VersionedBlob from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_info import CATCoinData, CATInfo, CRCATInfo @@ -98,13 +99,17 @@ SignedTransaction, SigningInstructions, SigningResponse, - SigningTarget, Spend, SumHint, TransactionInfo, UnsignedTransaction, ) -from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton, get_singleton_id_from_puzzle +from chia.wallet.singleton import ( + SINGLETON_LAUNCHER_PUZZLE, + create_singleton_puzzle, + get_inner_puzzle_from_singleton, + get_singleton_id_from_puzzle, +) from chia.wallet.trade_manager import TradeManager from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus @@ -124,6 +129,9 @@ last_change_height_cs, ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType +from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash, match_vault_puzzle +from chia.wallet.vault.vault_root import VaultRoot +from chia.wallet.vault.vault_wallet import Vault from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, ProofsChecker, construct_pending_approval_state from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential @@ -138,9 +146,10 @@ from chia.wallet.wallet_interested_store import WalletInterestedStore from chia.wallet.wallet_nft_store import WalletNftStore from chia.wallet.wallet_pool_store import WalletPoolStore -from chia.wallet.wallet_protocol import WalletProtocol +from chia.wallet.wallet_protocol import MainWalletProtocol, WalletProtocol from chia.wallet.wallet_puzzle_store import WalletPuzzleStore from chia.wallet.wallet_retry_store import WalletRetryStore +from chia.wallet.wallet_singleton_store import WalletSingletonStore from chia.wallet.wallet_spend_bundle import WalletSpendBundle from chia.wallet.wallet_transaction_store import WalletTransactionStore from chia.wallet.wallet_user_store import WalletUserStore @@ -179,10 +188,10 @@ class WalletStateManager: db_path: Path db_wrapper: DBWrapper2 - main_wallet: Wallet + main_wallet: MainWalletProtocol wallets: dict[uint32, WalletProtocol[Any]] - private_key: Optional[PrivateKey] - root_pubkey: G1Element + private_key: Optional[SecretInfo[Any]] + observation_root: ObservationRoot trade_manager: TradeManager notification_manager: NotificationManager @@ -196,6 +205,7 @@ class WalletStateManager: wallet_node: WalletNode pool_store: WalletPoolStore dl_store: DataLayerStore + singleton_store: WalletSingletonStore default_cats: dict[str, Any] asset_to_wallet_map: dict[AssetType, Any] initial_num_public_keys: int @@ -203,14 +213,14 @@ class WalletStateManager: @staticmethod async def create( - private_key: Optional[PrivateKey], + private_key: Optional[SecretInfo[Any]], config: dict[str, Any], db_path: Path, constants: ConsensusConstants, server: ChiaServer, root_path: Path, wallet_node: WalletNode, - root_pubkey: Optional[G1Element] = None, + observation_root: Optional[ObservationRoot] = None, ) -> WalletStateManager: self = WalletStateManager() @@ -251,6 +261,7 @@ async def create( self.dl_store = await DataLayerStore.create(self.db_wrapper) self.interested_store = await WalletInterestedStore.create(self.db_wrapper) self.retry_store = await WalletRetryStore.create(self.db_wrapper) + self.singleton_store = await WalletSingletonStore.create(self.db_wrapper) self.default_cats = DEFAULT_CATS self.wallet_node = wallet_node @@ -265,21 +276,21 @@ async def create( self.private_key = private_key if private_key is None: # pragma: no cover - if root_pubkey is None: + if observation_root is None: raise ValueError("WalletStateManager requires either a root private key or root public key") else: - self.root_pubkey = root_pubkey + self.observation_root = observation_root else: - calculated_root_public_key: G1Element = private_key.get_g1() - if root_pubkey is not None: - assert root_pubkey == calculated_root_public_key - self.root_pubkey = calculated_root_public_key + calculated_root_public_key: ObservationRoot = private_key.public_key() + if observation_root is not None: + assert observation_root == calculated_root_public_key + self.observation_root = calculated_root_public_key - fingerprint = self.root_pubkey.get_fingerprint() + fingerprint = self.observation_root.get_fingerprint() puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, []) self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators) - self.main_wallet = await Wallet.create(self, main_wallet_info) + self.main_wallet = await self.get_main_wallet_driver(self.observation_root).create(self, main_wallet_info) self.wallets = {main_wallet_info.id: self.main_wallet} @@ -352,16 +363,34 @@ async def create( return self + def get_main_wallet_driver(self, observation_root: ObservationRoot) -> type[MainWalletProtocol]: + root_bytes: bytes = bytes(observation_root) + if len(root_bytes) == 48: + return Wallet + if len(root_bytes) == 32: + return Vault + + raise ValueError( # pragma: no cover + f"Could not find a valid wallet type for observation_root: {root_bytes.hex()}" + ) + def get_public_key_unhardened(self, index: uint32) -> G1Element: - return master_pk_to_wallet_pk_unhardened(self.root_pubkey, index) + if not isinstance(self.observation_root, G1Element): # pragma: no cover + # TODO: Add test coverage when vault wallet exists + raise ValueError("Public key derivation is not supported for non-G1Element keys") + return master_pk_to_wallet_pk_unhardened(self.observation_root, index) - async def get_private_key(self, puzzle_hash: bytes32) -> PrivateKey: + async def get_private_key(self, puzzle_hash: bytes32) -> SecretInfo[Any]: record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash) if record is None: raise ValueError(f"No key for puzzle hash: {puzzle_hash.hex()}") + sk = self.get_master_private_key() + # This will need to work when other key types are derivable but for now we will just sanitize and move on + assert isinstance(sk, PrivateKey) if record.hardened: - return master_sk_to_wallet_sk(self.get_master_private_key(), record.index) - return master_sk_to_wallet_sk_unhardened(self.get_master_private_key(), record.index) + return master_sk_to_wallet_sk(sk, record.index) + + return master_sk_to_wallet_sk_unhardened(sk, record.index) async def get_public_key(self, puzzle_hash: bytes32) -> bytes: record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash) @@ -373,7 +402,7 @@ async def get_public_key(self, puzzle_hash: bytes32) -> bytes: pk_bytes = bytes(record._pubkey) return pk_bytes - def get_master_private_key(self) -> PrivateKey: + def get_master_private_key(self) -> SecretInfo[Any]: if self.private_key is None: # pragma: no cover raise ValueError("Wallet is currently in observer mode and access to private key is denied") @@ -441,22 +470,29 @@ async def create_more_puzzle_hashes( return lowest_start_index = min(start_index_by_wallet.values()) - - # now derive the keysfrom lowest_start_index to last_index - # these maps derivation index to public key - hardened_keys: dict[int, G1Element] = {} - unhardened_keys: dict[int, G1Element] = {} - - if self.private_key is not None: - # Hardened - intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) + if not target_wallet.handle_own_derivation(): + # now derive the keysfrom start_index to last_index + # these maps derivation index to public key + hardened_keys: dict[int, G1Element] = {} + unhardened_keys: dict[int, G1Element] = {} + + # This function shoul work for other types of observation roots too + # However to generalize this function beyond pubkeys is beyond the scope of current work + # So we're just going to sanitize and move on + assert isinstance(self.observation_root, G1Element) + if self.private_key is not None: + assert isinstance(self.private_key, PrivateKey) + + if self.private_key is not None: + # Hardened + intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) + for index in range(lowest_start_index, last_index): + hardened_keys[index] = _derive_path(intermediate_sk, [index]).public_key() + + # Unhardened + intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.observation_root) for index in range(lowest_start_index, last_index): - hardened_keys[index] = _derive_path(intermediate_sk, [index]).get_g1() - - # Unhardened - intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.root_pubkey) - for index in range(lowest_start_index, last_index): - unhardened_keys[index] = _derive_pk_unhardened(intermediate_pk_un, [index]) + unhardened_keys[index] = _derive_pk_unhardened(intermediate_pk_un, [index]) for wallet_id, start_index in start_index_by_wallet.items(): target_wallet = self.wallets[wallet_id] @@ -467,38 +503,41 @@ async def create_more_puzzle_hashes( creating_msg = f"Creating puzzle hashes from {start_index} to {last_index - 1} for wallet_id: {wallet_id}" self.log.info(f"Start: {creating_msg}") for index in range(start_index, last_index): - pubkey: Optional[G1Element] = hardened_keys.get(index) - if pubkey is not None: - # Hardened - puzzlehash: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey) - self.log.debug(f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash.hex()}") + if target_wallet.handle_own_derivation(): + derivation_paths.extend(target_wallet.derivation_for_index(index)) + else: + pubkey: Optional[G1Element] = hardened_keys.get(index) + if pubkey is not None: + # Hardened + puzzlehash: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey) + self.log.debug(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_keys.get(index) + assert pubkey is not None + puzzlehash_unhardened: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey) + self.log.debug( + f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" + ) derivation_paths.append( DerivationRecord( uint32(index), - puzzlehash, + puzzlehash_unhardened, pubkey, target_wallet.type(), uint32(target_wallet.id()), - True, + False, ) ) - # Unhardened - pubkey = unhardened_keys.get(index) - assert pubkey is not None - puzzlehash_unhardened: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey) - self.log.debug( - f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" - ) - derivation_paths.append( - DerivationRecord( - uint32(index), - puzzlehash_unhardened, - pubkey, - target_wallet.type(), - uint32(target_wallet.id()), - False, - ) - ) self.log.info(f"Done: {creating_msg} Time: {time.time() - start_t} seconds") if len(derivation_paths) > 0: await self.puzzle_store.add_derivation_paths(derivation_paths) @@ -506,12 +545,13 @@ async def create_more_puzzle_hashes( await self.wallet_node.new_peak_queue.subscribe_to_puzzle_hashes( [record.puzzle_hash for record in derivation_paths] ) - if len(unhardened_keys) > 0: - self.state_changed("new_derivation_index", data_object={"index": last_index - 1}) - # By default, we'll mark previously generated unused puzzle hashes as used if we have new paths - if mark_existing_as_used and unused > 0 and len(unhardened_keys) > 0: - self.log.info(f"Updating last used derivation index: {unused - 1}") - await self.puzzle_store.set_used_up_to(uint32(unused - 1)) + if not target_wallet.handle_own_derivation(): + if len(unhardened_keys) > 0: + self.state_changed("new_derivation_index", data_object={"index": last_index - 1}) + # By default, we'll mark previously generated unused puzzle hashes as used if we have new paths + if mark_existing_as_used and unused > 0 and len(unhardened_keys) > 0: + self.log.info(f"Updating last used derivation index: {unused - 1}") + await self.puzzle_store.set_used_up_to(uint32(unused - 1)) async def update_wallet_puzzle_hashes(self, wallet_id: uint32) -> None: derivation_paths: list[DerivationRecord] = [] @@ -793,6 +833,10 @@ async def determine_coin_type( uncurried = uncurry_puzzle(coin_spend.puzzle_reveal) + vault_check = match_vault_puzzle(uncurried.mod, uncurried.args) + if vault_check: + return await self.handle_vault(coin_spend.puzzle_reveal.to_program(), coin_spend, coin_state), None + dao_ids = [] wallets = self.wallets.values() for wallet in wallets: @@ -984,7 +1028,7 @@ async def spend_clawback_coins( # 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( + inner_solution: Program = await self.main_wallet.make_solution( primaries=[ Payment( derivation_record.puzzle_hash, @@ -992,6 +1036,7 @@ async def spend_clawback_coins( memos, # Forward memo of the first coin ) ], + action_scope=action_scope, conditions=( extra_conditions if len(coin_spends) > 0 or fee == 0 @@ -1090,6 +1135,23 @@ async def is_standard_wallet_tx(self, coin_state: CoinState) -> bool: wallet_identifier = await self.get_wallet_identifier_for_puzzle_hash(coin_state.coin.puzzle_hash) return wallet_identifier is not None and wallet_identifier.type == WalletType.STANDARD_WALLET + async def handle_vault( + self, + puzzle: Program, + coin_spend: CoinSpend, + coin_state: CoinState, + ) -> Optional[WalletIdentifier]: + if isinstance(self.observation_root, VaultRoot): + for wallet in self.wallets.values(): + if wallet.type() == WalletType.STANDARD_WALLET: + assert isinstance(wallet, Vault) + # make sure we've got the singleton coin + if coin_state.coin.amount % 2 == 1: + # Update the vault singleton record + await wallet.update_vault_singleton(puzzle, coin_spend, coin_state) + return WalletIdentifier.create(wallet) + return None + async def handle_dao_cat( self, curried_args: Iterator[Program], @@ -1753,6 +1815,7 @@ async def _add_coin_states( wallet_identifier = WalletIdentifier(uint32(local_record.wallet_id), local_record.wallet_type) elif coin_state.created_height is not None: wallet_identifier, coin_data = await self.determine_coin_type(peer, coin_state, fork_height) + try: dl_wallet = self.get_dl_wallet() except ValueError: @@ -2203,7 +2266,6 @@ async def get_wallet_identifier_for_coin( coin_record = await self.coin_store.get_coin_record(coin.name()) if coin_record is not None: wallet_identifier = WalletIdentifier(uint32(coin_record.wallet_id), coin_record.wallet_type) - return wallet_identifier async def get_wallet_identifier_for_hinted_coin(self, coin: Coin, hint: bytes32) -> Optional[WalletIdentifier]: @@ -2339,6 +2401,7 @@ async def add_pending_transactions( [] if additional_signing_responses is None else additional_signing_responses, additional_signing_responses != [] and additional_signing_responses is not None, ) + if push: all_coins_names = [] async with self.db_wrapper.writer_maybe_transaction(): @@ -2659,29 +2722,7 @@ async def key_hints_for_pubkeys(self, pks: list[bytes]) -> KeyHints: ) async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: - pks: list[bytes] = [] - signing_targets: list[SigningTarget] = [] - for coin_spend in coin_spends: - _coin_spend = coin_spend.as_coin_spend() - # Get AGG_SIG conditions - conditions_dict = conditions_dict_for_solution( - _coin_spend.puzzle_reveal.to_program(), - _coin_spend.solution.to_program(), - self.constants.MAX_BLOCK_COST_CLVM, - ) - # Create signature - for pk, msg in pkm_pairs_for_conditions_dict( - conditions_dict, _coin_spend.coin, self.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - pk_bytes = bytes(pk) - pks.append(pk_bytes) - fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") - signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) - - return SigningInstructions( - await self.key_hints_for_pubkeys(pks), - signing_targets, - ) + return await self.main_wallet.gather_signing_info(coin_spends) async def gather_signing_info_for_bundles(self, bundles: list[WalletSpendBundle]) -> list[UnsignedTransaction]: utxs: list[UnsignedTransaction] = [] @@ -2816,3 +2857,77 @@ async def new_action_scope( extra_spends=extra_spends, ) as action_scope: yield action_scope + + async def create_vault_wallet( + self, + secp_pk: bytes, + hidden_puzzle_hash: bytes32, + genesis_challenge: bytes32, + action_scope: WalletActionScope, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, + fee: uint64 = uint64(0), + ) -> None: + """ + Returns a tx record for creating a new vault + """ + wallet = self.main_wallet + + # Get xch coin + amount = uint64(1) + coins = await wallet.select_coins(uint64(amount + fee), action_scope) + + # Create singleton launcher + origin = next(iter(coins)) + launcher_coin = Coin(origin.name(), SINGLETON_LAUNCHER_HASH, amount) + + vault_inner_puzzle_hash = get_vault_inner_puzzle_hash( + secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock + ) + vault_full_puzzle_hash = get_vault_full_puzzle_hash(launcher_coin.name(), vault_inner_puzzle_hash) + memos = [secp_pk, hidden_puzzle_hash] + if bls_pk: + memos.extend([bls_pk.to_bytes(), Program.to(timelock).as_atom()]) + + genesis_launcher_solution = Program.to([vault_full_puzzle_hash, amount, memos]) + announcement_message = genesis_launcher_solution.get_tree_hash() + + await wallet.generate_signed_transaction( + amount, + SINGLETON_LAUNCHER_HASH, + action_scope, + fee, + coins, + None, + origin_id=origin.name(), + extra_conditions=( + AssertCoinAnnouncement(asserted_id=launcher_coin.name(), asserted_msg=announcement_message), + ), + ) + + launcher_cs = make_spend(launcher_coin, SINGLETON_LAUNCHER_PUZZLE, genesis_launcher_solution) + launcher_sb = WalletSpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) + + async with action_scope.use() as interface: + interface.side_effects.transactions.append( + TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + amount=uint64(amount), + to_puzzle_hash=vault_inner_puzzle_hash, + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=launcher_sb, + additions=launcher_sb.additions(), + removals=launcher_sb.removals(), + wallet_id=wallet.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=launcher_sb.name(), + memos=[], + valid_times=ConditionValidTimes(), + ) + ) + await self.add_interested_puzzle_hashes([launcher_coin.name()], [self.main_wallet.id()]) diff --git a/mozilla-ca b/mozilla-ca index 0aecf4ed7c6f..efc15699d97e 160000 --- a/mozilla-ca +++ b/mozilla-ca @@ -1 +1 @@ -Subproject commit 0aecf4ed7c6f2b20a89d3d3386b866c1a3f03139 +Subproject commit efc15699d97e8180c0459c82cb0bd4993434af3b diff --git a/poetry.lock b/poetry.lock index 3c718d866ab4..e0844a3219f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2817,17 +2817,6 @@ files = [ {file = "types_aiofiles-23.2.0.20240311-py3-none-any.whl", hash = "sha256:ed10a8002d88c94220597b77304cf1a1d8cf489c7143fc3ffa2c96488b20fec7"}, ] -[[package]] -name = "types-cryptography" -version = "3.3.23.2" -description = "Typing stubs for cryptography" -optional = true -python-versions = "*" -files = [ - {file = "types-cryptography-3.3.23.2.tar.gz", hash = "sha256:09cc53f273dd4d8c29fa7ad11fefd9b734126d467960162397bc5e3e604dea75"}, - {file = "types_cryptography-3.3.23.2-py3-none-any.whl", hash = "sha256:b965d548f148f8e87f353ccf2b7bd92719fdf6c845ff7cedf2abb393a0643e4f"}, -] - [[package]] name = "types-pyyaml" version = "6.0.12.20240311" @@ -3226,11 +3215,11 @@ url = "https://pypi.chia.net/simple" reference = "chia" [extras] -dev = ["aiohttp_cors", "build", "coverage", "diff-cover", "lxml", "mypy", "pre-commit", "pre-commit", "py3createtorrent", "pyinstaller", "pytest", "pytest-cov", "pytest-mock", "pytest-monitor", "pytest-xdist", "ruff", "types-aiofiles", "types-cryptography", "types-pyyaml", "types-setuptools"] +dev = ["aiohttp_cors", "build", "coverage", "diff-cover", "lxml", "mypy", "pre-commit", "pre-commit", "py3createtorrent", "pyinstaller", "pytest", "pytest-cov", "pytest-mock", "pytest-monitor", "pytest-xdist", "ruff", "types-aiofiles", "types-pyyaml", "types-setuptools"] legacy-keyring = ["keyrings.cryptfile"] upnp = ["miniupnpc"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.13" -content-hash = "be734ac01cf9b84466a917368940b1502c2181ee0d63447e02de1ce869cede3c" +content-hash = "57d9403fba5efe6d7add3e3db59f003ba4e6e92a428492f6d065bbebc5b3dc82" diff --git a/pyproject.toml b/pyproject.toml index dd4b97963f51..b47c34303888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,6 @@ pytest-mock = { version = "3.14.0", optional = true } pytest-monitor = { version = "1.6.6", platform = "linux", optional = true } pytest-xdist = { version = "3.6.1", optional = true } types-aiofiles = { version = "23.2.0.20240311", optional = true } -types-cryptography = { version = "3.3.23.2", optional = true } types-pyyaml = { version = "6.0.12.20240311", optional = true } types-setuptools = { version = "70.0.0.20240524", optional = true } lxml = { version = "5.2.2", optional = true } @@ -104,7 +103,7 @@ ruff = { version = "0.7.1", optional = true } [tool.poetry.extras] -dev = ["aiohttp_cors", "build", "coverage", "diff-cover", "mypy", "pre-commit", "py3createtorrent", "pyinstaller", "pytest", "pytest-cov", "pytest-mock", "pytest-monitor", "pytest-xdist", "ruff", "types-aiofiles", "types-cryptography", "types-pyyaml", "types-setuptools", "lxml"] +dev = ["aiohttp_cors", "build", "coverage", "diff-cover", "mypy", "pre-commit", "py3createtorrent", "pyinstaller", "pytest", "pytest-cov", "pytest-mock", "pytest-monitor", "pytest-xdist", "ruff", "types-aiofiles", "types-pyyaml", "types-setuptools", "lxml"] upnp = ["miniupnpc"] legacy_keyring = ["keyrings.cryptfile"]