diff --git a/chia/_tests/wallet/nft_wallet/test_nft_wallet.py b/chia/_tests/wallet/nft_wallet/test_nft_wallet.py index d92f2a2d25d5..069a486ae316 100644 --- a/chia/_tests/wallet/nft_wallet/test_nft_wallet.py +++ b/chia/_tests/wallet/nft_wallet/test_nft_wallet.py @@ -665,6 +665,124 @@ async def test_nft_wallet_rpc_creation_and_list(wallet_environments: WalletTestF await env.rpc_client.count_nfts(NFTCountNFTs(uint32(50))) +@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN], reason="irrelevant") +@pytest.mark.parametrize("wallet_environments", [{"num_environments": 1, "blocks_needed": [1]}], indirect=True) +@pytest.mark.anyio +async def test_sign_message_by_nft_id(wallet_environments: WalletTestFramework) -> None: + env = wallet_environments.environments[0] + wallet_node = env.node + wallet = env.xch_wallet + + env.wallet_aliases = { + "xch": 1, + "nft": 2, + } + + nft_wallet_0 = await env.rpc_client.fetch("create_new_wallet", dict(wallet_type="nft_wallet", name="NFT WALLET 1")) + assert isinstance(nft_wallet_0, dict) + assert nft_wallet_0.get("success") + assert env.wallet_aliases["nft"] == nft_wallet_0["wallet_id"] + + async with wallet.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope: + wallet_ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager) + await env.rpc_client.mint_nft( + request=NFTMintNFTRequest( + wallet_id=uint32(env.wallet_aliases["nft"]), + royalty_address=encode_puzzle_hash(wallet_ph, AddressType.NFT.hrp(wallet_node.config)), + target_address=None, + hash=bytes32.from_hexstr("0xD4584AD463139FA8C0D9F68F4B59F185D4584AD463139FA8C0D9F68F4B59F185"), + uris=["https://www.chia.net/img/branding/chia-logo.svg"], + push=True, + ), + tx_config=wallet_environments.tx_config, + ) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + "xch": {"set_remainder": True}, # tested above + "nft": {"init": True, "pending_coin_removal_count": 1}, + }, + post_block_balance_updates={ + "xch": {"set_remainder": True}, # tested above + "nft": { + "pending_coin_removal_count": -1, + "unspent_coin_count": 1, + }, + }, + ) + ] + ) + + nft_list = await env.rpc_client.list_nfts(NFTGetNFTs(uint32(env.wallet_aliases["nft"]))) + nft_id = nft_list.nft_list[0].nft_id + + # Test general string + message = "Hello World" + response = await env.rpc_client.sign_message_by_id( + SignMessageByID( + id=nft_id, + message=message, + ) + ) + puzzle: Program = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, message)) + assert AugSchemeMPL.verify( + response.pubkey, + puzzle.get_tree_hash(), + response.signature, + ) + # Test hex string + message = "0123456789ABCDEF" + response = await env.rpc_client.sign_message_by_id( + SignMessageByID( + id=nft_id, + message=message, + is_hex=True, + ) + ) + puzzle = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, bytes.fromhex(message))) + + assert AugSchemeMPL.verify( + response.pubkey, + puzzle.get_tree_hash(), + response.signature, + ) + + # Test BLS sign string + message = "Hello World" + response = await env.rpc_client.sign_message_by_id( + SignMessageByID( + id=nft_id, + message=message, + is_hex=False, + safe_mode=False, + ) + ) + + assert AugSchemeMPL.verify( + response.pubkey, + bytes(message, "utf-8"), + response.signature, + ) + # Test BLS sign hex + message = "0123456789ABCDEF" + response = await env.rpc_client.sign_message_by_id( + SignMessageByID( + id=nft_id, + message=message, + is_hex=True, + safe_mode=False, + ) + ) + + assert AugSchemeMPL.verify( + response.pubkey, + hexstr_to_bytes(message), + response.signature, + ) + + @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN], reason="irrelevant") @pytest.mark.parametrize("wallet_environments", [{"num_environments": 1, "blocks_needed": [1]}], indirect=True) @pytest.mark.anyio diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index ee7582b0d600..8e151dc21f30 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -154,6 +154,7 @@ SendTransaction, SendTransactionMulti, SetWalletResyncOnStartup, + SignMessageByAddress, SpendClawbackCoins, SplitCoins, TakeOffer, @@ -3129,6 +3130,37 @@ async def test_verify_signature( assert res == rpc_response +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [1], + "reuse_puzhash": True, + "trusted": True, + } + ], + indirect=True, +) +@pytest.mark.anyio +@pytest.mark.limit_consensus_modes(reason="irrelevant") +async def test_sign_message_by_address(wallet_environments: WalletTestFramework) -> None: + client: WalletRpcClient = wallet_environments.environments[0].rpc_client + + message = "foo" + address = await client.get_next_address(GetNextAddress(uint32(1))) + signed_message = await client.sign_message_by_address(SignMessageByAddress(address.address, message)) + + await wallet_environments.environments[0].rpc_client.verify_signature( + VerifySignature( + message=message, + pubkey=signed_message.pubkey, + signature=signed_message.signature, + signing_mode=signed_message.signing_mode, + ) + ) + + @pytest.mark.parametrize( "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 0]}], diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index e15a0af2ac98..06c1dbfaf556 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -16,7 +16,6 @@ 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.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.wallet.conditions import ( AssertCoinAnnouncement, Condition, @@ -30,8 +29,6 @@ from chia.wallet.did_wallet.did_wallet_puzzles import match_did_puzzle, uncurry_innerpuz from chia.wallet.lineage_proof import LineageProof from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( - DEFAULT_HIDDEN_PUZZLE_HASH, - calculate_synthetic_secret_key, puzzle_for_pk, puzzle_hash_for_pk, ) @@ -898,27 +895,15 @@ def get_parent_for_coin(self, coin) -> LineageProof | None: return parent_info - async def sign_message(self, message: str, mode: SigningMode) -> tuple[G1Element, G2Element]: + async def current_p2_puzzle_hash(self) -> bytes32: if self.did_info.current_inner is None: raise ValueError("Missing DID inner puzzle.") puzzle_args = did_wallet_puzzles.uncurry_innerpuz(self.did_info.current_inner) if puzzle_args is not None: p2_puzzle, _, _, _, _ = puzzle_args - puzzle_hash = p2_puzzle.get_tree_hash() - private = await self.wallet_state_manager.get_private_key(puzzle_hash) - 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: - hex_message: bytes = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, bytes.fromhex(message))).get_tree_hash() - elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT: - hex_message = bytes(message, "utf-8") - elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT: - hex_message = bytes.fromhex(message) - else: - hex_message = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, message)).get_tree_hash() - return synthetic_pk, AugSchemeMPL.sign(synthetic_secret_key, hex_message) + return p2_puzzle.get_tree_hash() else: - raise ValueError("Invalid inner DID puzzle.") + raise ValueError("Invalid DID inner puzzle.") async def generate_new_decentralised_id( self, diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 7552a9c1a5bf..c357d1da6300 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -14,7 +14,6 @@ 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.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.util.casts import int_from_bytes, int_to_bytes from chia.util.hash import std_hash from chia.wallet.conditions import ( @@ -38,8 +37,6 @@ from chia.wallet.outer_puzzles import AssetType, construct_puzzle, match_puzzle, solve_puzzle from chia.wallet.puzzle_drivers import PuzzleInfo, Solver from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( - DEFAULT_HIDDEN_PUZZLE_HASH, - calculate_synthetic_secret_key, puzzle_for_pk, ) from chia.wallet.singleton import SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, create_singleton_puzzle @@ -497,23 +494,11 @@ async def get_puzzle_info(self, nft_id: bytes32) -> PuzzleInfo: else: return puzzle_info - async def sign_message(self, message: str, nft: NFTCoinInfo, mode: SigningMode) -> tuple[G1Element, G2Element]: + async def current_p2_puzzle_hash(self, nft: NFTCoinInfo) -> bytes32: uncurried_nft = UncurriedNFT.uncurry(*nft.full_puzzle.uncurry()) if uncurried_nft is not None: p2_puzzle = uncurried_nft.p2_puzzle - puzzle_hash = p2_puzzle.get_tree_hash() - private = await self.wallet_state_manager.get_private_key(puzzle_hash) - 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: - hex_message: bytes = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, bytes.fromhex(message))).get_tree_hash() - elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT: - hex_message = bytes(message, "utf-8") - elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT: - hex_message = bytes.fromhex(message) - else: - hex_message = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, message)).get_tree_hash() - return synthetic_pk, AugSchemeMPL.sign(synthetic_secret_key, hex_message) + return p2_puzzle.get_tree_hash() else: raise ValueError("Invalid NFT puzzle.") diff --git a/chia/wallet/util/signing.py b/chia/wallet/util/signing.py new file mode 100644 index 000000000000..4fc94fb8f26c --- /dev/null +++ b/chia/wallet/util/signing.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey +from chia_rs.sized_bytes import bytes32 + +from chia.types.blockchain_format.program import Program +from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode +from chia.util.bech32m import decode_puzzle_hash +from chia.util.byte_types import hexstr_to_bytes +from chia.wallet.puzzles import p2_delegated_conditions +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key +from chia.wallet.wallet_request_types import VerifySignatureResponse + +# CHIP-0002 message signing as documented at: +# https://github.com/Chia-Network/chips/blob/80e4611fe52b174bf1a0382b9dff73805b18b8c6/CHIPs/chip-0002.md + + +def verify_signature( + *, signing_mode: SigningMode, public_key: G1Element, message: str, signature: G2Element, address: str | None +) -> VerifySignatureResponse: + """ + Given a public key, message and signature, verify if it is valid. + :param request: + :return: + """ + if signing_mode in {SigningMode.CHIP_0002, SigningMode.CHIP_0002_P2_DELEGATED_CONDITIONS}: + # CHIP-0002 message signatures are made over the tree hash of: + # ("Chia Signed Message", message) + message_to_verify: bytes = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, message)).get_tree_hash() + elif signing_mode == SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT: + # Message is expected to be a hex string + message_to_verify = hexstr_to_bytes(message) + elif signing_mode == SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT: + # Message is expected to be a UTF-8 string + message_to_verify = bytes(message, "utf-8") + else: + raise ValueError(f"Unsupported signing mode: {signing_mode!r}") + + # Verify using the BLS message augmentation scheme + is_valid = AugSchemeMPL.verify( + public_key, + message_to_verify, + signature, + ) + if address is not None: + # For signatures made by the sign_message_by_address/sign_message_by_id + # endpoints, the "address" field should contain the p2_address of the NFT/DID + # that was used to sign the message. + puzzle_hash: bytes32 = decode_puzzle_hash(address) + expected_puzzle_hash: bytes32 | None = None + if signing_mode == SigningMode.CHIP_0002_P2_DELEGATED_CONDITIONS: + puzzle = p2_delegated_conditions.puzzle_for_pk(Program.to(public_key)) + expected_puzzle_hash = bytes32(puzzle.get_tree_hash()) + else: + expected_puzzle_hash = puzzle_hash_for_synthetic_public_key(public_key) + if puzzle_hash != expected_puzzle_hash: + return VerifySignatureResponse(isValid=False, error="Public key doesn't match the address") + if is_valid: + return VerifySignatureResponse(isValid=is_valid) + else: + return VerifySignatureResponse(isValid=False, error="Signature is invalid.") + + +@dataclass(kw_only=True, frozen=True) +class SignMessageResponse: + pubkey: G1Element + signature: G2Element + + +def sign_message(secret_key: PrivateKey, message: str, mode: SigningMode) -> SignMessageResponse: + public_key = secret_key.get_g1() + if mode == SigningMode.CHIP_0002_HEX_INPUT: + hex_message: bytes = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, bytes.fromhex(message))).get_tree_hash() + elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT: + hex_message = bytes(message, "utf-8") + elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT: + hex_message = bytes.fromhex(message) + else: + hex_message = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, message)).get_tree_hash() + return SignMessageResponse( + pubkey=public_key, + signature=AugSchemeMPL.sign(secret_key, hex_message), + ) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index c21a59ea2038..9ae49839f572 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -12,7 +12,6 @@ from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.serialized_program import SerializedProgram from chia.types.coin_spend import make_spend -from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.util.hash import std_hash from chia.wallet.coin_selection import select_coins from chia.wallet.conditions import ( @@ -112,6 +111,9 @@ def type(cls) -> WalletType: def id(self) -> uint32: return self.wallet_id + def convert_secret_key_to_synthetic(self, secret_key: PrivateKey) -> PrivateKey: + return calculate_synthetic_secret_key(secret_key, DEFAULT_HIDDEN_PUZZLE_HASH) + async def get_confirmed_balance(self, record_list: set[WalletCoinRecord] | None = None) -> uint128: return await self.wallet_state_manager.get_confirmed_balance_for_wallet(self.id(), record_list) @@ -361,22 +363,6 @@ async def _generate_unsigned_transaction( self.log.debug(f"Spends is {spends}") return spends - async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> tuple[G1Element, G2Element]: - # 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) - 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: - hex_message: bytes = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, bytes.fromhex(message))).get_tree_hash() - elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT: - hex_message = bytes(message, "utf-8") - elif mode == SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT: - hex_message = bytes.fromhex(message) - else: - hex_message = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, message)).get_tree_hash() - return synthetic_pk, AugSchemeMPL.sign(synthetic_secret_key, hex_message) - async def generate_signed_transaction( self, amounts: list[uint64], diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index 045aed5c299d..dbd40c0a2ea3 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -13,6 +13,7 @@ from chia.data_layer.singleton_record import SingletonRecord from chia.pools.pool_wallet_info import PoolWalletInfo from chia.types.blockchain_format.program import Program +from chia.types.signing_mode import SigningMode from chia.util.byte_types import hexstr_to_bytes from chia.util.hash import std_hash from chia.util.streamable import Streamable, streamable @@ -374,6 +375,18 @@ class VerifySignature(Streamable): signing_mode: str | None = None address: str | None = None + @property + def signing_mode_enum(self) -> SigningMode: + # Default to BLS_MESSAGE_AUGMENTATION_HEX_INPUT as this RPC was originally designed to verify + # signatures made by `chia keys sign`, which uses BLS_MESSAGE_AUGMENTATION_HEX_INPUT + if self.signing_mode is None: + return SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT + else: + try: + return SigningMode(self.signing_mode) + except ValueError: + raise ValueError(f"Invalid signing mode: {self.signing_mode!r}") + @streamable @dataclass(frozen=True) @@ -382,6 +395,17 @@ class VerifySignatureResponse(Streamable): error: str | None = None +def signing_mode_enum(request: SignMessageByAddress | SignMessageByID) -> SigningMode: + if request.is_hex and request.safe_mode: + return SigningMode.CHIP_0002_HEX_INPUT + elif not request.is_hex and not request.safe_mode: + return SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT + elif request.is_hex and not request.safe_mode: + return SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT + + return SigningMode.CHIP_0002 + + @streamable @dataclass(frozen=True) class SignMessageByAddress(Streamable): @@ -390,6 +414,10 @@ class SignMessageByAddress(Streamable): is_hex: bool = False safe_mode: bool = True + @property + def signing_mode_enum(self) -> SigningMode: + return signing_mode_enum(self) + @streamable @dataclass(frozen=True) @@ -407,6 +435,10 @@ class SignMessageByID(Streamable): is_hex: bool = False safe_mode: bool = True + @property + def signing_mode_enum(self) -> SigningMode: + return signing_mode_enum(self) + @streamable @dataclass(frozen=True) diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index 7feeea0cd750..83c493ff2dfa 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -25,9 +25,7 @@ from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import coin_as_list from chia.types.blockchain_format.program import Program -from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash -from chia.util.byte_types import hexstr_to_bytes from chia.util.config import load_config from chia.util.errors import KeychainIsLocked from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic @@ -69,9 +67,7 @@ from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT from chia.wallet.outer_puzzles import AssetType from chia.wallet.puzzle_drivers import PuzzleInfo -from chia.wallet.puzzles import p2_delegated_conditions from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key from chia.wallet.signer_protocol import SigningResponse from chia.wallet.singleton import ( SINGLETON_LAUNCHER_PUZZLE_HASH, @@ -88,6 +84,7 @@ from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.curry_and_treehash import NIL_TREEHASH from chia.wallet.util.query_filter import HashFilter +from chia.wallet.util.signing import sign_message, verify_signature from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType 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 @@ -102,7 +99,6 @@ from chia.wallet.wallet_coin_store import CoinRecordOrder, GetCoinRecords, unspent_range from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_node import WalletNode, get_wallet_db_path -from chia.wallet.wallet_protocol import WalletProtocol from chia.wallet.wallet_request_types import ( AddKey, AddKeyResponse, @@ -1835,57 +1831,13 @@ async def send_notification( @marshal async def verify_signature(self, request: VerifySignature) -> VerifySignatureResponse: - """ - Given a public key, message and signature, verify if it is valid. - :param request: - :return: - """ - # Default to BLS_MESSAGE_AUGMENTATION_HEX_INPUT as this RPC was originally designed to verify - # signatures made by `chia keys sign`, which uses BLS_MESSAGE_AUGMENTATION_HEX_INPUT - if request.signing_mode is None: - signing_mode = SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT - else: - try: - signing_mode = SigningMode(request.signing_mode) - except ValueError: - raise ValueError(f"Invalid signing mode: {request.signing_mode!r}") - - if signing_mode in {SigningMode.CHIP_0002, SigningMode.CHIP_0002_P2_DELEGATED_CONDITIONS}: - # CHIP-0002 message signatures are made over the tree hash of: - # ("Chia Signed Message", message) - message_to_verify: bytes = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, request.message)).get_tree_hash() - elif signing_mode == SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT: - # Message is expected to be a hex string - message_to_verify = hexstr_to_bytes(request.message) - elif signing_mode == SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT: - # Message is expected to be a UTF-8 string - message_to_verify = bytes(request.message, "utf-8") - else: - raise ValueError(f"Unsupported signing mode: {request.signing_mode!r}") - - # Verify using the BLS message augmentation scheme - is_valid = AugSchemeMPL.verify( - request.pubkey, - message_to_verify, - request.signature, - ) - if request.address is not None: - # For signatures made by the sign_message_by_address/sign_message_by_id - # endpoints, the "address" field should contain the p2_address of the NFT/DID - # that was used to sign the message. - puzzle_hash: bytes32 = decode_puzzle_hash(request.address) - expected_puzzle_hash: bytes32 | None = None - if signing_mode == SigningMode.CHIP_0002_P2_DELEGATED_CONDITIONS: - puzzle = p2_delegated_conditions.puzzle_for_pk(Program.to(request.pubkey)) - expected_puzzle_hash = bytes32(puzzle.get_tree_hash()) - else: - expected_puzzle_hash = puzzle_hash_for_synthetic_public_key(request.pubkey) - if puzzle_hash != expected_puzzle_hash: - return VerifySignatureResponse(isValid=False, error="Public key doesn't match the address") - if is_valid: - return VerifySignatureResponse(isValid=is_valid) - else: - return VerifySignatureResponse(isValid=False, error="Signature is invalid.") + return verify_signature( + signing_mode=request.signing_mode_enum, + public_key=request.pubkey, + message=request.message, + signature=request.signature, + address=request.address, + ) @marshal async def sign_message_by_address(self, request: SignMessageByAddress) -> SignMessageByAddressResponse: @@ -1894,21 +1846,18 @@ async def sign_message_by_address(self, request: SignMessageByAddress) -> SignMe :param request: :return: """ - puzzle_hash: bytes32 = decode_puzzle_hash(request.address) - mode: SigningMode = SigningMode.CHIP_0002 - if request.is_hex and request.safe_mode: - mode = SigningMode.CHIP_0002_HEX_INPUT - elif not request.is_hex and not request.safe_mode: - mode = SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT - elif request.is_hex and not request.safe_mode: - mode = SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT - pubkey, signature = await self.service.wallet_state_manager.main_wallet.sign_message( - request.message, puzzle_hash, mode + synthetic_secret_key = self.service.wallet_state_manager.main_wallet.convert_secret_key_to_synthetic( + await self.service.wallet_state_manager.get_private_key(decode_puzzle_hash(request.address)) + ) + signing_response = sign_message( + secret_key=synthetic_secret_key, + message=request.message, + mode=request.signing_mode_enum, ) return SignMessageByAddressResponse( - pubkey=pubkey, - signature=signature, - signing_mode=mode.value, + pubkey=signing_response.pubkey, + signature=signing_response.signature, + signing_mode=request.signing_mode_enum.value, ) @marshal @@ -1919,53 +1868,67 @@ async def sign_message_by_id(self, request: SignMessageByID) -> SignMessageByIDR :return: """ entity_id: bytes32 = decode_puzzle_hash(request.id) - selected_wallet: WalletProtocol[Any] | None = None - mode: SigningMode = SigningMode.CHIP_0002 - if request.is_hex and request.safe_mode: - mode = SigningMode.CHIP_0002_HEX_INPUT - elif not request.is_hex and not request.safe_mode: - mode = SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT - elif request.is_hex and not request.safe_mode: - mode = SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT if is_valid_address(request.id, {AddressType.DID}, self.service.config): + did_wallet: DIDWallet | None = None for wallet in self.service.wallet_state_manager.wallets.values(): if wallet.type() == WalletType.DECENTRALIZED_ID.value: assert isinstance(wallet, DIDWallet) assert wallet.did_info.origin_coin is not None if wallet.did_info.origin_coin.name() == entity_id: - selected_wallet = wallet + did_wallet = wallet break - if selected_wallet is None: + if did_wallet is None: raise ValueError(f"DID for {entity_id.hex()} doesn't exist.") - assert isinstance(selected_wallet, DIDWallet) - pubkey, signature = await selected_wallet.sign_message(request.message, mode) - latest_coin_id = (await selected_wallet.get_coin()).name() + synthetic_secret_key = self.service.wallet_state_manager.main_wallet.convert_secret_key_to_synthetic( + await self.service.wallet_state_manager.get_private_key(await did_wallet.current_p2_puzzle_hash()) + ) + latest_coin_id = (await did_wallet.get_coin()).name() + signing_response = sign_message( + secret_key=synthetic_secret_key, + message=request.message, + mode=request.signing_mode_enum, + ) + return SignMessageByIDResponse( + pubkey=signing_response.pubkey, + signature=signing_response.signature, + signing_mode=request.signing_mode_enum.value, + latest_coin_id=latest_coin_id, + ) elif is_valid_address(request.id, {AddressType.NFT}, self.service.config): + nft_wallet: NFTWallet | None = None target_nft: NFTCoinInfo | None = None for wallet in self.service.wallet_state_manager.wallets.values(): if wallet.type() == WalletType.NFT.value: assert isinstance(wallet, NFTWallet) nft: NFTCoinInfo | None = await wallet.get_nft(entity_id) if nft is not None: - selected_wallet = wallet + nft_wallet = wallet target_nft = nft break - if selected_wallet is None or target_nft is None: + if nft_wallet is None or target_nft is None: raise ValueError(f"NFT for {entity_id.hex()} doesn't exist.") - assert isinstance(selected_wallet, NFTWallet) - pubkey, signature = await selected_wallet.sign_message(request.message, target_nft, mode) + assert isinstance(nft_wallet, NFTWallet) + synthetic_secret_key = self.service.wallet_state_manager.main_wallet.convert_secret_key_to_synthetic( + await self.service.wallet_state_manager.get_private_key( + await nft_wallet.current_p2_puzzle_hash(target_nft) + ) + ) latest_coin_id = target_nft.coin.name() + signing_response = sign_message( + secret_key=synthetic_secret_key, + message=request.message, + mode=request.signing_mode_enum, + ) + return SignMessageByIDResponse( + pubkey=signing_response.pubkey, + signature=signing_response.signature, + signing_mode=request.signing_mode_enum.value, + latest_coin_id=latest_coin_id, + ) else: raise ValueError(f"Unknown ID type, {request.id}") - return SignMessageByIDResponse( - pubkey=pubkey, - signature=signature, - latest_coin_id=latest_coin_id, - signing_mode=mode.value, - ) - ########################################################################################## # CATs and Trading ##########################################################################################