Merge main into checkpoint/main_from_release_1.6.0_12fcaf0e982590efcf26a954c67cd760035d9e6e

This commit is contained in:
Amine Khaldi 2022-08-30 13:25:43 +01:00
commit aacad8fecf
No known key found for this signature in database
GPG Key ID: B1C074FFC904E2D9
20 changed files with 817 additions and 165 deletions

View File

@ -21,6 +21,8 @@ keychain_commands = [
"get_key_for_fingerprint",
"get_key",
"get_keys",
"set_label",
"delete_label",
]
log = logging.getLogger(__name__)
@ -32,6 +34,12 @@ KEYCHAIN_ERR_KEY_NOT_FOUND = "key not found"
KEYCHAIN_ERR_MALFORMED_REQUEST = "malformed request"
@streamable
@dataclass(frozen=True)
class EmptyResponse(Streamable):
pass
@streamable
@dataclass(frozen=True)
class GetKeyResponse(Streamable):
@ -63,6 +71,27 @@ class GetKeysRequest(Streamable):
return GetKeysResponse(keys=keychain.get_keys(self.include_secrets))
@streamable
@dataclass(frozen=True)
class SetLabelRequest(Streamable):
fingerprint: uint32
label: str
def run(self, keychain: Keychain) -> EmptyResponse:
keychain.set_label(int(self.fingerprint), self.label)
return EmptyResponse()
@streamable
@dataclass(frozen=True)
class DeleteLabelRequest(Streamable):
fingerprint: uint32
def run(self, keychain: Keychain) -> EmptyResponse:
keychain.delete_label(self.fingerprint)
return EmptyResponse()
class KeychainServer:
"""
Implements a remote keychain service for clients to perform key operations on
@ -113,6 +142,10 @@ class KeychainServer:
return await self.run_request(data, GetKeyRequest)
elif command == "get_keys":
return await self.run_request(data, GetKeysRequest)
elif command == "set_label":
return await self.run_request(data, SetLabelRequest)
elif command == "delete_label":
return await self.run_request(data, DeleteLabelRequest)
return {}
except Exception as e:
log.exception(e)
@ -123,6 +156,8 @@ class KeychainServer:
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
mnemonic = request.get("mnemonic", None)
label = request.get("label", None)
if mnemonic is None:
return {
"success": False,
@ -131,7 +166,7 @@ class KeychainServer:
}
try:
self.get_keychain_for_request(request).add_private_key(mnemonic)
self.get_keychain_for_request(request).add_private_key(mnemonic, label)
except KeyError as e:
return {
"success": False,

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import io
from typing import Callable, Dict, List, Set, Tuple, Optional, Any
@ -6,7 +7,6 @@ from clvm.casts import int_from_bytes
from clvm.EvalError import EvalError
from clvm.serialize import sexp_from_stream, sexp_to_stream
from chia_rs import MEMPOOL_MODE, run_chia_program, serialized_length, run_generator, tree_hash
from clvm_tools.curry import uncurry
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.hash import std_hash
@ -32,11 +32,18 @@ class Program(SExp):
sexp_to_stream(self, f)
@classmethod
def from_bytes(cls, blob: bytes) -> "Program":
f = io.BytesIO(blob)
result = cls.parse(f) # noqa
assert f.read() == b""
return result
def from_bytes(cls, blob: bytes) -> Program:
# this runs the program "1", which just returns the first arugment.
# the first argument is the buffer we want to parse. This effectively
# leverages the rust parser and LazyNode, making it a lot faster to
# parse serialized programs into a python compatible structure
cost, ret = run_chia_program(
b"\x01",
blob,
50,
0,
)
return Program.to(ret)
@classmethod
def fromhex(cls, hexstr: str) -> "Program":
@ -136,11 +143,37 @@ class Program(SExp):
fixed_args = [4, (1, arg), fixed_args]
return Program.to([2, (1, self), fixed_args])
def uncurry(self) -> Tuple["Program", "Program"]:
r = uncurry(self)
if r is None:
def uncurry(self) -> Tuple[Program, Program]:
def match(o: SExp, expected: bytes) -> None:
if o.atom != expected:
raise ValueError(f"expected: {expected.hex()}")
try:
# (2 (1 . <mod>) <args>)
ev, quoted_inner, args_list = self.as_iter()
match(ev, b"\x02")
match(quoted_inner.pair[0], b"\x01")
mod = quoted_inner.pair[1]
args = []
while args_list.pair is not None:
# (4 (1 . <arg>) <rest>)
cons, quoted_arg, rest = args_list.as_iter()
match(cons, b"\x04")
match(quoted_arg.pair[0], b"\x01")
args.append(quoted_arg.pair[1])
args_list = rest
match(args_list, b"\x01")
return Program.to(mod), Program.to(args)
except ValueError: # too many values to unpack
# when unpacking as_iter()
# or when a match() fails
return self, self.to(0)
except TypeError: # NoneType not subscriptable
# when an object is not a pair or atom as expected
return self, self.to(0)
except EvalError: # first of non-cons
# when as_iter() fails
return self, self.to(0)
return r
def as_int(self) -> int:
return int_from_bytes(self.as_atom())

View File

@ -192,6 +192,7 @@ class KeyDataSecrets(Streamable):
class KeyData(Streamable):
fingerprint: uint32
public_key: G1Element
label: Optional[str]
secrets: Optional[KeyDataSecrets]
def __post_init__(self) -> None:
@ -203,21 +204,22 @@ class KeyData(Streamable):
raise KeychainKeyDataMismatch("fingerprint")
@classmethod
def from_mnemonic(cls, mnemonic: str) -> KeyData:
def from_mnemonic(cls, mnemonic: str, label: Optional[str] = None) -> KeyData:
private_key = AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic))
return cls(
fingerprint=private_key.get_g1().get_fingerprint(),
public_key=private_key.get_g1(),
label=label,
secrets=KeyDataSecrets.from_mnemonic(mnemonic),
)
@classmethod
def from_entropy(cls, entropy: bytes) -> KeyData:
return cls.from_mnemonic(bytes_to_mnemonic(entropy))
def from_entropy(cls, entropy: bytes, label: Optional[str] = None) -> KeyData:
return cls.from_mnemonic(bytes_to_mnemonic(entropy), label)
@classmethod
def generate(cls) -> KeyData:
return cls.from_mnemonic(generate_mnemonic())
def generate(cls, label: Optional[str] = None) -> KeyData:
return cls.from_mnemonic(generate_mnemonic(), label)
@property
def mnemonic(self) -> List[str]:
@ -279,11 +281,13 @@ class Keychain:
str_bytes = bytes.fromhex(read_str)
public_key = G1Element.from_bytes(str_bytes[: G1Element.SIZE])
fingerprint = public_key.get_fingerprint()
entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32]
return KeyData(
fingerprint=public_key.get_fingerprint(),
fingerprint=fingerprint,
public_key=public_key,
label=self.keyring_wrapper.get_label(fingerprint),
secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets else None,
)
@ -299,7 +303,7 @@ class Keychain:
except KeychainUserNotFound:
return index
def add_private_key(self, mnemonic: str) -> PrivateKey:
def add_private_key(self, mnemonic: str, label: Optional[str] = None) -> PrivateKey:
"""
Adds a private key to the keychain, with the given entropy and passphrase. The
keychain itself will store the public key, and the entropy bytes,
@ -315,13 +319,37 @@ class Keychain:
# Prevents duplicate add
raise KeychainFingerprintExists(fingerprint)
# Try to set the label first, it may fail if the label is invalid or already exists.
# This can probably just be moved into `FileKeyring.set_passphrase` after the legacy keyring stuff was dropped.
if label is not None:
self.keyring_wrapper.set_label(fingerprint, label)
try:
self.keyring_wrapper.set_passphrase(
self.service,
get_private_key_user(self.user, index),
bytes(key.get_g1()).hex() + entropy.hex(),
)
except Exception:
if label is not None:
self.keyring_wrapper.delete_label(fingerprint)
raise
return key
def set_label(self, fingerprint: int, label: str) -> None:
"""
Assigns the given label to the first key with the given fingerprint.
"""
self.get_key(fingerprint) # raise if the fingerprint doesn't exist
self.keyring_wrapper.set_label(fingerprint, label)
def delete_label(self, fingerprint: int) -> None:
"""
Removes the label assigned to the key with the given fingerprint.
"""
self.keyring_wrapper.delete_label(fingerprint)
def get_first_private_key(self) -> Optional[Tuple[PrivateKey, bytes]]:
"""
Returns the first key in the keychain that has one of the passed in passphrases.

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Any, List, Optional
from typing import Callable, List, Optional
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
@ -19,12 +19,11 @@ from chia.wallet.puzzles.cat_loader import CAT_MOD
@dataclass(frozen=True)
class CATOuterPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
_match: Callable[[Program], Optional[PuzzleInfo]]
_construct: Callable[[PuzzleInfo, Program], Program]
_solve: Callable[[PuzzleInfo, Solver, Program, Program], Program]
_get_inner_puzzle: Callable[[PuzzleInfo, Program], Optional[Program]]
_get_inner_solution: Callable[[PuzzleInfo, Program], Optional[Program]]
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
args = match_cat_puzzle(*puzzle.uncurry())
@ -45,16 +44,18 @@ class CATOuterPuzzle:
if args is None:
raise ValueError("This driver is not for the specified puzzle reveal")
_, _, inner_puzzle = args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(also, inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.first()
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
also = constructor.also()
if also:
deep_inner_solution: Optional[Program] = self._get_inner_solution(also, my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
@ -63,8 +64,9 @@ class CATOuterPuzzle:
return bytes32(constructor["tail"])
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
if constructor.also() is not None:
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
inner_puzzle = self._construct(also, inner_puzzle)
return construct_cat_puzzle(CAT_MOD, constructor["tail"], inner_puzzle)
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
@ -91,9 +93,10 @@ class CATOuterPuzzle:
target_coin = coin
parent_spend: CoinSpend = CoinSpend.from_bytes(spend_prog.as_python())
parent_coin: Coin = parent_spend.coin
if constructor.also() is not None:
puzzle = self._construct(constructor.also(), puzzle)
solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
also = constructor.also()
if also is not None:
puzzle = self._construct(also, puzzle)
solution = self._solve(also, solver, inner_puzzle, inner_solution)
args = match_cat_puzzle(*parent_spend.puzzle_reveal.to_program().uncurry())
assert args is not None
_, _, parent_inner_puzzle = args

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
from typing import Callable, List, Optional, Tuple
from clvm_tools.binutils import disassemble
@ -31,12 +31,11 @@ def solution_for_metadata_layer(amount: uint64, inner_solution: Program) -> Prog
@dataclass(frozen=True)
class MetadataOuterPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
_match: Callable[[Program], Optional[PuzzleInfo]]
_construct: Callable[[PuzzleInfo, Program], Program]
_solve: Callable[[PuzzleInfo, Solver, Program, Program], Program]
_get_inner_puzzle: Callable[[PuzzleInfo, Program], Optional[Program]]
_get_inner_solution: Callable[[PuzzleInfo, Program], Optional[Program]]
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_metadata_layer_puzzle(puzzle)
@ -59,16 +58,18 @@ class MetadataOuterPuzzle:
return bytes32(constructor["updater_hash"])
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
if constructor.also() is not None:
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
inner_puzzle = self._construct(also, inner_puzzle)
return puzzle_for_metadata_layer(constructor["metadata"], constructor["updater_hash"], inner_puzzle)
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
matched, curried_args = match_metadata_layer_puzzle(puzzle_reveal)
if matched:
_, _, _, inner_puzzle = curried_args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(also, inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
@ -77,8 +78,9 @@ class MetadataOuterPuzzle:
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.first()
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
also = constructor.also()
if also:
deep_inner_solution: Optional[Program] = self._get_inner_solution(also, my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
@ -86,8 +88,9 @@ class MetadataOuterPuzzle:
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
coin_bytes: bytes = solver["coin"]
coin: Coin = Coin(bytes32(coin_bytes[0:32]), bytes32(coin_bytes[32:64]), uint64.from_bytes(coin_bytes[64:72]))
if constructor.also() is not None:
inner_solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
also = constructor.also()
if also is not None:
inner_solution = self._solve(also, solver, inner_puzzle, inner_solution)
return solution_for_metadata_layer(
uint64(coin.amount),
inner_solution,

View File

@ -21,6 +21,7 @@ SINGLETON_MOD_HASH = SINGLETON_TOP_LAYER_MOD.get_tree_hash()
NFT_STATE_LAYER_MOD_HASH = NFT_STATE_LAYER_MOD.get_tree_hash()
NFT_METADATA_UPDATER = load_clvm("nft_metadata_updater_default.clvm")
NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm")
NFT_OWNERSHIP_LAYER_HASH = NFT_OWNERSHIP_LAYER.get_tree_hash()
NFT_TRANSFER_PROGRAM_DEFAULT = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm")
STANDARD_PUZZLE_MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.clvm")
@ -39,6 +40,7 @@ def create_nft_layer_puzzle_with_curry_params(
def create_full_puzzle_with_nft_puzzle(singleton_id: bytes32, inner_puzzle: Program) -> Program:
if log.isEnabledFor(logging.DEBUG):
log.debug(
"Creating full NFT puzzle with inner puzzle: \n%r\n%r",
singleton_id,
@ -47,6 +49,7 @@ def create_full_puzzle_with_nft_puzzle(singleton_id: bytes32, inner_puzzle: Prog
singleton_struct = Program.to((SINGLETON_MOD_HASH, (singleton_id, LAUNCHER_PUZZLE_HASH)))
full_puzzle = SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, inner_puzzle)
if log.isEnabledFor(logging.DEBUG):
log.debug("Created NFT full puzzle with inner: %s", full_puzzle.get_tree_hash())
return full_puzzle
@ -54,6 +57,7 @@ def create_full_puzzle_with_nft_puzzle(singleton_id: bytes32, inner_puzzle: Prog
def create_full_puzzle(
singleton_id: bytes32, metadata: Program, metadata_updater_puzhash: bytes32, inner_puzzle: Program
) -> Program:
if log.isEnabledFor(logging.DEBUG):
log.debug(
"Creating full NFT puzzle with: \n%r\n%r\n%r\n%r",
singleton_id,
@ -65,6 +69,7 @@ def create_full_puzzle(
singleton_inner_puzzle = create_nft_layer_puzzle_with_curry_params(metadata, metadata_updater_puzhash, inner_puzzle)
full_puzzle = SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, singleton_inner_puzzle)
if log.isEnabledFor(logging.DEBUG):
log.debug("Created NFT full puzzle: %s", full_puzzle.get_tree_hash())
return full_puzzle
@ -171,7 +176,7 @@ def construct_ownership_layer(
inner_puzzle: Program,
) -> Program:
return NFT_OWNERSHIP_LAYER.curry(
NFT_OWNERSHIP_LAYER.get_tree_hash(),
NFT_OWNERSHIP_LAYER_HASH,
current_owner,
transfer_program,
inner_puzzle,

View File

@ -31,7 +31,7 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
calculate_synthetic_secret_key,
puzzle_for_pk,
)
from chia.wallet.trading.offer import OFFER_MOD, NotarizedPayment, Offer
from chia.wallet.trading.offer import OFFER_MOD, OFFER_MOD_HASH, NotarizedPayment, Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.debug_spend_bundle import disassemble
@ -168,8 +168,11 @@ class NFTWallet:
uncurried_nft = UncurriedNFT.uncurry(*puzzle.uncurry())
assert uncurried_nft is not None
self.log.info(
f"found the info for NFT coin {coin_name} {uncurried_nft.inner_puzzle} {uncurried_nft.singleton_struct}"
self.log.debug(
"found the info for NFT coin %s %s %s",
coin_name.hex(),
uncurried_nft.inner_puzzle,
uncurried_nft.singleton_struct,
)
singleton_id = uncurried_nft.singleton_launcher_id
parent_inner_puzhash = uncurried_nft.nft_state_layer.get_tree_hash()
@ -181,7 +184,7 @@ class NFTWallet:
] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(p2_puzzle_hash)
self.log.debug("Record for %s is: %s", p2_puzzle_hash, derivation_record)
if derivation_record is None:
self.log.debug(f"Not our NFT, pointing to {p2_puzzle_hash}, skipping")
self.log.debug("Not our NFT, pointing to %s, skipping", p2_puzzle_hash)
return
p2_puzzle = puzzle_for_pk(derivation_record.pubkey)
if uncurried_nft.supports_did:
@ -199,14 +202,15 @@ class NFTWallet:
"Created NFT full puzzle with inner: %s",
nft_puzzles.create_full_puzzle_with_nft_puzzle(singleton_id, uncurried_nft.inner_puzzle),
)
child_puzzle_hash = child_puzzle.get_tree_hash()
for new_coin in coin_spend.additions():
self.log.debug(
"Comparing addition: %s with %s, amount: %s ",
new_coin.puzzle_hash,
child_puzzle.get_tree_hash(),
child_puzzle_hash,
new_coin.amount,
)
if new_coin.puzzle_hash == child_puzzle.get_tree_hash():
if new_coin.puzzle_hash == child_puzzle_hash:
child_coin = new_coin
break
else:
@ -322,7 +326,7 @@ class NFTWallet:
origin = coins.copy().pop()
genesis_launcher_puz = nft_puzzles.LAUNCHER_PUZZLE
# nft_id == singleton_id == launcher_id == launcher_coin.name()
launcher_coin = Coin(origin.name(), genesis_launcher_puz.get_tree_hash(), uint64(amount))
launcher_coin = Coin(origin.name(), nft_puzzles.LAUNCHER_PUZZLE_HASH, uint64(amount))
self.log.debug("Generating NFT with launcher coin %s and metadata: %s", launcher_coin, metadata)
p2_inner_puzzle = await self.standard_wallet.get_new_puzzle()
@ -346,9 +350,10 @@ class NFTWallet:
eve_fullpuz = nft_puzzles.create_full_puzzle(
launcher_coin.name(), metadata, NFT_METADATA_UPDATER.get_tree_hash(), inner_puzzle
)
eve_fullpuz_hash = eve_fullpuz.get_tree_hash()
# launcher announcement
announcement_set: Set[Announcement] = set()
announcement_message = Program.to([eve_fullpuz.get_tree_hash(), amount, []]).get_tree_hash()
announcement_message = Program.to([eve_fullpuz_hash, amount, []]).get_tree_hash()
announcement_set.add(Announcement(launcher_coin.name(), announcement_message))
self.log.debug(
@ -357,7 +362,7 @@ class NFTWallet:
# store the launcher transaction in the wallet state
tx_record: Optional[TransactionRecord] = await self.standard_wallet.generate_signed_transaction(
uint64(amount),
genesis_launcher_puz.get_tree_hash(),
nft_puzzles.LAUNCHER_PUZZLE_HASH,
fee,
origin.name(),
coins,
@ -365,13 +370,13 @@ class NFTWallet:
False,
announcement_set,
)
genesis_launcher_solution = Program.to([eve_fullpuz.get_tree_hash(), amount, []])
genesis_launcher_solution = Program.to([eve_fullpuz_hash, amount, []])
# launcher spend to generate the singleton
launcher_cs = CoinSpend(launcher_coin, genesis_launcher_puz, genesis_launcher_solution)
launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([]))
eve_coin = Coin(launcher_coin.name(), eve_fullpuz.get_tree_hash(), uint64(amount))
eve_coin = Coin(launcher_coin.name(), eve_fullpuz_hash, uint64(amount))
if tx_record is None or tx_record.spend_bundle is None:
self.log.error("Couldn't produce a launcher spend")
@ -793,9 +798,7 @@ class NFTWallet:
for asset, amount in fungible_asset_dict.items(): # requested fungible items
if amount > 0:
settlement_ph: bytes32 = (
OFFER_MOD.get_tree_hash()
if asset is None
else construct_puzzle(driver_dict[asset], OFFER_MOD).get_tree_hash()
OFFER_MOD_HASH if asset is None else construct_puzzle(driver_dict[asset], OFFER_MOD).get_tree_hash()
)
trade_prices.append([uint64(math.floor(amount / offer_side_royalty_split)), settlement_ph])
@ -863,6 +866,7 @@ class NFTWallet:
for asset, payments in royalty_payments.items():
if asset is None: # xch offer
offer_puzzle = OFFER_MOD
royalty_ph = OFFER_MOD_HASH
else:
offer_puzzle = construct_puzzle(driver_dict[asset], OFFER_MOD)
royalty_ph = offer_puzzle.get_tree_hash()
@ -930,6 +934,7 @@ class NFTWallet:
inner_royalty_sol = Program.to([(launcher_id, [payment.as_condition_args()])])
if asset is None: # xch offer
offer_puzzle = OFFER_MOD
royalty_ph = OFFER_MOD_HASH
else:
offer_puzzle = construct_puzzle(driver_dict[asset], OFFER_MOD)
royalty_ph = offer_puzzle.get_tree_hash()

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple, Union
from typing import Callable, List, Optional, Tuple, Union
from clvm_tools.binutils import disassemble
@ -30,12 +30,11 @@ def solution_for_ownership_layer(inner_solution: Program) -> Program:
@dataclass(frozen=True)
class OwnershipOuterPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
_match: Callable[[Program], Optional[PuzzleInfo]]
_construct: Callable[[PuzzleInfo, Program], Program]
_solve: Callable[[PuzzleInfo, Solver, Program, Program], Program]
_get_inner_puzzle: Callable[[PuzzleInfo, Program], Optional[Program]]
_get_inner_solution: Callable[[PuzzleInfo, Program], Optional[Program]]
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_ownership_layer_puzzle(puzzle)
@ -61,8 +60,9 @@ class OwnershipOuterPuzzle:
return None
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
if constructor.also() is not None:
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
inner_puzzle = self._construct(also, inner_puzzle)
transfer_program_info: Union[PuzzleInfo, Program] = constructor["transfer_program"]
if isinstance(transfer_program_info, Program):
transfer_program: Program = transfer_program_info
@ -74,8 +74,9 @@ class OwnershipOuterPuzzle:
matched, curried_args = match_ownership_layer_puzzle(puzzle_reveal)
if matched:
_, _, _, inner_puzzle = curried_args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(also, inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
@ -84,13 +85,15 @@ class OwnershipOuterPuzzle:
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.first()
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
also = constructor.also()
if also:
deep_inner_solution: Optional[Program] = self._get_inner_solution(also, my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
if constructor.also() is not None:
inner_solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
also = constructor.also()
if also is not None:
inner_solution = self._solve(also, solver, inner_puzzle, inner_solution)
return solution_for_ownership_layer(inner_solution)

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Any, Optional
from typing import Callable, Optional
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
@ -18,12 +18,11 @@ from chia.wallet.puzzles.singleton_top_layer_v1_1 import (
@dataclass(frozen=True)
class SingletonOuterPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
_match: Callable[[Program], Optional[PuzzleInfo]]
_construct: Callable[[PuzzleInfo, Program], Program]
_solve: Callable[[PuzzleInfo, Solver, Program, Program], Program]
_get_inner_puzzle: Callable[[PuzzleInfo, Program], Optional[Program]]
_get_inner_solution: Callable[[PuzzleInfo, Program], Optional[Program]]
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_singleton_puzzle(puzzle)
@ -45,8 +44,9 @@ class SingletonOuterPuzzle:
return bytes32(constructor["launcher_id"])
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
if constructor.also() is not None:
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
inner_puzzle = self._construct(also, inner_puzzle)
launcher_hash = constructor["launcher_ph"] if "launcher_ph" in constructor else SINGLETON_LAUNCHER_HASH
return puzzle_for_singleton(constructor["launcher_id"], inner_puzzle, launcher_hash)
@ -54,8 +54,9 @@ class SingletonOuterPuzzle:
matched, curried_args = match_singleton_puzzle(puzzle_reveal)
if matched:
_, inner_puzzle = curried_args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
also = constructor.also()
if also is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(also, inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
@ -64,8 +65,9 @@ class SingletonOuterPuzzle:
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.at("rrf")
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
also = constructor.also()
if also:
deep_inner_solution: Optional[Program] = self._get_inner_solution(also, my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
@ -75,8 +77,9 @@ class SingletonOuterPuzzle:
coin: Coin = Coin(bytes32(coin_bytes[0:32]), bytes32(coin_bytes[32:64]), uint64.from_bytes(coin_bytes[64:72]))
parent_spend: CoinSpend = CoinSpend.from_bytes(solver["parent_spend"])
parent_coin: Coin = parent_spend.coin
if constructor.also() is not None:
inner_solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
also = constructor.also()
if also is not None:
inner_solution = self._solve(also, solver, inner_puzzle, inner_solution)
matched, curried_args = match_singleton_puzzle(parent_spend.puzzle_reveal.to_program())
assert matched
_, parent_inner_puzzle = curried_args

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
from typing import Callable, List, Optional, Tuple
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
@ -39,12 +39,11 @@ def solution_for_transfer_program(
@dataclass(frozen=True)
class TransferProgramPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
_match: Callable[[Program], Optional[PuzzleInfo]]
_construct: Callable[[PuzzleInfo, Program], Program]
_solve: Callable[[PuzzleInfo, Solver, Program, Program], Program]
_get_inner_puzzle: Callable[[PuzzleInfo, Program], Optional[Program]]
_get_inner_solution: Callable[[PuzzleInfo, Program], Optional[Program]]
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_transfer_program_puzzle(puzzle)

View File

@ -63,11 +63,11 @@ def get_inner_solution(constructor: PuzzleInfo, solution: Program) -> Optional[P
return driver_lookup[AssetType(constructor.type())].get_inner_solution(constructor, solution)
def create_asset_id(constructor: PuzzleInfo) -> bytes32:
return driver_lookup[AssetType(constructor.type())].asset_id(constructor) # type: ignore
def create_asset_id(constructor: PuzzleInfo) -> Optional[bytes32]:
return driver_lookup[AssetType(constructor.type())].asset_id(constructor)
function_args = [match_puzzle, create_asset_id, construct_puzzle, solve_puzzle, get_inner_puzzle, get_inner_solution]
function_args = (match_puzzle, construct_puzzle, solve_puzzle, get_inner_puzzle, get_inner_solution)
driver_lookup: Dict[AssetType, DriverProtocol] = {
AssetType.CAT: CATOuterPuzzle(*function_args),

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
@ -51,7 +53,7 @@ class PuzzleInfo:
def type(self) -> str:
return str(self.info["type"])
def also(self) -> Optional["PuzzleInfo"]:
def also(self) -> Optional[PuzzleInfo]:
if "also" in self.info:
return PuzzleInfo(self.info["also"])
else:

View File

@ -199,7 +199,7 @@ class TradeManager:
coins_of_interest = []
for trade_offer in all_pending:
coins_of_interest.extend([c.name() for c in Offer.from_bytes(trade_offer.offer).get_involved_coins()])
coins_of_interest.extend([c.name() for c in trade_offer.coins_of_interest])
result = {}
coin_records = await self.wallet_state_manager.coin_store.get_multiple_coin_records(coins_of_interest)

View File

@ -31,6 +31,7 @@ from chia.wallet.util.puzzle_compression import (
)
OFFER_MOD = load_clvm("settlement_payments.clvm")
OFFER_MOD_HASH = OFFER_MOD.get_tree_hash()
ZERO_32 = bytes32([0] * 32)
@ -69,7 +70,7 @@ class Offer:
@staticmethod
def ph() -> bytes32:
return OFFER_MOD.get_tree_hash()
return OFFER_MOD_HASH
@staticmethod
def notarize_payments(
@ -102,7 +103,7 @@ class Offer:
raise ValueError("Cannot calculate announcements without driver of requested item")
settlement_ph: bytes32 = construct_puzzle(driver_dict[asset_id], OFFER_MOD).get_tree_hash()
else:
settlement_ph = OFFER_MOD.get_tree_hash()
settlement_ph = OFFER_MOD_HASH
msg: bytes32 = Program.to((payments[0].nonce, [p.as_condition_args() for p in payments])).get_tree_hash()
announcements.append(Announcement(settlement_ph, msg))
@ -110,13 +111,6 @@ class Offer:
return announcements
def __post_init__(self) -> None:
# Verify that there is at least something being offered
offered_coins: Dict[Optional[bytes32], List[Coin]] = self.get_offered_coins()
if offered_coins == {}:
raise ValueError("Bundle is not offering anything")
if self.get_requested_payments() == {}:
raise ValueError("Bundle is not requesting anything")
# Verify that there are no duplicate payments
for payments in self.requested_payments.values():
payment_programs: List[bytes32] = [p.name() for p in payments]
@ -154,7 +148,7 @@ class Offer:
def get_offered_coins(self) -> Dict[Optional[bytes32], List[Coin]]:
offered_coins: Dict[Optional[bytes32], List[Coin]] = {}
OFFER_HASH: bytes32 = OFFER_MOD.get_tree_hash()
OFFER_HASH: bytes32 = OFFER_MOD_HASH
for parent_spend in self.bundle.coin_spends:
coins_for_this_spend: List[Coin] = []

View File

@ -2,7 +2,6 @@ from unittest import TestCase
from chia.types.blockchain_format.program import Program
from clvm.EvalError import EvalError
from clvm_tools.curry import uncurry
from clvm.operators import KEYWORD_TO_ATOM
from clvm_tools.binutils import assemble, disassemble
@ -49,7 +48,7 @@ def check_idempotency(f, *args):
curried = prg.curry(*args)
r = disassemble(curried)
f_0, args_0 = uncurry(curried)
f_0, args_0 = curried.uncurry()
assert disassemble(f_0) == disassemble(f)
assert disassemble(args_0) == disassemble(Program.to(list(args)))
@ -67,3 +66,33 @@ def test_curry_uncurry():
# passing "args" here wraps the arguments in a list
actual_disassembly = check_idempotency(f, args)
assert actual_disassembly == f"(a (q {PLUS} 2 5) (c (q {PLUS} (q . 50) (q . 60)) 1))"
def test_uncurry_not_curried():
# this function has not been curried
plus = Program.to(assemble("(+ 2 5)"))
assert plus.uncurry() == (plus, Program.to(0))
def test_uncurry():
# this is a positive test
plus = Program.to(assemble("(2 (q . (+ 2 5)) (c (q . 1) 1))"))
assert plus.uncurry() == (Program.to(assemble("(+ 2 5)")), Program.to([1]))
def test_uncurry_top_level_garbage():
# there's garbage at the end of the top-level list
plus = Program.to(assemble("(2 (q . 1) (c (q . 1) (q . 1)) (q . 0x1337))"))
assert plus.uncurry() == (plus, Program.to(0))
def test_uncurry_not_pair():
# the second item in the list is expected to be a pair, with a qoute
plus = Program.to(assemble("(2 1 (c (q . 1) (q . 1)))"))
assert plus.uncurry() == (plus, Program.to(0))
def test_uncurry_args_garbage():
# there's garbage at the end of the args list
plus = Program.to(assemble("(2 (q . 1) (c (q . 1) (q . 1) (q . 0x1337)))"))
assert plus.uncurry() == (plus, Program.to(0))

View File

@ -7,6 +7,7 @@ import pytest
from dataclasses import replace
from typing import Any, Dict, List, Type
from chia.daemon.keychain_server import DeleteLabelRequest, SetLabelRequest
from chia.server.outbound_message import NodeType
from chia.types.peer_info import PeerInfo
from chia.util.ints import uint16
@ -56,6 +57,41 @@ def get_keys_response_data(keys: List[KeyData]) -> Dict[str, object]:
return {"success": True, **GetKeysResponse(keys=keys).to_json_dict()}
def label_missing_response_data(request_type: Type[Any]) -> Dict[str, Any]:
return {
"success": False,
"error": "malformed request",
"error_details": {"message": f"1 field missing for {request_type.__name__}: label"},
}
def label_exists_response_data(fingerprint: int, label: str) -> Dict[str, Any]:
return {
"success": False,
"error": "malformed request",
"error_details": {"message": f"label {label!r} already exists for fingerprint {str(fingerprint)!r}"},
}
label_empty_response_data = {
"success": False,
"error": "malformed request",
"error_details": {"message": "label can't be empty or whitespace only"},
}
label_too_long_response_data = {
"success": False,
"error": "malformed request",
"error_details": {"message": "label exceeds max length: 66/65"},
}
label_newline_or_tab_response_data = {
"success": False,
"error": "malformed request",
"error_details": {"message": "label can't contain newline or tab"},
}
def assert_response(response: aiohttp.http_websocket.WSMessage, expected_response_data: Dict[str, Any]) -> None:
# Expect: JSON response
assert response.type == aiohttp.WSMsgType.TEXT
@ -237,6 +273,31 @@ async def test_add_private_key(daemon_connection_and_temp_keychain):
assert_response(await ws.receive(), invalid_mnemonic_response_data)
@pytest.mark.asyncio
async def test_add_private_key_label(daemon_connection_and_temp_keychain):
ws, keychain = daemon_connection_and_temp_keychain
async def assert_add_private_key_with_label(key_data: KeyData, request: Dict[str, object]) -> None:
await ws.send_str(create_payload("add_private_key", request, "test", "daemon"))
assert_response(await ws.receive(), success_response_data)
await ws.send_str(
create_payload("get_key", {"fingerprint": key_data.fingerprint, "include_secrets": True}, "test", "daemon")
)
assert_response(await ws.receive(), get_key_response_data(key_data))
# without `label` parameter
key_data_0 = KeyData.generate()
await assert_add_private_key_with_label(key_data_0, {"mnemonic": key_data_0.mnemonic_str()})
# with `label=None`
key_data_1 = KeyData.generate()
await assert_add_private_key_with_label(key_data_1, {"mnemonic": key_data_1.mnemonic_str(), "label": None})
# with `label="key_2"`
key_data_2 = KeyData.generate("key_2")
await assert_add_private_key_with_label(
key_data_1, {"mnemonic": key_data_2.mnemonic_str(), "label": key_data_2.label}
)
@pytest.mark.asyncio
async def test_get_key(daemon_connection_and_temp_keychain):
ws, keychain = daemon_connection_and_temp_keychain
@ -300,3 +361,110 @@ async def test_get_keys(daemon_connection_and_temp_keychain):
# with `include_secrets=True`
await ws.send_str(create_payload("get_keys", {"include_secrets": True}, "test", "daemon"))
assert_response(await ws.receive(), get_keys_response_data(keys_added))
@pytest.mark.asyncio
async def test_key_renaming(daemon_connection_and_temp_keychain):
ws, keychain = daemon_connection_and_temp_keychain
keychain.add_private_key(test_key_data.mnemonic_str())
# Rename the key three times
for i in range(3):
key_data = replace(test_key_data_no_secrets, label=f"renaming_{i}")
await ws.send_str(
create_payload(
"set_label", {"fingerprint": key_data.fingerprint, "label": key_data.label}, "test", "daemon"
)
)
assert_response(await ws.receive(), success_response_data)
await ws.send_str(create_payload("get_key", {"fingerprint": key_data.fingerprint}, "test", "daemon"))
assert_response(
await ws.receive(),
{
"success": True,
"key": key_data.to_json_dict(),
},
)
@pytest.mark.asyncio
async def test_key_label_deletion(daemon_connection_and_temp_keychain):
ws, keychain = daemon_connection_and_temp_keychain
keychain.add_private_key(test_key_data.mnemonic_str(), "key_0")
assert keychain.get_key(test_key_data.fingerprint).label == "key_0"
await ws.send_str(create_payload("delete_label", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
assert_response(await ws.receive(), success_response_data)
assert keychain.get_key(test_key_data.fingerprint).label is None
await ws.send_str(create_payload("delete_label", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
assert_response(await ws.receive(), fingerprint_not_found_response_data(test_key_data.fingerprint))
@pytest.mark.parametrize(
"method, parameter, response_data_dict",
[
(
"set_label",
{"fingerprint": test_key_data.fingerprint, "label": "new_label"},
success_response_data,
),
(
"set_label",
{"label": "new_label"},
fingerprint_missing_response_data(SetLabelRequest),
),
(
"set_label",
{"fingerprint": test_key_data.fingerprint},
label_missing_response_data(SetLabelRequest),
),
(
"set_label",
{"fingerprint": test_key_data.fingerprint, "label": ""},
label_empty_response_data,
),
(
"set_label",
{"fingerprint": test_key_data.fingerprint, "label": "a" * 66},
label_too_long_response_data,
),
(
"set_label",
{"fingerprint": test_key_data.fingerprint, "label": "a\nb"},
label_newline_or_tab_response_data,
),
(
"set_label",
{"fingerprint": test_key_data.fingerprint, "label": "a\tb"},
label_newline_or_tab_response_data,
),
(
"set_label",
{"fingerprint": test_key_data.fingerprint, "label": "key_0"},
label_exists_response_data(test_key_data.fingerprint, "key_0"),
),
(
"delete_label",
{"fingerprint": test_key_data.fingerprint},
success_response_data,
),
(
"delete_label",
{},
fingerprint_missing_response_data(DeleteLabelRequest),
),
(
"delete_label",
{"fingerprint": 123456},
fingerprint_not_found_response_data(123456),
),
],
)
@pytest.mark.asyncio
async def test_key_label_methods(
daemon_connection_and_temp_keychain, method: str, parameter: Dict[str, Any], response_data_dict: Dict[str, Any]
) -> None:
ws, keychain = daemon_connection_and_temp_keychain
keychain.add_private_key(test_key_data.mnemonic_str(), "key_0")
await ws.send_str(create_payload(method, parameter, "test", "daemon"))
assert_response(await ws.receive(), response_data_dict)

View File

@ -13,6 +13,8 @@ from chia.util.errors import (
KeychainKeyDataMismatch,
KeychainSecretsMissing,
KeychainFingerprintNotFound,
KeychainLabelExists,
KeychainLabelInvalid,
)
from chia.util.ints import uint32
from chia.util.keychain import (
@ -106,6 +108,41 @@ class TestKeychain(unittest.TestCase):
kc.add_private_key(bytes_to_mnemonic(token_bytes(32)))
assert kc.get_first_public_key() is not None
@using_temp_file_keyring()
def test_add_private_key_label(self):
keychain: Keychain = Keychain(user="testing-1.8.0", service="chia-testing-1.8.0")
key_data_0 = KeyData.generate(label="key_0")
key_data_1 = KeyData.generate(label="key_1")
key_data_2 = KeyData.generate(label=None)
keychain.add_private_key(mnemonic=key_data_0.mnemonic_str(), label=key_data_0.label)
assert key_data_0 == keychain.get_key(key_data_0.fingerprint, include_secrets=True)
# Try to add a new key with an existing label should raise
with pytest.raises(KeychainLabelExists) as e:
keychain.add_private_key(mnemonic=key_data_1.mnemonic_str(), label=key_data_0.label)
assert e.value.fingerprint == key_data_0.fingerprint
assert e.value.label == key_data_0.label
# Adding the same key with a valid label should work fine
keychain.add_private_key(mnemonic=key_data_1.mnemonic_str(), label=key_data_1.label)
assert key_data_1 == keychain.get_key(key_data_1.fingerprint, include_secrets=True)
# Trying to add an existing key should not have an impact on the existing label
with pytest.raises(KeychainFingerprintExists):
keychain.add_private_key(mnemonic=key_data_0.mnemonic_str(), label="other label")
assert key_data_0 == keychain.get_key(key_data_0.fingerprint, include_secrets=True)
# Adding a key with no label should not assign any label
keychain.add_private_key(mnemonic=key_data_2.mnemonic_str(), label=key_data_2.label)
assert key_data_2 == keychain.get_key(key_data_2.fingerprint, include_secrets=True)
# All added keys should still be valid with their label
assert all(
key_data in [key_data_0, key_data_1, key_data_2] for key_data in keychain.get_keys(include_secrets=True)
)
@using_temp_file_keyring()
def test_bip39_eip2333_test_vector(self):
kc: Keychain = Keychain(user="testing-1.8.0", service="chia-testing-1.8.0")
@ -171,29 +208,33 @@ def test_key_data_secrets_creation(input_data: object, from_method: Callable[...
assert secrets.private_key == private_key
def test_key_data_generate() -> None:
key_data = KeyData.generate()
@pytest.mark.parametrize("label", [None, "key"])
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.label == label
@pytest.mark.parametrize("label", [None, "key"])
@pytest.mark.parametrize(
"input_data, from_method", [(mnemonic, KeyData.from_mnemonic), (entropy, KeyData.from_entropy)]
)
def test_key_data_creation(input_data: object, from_method: Callable[..., KeyData]) -> None:
key_data = from_method(input_data)
def test_key_data_creation(input_data: object, from_method: Callable[..., KeyData], label: Optional[str]) -> None:
key_data = from_method(input_data, label)
assert key_data.fingerprint == fingerprint
assert key_data.public_key == public_key
assert key_data.mnemonic == mnemonic.split()
assert key_data.mnemonic_str() == mnemonic
assert key_data.entropy == entropy
assert key_data.private_key == private_key
assert key_data.label == label
def test_key_data_without_secrets() -> None:
key_data = KeyData(fingerprint, public_key, None)
key_data = KeyData(fingerprint, public_key, None, None)
assert key_data.secrets is None
with pytest.raises(KeychainSecretsMissing):
@ -225,11 +266,13 @@ def test_key_data_secrets_post_init(input_data: Tuple[List[str], bytes, PrivateK
@pytest.mark.parametrize(
"input_data, data_type",
[
((fingerprint, G1Element(), KeyDataSecrets(mnemonic.split(), entropy, private_key)), "public_key"),
((fingerprint, G1Element(), None), "fingerprint"),
((fingerprint, G1Element(), None, KeyDataSecrets(mnemonic.split(), entropy, private_key)), "public_key"),
((fingerprint, G1Element(), None, None), "fingerprint"),
],
)
def test_key_data_post_init(input_data: Tuple[uint32, G1Element, Optional[KeyDataSecrets]], data_type: str) -> None:
def test_key_data_post_init(
input_data: Tuple[uint32, G1Element, Optional[str], Optional[KeyDataSecrets]], data_type: str
) -> None:
with pytest.raises(KeychainKeyDataMismatch, match=data_type):
KeyData(*input_data)
@ -283,3 +326,92 @@ def test_get_keys(include_secrets: bool, get_temp_keyring: Keychain):
assert keychain.get_keys(include_secrets) == expected_keys
# Should be empty again
assert keychain.get_keys(include_secrets) == []
def test_set_label(get_temp_keyring: Keychain) -> None:
keychain: Keychain = get_temp_keyring
# Generate a key and add it without label
key_data_0 = KeyData.generate(label=None)
keychain.add_private_key(mnemonic=key_data_0.mnemonic_str(), label=None)
assert key_data_0 == keychain.get_key(key_data_0.fingerprint, include_secrets=True)
# Set a label and validate it
key_data_0 = replace(key_data_0, label="key_0")
assert key_data_0.label is not None
keychain.set_label(fingerprint=key_data_0.fingerprint, label=key_data_0.label)
assert key_data_0 == keychain.get_key(fingerprint=key_data_0.fingerprint, include_secrets=True)
# Try to add the same label for a fingerprint where don't have a key for
with pytest.raises(KeychainFingerprintNotFound):
keychain.set_label(fingerprint=123456, label=key_data_0.label)
# Add a second key
key_data_1 = KeyData.generate(label="key_1")
assert key_data_1.label is not None
keychain.add_private_key(key_data_1.mnemonic_str())
# Try to set the already existing label for the second key
with pytest.raises(KeychainLabelExists) as e:
keychain.set_label(fingerprint=key_data_1.fingerprint, label=key_data_0.label)
assert e.value.fingerprint == key_data_0.fingerprint
assert e.value.label == key_data_0.label
# Set a different label to the second key and validate it
keychain.set_label(fingerprint=key_data_1.fingerprint, label=key_data_1.label)
assert key_data_0 == keychain.get_key(fingerprint=key_data_0.fingerprint, include_secrets=True)
# All added keys should still be valid with their label
assert all(key_data in [key_data_0, key_data_1] for key_data in keychain.get_keys(include_secrets=True))
@pytest.mark.parametrize(
"label, message",
[
("", "label can't be empty or whitespace only"),
(" ", "label can't be empty or whitespace only"),
("a\nb", "label can't contain newline or tab"),
("a\tb", "label can't contain newline or tab"),
("a" * 66, "label exceeds max length: 66/65"),
("a" * 70, "label exceeds max length: 70/65"),
],
)
def test_set_label_invalid_labels(label: str, message: str, get_temp_keyring: Keychain) -> None:
keychain: Keychain = get_temp_keyring
key_data = KeyData.generate()
keychain.add_private_key(key_data.mnemonic_str())
with pytest.raises(KeychainLabelInvalid, match=message) as e:
keychain.set_label(key_data.fingerprint, label)
assert e.value.label == label
def test_delete_label(get_temp_keyring: Keychain) -> None:
keychain: Keychain = get_temp_keyring
# Generate two keys and add them to the keychain
key_data_0 = KeyData.generate(label="key_0")
key_data_1 = KeyData.generate(label="key_1")
def assert_delete_raises():
# Try to delete the labels should fail now since they are gone already
for key_data in [key_data_0, key_data_1]:
with pytest.raises(KeychainFingerprintNotFound) as e:
keychain.delete_label(key_data.fingerprint)
assert e.value.fingerprint == key_data.fingerprint
# Should pass here since the keys are not added yet
assert_delete_raises()
for key in [key_data_0, key_data_1]:
keychain.add_private_key(mnemonic=key.mnemonic_str(), label=key.label)
assert key == keychain.get_key(key.fingerprint, include_secrets=True)
# Delete the label of the first key, validate it was removed and make sure the other key retains its label
keychain.delete_label(key_data_0.fingerprint)
assert replace(key_data_0, label=None) == keychain.get_key(key_data_0.fingerprint, include_secrets=True)
assert key_data_1 == keychain.get_key(key_data_1.fingerprint, include_secrets=True)
# Re-add the label of the first key
assert key_data_0.label is not None
keychain.set_label(key_data_0.fingerprint, key_data_0.label)
# Delete the label of the second key
keychain.delete_label(key_data_1.fingerprint)
assert key_data_0 == keychain.get_key(key_data_0.fingerprint, include_secrets=True)
assert replace(key_data_1, label=None) == keychain.get_key(key_data_1.fingerprint, include_secrets=True)
# Delete the label of the first key again, now both should have no label
keychain.delete_label(key_data_0.fingerprint)
assert replace(key_data_0, label=None) == keychain.get_key(key_data_0.fingerprint, include_secrets=True)
assert replace(key_data_1, label=None) == keychain.get_key(key_data_1.fingerprint, include_secrets=True)
# Should pass here since the key labels are both removed here
assert_delete_raises()

View File

@ -9,14 +9,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import CoinSpend
from chia.util.ints import uint64
from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle
from chia.wallet.outer_puzzles import (
construct_puzzle,
create_asset_id,
get_inner_puzzle,
get_inner_solution,
match_puzzle,
solve_puzzle,
)
from chia.wallet.outer_puzzles import construct_puzzle, get_inner_puzzle, get_inner_solution, match_puzzle, solve_puzzle
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
from chia.wallet.puzzles.cat_loader import CAT_MOD
@ -37,7 +30,6 @@ def test_cat_outer_puzzle() -> None:
assert inside_cat_driver["tail"] == tail
assert construct_puzzle(cat_driver, ACS) == double_cat_puzzle
assert get_inner_puzzle(cat_driver, double_cat_puzzle) == ACS
assert create_asset_id(cat_driver) == tail
# Set up for solve
parent_coin = Coin(tail, double_cat_puzzle.get_tree_hash(), uint64(100))

View File

@ -7,14 +7,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.ints import uint16
from chia.wallet.nft_wallet.ownership_outer_puzzle import puzzle_for_ownership_layer
from chia.wallet.nft_wallet.transfer_program_puzzle import puzzle_for_transfer_program
from chia.wallet.outer_puzzles import (
construct_puzzle,
create_asset_id,
get_inner_puzzle,
get_inner_solution,
match_puzzle,
solve_puzzle,
)
from chia.wallet.outer_puzzles import construct_puzzle, get_inner_puzzle, get_inner_solution, match_puzzle, solve_puzzle
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
@ -56,7 +49,6 @@ def test_ownership_outer_puzzle() -> None:
assert construct_puzzle(ownership_driver_empty, ACS) == ownership_puzzle_empty
assert construct_puzzle(ownership_driver_default, ACS) == ownership_puzzle_default
assert get_inner_puzzle(ownership_driver, ownership_puzzle) == ACS
assert create_asset_id(ownership_driver) is None
# Set up for solve
inner_solution = Program.to(

View File

@ -0,0 +1,226 @@
import cProfile
from contextlib import contextmanager
from typing import Iterator
import pytest
from chia.wallet.trading.offer import Offer
from tests.util.misc import assert_runtime
with_profile = False
# gprof2dot -f pstats offer-parsing.profile >p.dot && dot -Tpng p.dot >offer-parsing.png
# gprof2dot -f pstats offered-coins.profile >c.dot && dot -Tpng c.dot >offered-coins.png
@contextmanager
def enable_profiler(name: str) -> Iterator[None]:
if not with_profile:
yield
return
with cProfile.Profile() as pr:
yield
pr.create_stats()
pr.dump_stats(f"{name}.profile")
@pytest.mark.benchmark
def test_offer_parsing_performance() -> None:
offer_bytes = bytes.fromhex(test_offer)
with assert_runtime(seconds=16, label="Offer.from_bytes()"):
with enable_profiler("offer-parsing"):
for _ in range(100):
o = Offer.from_bytes(offer_bytes)
assert o is not None
@pytest.mark.benchmark
def test_offered_coins_performance() -> None:
offer_bytes = bytes.fromhex(test_offer)
o = Offer.from_bytes(offer_bytes)
with assert_runtime(seconds=10, label="Offer.from_bytes()"):
with enable_profiler("offered-coins"):
for _ in range(100):
c = o.get_offered_coins()
assert len(c.items()) > 0
test_offer = str(
"0000000200000000000000000000000000000000000000000000000000000000"
"00000000bae24162efbd568f89bc7a340798a6118df0189eb9e3f8697bcea27a"
"f99f8f790000000000000000ff02ffff01ff02ff0affff04ff02ffff04ff03ff"
"80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0c"
"ffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16"
"ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff808080"
"80ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff04ffff04ff08"
"ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff"
"010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1eff"
"ff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080"
"808080ffff01ff0bffff0101ff058080ff0180ff018080ffffa0113e4b68cb75"
"5a6e4347f4d93e3d942ad1d89aadef6536dad229fe5fbe6ab232ffffa0e13f56"
"72075e5ac9a50bcf080fca54762b2a59e22f37951f56802603ec2fe6e1ff64ff"
"80808080ee2b6845e1c317976b002adc4d1dc48d2b752b9f47a3c1ecad4df36a"
"2905d5add1ee4d5798f94b10f9487e314a9561a7a757d2fcf29d8d461106f04b"
"8e303b790000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2f"
"ff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff"
"04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02"
"ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff"
"02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff01"
"80ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080"
"ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff"
"02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ff"
"ff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01"
"ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff"
"0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff"
"8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ff"
"ff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080"
"ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01"
"ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff"
"03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02"
"ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01"
"ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff"
"04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080"
"ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080"
"8080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02"
"ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02"
"ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff"
"09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff"
"0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff"
"0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04"
"ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ff"
"ff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff8080"
"8080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101"
"ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff"
"30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa0"
"7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f"
"ffa02268aba6ee7b6a26b6f8abc2c00938e413a8aa128d1ba1bdc4a9bfb84e62"
"aa2da0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cd"
"c13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff"
"05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff"
"17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffff"
"ff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff"
"04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0b"
"ff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080"
"ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ff"
"ff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff"
"04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03"
"ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80"
"808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff01"
"01ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ff"
"ff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff"
"01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff"
"04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02"
"ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5f"
"ff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e8"
"80ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff"
"ff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff"
"82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fff"
"ff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080"
"808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff"
"04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ff"
"ff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27"
"ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818f"
"ff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af"
"8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d8002"
"6e870519aaa66334aef8304f5d0393c2ffff04ffff01ffff75ff9d6874747073"
"3a2f2f70696373756d2e70686f746f732f3337372f38313180ffff68a0452062"
"a44018653e22198e70a0e756641361b8ec3bc466c1924a38d372e1a945ffff82"
"6d75ff9668747470733a2f2f7777772e6d656a69612e636f6d2f80ffff826c75"
"ff93687474703a2f2f616775697272652e6e65742f80ffff82736e01ffff8273"
"7401ffff826d68a01f462ea72e639eca6ebe792caeb296491177454fe2c763cb"
"9b08e52e85c02712ffff826c68a0b794d0dfa36ac60ff17b0b3649adbc44a703"
"8713bf8acfeaf4bb57dd276dd7ec80ffff04ffff01a0fe8a4b4e27a2e29a4d3f"
"c7ce9d527adbcaccbab6ada3903ccf3ba9a769d2d78bffff04ffff01ff02ffff"
"01ff02ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff17ffff04ff0bffff"
"04ffff02ff2fff5f80ff80808080808080ffff04ffff01ffffff82ad4cff0233"
"ffff3e04ff81f601ffffff0102ffff02ffff03ff05ffff01ff02ff2affff04ff"
"02ffff04ff0dffff04ffff0bff32ffff0bff3cff3480ffff0bff32ffff0bff32"
"ffff0bff3cff2280ff0980ffff0bff32ff0bffff0bff3cff8080808080ff8080"
"808080ffff010b80ff0180ff04ffff04ff38ffff04ffff02ff36ffff04ff02ff"
"ff04ff05ffff04ff27ffff04ffff02ff2effff04ff02ffff04ffff02ffff03ff"
"81afffff0181afffff010b80ff0180ff80808080ffff04ffff0bff3cff4f80ff"
"ff04ffff0bff3cff0580ff8080808080808080ff378080ff82016f80ffffff02"
"ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff2f"
"ffff01ff80ff808080808080808080ff0bff32ffff0bff3cff2880ffff0bff32"
"ffff0bff32ffff0bff3cff2280ff0580ffff0bff32ffff02ff2affff04ff02ff"
"ff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080"
"ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ff"
"ff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff"
"01ff0bffff0101ff058080ff0180ff02ffff03ff5fffff01ff02ffff03ffff09"
"ff82011fff3880ffff01ff02ffff03ffff09ffff18ff82059f80ff3c80ffff01"
"ff02ffff03ffff20ff81bf80ffff01ff02ff3effff04ff02ffff04ff05ffff04"
"ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff82019fffff04ff82017f"
"ff80808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02"
"ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81"
"dfffff04ff81bfffff04ff82017fff808080808080808080808080ff0180ffff"
"01ff02ffff03ffff09ff82011fff2c80ffff01ff02ffff03ffff20ff82017f80"
"ffff01ff04ffff04ff24ffff04ffff0eff10ffff02ff2effff04ff02ffff04ff"
"82019fff8080808080ff808080ffff02ff3effff04ff02ffff04ff05ffff04ff"
"0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ffff02ff0bff"
"ff04ff17ffff04ff2fffff04ff82019fff8080808080ff808080808080808080"
"8080ffff01ff088080ff0180ffff01ff02ffff03ffff09ff82011fff2480ffff"
"01ff02ffff03ffff20ffff02ffff03ffff09ffff0122ffff0dff82029f8080ff"
"ff01ff02ffff03ffff09ffff0cff82029fff80ffff010280ff1080ffff01ff01"
"01ff8080ff0180ff8080ff018080ffff01ff04ff819fffff02ff3effff04ff02"
"ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bf"
"ffff04ff82017fff8080808080808080808080ffff01ff088080ff0180ffff01"
"ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff"
"04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080"
"808080ff018080ff018080ff0180ffff01ff02ff3affff04ff02ffff04ff05ff"
"ff04ff0bffff04ff81bfffff04ffff02ffff03ff82017fffff0182017fffff01"
"ff02ff0bffff04ff17ffff04ff2fffff01ff808080808080ff0180ff80808080"
"80808080ff0180ff018080ffff04ffff01a0c5abea79afaa001b5427dfa0c8cf"
"42ca6f38f5841b78f9b3c252733eb2de2726ffff04ffff0180ffff04ffff01ff"
"02ffff01ff02ffff01ff02ffff03ff81bfffff01ff04ff82013fffff04ff80ff"
"ff04ffff02ffff03ffff22ff82013fffff20ffff09ff82013fff2f808080ffff"
"01ff04ffff04ff10ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04"
"ff8205bfffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff82013f"
"ff1d8080ff80808080ff808080808080ff1580ff808080ffff02ff16ffff04ff"
"02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080"
"ffff01ff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04"
"ff15ff8080808080808080ff0180ff80808080ffff01ff04ff2fffff01ff80ff"
"80808080ff0180ffff04ffff01ffffff3f02ff04ff0101ffff822710ff02ff02"
"ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff"
"0bff2cff1480ffff0bff2affff0bff2affff0bff2cff3c80ff0980ffff0bff2a"
"ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff"
"03ff17ffff01ff04ffff04ff10ffff04ffff0bff81a7ffff02ff3effff04ff02"
"ffff04ffff04ff2fffff04ffff04ff05ffff04ffff05ffff14ffff12ff47ff0b"
"80ff128080ffff04ffff04ff05ff8080ff80808080ff808080ff8080808080ff"
"808080ffff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff"
"2fff8080808080808080ff8080ff0180ffff0bff2affff0bff2cff1880ffff0b"
"ff2affff0bff2affff0bff2cff3c80ff0580ffff0bff2affff02ff3affff04ff"
"02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff808080"
"8080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02"
"ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ff"
"ff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01ffa07faa3253bf"
"ddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa02268ab"
"a6ee7b6a26b6f8abc2c00938e413a8aa128d1ba1bdc4a9bfb84e62aa2da0eff0"
"7522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff"
"04ffff01a003d5d19244dfe1fffc3de5f9e1ded13bd5fb47340e798c9d042d7c"
"d9a101ca09ffff04ffff0182012cff0180808080ffff04ffff01ff02ffff01ff"
"02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1e"
"ffff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01"
"ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05"
"ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff"
"17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff"
"0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff"
"04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff01"
"8080ffff04ffff01b0b4652a5c069d8498cd4b0dd1bd5198176078b466264651"
"7d51e28344b2e714d8cefb781e4dbb5d4cc7e0cba7b4b50d77ff018080ff0180"
"80808080ff018080808080ff01808080ffffa02268aba6ee7b6a26b6f8abc2c0"
"0938e413a8aa128d1ba1bdc4a9bfb84e62aa2dffa012b92f169ae6991c481b3f"
"43e17b821cd8aec6bb1614bbe9042e6f9c734979aeff0180ff01ffffffff80ff"
"ff01ffff81f6ff80ffffff64ffa0bae24162efbd568f89bc7a340798a6118df0"
"189eb9e3f8697bcea27af99f8f798080ff8080ffff33ffa0bae24162efbd568f"
"89bc7a340798a6118df0189eb9e3f8697bcea27af99f8f79ff01ffffa0bae241"
"62efbd568f89bc7a340798a6118df0189eb9e3f8697bcea27af99f8f798080ff"
"ff3fffa01791f8e6d86d66bca42867c0be163909c07c46dfb3bb6660f1fe8b6b"
"0cb952e48080ff808080808096a0c4136217c2c2cc4eb525ba7aa14d166d0353"
"9e5d1ce733a28592fc4adf52a92f873e96ac8a3dfc02964f102dca750768cade"
"7acbf0055da31d080b9894768971906509062e2255634f14e4e6f7acd68b7c40"
"d1526e5ca0b489b7afd60762",
)