Fix hint parsing for CATs and DIDs (#15259)

* Fix hint parsing for CATs and DIDs

* Handle not hinted coins

* Fix compute_coin_hints condition checking

* don't try to sync non-singleton children of singleton

* coverage ignores

* rename function

* Make function do what it says

* wallet: Some suggestions from #15274 for #15259 (#15547)

* wallet: Some suggestions from #15274 for #15259

* Return a dict with the coin id as key

* Drop `compute_hint_for_coin`, Test `compute_spend_hints_and_additions`

---------

Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com>
This commit is contained in:
Matt Hauff 2023-06-30 11:42:09 -07:00 committed by GitHub
parent a449a2feae
commit 424c51072d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 103 additions and 42 deletions

View File

@ -72,7 +72,7 @@ from chia.wallet.trading.offer import Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle
from chia.wallet.util.address_type import AddressType, is_valid_address
from chia.wallet.util.compute_hints import compute_coin_hints
from chia.wallet.util.compute_hints import compute_spend_hints_and_additions
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter
from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType
@ -1981,27 +1981,25 @@ class WalletRpcApi:
return {"success": False, "error": "The coin is not a DID."}
p2_puzzle, recovery_list_hash, num_verification, singleton_struct, metadata = curried_args
hint_list = compute_coin_hints(coin_spend)
derivation_record = None
hinted_coins = compute_spend_hints_and_additions(coin_spend)
# Hint is required, if it doesn't have any hint then it should be invalid
is_invalid = len(hint_list) == 0
for hint in hint_list:
derivation_record = (
await self.service.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
bytes32(hint)
)
)
if derivation_record is not None:
is_invalid = False
hint: Optional[bytes32] = None
for hinted_coin in hinted_coins.values():
if hinted_coin.coin.amount % 2 == 1 and hinted_coin.hint is not None:
hint = hinted_coin.hint
break
is_invalid = True
if is_invalid:
if hint is None:
# This is an invalid DID, check if we are owner
derivation_record = (
await self.service.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
p2_puzzle.get_tree_hash()
)
)
else:
derivation_record = (
await self.service.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(hint)
)
launcher_id = singleton_struct.rest().first().as_python()
if derivation_record is None:
return {"success": False, "error": f"This DID {launcher_id.hex()} is not belong to the connected wallet"}

View File

@ -1,22 +1,37 @@
from __future__ import annotations
from typing import List
from dataclasses import dataclass
from typing import Dict, Optional
from chia.types.blockchain_format.program import INFINITE_COST
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import INFINITE_COST, Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import CoinSpend
from chia.types.condition_opcodes import ConditionOpcode
from chia.util.ints import uint64
def compute_coin_hints(cs: CoinSpend) -> List[bytes32]:
@dataclass(frozen=True)
class HintedCoin:
coin: Coin
hint: Optional[bytes32]
def compute_spend_hints_and_additions(cs: CoinSpend) -> Dict[bytes32, HintedCoin]:
_, result_program = cs.puzzle_reveal.run_with_cost(INFINITE_COST, cs.solution)
h_list: List[bytes32] = []
for condition_data in result_program.as_python():
condition = condition_data[0]
args = condition_data[1:]
if condition == ConditionOpcode.CREATE_COIN and len(args) > 2:
if isinstance(args[2], list):
if isinstance(args[2][0], bytes):
h_list.append(bytes32(args[2][0]))
return h_list
hinted_coins: Dict[bytes32, HintedCoin] = {}
for condition in result_program.as_iter():
if condition.at("f").atom == ConditionOpcode.CREATE_COIN: # It's a create coin:
coin: Coin = Coin(cs.coin.name(), bytes32(condition.at("rf").atom), uint64(condition.at("rrf").as_int()))
hint: Optional[bytes32] = None
if (
condition.at("rrr") != Program.to(None) # There's more than two arguments
and condition.at("rrrf").atom is None # The 3rd argument is a cons
):
potential_hint: bytes = condition.at("rrrff").atom
if len(potential_hint) == 32:
hint = bytes32(potential_hint)
hinted_coins[bytes32(coin.name())] = HintedCoin(coin, hint)
return hinted_coins

View File

@ -77,7 +77,7 @@ from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle
from chia.wallet.util.address_type import AddressType
from chia.wallet.util.compute_hints import compute_coin_hints
from chia.wallet.util.compute_hints import compute_spend_hints_and_additions
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager
from chia.wallet.util.query_filter import HashFilter
@ -681,12 +681,12 @@ class WalletStateManager:
# hint
# First spend where 1 mojo coin -> Singleton launcher -> NFT -> NFT
uncurried_nft = UncurriedNFT.uncurry(uncurried.mod, uncurried.args)
if uncurried_nft is not None:
if uncurried_nft is not None and coin_state.coin.amount % 2 == 1:
return await self.handle_nft(coin_spend, uncurried_nft, parent_coin_state, coin_state)
# Check if the coin is a DID
did_curried_args = match_did_puzzle(uncurried.mod, uncurried.args)
if did_curried_args is not None:
if did_curried_args is not None and coin_state.coin.amount % 2 == 1:
return await self.handle_did(did_curried_args, parent_coin_state, coin_state, coin_spend, peer)
# Check if the coin is clawback
@ -865,12 +865,9 @@ class WalletStateManager:
"""
mod_hash, tail_hash, inner_puzzle = curried_args
hint_list = compute_coin_hints(coin_spend)
derivation_record = None
for hint in hint_list:
derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(bytes32(hint))
if derivation_record is not None:
break
hinted_coin = compute_spend_hints_and_additions(coin_spend)[coin_state.coin.name()]
assert hinted_coin.hint is not None, f"hint missing for coin {hinted_coin.coin}"
derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(hinted_coin.hint)
if derivation_record is None:
self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}")
@ -924,13 +921,9 @@ class WalletStateManager:
inner_puzzle_hash = p2_puzzle.get_tree_hash()
self.log.info(f"parent: {parent_coin_state.coin.name()} inner_puzzle_hash for parent is {inner_puzzle_hash}")
hint_list = compute_coin_hints(coin_spend)
derivation_record = None
for hint in hint_list:
derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(bytes32(hint))
if derivation_record is not None:
break
hinted_coin = compute_spend_hints_and_additions(coin_spend)[coin_state.coin.name()]
assert hinted_coin.hint is not None, f"hint missing for coin {hinted_coin.coin}"
derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(hinted_coin.hint)
launch_id: bytes32 = bytes32(bytes(singleton_struct.rest().first())[1:])
if derivation_record is None:

View File

@ -17,8 +17,14 @@ from types import TracebackType
from typing import Any, Callable, Collection, Iterator, List, Optional, Type, Union
import pytest
from chia_rs import Coin
from typing_extensions import Protocol, final
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.condition_opcodes import ConditionOpcode
from chia.util.hash import std_hash
from chia.util.ints import uint64
from chia.wallet.util.compute_hints import HintedCoin
from tests.core.data_layer.util import ChiaRoot
@ -332,3 +338,32 @@ class DataCasesDecorator(Protocol):
def named_datacases(name: str) -> DataCasesDecorator:
return functools.partial(datacases, _name=name)
@dataclasses.dataclass
class CoinGenerator:
_seed: int = -1
def _get_hash(self) -> bytes32:
self._seed += 1
return std_hash(self._seed)
def _get_amount(self) -> uint64:
self._seed += 1
return uint64(self._seed)
def get(self, parent_coin_id: Optional[bytes32] = None, include_hint: bool = True) -> HintedCoin:
if parent_coin_id is None:
parent_coin_id = self._get_hash()
hint = None
if include_hint:
hint = self._get_hash()
return HintedCoin(Coin(parent_coin_id, self._get_hash(), self._get_amount()), hint)
def coin_creation_args(hinted_coin: HintedCoin) -> List[Any]:
if hinted_coin.hint is not None:
memos = [hinted_coin.hint]
else:
memos = []
return [ConditionOpcode.CREATE_COIN, hinted_coin.coin.puzzle_hash, hinted_coin.coin.amount, memos]

20
tests/wallet/test_util.py Normal file
View File

@ -0,0 +1,20 @@
from __future__ import annotations
from chia.types.blockchain_format.program import Program
from chia.types.coin_spend import CoinSpend
from chia.wallet.util.compute_hints import compute_spend_hints_and_additions
from tests.util.misc import CoinGenerator, coin_creation_args
def test_compute_spend_hints_and_additions() -> None:
coin_generator = CoinGenerator()
parent_coin = coin_generator.get()
hinted_coins = [coin_generator.get(parent_coin.coin.name(), include_hint=i % 2 == 0) for i in range(10)]
create_coin_args = [coin_creation_args(create_coin) for create_coin in hinted_coins]
coin_spend = CoinSpend(
parent_coin.coin,
Program.to(1),
Program.to(create_coin_args),
)
expected_dict = {hinted_coin.coin.name(): hinted_coin for hinted_coin in hinted_coins}
assert compute_spend_hints_and_additions(coin_spend) == expected_dict