From b8dc71f439b5e1f7be0493a9ad9b7a6662d17188 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 10 Aug 2023 17:14:52 -0700 Subject: [PATCH 01/40] Modernize `SConstruct`. --- SConstruct | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SConstruct b/SConstruct index d7adb29..c2b4e56 100644 --- a/SConstruct +++ b/SConstruct @@ -5,7 +5,7 @@ import setuptools_scm import pytoml -metadata = dict(pytoml.load(open("pyproject.toml")))["tool"]["enscons"] +metadata = dict(pytoml.load(open("pyproject.toml")))["project"] metadata["version"] = setuptools_scm.get_version(local_scheme="no-local-version") full_tag = "py3-none-any" # pure Python packages compatible with 2+3 diff --git a/pyproject.toml b/pyproject.toml index 993ec4a..b35c0be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -[tool.enscons] +[project] name = "hsms" description = "Hardware security module simulator for chia bls12_381 signatures" authors = ["Richard Kiss "] @@ -10,11 +10,11 @@ dependencies = ["blspy==1.0.16", "segno==1.4.1", "clvm_rs==0.2.5", "clvm_tools_r packages = ["hsms"] # version is defined with `setuptools_scm`. See `SConstruct` file. -[tool.enscons.optional_dependencies] +[project.optional_dependencies] test = ["nose", "coverage"] dev = ["flake8>=4.0.1", "black>=22.6"] -[tool.enscons.entry_points] +[project.entry_points] console_scripts = [ "hsms = hsms.cmds.hsms:main", "hsmpk = hsms.cmds.hsmpk:main", From 64fc398f2728ee1696edfda5f4765752b5e6a19b Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 10 Aug 2023 17:19:11 -0700 Subject: [PATCH 02/40] Use `chialisp_puzzles` and `chia_base`. --- hsms/puzzles/load_clvm.py | 43 ------------------- hsms/puzzles/p2_conditions.py | 4 +- .../p2_delegated_puzzle_or_hidden_puzzle.py | 7 +-- pyproject.toml | 3 +- 4 files changed, 8 insertions(+), 49 deletions(-) delete mode 100644 hsms/puzzles/load_clvm.py diff --git a/hsms/puzzles/load_clvm.py b/hsms/puzzles/load_clvm.py deleted file mode 100644 index 6b3812a..0000000 --- a/hsms/puzzles/load_clvm.py +++ /dev/null @@ -1,43 +0,0 @@ -import pathlib - -import pkg_resources -from clvm_rs import Program -from clvm_tools_rs import compile_clvm - - -def load_clvm(clvm_filename, package_or_requirement=__name__) -> Program: - """ - This function takes a .clvm file in the given package and compiles it to a - .clvm.hex file if the .hex file is missing or older than the .clvm file, then - returns the contents of the .hex file as a `Program`. - - clvm_filename: file name - package_or_requirement: usually `__name__` if the clvm file is in the same package - """ - - hex_filename = f"{clvm_filename}.hex" - - try: - if pkg_resources.resource_exists(package_or_requirement, clvm_filename): - full_path = pathlib.Path( - pkg_resources.resource_filename(package_or_requirement, clvm_filename) - ) - output = full_path.parent / hex_filename - compile_clvm( - str(full_path), - str(output), - search_paths=[ - str(full_path.parent), - str(full_path.parent.joinpath("include")), - ], - ) - except NotImplementedError: - # pyinstaller doesn't support `pkg_resources.resource_exists` - # so we just fall through to loading the hex clvm - pass - - clvm_hex = pkg_resources.resource_string( - package_or_requirement, hex_filename - ).decode("utf8") - clvm_blob = bytes.fromhex(clvm_hex) - return Program.from_bytes(clvm_blob) diff --git a/hsms/puzzles/p2_conditions.py b/hsms/puzzles/p2_conditions.py index 9abcda1..1fa38ff 100644 --- a/hsms/puzzles/p2_conditions.py +++ b/hsms/puzzles/p2_conditions.py @@ -12,9 +12,9 @@ from clvm_rs import Program -from .load_clvm import load_clvm +from chialisp_puzzles import load_puzzle -MOD = load_clvm("p2_conditions.cl") +MOD = load_puzzle("p2_conditions") def puzzle_for_conditions(conditions) -> Program: diff --git a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py index 47a9a4b..303ca84 100644 --- a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py +++ b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py @@ -64,7 +64,8 @@ from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent from hsms.streamables import bytes32 -from .load_clvm import load_clvm +from chialisp_puzzles import load_puzzle + from .p2_conditions import puzzle_for_conditions DEFAULT_HIDDEN_PUZZLE = Program.from_bytes( @@ -73,9 +74,9 @@ DEFAULT_HIDDEN_PUZZLE_HASH = DEFAULT_HIDDEN_PUZZLE.tree_hash() -MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.cl") +MOD = load_puzzle("p2_delegated_puzzle_or_hidden_puzzle") -SYNTHETIC_MOD = load_clvm("calculate_synthetic_public_key.cl") +SYNTHETIC_MOD = load_puzzle("calculate_synthetic_public_key") def calculate_synthetic_offset( diff --git a/pyproject.toml b/pyproject.toml index b35c0be..d7df458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" repository = "https://github.com/chia-network/hsms.git" readme = "README.md" src_root = "." -dependencies = ["blspy==1.0.16", "segno==1.4.1", "clvm_rs==0.2.5", "clvm_tools_rs==0.1.30"] +dependencies = ["segno==1.4.1", "chia_base==0.1.2", "chialisp_puzzles==0.1.0"] packages = ["hsms"] # version is defined with `setuptools_scm`. See `SConstruct` file. @@ -22,6 +22,7 @@ console_scripts = [ "hsmmerge = hsms.cmds.hsmmerge:main", "hsm_test_spend = hsms.cmds.hsm_test_spend:main", "hsm_dump_sb = hsms.cmds.hsm_dump_sb:main", + "hsm_dump_us = hsms.cmds.hsm_dump_us:main", "qrint = hsms.cmds.qrint:main", "hsmwizard = hsms.cmds.hsmwizard:main", "poser_gen = hsms.cmds.poser_gen:main", From 8c77afd17db1012cf64e214f1a838d66a9ae50bf Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 10 Aug 2023 18:12:45 -0700 Subject: [PATCH 03/40] Use `chia_base` to greatly simplify hsms. --- hsms/atoms/__init__.py | 4 - hsms/atoms/ints.py | 33 ---- hsms/atoms/sized_bytes.py | 6 - hsms/bls12_381/__init__.py | 6 - hsms/bls12_381/bls_public_key.py | 102 ------------ hsms/bls12_381/bls_secret_exponent.py | 116 -------------- hsms/bls12_381/bls_signature.py | 72 --------- hsms/bls12_381/secret_key_utils.py | 16 -- hsms/cmds/hsm_dump_sb.py | 3 +- hsms/cmds/hsm_test_spend.py | 19 +-- hsms/cmds/hsmgen.py | 2 +- hsms/cmds/hsmmerge.py | 7 +- hsms/cmds/hsmpk.py | 8 +- hsms/cmds/hsms.py | 9 +- hsms/cmds/hsmwizard.py | 2 +- hsms/cmds/poser_gen.py | 5 +- hsms/cmds/poser_verify.py | 2 +- hsms/contrib/__init__.py | 0 hsms/contrib/bech32m.py | 150 ------------------ hsms/debug/debug_spend_bundle.py | 10 +- hsms/make_unsigned_tx.py | 82 ---------- hsms/meta/__init__.py | 2 - hsms/meta/bin_methods.py | 22 --- hsms/meta/hexbytes.py | 11 -- hsms/meta/make_sized_bytes.py | 41 ----- hsms/meta/streamable.py | 53 ------- hsms/meta/struct_stream.py | 19 --- hsms/process/sign.py | 7 +- hsms/process/signing_hints.py | 3 +- hsms/process/unsigned_spend.py | 6 +- .../puzzles/calculate_synthetic_public_key.cl | 5 - hsms/puzzles/condition_codes.clvm | 38 ----- hsms/puzzles/p2_conditions.cl | 3 - .../p2_delegated_puzzle_or_hidden_puzzle.cl | 91 ----------- .../p2_delegated_puzzle_or_hidden_puzzle.py | 4 +- hsms/streamables/__init__.py | 7 +- hsms/streamables/coin.py | 37 ----- hsms/streamables/coin_spend.py | 66 ++++---- hsms/streamables/spend_bundle.py | 43 ----- hsms/util/bech32.py | 23 --- hsms/util/qrint_encoding.py | 2 +- hsms/util/std_hash.py | 10 -- tests/cmds/hsm_test_1.txt | 2 + tests/cmds/hsmpk-1.txt | 2 + tests/cmds/hsmpk-2.txt | 2 + tests/full_life_cycle.sh | 2 +- tests/generate.py | 4 +- tests/test_bls.py | 129 --------------- tests/test_cmds.py | 126 +++++++++++++++ tests/test_lifecycle.py | 6 +- 50 files changed, 219 insertions(+), 1201 deletions(-) delete mode 100644 hsms/atoms/__init__.py delete mode 100644 hsms/atoms/ints.py delete mode 100644 hsms/atoms/sized_bytes.py delete mode 100644 hsms/bls12_381/__init__.py delete mode 100644 hsms/bls12_381/bls_public_key.py delete mode 100644 hsms/bls12_381/bls_secret_exponent.py delete mode 100644 hsms/bls12_381/bls_signature.py delete mode 100644 hsms/bls12_381/secret_key_utils.py delete mode 100644 hsms/contrib/__init__.py delete mode 100644 hsms/contrib/bech32m.py delete mode 100644 hsms/make_unsigned_tx.py delete mode 100644 hsms/meta/__init__.py delete mode 100644 hsms/meta/bin_methods.py delete mode 100644 hsms/meta/hexbytes.py delete mode 100644 hsms/meta/make_sized_bytes.py delete mode 100644 hsms/meta/streamable.py delete mode 100644 hsms/meta/struct_stream.py delete mode 100644 hsms/puzzles/calculate_synthetic_public_key.cl delete mode 100644 hsms/puzzles/condition_codes.clvm delete mode 100644 hsms/puzzles/p2_conditions.cl delete mode 100644 hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.cl delete mode 100644 hsms/streamables/coin.py delete mode 100644 hsms/streamables/spend_bundle.py delete mode 100644 hsms/util/bech32.py delete mode 100644 hsms/util/std_hash.py create mode 100644 tests/cmds/hsm_test_1.txt create mode 100644 tests/cmds/hsmpk-1.txt create mode 100644 tests/cmds/hsmpk-2.txt delete mode 100644 tests/test_bls.py create mode 100644 tests/test_cmds.py diff --git a/hsms/atoms/__init__.py b/hsms/atoms/__init__.py deleted file mode 100644 index 2407f8e..0000000 --- a/hsms/atoms/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from hsms.meta.hexbytes import hexbytes # noqa - -from .ints import int8, uint8, int16, uint16, int32, uint32, int64, uint64 # noqa -from .sized_bytes import bytes32, bytes96 # noqa diff --git a/hsms/atoms/ints.py b/hsms/atoms/ints.py deleted file mode 100644 index ec14dea..0000000 --- a/hsms/atoms/ints.py +++ /dev/null @@ -1,33 +0,0 @@ -from hsms.meta.struct_stream import struct_stream - - -class int8(int, struct_stream): - PACK = "!b" - - -class uint8(int, struct_stream): - PACK = "!B" - - -class int16(int, struct_stream): - PACK = "!h" - - -class uint16(int, struct_stream): - PACK = "!H" - - -class int32(int, struct_stream): - PACK = "!l" - - -class uint32(int, struct_stream): - PACK = "!L" - - -class int64(int, struct_stream): - PACK = "!q" - - -class uint64(int, struct_stream): - PACK = "!Q" diff --git a/hsms/atoms/sized_bytes.py b/hsms/atoms/sized_bytes.py deleted file mode 100644 index 6d0b477..0000000 --- a/hsms/atoms/sized_bytes.py +++ /dev/null @@ -1,6 +0,0 @@ -from hsms.meta.make_sized_bytes import make_sized_bytes - - -bytes32 = make_sized_bytes(32) -bytes48 = make_sized_bytes(48) -bytes96 = make_sized_bytes(96) diff --git a/hsms/bls12_381/__init__.py b/hsms/bls12_381/__init__.py deleted file mode 100644 index 08d0147..0000000 --- a/hsms/bls12_381/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .bls_public_key import BLSPublicKey -from .bls_secret_exponent import BLSSecretExponent -from .bls_signature import BLSSignature - - -__all__ = ["BLSPublicKey", "BLSSecretExponent", "BLSSignature"] diff --git a/hsms/bls12_381/bls_public_key.py b/hsms/bls12_381/bls_public_key.py deleted file mode 100644 index 7bf7919..0000000 --- a/hsms/bls12_381/bls_public_key.py +++ /dev/null @@ -1,102 +0,0 @@ -from typing import BinaryIO, List - -import blspy - -from ..atoms import hexbytes -from ..util.bech32 import bech32_decode, bech32_encode, Encoding - -from .secret_key_utils import public_key_from_int - -BECH32M_PUBLIC_KEY_PREFIX = "bls1238" - - -class BLSPublicKey: - def __init__(self, g1: blspy.G1Element): - assert isinstance(g1, blspy.G1Element) - self._g1 = g1 - - @classmethod - def from_bytes(cls, blob): - bls_public_hd_key = blspy.G1Element.from_bytes(blob) - return BLSPublicKey(bls_public_hd_key) - - @classmethod - def parse(cls, f: BinaryIO): - return cls.from_bytes(f.read(48)) - - @classmethod - def generator(cls): - return BLSPublicKey(blspy.G1Element.generator()) - - @classmethod - def zero(cls): - return cls(blspy.G1Element()) - - def stream(self, f: BinaryIO) -> None: - f.write(bytes(self._g1)) - - def __add__(self, other): - return BLSPublicKey(self._g1 + other._g1) - - def __mul__(self, other): - if self == self.generator(): - # this would be subject to timing attacks - return BLSPublicKey(public_key_from_int(other)) - if other == 0: - return self.zero() - if other == 1: - return self - parity = other & 1 - v = self.__mul__(other >> 1) - v += v - if parity: - v += self - return v - - def __rmul__(self, other): - return self.__mul__(other) - - def __eq__(self, other): - return self._g1 == other._g1 - - def __bytes__(self) -> bytes: - return hexbytes(self._g1) - - def child(self, index: int) -> "BLSPublicKey": - return BLSPublicKey( - blspy.AugSchemeMPL.derive_child_pk_unhardened(self._g1, index) - ) - - def child_for_path(self, path: List[int]) -> "BLSPublicKey": - r = self - for index in path: - r = self.child(index) - return r - - def fingerprint(self): - return self._g1.get_fingerprint() - - def as_bech32m(self): - return bech32_encode(BECH32M_PUBLIC_KEY_PREFIX, bytes(self), Encoding.BECH32M) - - @classmethod - def from_bech32m(cls, text: str) -> "BLSPublicKey": - r = bech32_decode(text, max_length=91) - if r is not None: - prefix, base8_data, encoding = r - if ( - encoding == Encoding.BECH32M - and prefix == BECH32M_PUBLIC_KEY_PREFIX - and len(base8_data) == 49 - ): - return cls.from_bytes(base8_data[:48]) - raise ValueError("not bls12_381 bech32m pubkey") - - def __hash__(self): - return bytes(self).__hash__() - - def __str__(self): - return self.as_bech32m() - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self) diff --git a/hsms/bls12_381/bls_secret_exponent.py b/hsms/bls12_381/bls_secret_exponent.py deleted file mode 100644 index b33f74e..0000000 --- a/hsms/bls12_381/bls_secret_exponent.py +++ /dev/null @@ -1,116 +0,0 @@ -from typing import BinaryIO, List, Optional - -import blspy - -from ..atoms import bytes32 -from ..util.std_hash import std_hash -from ..util.bech32 import bech32_decode, bech32_encode, Encoding - -from .bls_public_key import BLSPublicKey -from .bls_signature import BLSSignature -from .secret_key_utils import private_key_from_int - - -BECH32M_SECRET_EXPONENT_PREFIX = "se" - - -class BLSSecretExponent: - def __init__(self, sk: blspy.PrivateKey): - self._sk = sk - - @classmethod - def from_seed(cls, blob: bytes) -> "BLSSecretExponent": - secret_exponent = int.from_bytes(std_hash(blob), "big") - return cls.from_int(secret_exponent) - - @classmethod - def from_int(cls, secret_exponent) -> "BLSSecretExponent": - return cls(private_key_from_int(secret_exponent)) - - @classmethod - def from_bytes(cls, blob) -> "BLSSecretExponent": - return cls(blspy.PrivateKey.from_bytes(blob)) - - @classmethod - def parse(cls, f: BinaryIO): - return cls.from_bytes(f.read(32)) - - def stream(self, f: BinaryIO) -> None: - f.write(bytes(self._sk)) - - def fingerprint(self) -> int: - return self._sk.get_g1().get_fingerprint() - - def sign( - self, message_hash: bytes32, final_public_key: Optional[BLSPublicKey] = None - ) -> BLSSignature: - if final_public_key: - return BLSSignature( - blspy.AugSchemeMPL.sign(self._sk, message_hash, final_public_key._g1) - ) - return BLSSignature(blspy.AugSchemeMPL.sign(self._sk, message_hash)) - - def public_key(self) -> BLSPublicKey: - return BLSPublicKey(self._sk.get_g1()) - - def secret_exponent(self): - return int.from_bytes(bytes(self), "big") - - def hardened_child(self, index: int) -> "BLSSecretExponent": - return BLSSecretExponent(blspy.AugSchemeMPL.derive_child_sk(self._sk, index)) - - def child(self, index: int) -> "BLSSecretExponent": - return BLSSecretExponent( - blspy.AugSchemeMPL.derive_child_sk_unhardened(self._sk, index) - ) - - def child_for_path(self, path: List[int]) -> "BLSSecretExponent": - r = self - for index in path: - r = self.child(index) - return r - - def as_bech32m(self): - return bech32_encode( - BECH32M_SECRET_EXPONENT_PREFIX, bytes(self), Encoding.BECH32M - ) - - @classmethod - def from_bech32m(cls, text: str) -> "BLSSecretExponent": - r = bech32_decode(text) - if r is not None: - prefix, base8_data, encoding = r - if ( - encoding == Encoding.BECH32M - and prefix == BECH32M_SECRET_EXPONENT_PREFIX - and len(base8_data) == 33 - ): - return cls.from_bytes(base8_data[:32]) - raise ValueError("not secret exponent") - - @classmethod - def zero(cls) -> "BLSSecretExponent": - return ZERO - - def __add__(self, other): - return self.from_int(int(self) + int(other)) - - def __int__(self): - return self.secret_exponent() - - def __eq__(self, other): - if isinstance(other, int): - other = BLSSecretExponent.from_int(other) - return self._sk == other._sk - - def __bytes__(self): - return bytes(self._sk) - - def __str__(self): - return "" % self.public_key() - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self) - - -ZERO = BLSSecretExponent.from_int(0) diff --git a/hsms/bls12_381/bls_signature.py b/hsms/bls12_381/bls_signature.py deleted file mode 100644 index a144810..0000000 --- a/hsms/bls12_381/bls_signature.py +++ /dev/null @@ -1,72 +0,0 @@ -from dataclasses import dataclass -from typing import BinaryIO, Iterator, List, Tuple - -import blspy - -from ..atoms import bytes32, bytes96 - -from .bls_public_key import BLSPublicKey - -ZERO96 = bytes96([0] * 96) - - -class BLSSignature: - """ - This wraps the blspy version and resolves a couple edge cases - around aggregation and validation. - """ - - @dataclass - class aggsig_pair: - public_key: BLSPublicKey - message_hash: bytes32 - - def __init__(self, g2: blspy.G2Element): - assert isinstance(g2, blspy.G2Element) - self._g2 = g2 - - @classmethod - def from_bytes(cls, blob): - bls_public_hd_key = blspy.G2Element.from_bytes(blob) - return cls(bls_public_hd_key) - - @classmethod - def parse(cls, f: BinaryIO): - return cls.from_bytes(bytes96.parse(f)) - - @classmethod - def generator(cls): - return cls(blspy.G2Element.generator()) - - @classmethod - def zero(cls): - return cls(blspy.G2Element()) - - def stream(self, f): - f.write(bytes(self._g2)) - - def __add__(self, other): - return self.__class__(self._g2 + other._g2) - - def __eq__(self, other): - return self._g2 == other._g2 - - def __bytes__(self) -> bytes: - return bytes(self._g2) - - def __str__(self): - return bytes(self._g2).hex() - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self) - - def validate(self, hash_key_pairs: Iterator[aggsig_pair]) -> bool: - return self.verify([(_.public_key, _.message_hash) for _ in hash_key_pairs]) - - def verify(self, hash_key_pairs: List[Tuple[BLSPublicKey, bytes32]]) -> bool: - public_keys: List[blspy.G1Element] = [_[0]._g1 for _ in hash_key_pairs] - message_hashes: List[bytes32] = [_[1] for _ in hash_key_pairs] - - return blspy.AugSchemeMPL.aggregate_verify( - public_keys, message_hashes, self._g2 - ) diff --git a/hsms/bls12_381/secret_key_utils.py b/hsms/bls12_381/secret_key_utils.py deleted file mode 100644 index 7c82254..0000000 --- a/hsms/bls12_381/secret_key_utils.py +++ /dev/null @@ -1,16 +0,0 @@ -import blspy - - -GROUP_ORDER = ( - 52435875175126190479447740508185965837690552500527637822603658699938581184513 -) - - -def private_key_from_int(secret_exponent: int) -> blspy.PrivateKey: - secret_exponent %= GROUP_ORDER - blob = secret_exponent.to_bytes(32, "big") - return blspy.PrivateKey.from_bytes(blob) - - -def public_key_from_int(secret_exponent: int) -> blspy.G1Element: - return private_key_from_int(secret_exponent).get_g1() diff --git a/hsms/cmds/hsm_dump_sb.py b/hsms/cmds/hsm_dump_sb.py index 4c53093..76ce328 100755 --- a/hsms/cmds/hsm_dump_sb.py +++ b/hsms/cmds/hsm_dump_sb.py @@ -3,7 +3,8 @@ import argparse -from hsms.streamables import SpendBundle +from chia_base.core import SpendBundle + from hsms.debug.debug_spend_bundle import debug_spend_bundle diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index 2b168e8..53711ea 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -4,7 +4,8 @@ from clvm_rs import Program -from hsms.bls12_381 import BLSPublicKey +from chia_base.bls12_381 import BLSPublicKey +from chia_base.core import Coin from hsms.process.signing_hints import SumHint, PathHint @@ -14,7 +15,7 @@ solution_for_conditions, calculate_synthetic_offset, ) -from hsms.streamables import Coin, CoinSpend +from hsms.streamables import CoinSpend from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.conlang import CREATE_COIN from hsms.util.byte_chunks import ( @@ -33,7 +34,7 @@ def hsm_test_spend(args, parser): root_public_keys = [ - BLSPublicKey.from_bech32m(_.readline()[:-1]) for _ in args.public_key_file + BLSPublicKey.from_bech32m(_) for _ in args.public_key ] paths = [[index, index + 1] for index in range(len(root_public_keys))] @@ -104,18 +105,18 @@ def create_parser(): type=int, ) parser.add_argument( - "public_key_file", - metavar="path-to-public-key", + "public_key", + metavar="public-key", nargs="+", - help="file containing a single bech32m-encoded public key", - type=argparse.FileType("r"), + help="bech32m-encoded public key", + type=str, ) return parser -def main(): +def main(argv=None): parser = create_parser() - args = parser.parse_args() + args = parser.parse_args(argv) return hsm_test_spend(args, parser) diff --git a/hsms/cmds/hsmgen.py b/hsms/cmds/hsmgen.py index ac20419..6551d74 100644 --- a/hsms/cmds/hsmgen.py +++ b/hsms/cmds/hsmgen.py @@ -1,6 +1,6 @@ import secrets -from hsms.bls12_381 import BLSSecretExponent +from chia_base.bls12_381 import BLSSecretExponent def main(): diff --git a/hsms/cmds/hsmmerge.py b/hsms/cmds/hsmmerge.py index bf1658a..4abe423 100755 --- a/hsms/cmds/hsmmerge.py +++ b/hsms/cmds/hsmmerge.py @@ -4,10 +4,11 @@ import argparse -from hsms.bls12_381 import BLSSignature +from chia_base.bls12_381 import BLSSignature +from chia_base.core import SpendBundle + from hsms.process.unsigned_spend import UnsignedSpend from hsms.process.sign import generate_synthetic_offset_signatures -from hsms.streamables import bytes96, SpendBundle from hsms.util.qrint_encoding import a2b_qrint @@ -19,7 +20,7 @@ def create_spend_bundle(unsigned_spend: UnsignedSpend, signatures: List[BLSSigna all_signatures = signatures + [sig_info.signature for sig_info in extra_signatures] total_signature = sum(all_signatures, start=all_signatures[0].zero()) - return SpendBundle(unsigned_spend.coin_spends, bytes96(total_signature)) + return SpendBundle(unsigned_spend.coin_spends, total_signature) def file_or_string(p) -> str: diff --git a/hsms/cmds/hsmpk.py b/hsms/cmds/hsmpk.py index 41faaa2..b028557 100644 --- a/hsms/cmds/hsmpk.py +++ b/hsms/cmds/hsmpk.py @@ -1,10 +1,12 @@ import sys -from hsms.bls12_381 import BLSSecretExponent +from chia_base.bls12_381 import BLSSecretExponent -def main(): - for arg in sys.argv[1:]: +def main(argv=None): + if argv is None: + argv = sys.argv[1:] + for arg in argv: secret_exponent = BLSSecretExponent.from_bech32m(arg) print(secret_exponent.public_key().as_bech32m()) diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index a39dffb..77cf4b2 100755 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -5,7 +5,7 @@ import argparse import io -import readline # noqa: this allows long lines on stdin +import readline # noqa: F401 this allows long lines on stdin import subprocess import sys import zlib @@ -14,13 +14,14 @@ import segno -from hsms.bls12_381 import BLSSecretExponent, BLSSignature +from chia_base.atoms import bytes32 +from chia_base.bls12_381 import BLSSecretExponent, BLSSignature +from chia_base.util.bech32 import bech32_encode + from hsms.consensus.conditions import conditions_by_opcode from hsms.process.sign import conditions_for_coin_spend, sign from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles import conlang -from hsms.streamables import bytes32 -from hsms.util.bech32 import bech32_encode from hsms.util.byte_chunks import ChunkAssembler from hsms.util.qrint_encoding import a2b_qrint, b2a_qrint diff --git a/hsms/cmds/hsmwizard.py b/hsms/cmds/hsmwizard.py index 1fad28e..4505523 100755 --- a/hsms/cmds/hsmwizard.py +++ b/hsms/cmds/hsmwizard.py @@ -10,7 +10,7 @@ import segno -from hsms.bls12_381 import BLSSecretExponent +from chia_base.bls12_381 import BLSSecretExponent import hsms.cmds.hsms diff --git a/hsms/cmds/poser_gen.py b/hsms/cmds/poser_gen.py index 7dfd272..129f1a6 100644 --- a/hsms/cmds/poser_gen.py +++ b/hsms/cmds/poser_gen.py @@ -3,8 +3,9 @@ import argparse -from hsms.bls12_381 import BLSPublicKey -from hsms.streamables.coin import Coin +from chia_base.bls12_381 import BLSPublicKey +from chia_base.core.coin import Coin + from hsms.streamables.coin_spend import CoinSpend from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( diff --git a/hsms/cmds/poser_verify.py b/hsms/cmds/poser_verify.py index fd77d7d..20da737 100644 --- a/hsms/cmds/poser_verify.py +++ b/hsms/cmds/poser_verify.py @@ -2,7 +2,7 @@ import argparse -from hsms.bls12_381 import BLSPublicKey, BLSSignature +from chia_base.bls12_381 import BLSPublicKey, BLSSignature DEFAULT_PARENT_COIN_ID = sha256(b"fake_id").digest() diff --git a/hsms/contrib/__init__.py b/hsms/contrib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/hsms/contrib/bech32m.py b/hsms/contrib/bech32m.py deleted file mode 100644 index 4112c24..0000000 --- a/hsms/contrib/bech32m.py +++ /dev/null @@ -1,150 +0,0 @@ -# adapted from -# https://github.com/sipa/bech32/blob/ef0181a25c644e0404180d977da19f7c5d441f89/ref/python/segwit_addr.py - -# Copyright (c) 2017, 2020 Pieter Wuille -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -"""Reference implementation for Bech32/Bech32m and segwit addresses.""" - - -class Encoding: - """Enumeration type to list the various supported encodings.""" - - BECH32 = 1 - BECH32M = 2 - - -CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" -BECH32M_CONST = 0x2BC830A3 - - -def bech32_polymod(values): - """Internal function that computes the Bech32 checksum.""" - generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] - chk = 1 - for value in values: - top = chk >> 25 - chk = (chk & 0x1FFFFFF) << 5 ^ value - for i in range(5): - chk ^= generator[i] if ((top >> i) & 1) else 0 - return chk - - -def bech32_hrp_expand(hrp): - """Expand the HRP into values for checksum computation.""" - return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] - - -def bech32_verify_checksum(hrp, data): - """Verify a checksum given HRP and converted data characters.""" - const = bech32_polymod(bech32_hrp_expand(hrp) + data) - if const == 1: - return Encoding.BECH32 - if const == BECH32M_CONST: - return Encoding.BECH32M - return None - - -def bech32_create_checksum(hrp, data, spec): - """Compute the checksum values given HRP and data.""" - values = bech32_hrp_expand(hrp) + data - const = BECH32M_CONST if spec == Encoding.BECH32M else 1 - polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const - return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] - - -def bech32_encode(hrp, data, spec): - """Compute a Bech32 string given HRP and data values.""" - combined = data + bech32_create_checksum(hrp, data, spec) - return hrp + "1" + "".join([CHARSET[d] for d in combined]) - - -def bech32_decode(bech, max_length=90): - """Validate a Bech32/Bech32m string, and determine HRP and data.""" - if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( - bech.lower() != bech and bech.upper() != bech - ): - return (None, None, None) - bech = bech.lower() - pos = bech.rfind("1") - if pos < 1 or pos + 7 > len(bech) or len(bech) > max_length: - return (None, None, None) - if not all(x in CHARSET for x in bech[pos + 1 :]): - return (None, None, None) - hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos + 1 :]] - spec = bech32_verify_checksum(hrp, data) - if spec is None: - return (None, None, None) - return (hrp, data[:-6], spec) - - -def convertbits(data, frombits, tobits, pad=True): - """General power-of-2 base conversion.""" - acc = 0 - bits = 0 - ret = [] - maxv = (1 << tobits) - 1 - max_acc = (1 << (frombits + tobits - 1)) - 1 - for value in data: - if value < 0 or (value >> frombits): - return None - acc = ((acc << frombits) | value) & max_acc - bits += frombits - while bits >= tobits: - bits -= tobits - ret.append((acc >> bits) & maxv) - if pad: - if bits: - ret.append((acc << (tobits - bits)) & maxv) - elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None - return ret - - -def decode(hrp, addr): - """Decode a segwit address.""" - hrpgot, data, spec = bech32_decode(addr) - if hrpgot != hrp: - return (None, None) - decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: - return (None, None) - if data[0] > 16: - return (None, None) - if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) - if ( - data[0] == 0 - and spec != Encoding.BECH32 - or data[0] != 0 - and spec != Encoding.BECH32M - ): - return (None, None) - return (data[0], decoded) - - -def encode(hrp, witver, witprog): - """Encode a segwit address.""" - spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) - if decode(hrp, ret) == (None, None): - return None - return ret diff --git a/hsms/debug/debug_spend_bundle.py b/hsms/debug/debug_spend_bundle.py index 55b27dc..5c85e77 100644 --- a/hsms/debug/debug_spend_bundle.py +++ b/hsms/debug/debug_spend_bundle.py @@ -2,13 +2,13 @@ from clvm_rs import Program -from hsms.bls12_381 import BLSSignature +from chia_base.core import Coin +from chia_base.util.std_hash import std_hash + from hsms.clvm.disasm import disassemble as bu_disassemble, KEYWORD_FROM_ATOM from hsms.consensus.conditions import conditions_by_opcode from hsms.process.sign import generate_verify_pairs from hsms.puzzles import conlang -from hsms.streamables import Coin -from hsms.util.std_hash import std_hash KFA = {bytes([getattr(conlang, k)]): k for k in dir(conlang) if k[0] in "ACR"} @@ -219,7 +219,7 @@ def debug_spend_bundle( print() print("=" * 80) print() - signature = BLSSignature.from_bytes(spend_bundle.aggregated_signature) + signature = spend_bundle.aggregated_signature validates = signature.verify(list(zip(pks, msgs))) print(f"aggregated signature check pass: {validates}") print(f"pks: {pks}") @@ -227,5 +227,5 @@ def debug_spend_bundle( print(f" msg_data: {[msg.hex()[:-128] for msg in msgs]}") print(f" coin_ids: {[msg.hex()[-128:-64] for msg in msgs]}") print(f" add_data: {[msg.hex()[-64:] for msg in msgs]}") - print(f"signature: {spend_bundle.aggregated_signature}") + print(f"signature: {signature}") return validates diff --git a/hsms/make_unsigned_tx.py b/hsms/make_unsigned_tx.py deleted file mode 100644 index 326554c..0000000 --- a/hsms/make_unsigned_tx.py +++ /dev/null @@ -1,82 +0,0 @@ -import hashlib - -from clvm_rs import Program - -from hsms.meta.hexbytes import hexbytes -from hsms.multisig.pst import PartiallySignedTransaction -from hsms.streamables import Coin, CoinSpend - -from clvm_tools.binutils import assemble - - -PAY_TO_AGGSIG_ME_PROG = """(q (50 - 0x8ba79a9ccd362086d552a6f56da7fe612959b0dd372350ad798c77c2170de2163a00e499928cc40547a7a8a5e2cde6be - 0x4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a))""" - -PAY_TO_AGGSIG_ME = Program.to(assemble(PAY_TO_AGGSIG_ME_PROG)) - - -def make_coin(): - parent_id = hashlib.sha256(bytes([1] * 32)).digest() - puzzle_hash = PAY_TO_AGGSIG_ME.tree_hash() - coin = Coin(parent_id, puzzle_hash, 10000) - return coin - - -def make_coin_spends(): - coin = make_coin() - coin_spend = CoinSpend(coin, PAY_TO_AGGSIG_ME, Program.to(0)) - return [coin_spend] - - -def main(): - coin_spends = make_coin_spends() - print(coin_spends) - - print(bytes(coin_spends).hex()) - - d = PartiallySignedTransaction( - coin_spends=list(coin_spends), - sigs=[], - delegated_solution=Program.to(0), - hd_hints={ - bytes.fromhex("c34eb867"): { - "hd_fingerprint": bytes.fromhex("0b92dcdd"), - "index": 0, - } - }, - ) - - t = bytes(d) - print() - print(t.hex()) - - -def round_trip(): - coin_spends = make_coin_spends() - d = PartiallySignedTransaction( - coin_spends=coin_spends, - sigs=[], - delegated_solution=Program.to(0), - hd_hints={ - 1253746868: { - "hd_fingerprint": 194174173, - "index": [1, 5, 19], - } - }, - ) - - breakpoint() - print(coin_spends[0].puzzle_reveal) - - b = hexbytes(d) - breakpoint() - d1 = PartiallySignedTransaction.from_bytes(b) - b1 = hexbytes(d1) - print(b) - print(b1) - - assert b == b1 - - -round_trip() diff --git a/hsms/meta/__init__.py b/hsms/meta/__init__.py deleted file mode 100644 index e44f7a9..0000000 --- a/hsms/meta/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .bin_methods import bin_methods # noqa -from .streamable import streamable # noqa diff --git a/hsms/meta/bin_methods.py b/hsms/meta/bin_methods.py deleted file mode 100644 index 02cde70..0000000 --- a/hsms/meta/bin_methods.py +++ /dev/null @@ -1,22 +0,0 @@ -import io - -from typing import Any - -from .hexbytes import hexbytes - - -class bin_methods: - """ - Create "from_bytes" and "__bytes__" methods in terms of "parse" and "stream" - methods. - """ - - @classmethod - def from_bytes(cls, blob: bytes) -> Any: - f = io.BytesIO(blob) - return cls.parse(f) - - def __bytes__(self) -> hexbytes: - f = io.BytesIO() - self.stream(f) - return hexbytes(f.getvalue()) diff --git a/hsms/meta/hexbytes.py b/hsms/meta/hexbytes.py deleted file mode 100644 index 91cef9e..0000000 --- a/hsms/meta/hexbytes.py +++ /dev/null @@ -1,11 +0,0 @@ -class hexbytes(bytes): - """ - This is a subclass of bytes that prints itself out as hex, - which is much easier on the eyes for binary data that is very non-ascii . - """ - - def __str__(self): - return self.hex() - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, str(self)) diff --git a/hsms/meta/make_sized_bytes.py b/hsms/meta/make_sized_bytes.py deleted file mode 100644 index 97a2dd2..0000000 --- a/hsms/meta/make_sized_bytes.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any, BinaryIO - -from .bin_methods import bin_methods -from .hexbytes import hexbytes - - -def make_sized_bytes(size): - """ - Create a streamable type that subclasses "hexbytes" but requires instances - to be a certain, fixed size. - """ - name = "bytes%d" % size - - def __new__(self, v): - v = bytes(v) - if not isinstance(v, bytes) or len(v) != size: - raise ValueError("bad %s initializer %s" % (name, v)) - return hexbytes.__new__(self, v) - - @classmethod - def parse(cls, f: BinaryIO) -> Any: - b = f.read(size) - assert len(b) == size - return cls(b) - - def stream(self, f): - f.write(self) - - def __str__(self): - return self.hex() - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, str(self)) - - namespace = dict( - __new__=__new__, parse=parse, stream=stream, __str__=__str__, __repr__=__repr__ - ) - - cls = type(name, (hexbytes, bin_methods), namespace) - - return cls diff --git a/hsms/meta/streamable.py b/hsms/meta/streamable.py deleted file mode 100644 index 869beda..0000000 --- a/hsms/meta/streamable.py +++ /dev/null @@ -1,53 +0,0 @@ -import dataclasses - -from typing import Type, BinaryIO, TypeVar, get_type_hints - -from .bin_methods import bin_methods - - -T = TypeVar("T") - - -def streamable(cls: T) -> T: - """ - This is a decorator for class definitions. It applies the dataclasses.dataclass - decorator, and also allows fields to be cast to their expected type. The resulting - class also gets parse and stream for free, as long as all its constituent elements - have it. - """ - - class _local: - def __init__(self, *args): - fields = get_type_hints(self) - la, lf = len(args), len(fields) - if la != lf: - raise ValueError("got %d and expected %d args" % (la, lf)) - for a, (f_name, f_type) in zip(args, fields.items()): - if not isinstance(a, f_type): - a = f_type(a) - if not isinstance(a, f_type): - raise ValueError("wrong type for %s" % f_name) - object.__setattr__(self, f_name, a) - - @classmethod - def parse(cls: Type[cls.__name__], f: BinaryIO) -> cls.__name__: - values = [] - for f_name, f_type in get_type_hints(cls).items(): - if hasattr(f_type, "parse"): - values.append(f_type.parse(f)) - else: - raise NotImplementedError - return cls(*values) - - def stream(self, f: BinaryIO) -> None: - for f_name, f_type in get_type_hints(self).items(): - v = getattr(self, f_name) - if hasattr(f_type, "stream"): - v.stream(f) - else: - raise NotImplementedError("can't stream %s: %s" % (v, f_name)) - - cls1 = dataclasses.dataclass(cls, frozen=True, init=False) - - cls2 = type(cls.__name__, (cls1, bin_methods, _local), {}) - return cls2 diff --git a/hsms/meta/struct_stream.py b/hsms/meta/struct_stream.py deleted file mode 100644 index 31bca9a..0000000 --- a/hsms/meta/struct_stream.py +++ /dev/null @@ -1,19 +0,0 @@ -import struct - -from typing import Any, BinaryIO - -from .bin_methods import bin_methods - - -class struct_stream(bin_methods): - """ - Create a class that can parse and stream itself based on a struct.pack - template string. - """ - - @classmethod - def parse(cls, f: BinaryIO) -> Any: - return cls(*struct.unpack(cls.PACK, f.read(struct.calcsize(cls.PACK)))) - - def stream(self, f): - f.write(struct.pack(self.PACK, self)) diff --git a/hsms/process/sign.py b/hsms/process/sign.py index d57a4fb..97fdf7d 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -4,10 +4,11 @@ from clvm_rs import Program -from hsms.atoms import hexbytes -from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.atoms import bytes32, hexbytes +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent + from hsms.consensus.conditions import conditions_by_opcode -from hsms.streamables import bytes32, CoinSpend +from hsms.streamables import CoinSpend from hsms.puzzles.conlang import AGG_SIG_ME, AGG_SIG_UNSAFE from .signing_hints import SumHint, SumHints, PathHint, PathHints diff --git a/hsms/process/signing_hints.py b/hsms/process/signing_hints.py index 7864af8..affe46b 100644 --- a/hsms/process/signing_hints.py +++ b/hsms/process/signing_hints.py @@ -1,7 +1,8 @@ from dataclasses import dataclass from typing import Dict, List -from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent + from hsms.util.clvm_serialization import ( clvm_to_list_of_ints, clvm_to_list, diff --git a/hsms/process/unsigned_spend.py b/hsms/process/unsigned_spend.py index c96a235..790b5a5 100644 --- a/hsms/process/unsigned_spend.py +++ b/hsms/process/unsigned_spend.py @@ -5,9 +5,11 @@ from clvm_rs import Program -from hsms.bls12_381 import BLSPublicKey, BLSSignature +from chia_base.atoms import bytes32 +from chia_base.bls12_381 import BLSPublicKey, BLSSignature + from hsms.process.signing_hints import SumHint, PathHint -from hsms.streamables import bytes32, CoinSpend +from hsms.streamables import CoinSpend from hsms.util.byte_chunks import assemble_chunks, create_chunks_for_blob from hsms.util.clvm_serialization import ( transform_dict, diff --git a/hsms/puzzles/calculate_synthetic_public_key.cl b/hsms/puzzles/calculate_synthetic_public_key.cl deleted file mode 100644 index 98d9364..0000000 --- a/hsms/puzzles/calculate_synthetic_public_key.cl +++ /dev/null @@ -1,5 +0,0 @@ -(mod - (public_key hidden_puzzle_hash) - - (point_add public_key (pubkey_for_exp (sha256 public_key hidden_puzzle_hash))) -) diff --git a/hsms/puzzles/condition_codes.clvm b/hsms/puzzles/condition_codes.clvm deleted file mode 100644 index f8f607c..0000000 --- a/hsms/puzzles/condition_codes.clvm +++ /dev/null @@ -1,38 +0,0 @@ -; See chia/types/condition_opcodes.py - -( - (defconstant AGG_SIG_UNSAFE 49) - (defconstant AGG_SIG_ME 50) - - ; the conditions below reserve coin amounts and have to be accounted for in output totals - - (defconstant CREATE_COIN 51) - (defconstant RESERVE_FEE 52) - - ; the conditions below deal with announcements, for inter-coin communication - - ; coin announcements - (defconstant CREATE_COIN_ANNOUNCEMENT 60) - (defconstant ASSERT_COIN_ANNOUNCEMENT 61) - - ; puzzle announcements - (defconstant CREATE_PUZZLE_ANNOUNCEMENT 62) - (defconstant ASSERT_PUZZLE_ANNOUNCEMENT 63) - - ; the conditions below let coins inquire about themselves - - (defconstant ASSERT_MY_COIN_ID 70) - (defconstant ASSERT_MY_PARENT_ID 71) - (defconstant ASSERT_MY_PUZZLEHASH 72) - (defconstant ASSERT_MY_AMOUNT 73) - - ; the conditions below ensure that we're "far enough" in the future - - ; wall-clock time - (defconstant ASSERT_SECONDS_RELATIVE 80) - (defconstant ASSERT_SECONDS_ABSOLUTE 81) - - ; block index - (defconstant ASSERT_HEIGHT_RELATIVE 82) - (defconstant ASSERT_HEIGHT_ABSOLUTE 83) -) diff --git a/hsms/puzzles/p2_conditions.cl b/hsms/puzzles/p2_conditions.cl deleted file mode 100644 index dd90b75..0000000 --- a/hsms/puzzles/p2_conditions.cl +++ /dev/null @@ -1,3 +0,0 @@ -(mod (conditions) - (qq (q . (unquote conditions))) -) diff --git a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.cl b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.cl deleted file mode 100644 index aa85f37..0000000 --- a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.cl +++ /dev/null @@ -1,91 +0,0 @@ -; build a pay-to delegated puzzle or hidden puzzle -; coins can be unlocked by signing a delegated puzzle and its solution -; OR by revealing the hidden puzzle and the underlying original key - -; glossary of parameter names: - -; hidden_puzzle: a "hidden puzzle" that can be revealed and used as an alternate -; way to unlock the underlying funds -; -; synthetic_key_offset: a private key cryptographically generated using the hidden -; puzzle and as inputs `original_public_key` -; -; synthetic_public_key: the public key that is the sum of `original_public_key` and the -; public key corresponding to `synthetic_key_offset` -; -; original_public_key: a public key, where knowledge of the corresponding private key -; represents ownership of the file -; -; delegated_puzzle: a delegated puzzle, as in "graftroot", which should return the -; desired conditions. -; -; solution: the solution to the delegated puzzle - - -(mod - ; A puzzle should commit to `synthetic_public_key` - ; - ; The solution should pass in 0 for `original_public_key` if it wants to use - ; an arbitrary `delegated_puzzle` (and `solution`) signed by the - ; `synthetic_public_key` (whose corresponding private key can be calculated - ; if you know the private key for `original_public_key`) - ; - ; Or you can solve the hidden puzzle by revealing the `original_public_key`, - ; the hidden puzzle in `delegated_puzzle`, and a solution to the hidden puzzle. - - (synthetic_public_key original_public_key delegated_puzzle solution) - - ; "assert" is a macro that wraps repeated instances of "if" - ; usage: (assert A0 A1 ... An R) - ; all of A0, A1, ... An must evaluate to non-null, or an exception is raised - ; return the value of R (if we get that far) - - (defmacro assert items - (if (r items) - (list if (f items) (c assert (r items)) (q . (x))) - (f items) - ) - ) - - (include condition_codes.clvm) - - ;; hash a tree - ;; This is used to calculate a puzzle hash given a puzzle program. - (defun sha256tree1 - (TREE) - (if (l TREE) - (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) - (sha256 1 TREE) - ) - ) - - ; "is_hidden_puzzle_correct" returns true iff the hidden puzzle is correctly encoded - - (defun-inline is_hidden_puzzle_correct (synthetic_public_key original_public_key delegated_puzzle) - (= - synthetic_public_key - (point_add - original_public_key - (pubkey_for_exp (sha256 original_public_key (sha256tree1 delegated_puzzle))) - ) - ) - ) - - ; "possibly_prepend_aggsig" is the main entry point - - (defun-inline possibly_prepend_aggsig (synthetic_public_key original_public_key delegated_puzzle conditions) - (if original_public_key - (assert - (is_hidden_puzzle_correct synthetic_public_key original_public_key delegated_puzzle) - conditions - ) - (c (list AGG_SIG_ME synthetic_public_key (sha256tree1 delegated_puzzle)) conditions) - ) - ) - - ; main entry point - - (possibly_prepend_aggsig - synthetic_public_key original_public_key delegated_puzzle - (a delegated_puzzle solution)) -) diff --git a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py index 303ca84..4d66cd0 100644 --- a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py +++ b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py @@ -61,8 +61,8 @@ from clvm_rs import Program -from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent -from hsms.streamables import bytes32 +from chia_base.atoms import bytes32 +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent from chialisp_puzzles import load_puzzle diff --git a/hsms/streamables/__init__.py b/hsms/streamables/__init__.py index ba4142b..e460f6f 100644 --- a/hsms/streamables/__init__.py +++ b/hsms/streamables/__init__.py @@ -1,6 +1,3 @@ -from hsms.atoms.sized_bytes import bytes32, bytes48, bytes96 # noqa +from .coin_spend import CoinSpend -from .coin import Coin # noqa -from .coin_spend import CoinSpend # noqa - -from .spend_bundle import SpendBundle # noqa +__all__ = [CoinSpend] \ No newline at end of file diff --git a/hsms/streamables/coin.py b/hsms/streamables/coin.py deleted file mode 100644 index 5d30fdb..0000000 --- a/hsms/streamables/coin.py +++ /dev/null @@ -1,37 +0,0 @@ -import io - -from clvm_rs import Program - -from hsms.atoms import uint64 -from hsms.meta import streamable -from hsms.util.std_hash import std_hash - -from . import bytes32 - - -@streamable -class Coin: - """ - This structure is used in the body for the reward and fees genesis coins. - """ - - parent_coin_info: bytes32 - puzzle_hash: bytes32 - amount: uint64 - - @classmethod - def from_bytes(cls, blob): - parent_coin_info = blob[:32] - puzzle_hash = blob[32:64] - amount = Program.int_from_bytes(blob[64:]) - return Coin(parent_coin_info, puzzle_hash, amount) - - def __bytes__(self): - f = io.BytesIO() - f.write(self.parent_coin_info) - f.write(self.puzzle_hash) - f.write(Program.int_to_bytes(self.amount)) - return f.getvalue() - - def name(self) -> bytes32: - return std_hash(bytes(self)) diff --git a/hsms/streamables/coin_spend.py b/hsms/streamables/coin_spend.py index 5a16241..631bf7d 100644 --- a/hsms/streamables/coin_spend.py +++ b/hsms/streamables/coin_spend.py @@ -1,46 +1,36 @@ from clvm_rs import Program -from .coin import Coin +from chia_base.core import Coin, CoinSpend -from hsms.meta import streamable from hsms.util.clvm_serialization import transform_as_struct -@streamable -class CoinSpend: - """ - This is a rather disparate data structure that validates coin transfers. It's - generally populated with data from different sources, since burned coins are - identified by name, so it is built up more often that it is streamed. - """ +def as_program(self): + return [ + [_.coin.parent_coin_info, _.puzzle_reveal, _.coin.amount, _.solution] + for _ in self.coin_spends + ] - coin: Coin - puzzle_reveal: Program - solution: Program +@classmethod +def from_program(cls, program) -> "CoinSpend": + parent_coin_info, puzzle_reveal, amount, solution = transform_as_struct( + program, + lambda x: x.atom, + lambda x: x, + lambda x: Program.to(x).as_int(), + lambda x: x, + ) + puzzle_reveal = Program.to(puzzle_reveal) + solution = Program.to(solution) + return cls( + Coin( + parent_coin_info, + puzzle_reveal.tree_hash(), + amount, + ), + puzzle_reveal, + solution, + ) - def as_program(self): - return [ - [_.coin.parent_coin_info, _.puzzle_reveal, _.coin.amount, _.solution] - for _ in self.coin_spends - ] - - @classmethod - def from_program(cls, program) -> "CoinSpend": - parent_coin_info, puzzle_reveal, amount, solution = transform_as_struct( - program, - lambda x: x.atom, - lambda x: x, - lambda x: Program.to(x).as_int(), - lambda x: x, - ) - puzzle_reveal = Program.to(puzzle_reveal) - solution = Program.to(solution) - return cls( - Coin( - parent_coin_info, - puzzle_reveal.tree_hash(), - amount, - ), - puzzle_reveal, - solution, - ) +CoinSpend.as_program = as_program +CoinSpend.from_program = from_program \ No newline at end of file diff --git a/hsms/streamables/spend_bundle.py b/hsms/streamables/spend_bundle.py deleted file mode 100644 index c5de753..0000000 --- a/hsms/streamables/spend_bundle.py +++ /dev/null @@ -1,43 +0,0 @@ -from dataclasses import dataclass -from typing import List - -import io - -from hsms.atoms import bytes96, uint32, hexbytes - -from .coin_spend import CoinSpend - - -@dataclass(frozen=True) -class SpendBundle: - """ - This is a list of coins being spent along with their solution programs, and a - single aggregated signature. This is the object that most closely corresponds to - a bitcoin transaction (although because of non-interactive signature aggregation, - the boundaries between transactions are more flexible than in bitcoin). - """ - - coin_spends: List[CoinSpend] - aggregated_signature: bytes96 - - def __add__(self, other: "SpendBundle") -> "SpendBundle": - return self.__class__( - self.coin_spends + other.coin_spends, - self.aggregated_signature + other.aggregated_signature, - ) - - def __bytes__(self) -> hexbytes: - s = ( - bytes(uint32(len(self.coin_spends))) - + b"".join(bytes(_) for _ in self.coin_spends) - + bytes(self.aggregated_signature) - ) - return hexbytes(s) - - @classmethod - def from_bytes(cls, blob) -> "SpendBundle": - f = io.BytesIO(blob) - count = uint32.parse(f) - coin_spends = [CoinSpend.parse(f) for _ in range(count)] - aggregated_signature = bytes96.from_bytes(f.read()) - return cls(coin_spends, aggregated_signature) diff --git a/hsms/util/bech32.py b/hsms/util/bech32.py deleted file mode 100644 index c48f1cd..0000000 --- a/hsms/util/bech32.py +++ /dev/null @@ -1,23 +0,0 @@ -# the API to `contrib.bech32m` is an abomination unto man. This API is slightly less bad - -from typing import Optional, Tuple - -from hsms.contrib.bech32m import ( - bech32_decode as bech32_decode5, - bech32_encode as bech32_encode5, - convertbits, - Encoding, -) - - -def bech32_decode(text, max_length: int = 90) -> Optional[Tuple[str, bytes, Encoding]]: - prefix, base5_data, encoding = bech32_decode5(text, max_length) - if prefix is None: - return None - base8_data = bytes(convertbits(base5_data, 5, 8)) - return prefix, base8_data, encoding - - -def bech32_encode(prefix: str, blob: bytes, encoding: int = Encoding.BECH32M) -> str: - base5_bin = convertbits(blob, 8, 5) - return bech32_encode5(prefix, base5_bin, encoding) diff --git a/hsms/util/qrint_encoding.py b/hsms/util/qrint_encoding.py index 463eb14..f5b645b 100644 --- a/hsms/util/qrint_encoding.py +++ b/hsms/util/qrint_encoding.py @@ -81,7 +81,7 @@ from typing import Tuple -from hsms.contrib.bech32m import convertbits +from chia_base.contrib.bech32m import convertbits def b2a_qrint_payload(blob: bytes, grouping_size_bits: int) -> Tuple[int, str]: diff --git a/hsms/util/std_hash.py b/hsms/util/std_hash.py deleted file mode 100644 index 52aaefa..0000000 --- a/hsms/util/std_hash.py +++ /dev/null @@ -1,10 +0,0 @@ -import hashlib - -from hsms.atoms import bytes32 - - -def std_hash(b) -> bytes32: - """ - The standard hash used in many places. - """ - return bytes32(hashlib.sha256(bytes(b)).digest()) diff --git a/tests/cmds/hsm_test_1.txt b/tests/cmds/hsm_test_1.txt new file mode 100644 index 0000000..ac836a2 --- /dev/null +++ b/tests/cmds/hsm_test_1.txt @@ -0,0 +1,2 @@ +hsm_test_spend bls12381jlca8fe3jltegf54vwxyl2dvplpk3rz0ja6tjpdpfcar79cm43vxc40g8luh5xh0lva0qzkmytrtk7l5wds +440471080944257678950586942698633700843411528606405041796537532860668245089043282051850937366804449198522799103833368097055342179256864400369754902247372269393304897873246637209585249171701502818174975837601687680701653118001738367692894771185761748674898168395377524515985896212582336215423798385782601253703651612056400429730016400778599185922362459376649249668907746182592152896640468053613610739695422546963587518862381953895702154968323421174291271502578615538724660615085744621375010673086445263211287957384099273797585073923017814186584483032793013206669704009775863817567881164226447845539164783508526070162180061298339296048192113275829112041735441207977805149278535109579050392830110564366531442537088541926014925577308013521611516823327354736163337843313005293421047844749276080652092077654857098021738132239400149988911858993407505308292892112175755314230215025725024920352154345692459209566694302898377182643728972131451602112628300051371204986367858560375097766473119274813170370898276396632955389837044578312544762982332716703172743330954284806430244972274816864047954121403451333758709845675915640537535224947919679145902075435612852486432225978261514620729547128263967254512269136691250806603720000000000 diff --git a/tests/cmds/hsmpk-1.txt b/tests/cmds/hsmpk-1.txt new file mode 100644 index 0000000..d88eb17 --- /dev/null +++ b/tests/cmds/hsmpk-1.txt @@ -0,0 +1,2 @@ +hsmpk se1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsu90w8t +bls12381jlca8fe3jltegf54vwxyl2dvplpk3rz0ja6tjpdpfcar79cm43vxc40g8luh5xh0lva0qzkmytrtk7l5wds diff --git a/tests/cmds/hsmpk-2.txt b/tests/cmds/hsmpk-2.txt new file mode 100644 index 0000000..0c7937b --- /dev/null +++ b/tests/cmds/hsmpk-2.txt @@ -0,0 +1,2 @@ +hsmpk se12ws3sajsc6hcrrlyztxezt20t7cpf86qm984njjw2efeymhwjuvqacaca8 +bls123813fy33gx3vgatpun8mvsxrmpvu29vez3hde3gkp5qkg4858v50rh5gnu2zs8dl852du8rzlyc83x7jtve75t diff --git a/tests/full_life_cycle.sh b/tests/full_life_cycle.sh index 3e3e6a9..2232f05 100755 --- a/tests/full_life_cycle.sh +++ b/tests/full_life_cycle.sh @@ -13,7 +13,7 @@ do fi done -hsm_test_spend -m 200 1.pub 2.pub > unsigned-test-spend.qri +hsm_test_spend -m 200 $(cat 1.pub) $(cat 2.pub) > unsigned-test-spend.qri echo $(cat unsigned-test-spend.qri) | hsms -y 1.se > sig.1 echo $(cat unsigned-test-spend.qri) | hsms -y 2.se > sig.2 diff --git a/tests/generate.py b/tests/generate.py index 9c3d33e..4f59896 100644 --- a/tests/generate.py +++ b/tests/generate.py @@ -1,8 +1,8 @@ import hashlib import hmac -from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent -from hsms.streamables import bytes32 +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.atoms import bytes32 def bytes32_generate(nonce: int, namespace: str = "bytes32") -> bytes32: diff --git a/tests/test_bls.py b/tests/test_bls.py deleted file mode 100644 index 7865912..0000000 --- a/tests/test_bls.py +++ /dev/null @@ -1,129 +0,0 @@ -from hashlib import sha256 - -import pytest - -from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent, BLSSignature - - -def try_stuff(g): - - m1 = g + g - m2 = m1 * 2 - assert m2 == m1 + m1 - assert m2 == 2 * m1 - h2 = m2 * 1 - assert bytes(m2) == bytes(h2) - assert h2 == 1 * m2 - assert m2 * 0 == BLSPublicKey.zero() - assert 0 * m2 == BLSPublicKey.zero() - - m4 = m2 + m2 - m9 = m4 + m4 + m1 - assert m1 * 9 == m9 - assert 9 * m1 == m9 - m3 = m2 + m1 - assert m9 == m3 + m3 + m3 - - for s in [m1, m2, m3, m4, m9]: - b = s.as_bech32m() - assert s == BLSPublicKey.from_bech32m(b) - - -def test_bls_public_key(): - gen = BLSPublicKey.generator() - assert bytes(gen).hex() == ( - "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a1" - "4e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb" - ) - zero = BLSPublicKey.zero() - assert bytes(zero).hex() == "c" + ("0" * 95) - - # when we multiply by the generator, we use a different code path - # to make it hard to shoot yourself in the timing-attack food - - # so we test with a generator a few non-generators - try_stuff(gen) - - my_generator = gen + gen - try_stuff(my_generator) - - for s in (2, 7**35, 11**135, 13**912): - try_stuff(gen * s) - - with pytest.raises(ValueError): - BLSPublicKey.from_bech32m("foo") - - -def test_bls_secret_exponent(): - se_m = BLSSecretExponent.from_seed(b"foo") - ev = "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" - assert bytes(se_m).hex() == ev - - se5 = BLSSecretExponent.from_int(5) - assert int(se5) == 5 - k = int(se_m) - assert k == int(ev, 16) - assert se5 + se_m == k + 5 - - zero = BLSSecretExponent.zero() - assert int(zero) == 0 - assert se_m + zero == se_m - - for s in [se_m, se5, zero]: - b = s.as_bech32m() - assert s == BLSSecretExponent.from_bech32m(b) - - ev = "31f6283bc95a37bee2053ab2b57a88606b948aea646adb47dc463fd8f465e9a7" - child = se5.child(10) - assert bytes(child).hex() == ev - - # check that derivation works before casting to pubkey or after - pse5 = se5.public_key() - p_child = pse5.child(10) - assert p_child == child.public_key() - - ev = "49a56bb020d2879c50f7f88de1b511e0bb6eeaa1c160dd5e61f4d281af5a0a9c" - child = se5.hardened_child(10) - assert bytes(child).hex() == ev - - ev = 768461592 - assert se5.fingerprint() == ev - assert se5.public_key().fingerprint() == ev - - with pytest.raises(ValueError): - BLSSecretExponent.from_bech32m("foo") - - -def test_bls_signature(): - se5 = BLSSecretExponent.from_int(5) - message_hash = sha256(b"foo").digest() - sig = se5.sign(message_hash) - ev = ( - "8af37ffbe5be8977c8a02c492ef242d4b911f1cefdbbe20560908b6e9aaf6ee39c1e9f64b838c1ccbe45d5f88f9a190a" - "001613eafc47ed2a6a4abeb3791d861796a6b493c7b7d6bdfa5b5e97fbf4d4389101a0fedb23385fe42fa1d0865c4d0f" - ) - assert bytes(sig).hex() == ev - - p5 = se5.public_key() - aggsig_pair = BLSSignature.aggsig_pair(p5, message_hash) - assert sig.validate([aggsig_pair]) - - zero = BLSSignature.zero() - assert sig + zero == sig - - se7 = BLSSecretExponent.from_int(7) - se12 = BLSSecretExponent.from_int(12) - - p12 = se12.public_key() - - # do some fancy signing - sig5 = se5.sign(message_hash, final_public_key=p12) - sig7 = se7.sign(message_hash, final_public_key=p12) - sig12 = se12.sign(message_hash) - assert sig5 + sig7 == sig12 - - ev = ( - "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" - "024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8" - ) - assert bytes(BLSSignature.generator()).hex() == ev diff --git a/tests/test_cmds.py b/tests/test_cmds.py new file mode 100644 index 0000000..739bf43 --- /dev/null +++ b/tests/test_cmds.py @@ -0,0 +1,126 @@ +import io +import os +import pkg_resources +import shlex +import sys +import unittest + + +# If the REPAIR environment variable is set, any tests failing due to +# wrong output will be corrected. Be sure to do a "git diff" to validate that +# you're getting changes you expect. + +REPAIR = os.getenv("REPAIR", 0) + + +def get_test_cases(path): + PREFIX = os.path.dirname(__file__) + TESTS_PATH = os.path.join(PREFIX, path) + paths = [] + for dirpath, dirnames, filenames in os.walk(TESTS_PATH): + for fn in filenames: + if fn.endswith(".txt") and fn[0] != ".": + paths.append(os.path.join(dirpath, fn)) + paths.sort() + test_cases = [] + for p in paths: + with open(p) as f: + # allow "#" comments at the beginning of the file + cmd_lines = [] + comments = [] + while 1: + line = f.readline().rstrip() + if len(line) < 1 or line[0] != "#": + if line[-1:] == "\\": + cmd_lines.append(line[:-1]) + continue + cmd_lines.append(line) + break + comments.append(line + "\n") + expected_output = f.read() + test_name = os.path.relpath(p, PREFIX).replace(".", "_").replace("/", "_") + test_cases.append((test_name, cmd_lines, expected_output, comments, p)) + return test_cases + + +class TestCmds(unittest.TestCase): + def invoke_tool(self, cmd_line): + + # capture io + stdout_buffer = io.StringIO() + stderr_buffer = io.StringIO() + + old_stdout = sys.stdout + old_stderr = sys.stderr + + sys.stdout = stdout_buffer + sys.stderr = stderr_buffer + + args = shlex.split(cmd_line) + f = pkg_resources.load_entry_point("hsms", "console_scripts", args[0]) + v = f(args[1:]) + + sys.stdout = old_stdout + sys.stderr = old_stderr + + return v, stdout_buffer.getvalue(), stderr_buffer.getvalue() + + +def make_f(cmd_lines, expected_output, comments, path): + def f(self): + cmd = "".join(cmd_lines) + for c in cmd.split(";"): + r, actual_output, actual_stderr = self.invoke_tool(c) + if actual_output != expected_output: + print(path) + print(cmd) + print(actual_output) + print(expected_output) + if REPAIR: + f = open(path, "w") + f.write("".join(comments)) + for line in cmd_lines[:-1]: + f.write(line) + f.write("\\\n") + f.write(cmd_lines[-1]) + f.write("\n") + f.write(actual_output) + f.close() + self.assertEqual(expected_output, actual_output) + + return f + + +def inject(*paths): + for path in paths: + for idx, (name, i, o, comments, path) in enumerate(get_test_cases(path)): + name_of_f = "test_%s" % name + setattr(TestCmds, name_of_f, make_f(i, o, comments, path)) + + +inject("cmds") + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() + + +""" +Copyright 2023 Chia Network Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 8c58440..4aeae0f 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1,5 +1,7 @@ from tests.generate import se_generate, bytes32_generate, uint256_generate +from chia_base.core import Coin, SpendBundle + from hsms.debug.debug_spend_bundle import debug_spend_bundle from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE, @@ -7,7 +9,7 @@ solution_for_conditions, calculate_synthetic_offset, ) -from hsms.streamables import bytes96, Coin, CoinSpend, SpendBundle +from hsms.streamables import CoinSpend from hsms.process.sign import sign, generate_synthetic_offset_signatures from hsms.process.signing_hints import SumHint, PathHint from hsms.process.unsigned_spend import UnsignedSpend @@ -158,4 +160,4 @@ def create_spend_bundle(unsigned_spend, signatures): all_signatures = [sig_info.signature for sig_info in signatures + extra_signatures] total_signature = sum(all_signatures, start=all_signatures[0].zero()) - return SpendBundle(unsigned_spend.coin_spends, bytes96(total_signature)) + return SpendBundle(unsigned_spend.coin_spends, total_signature) From cb8522d3abe1c012bb3c4a320d9772918fb8ff6a Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 11 Aug 2023 15:53:09 -0700 Subject: [PATCH 04/40] Bump `chia_base` to 0.1.3. `chmod -x` --- hsms/cmds/hsm_dump_sb.py | 0 hsms/cmds/hsmmerge.py | 0 hsms/cmds/hsms.py | 0 hsms/cmds/hsmwizard.py | 0 hsms/cmds/qrint.py | 0 hsms/consensus/conditions.py | 10 ++-------- hsms/process/sign.py | 8 +++++--- hsms/process/unsigned_spend.py | 4 ++-- pyproject.toml | 2 +- 9 files changed, 10 insertions(+), 14 deletions(-) mode change 100755 => 100644 hsms/cmds/hsm_dump_sb.py mode change 100755 => 100644 hsms/cmds/hsmmerge.py mode change 100755 => 100644 hsms/cmds/hsms.py mode change 100755 => 100644 hsms/cmds/hsmwizard.py mode change 100755 => 100644 hsms/cmds/qrint.py diff --git a/hsms/cmds/hsm_dump_sb.py b/hsms/cmds/hsm_dump_sb.py old mode 100755 new mode 100644 diff --git a/hsms/cmds/hsmmerge.py b/hsms/cmds/hsmmerge.py old mode 100755 new mode 100644 diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py old mode 100755 new mode 100644 diff --git a/hsms/cmds/hsmwizard.py b/hsms/cmds/hsmwizard.py old mode 100755 new mode 100644 diff --git a/hsms/cmds/qrint.py b/hsms/cmds/qrint.py old mode 100755 new mode 100644 diff --git a/hsms/consensus/conditions.py b/hsms/consensus/conditions.py index 8a58b16..5cb258e 100644 --- a/hsms/consensus/conditions.py +++ b/hsms/consensus/conditions.py @@ -1,17 +1,11 @@ -from typing import Dict, Iterable, List +from typing import Dict, List from clvm_rs import Program -def iter_program(program: Program) -> Iterable[Program]: - while program.pair: - yield Program.to(program.pair[0]) - program = program.pair[1] - - def conditions_by_opcode(conditions: Program) -> Dict[int, List[Program]]: d: Dict[int, List[Program]] = {} - for _ in iter_program(conditions): + for _ in conditions.as_iter(): if _.pair: d.setdefault(Program.to(_.pair[0]).as_int(), []).append(_) return d diff --git a/hsms/process/sign.py b/hsms/process/sign.py index 97fdf7d..ce06b50 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -14,6 +14,7 @@ from .signing_hints import SumHint, SumHints, PathHint, PathHints from .unsigned_spend import SignatureInfo, UnsignedSpend +MAX_COST = 1 << 34 @dataclass class SignatureMetadata: @@ -27,9 +28,10 @@ class SignatureMetadata: def conditions_for_coin_spend(coin_spend: CoinSpend) -> Program: if coin_spend not in CONDITIONS_FOR_COIN_SPEND: - CONDITIONS_FOR_COIN_SPEND[coin_spend] = coin_spend.puzzle_reveal.run_with_cost( - coin_spend.solution, max_cost=1<<32 - )[1] + _cost, r = coin_spend.puzzle_reveal.run_with_cost( + coin_spend.solution, max_cost=MAX_COST + ) + CONDITIONS_FOR_COIN_SPEND[coin_spend] = r return CONDITIONS_FOR_COIN_SPEND[coin_spend] diff --git a/hsms/process/unsigned_spend.py b/hsms/process/unsigned_spend.py index 790b5a5..ef4a0fd 100644 --- a/hsms/process/unsigned_spend.py +++ b/hsms/process/unsigned_spend.py @@ -1,8 +1,8 @@ -import zlib - from dataclasses import dataclass from typing import List +import zlib + from clvm_rs import Program from chia_base.atoms import bytes32 diff --git a/pyproject.toml b/pyproject.toml index d7df458..dd851e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" repository = "https://github.com/chia-network/hsms.git" readme = "README.md" src_root = "." -dependencies = ["segno==1.4.1", "chia_base==0.1.2", "chialisp_puzzles==0.1.0"] +dependencies = ["segno==1.4.1", "chia_base==0.1.3", "chialisp_puzzles==0.1.0"] packages = ["hsms"] # version is defined with `setuptools_scm`. See `SConstruct` file. From bd07a878569a5d18228166791221ded63a405011 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 11 Aug 2023 16:41:45 -0700 Subject: [PATCH 05/40] Get `full_life_cycle.sh` working --- hsms/cmds/hsm_dump_sb.py | 3 --- hsms/cmds/hsm_test_spend.py | 15 ++++++++++++--- hsms/cmds/hsmmerge.py | 2 -- hsms/cmds/hsms.py | 2 -- hsms/cmds/hsmwizard.py | 3 --- hsms/cmds/qrint.py | 14 -------------- tests/full_life_cycle.sh | 7 ++++--- 7 files changed, 16 insertions(+), 30 deletions(-) diff --git a/hsms/cmds/hsm_dump_sb.py b/hsms/cmds/hsm_dump_sb.py index 76ce328..7cb6b77 100644 --- a/hsms/cmds/hsm_dump_sb.py +++ b/hsms/cmds/hsm_dump_sb.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python - - import argparse from chia_base.core import SpendBundle diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index 53711ea..0f6b518 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -82,9 +82,12 @@ def hsm_test_spend(args, parser): ) b = bytes(unsigned_spend) - cb = zlib.compress(b) - optimal_size = optimal_chunk_size_for_max_chunk_size(len(cb), args.max_chunk_size) - chunks = create_chunks_for_blob(cb, optimal_size) + if args.no_chunks: + chunks = [b] + else: + cb = zlib.compress(b) + optimal_size = optimal_chunk_size_for_max_chunk_size(len(cb), args.max_chunk_size) + chunks = create_chunks_for_blob(cb, optimal_size) for chunk in chunks: print(b2a_qrint(chunk)) @@ -104,6 +107,12 @@ def create_parser(): help="maximum number of bytes encoded into each chunk", type=int, ) + parser.add_argument( + "-n", + "--no-chunks", + action='store_true', + help="don't compress or chunk output", + ) parser.add_argument( "public_key", metavar="public-key", diff --git a/hsms/cmds/hsmmerge.py b/hsms/cmds/hsmmerge.py index 4abe423..1447a59 100644 --- a/hsms/cmds/hsmmerge.py +++ b/hsms/cmds/hsmmerge.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - from typing import List import argparse diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index 77cf4b2..c490ab1 100644 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - from decimal import Decimal from typing import BinaryIO, Iterable, List, TextIO diff --git a/hsms/cmds/hsmwizard.py b/hsms/cmds/hsmwizard.py index 4505523..a1b435c 100644 --- a/hsms/cmds/hsmwizard.py +++ b/hsms/cmds/hsmwizard.py @@ -1,6 +1,3 @@ -#!/bin/env python3 - - from pathlib import Path import argparse diff --git a/hsms/cmds/qrint.py b/hsms/cmds/qrint.py index e8d6dac..170f723 100644 --- a/hsms/cmds/qrint.py +++ b/hsms/cmds/qrint.py @@ -1,24 +1,10 @@ -#!/usr/bin/env python - import argparse import os.path from hsms.process.sign import generate_synthetic_offset_signatures -from hsms.streamables import SpendBundle from hsms.util.qrint_encoding import a2b_qrint, b2a_qrint -def create_spend_bundle(unsigned_spend, signatures): - extra_signatures = generate_synthetic_offset_signatures(unsigned_spend) - - # now let's try adding them all together and creating a `SpendBundle` - - all_signatures = signatures + [sig_info.signature for sig_info in extra_signatures] - total_signature = sum(all_signatures, start=all_signatures[0].zero()) - - return SpendBundle(unsigned_spend.coin_spends, total_signature) - - def file_or_string(p) -> str: try: with open(p) as f: diff --git a/tests/full_life_cycle.sh b/tests/full_life_cycle.sh index 2232f05..8025c90 100755 --- a/tests/full_life_cycle.sh +++ b/tests/full_life_cycle.sh @@ -14,11 +14,12 @@ do done hsm_test_spend -m 200 $(cat 1.pub) $(cat 2.pub) > unsigned-test-spend.qri +hsm_test_spend -n -m 200 $(cat 1.pub) $(cat 2.pub) > unsigned-test-spend-unchunked.qri -echo $(cat unsigned-test-spend.qri) | hsms -y 1.se > sig.1 -echo $(cat unsigned-test-spend.qri) | hsms -y 2.se > sig.2 +cat unsigned-test-spend.qri | hsms -y 1.se > sig.1 +cat unsigned-test-spend.qri | hsms -y 2.se > sig.2 -hsmmerge unsigned-test-spend.qri $(cat sig.1) $(cat sig.2) > spendbundle.hex +hsmmerge unsigned-test-spend-unchunked.qri $(cat sig.1) $(cat sig.2) > spendbundle.hex hsm_dump_sb spendbundle.hex From 127b817caf0299f00776b1a49d63029c5773ca6e Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 11 Aug 2023 16:44:05 -0700 Subject: [PATCH 06/40] `black` --- hsms/cmds/hsm_test_spend.py | 10 +++++----- hsms/cmds/poser_verify.py | 2 +- hsms/cmds/qrint.py | 1 - hsms/process/sign.py | 1 + hsms/streamables/__init__.py | 2 +- hsms/streamables/coin_spend.py | 4 +++- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index 0f6b518..eaf1543 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -33,9 +33,7 @@ def hsm_test_spend(args, parser): - root_public_keys = [ - BLSPublicKey.from_bech32m(_) for _ in args.public_key - ] + root_public_keys = [BLSPublicKey.from_bech32m(_) for _ in args.public_key] paths = [[index, index + 1] for index in range(len(root_public_keys))] @@ -86,7 +84,9 @@ def hsm_test_spend(args, parser): chunks = [b] else: cb = zlib.compress(b) - optimal_size = optimal_chunk_size_for_max_chunk_size(len(cb), args.max_chunk_size) + optimal_size = optimal_chunk_size_for_max_chunk_size( + len(cb), args.max_chunk_size + ) chunks = create_chunks_for_blob(cb, optimal_size) for chunk in chunks: print(b2a_qrint(chunk)) @@ -110,7 +110,7 @@ def create_parser(): parser.add_argument( "-n", "--no-chunks", - action='store_true', + action="store_true", help="don't compress or chunk output", ) parser.add_argument( diff --git a/hsms/cmds/poser_verify.py b/hsms/cmds/poser_verify.py index 20da737..68354cb 100644 --- a/hsms/cmds/poser_verify.py +++ b/hsms/cmds/poser_verify.py @@ -21,7 +21,7 @@ def bytes32_fromhex(s: str) -> bytes: DESCRIPTION = "Proof of secret exponent request verifier." EPILOG = ( 'Note: signature output from hsms is in "qrint" form. ' - 'Use `qrint -H` to convert it to hex.' + "Use `qrint -H` to convert it to hex." ) diff --git a/hsms/cmds/qrint.py b/hsms/cmds/qrint.py index 170f723..bfad404 100644 --- a/hsms/cmds/qrint.py +++ b/hsms/cmds/qrint.py @@ -1,7 +1,6 @@ import argparse import os.path -from hsms.process.sign import generate_synthetic_offset_signatures from hsms.util.qrint_encoding import a2b_qrint, b2a_qrint diff --git a/hsms/process/sign.py b/hsms/process/sign.py index ce06b50..de68447 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -16,6 +16,7 @@ MAX_COST = 1 << 34 + @dataclass class SignatureMetadata: partial_public_key: BLSPublicKey diff --git a/hsms/streamables/__init__.py b/hsms/streamables/__init__.py index e460f6f..a005853 100644 --- a/hsms/streamables/__init__.py +++ b/hsms/streamables/__init__.py @@ -1,3 +1,3 @@ from .coin_spend import CoinSpend -__all__ = [CoinSpend] \ No newline at end of file +__all__ = [CoinSpend] diff --git a/hsms/streamables/coin_spend.py b/hsms/streamables/coin_spend.py index 631bf7d..c29b01d 100644 --- a/hsms/streamables/coin_spend.py +++ b/hsms/streamables/coin_spend.py @@ -11,6 +11,7 @@ def as_program(self): for _ in self.coin_spends ] + @classmethod def from_program(cls, program) -> "CoinSpend": parent_coin_info, puzzle_reveal, amount, solution = transform_as_struct( @@ -32,5 +33,6 @@ def from_program(cls, program) -> "CoinSpend": solution, ) + CoinSpend.as_program = as_program -CoinSpend.from_program = from_program \ No newline at end of file +CoinSpend.from_program = from_program From 762368a3ec3a6a9003d7ab7082093d622a3f030f Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 11 Aug 2023 17:08:31 -0700 Subject: [PATCH 07/40] Refactor clvm_serialization --- hsms/cmds/hsm_test_spend.py | 3 +-- hsms/process/sign.py | 2 +- hsms/process/unsigned_spend.py | 24 ++++++++++++++++++--- hsms/streamables/__init__.py | 3 --- hsms/streamables/coin_spend.py | 38 --------------------------------- hsms/util/clvm_serialization.py | 12 +++++++++++ tests/test_lifecycle.py | 3 +-- 7 files changed, 36 insertions(+), 49 deletions(-) delete mode 100644 hsms/streamables/__init__.py delete mode 100644 hsms/streamables/coin_spend.py diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index eaf1543..05cb6a0 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -5,7 +5,7 @@ from clvm_rs import Program from chia_base.bls12_381 import BLSPublicKey -from chia_base.core import Coin +from chia_base.core import Coin, CoinSpend from hsms.process.signing_hints import SumHint, PathHint @@ -15,7 +15,6 @@ solution_for_conditions, calculate_synthetic_offset, ) -from hsms.streamables import CoinSpend from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.conlang import CREATE_COIN from hsms.util.byte_chunks import ( diff --git a/hsms/process/sign.py b/hsms/process/sign.py index de68447..2b3d943 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -6,9 +6,9 @@ from chia_base.atoms import bytes32, hexbytes from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.core import CoinSpend from hsms.consensus.conditions import conditions_by_opcode -from hsms.streamables import CoinSpend from hsms.puzzles.conlang import AGG_SIG_ME, AGG_SIG_UNSAFE from .signing_hints import SumHint, SumHints, PathHint, PathHints diff --git a/hsms/process/unsigned_spend.py b/hsms/process/unsigned_spend.py index ef4a0fd..b51714b 100644 --- a/hsms/process/unsigned_spend.py +++ b/hsms/process/unsigned_spend.py @@ -7,14 +7,18 @@ from chia_base.atoms import bytes32 from chia_base.bls12_381 import BLSPublicKey, BLSSignature +from chia_base.core import Coin, CoinSpend from hsms.process.signing_hints import SumHint, PathHint -from hsms.streamables import CoinSpend from hsms.util.byte_chunks import assemble_chunks, create_chunks_for_blob from hsms.util.clvm_serialization import ( + as_atom, + as_int, + clvm_to_list, + no_op, transform_dict, transform_dict_by_key, - clvm_to_list, + transform_as_struct, ) @@ -68,8 +72,22 @@ def from_chunks(cls, chunks: List[bytes]) -> "UnsignedSpend": return UnsignedSpend.from_bytes(zlib.decompress(assemble_chunks(chunks))) +def coin_spend_from_program(program: Program) -> CoinSpend: + struct = transform_as_struct(program, as_atom, no_op, as_int, no_op) + parent_coin_info, puzzle_reveal, amount, solution = struct + return CoinSpend( + Coin( + parent_coin_info, + puzzle_reveal.tree_hash(), + amount, + ), + puzzle_reveal, + solution, + ) + + UNSIGNED_SPEND_TRANSFORMER = { - "c": lambda x: clvm_to_list(x, CoinSpend.from_program), + "c": lambda x: clvm_to_list(x, coin_spend_from_program), "s": lambda x: clvm_to_list(x, SumHint.from_program), "p": lambda x: clvm_to_list(x, PathHint.from_program), "a": lambda x: x.atom, diff --git a/hsms/streamables/__init__.py b/hsms/streamables/__init__.py deleted file mode 100644 index a005853..0000000 --- a/hsms/streamables/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .coin_spend import CoinSpend - -__all__ = [CoinSpend] diff --git a/hsms/streamables/coin_spend.py b/hsms/streamables/coin_spend.py deleted file mode 100644 index c29b01d..0000000 --- a/hsms/streamables/coin_spend.py +++ /dev/null @@ -1,38 +0,0 @@ -from clvm_rs import Program - -from chia_base.core import Coin, CoinSpend - -from hsms.util.clvm_serialization import transform_as_struct - - -def as_program(self): - return [ - [_.coin.parent_coin_info, _.puzzle_reveal, _.coin.amount, _.solution] - for _ in self.coin_spends - ] - - -@classmethod -def from_program(cls, program) -> "CoinSpend": - parent_coin_info, puzzle_reveal, amount, solution = transform_as_struct( - program, - lambda x: x.atom, - lambda x: x, - lambda x: Program.to(x).as_int(), - lambda x: x, - ) - puzzle_reveal = Program.to(puzzle_reveal) - solution = Program.to(solution) - return cls( - Coin( - parent_coin_info, - puzzle_reveal.tree_hash(), - amount, - ), - puzzle_reveal, - solution, - ) - - -CoinSpend.as_program = as_program -CoinSpend.from_program = from_program diff --git a/hsms/util/clvm_serialization.py b/hsms/util/clvm_serialization.py index 96ff030..3f90184 100644 --- a/hsms/util/clvm_serialization.py +++ b/hsms/util/clvm_serialization.py @@ -77,3 +77,15 @@ def clvm_list_to_dict( ) -> Dict[K, V]: r = clvm_to_list(items, lambda obj: from_clvm_f_to_kv(obj.pair[0], obj.pair[1])) return dict(r) + + +def no_op(x): + return x + + +def as_atom(x): + return x.atom + + +def as_int(x): + return x.as_int() diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 4aeae0f..95468a1 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1,6 +1,6 @@ from tests.generate import se_generate, bytes32_generate, uint256_generate -from chia_base.core import Coin, SpendBundle +from chia_base.core import Coin, CoinSpend, SpendBundle from hsms.debug.debug_spend_bundle import debug_spend_bundle from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( @@ -9,7 +9,6 @@ solution_for_conditions, calculate_synthetic_offset, ) -from hsms.streamables import CoinSpend from hsms.process.sign import sign, generate_synthetic_offset_signatures from hsms.process.signing_hints import SumHint, PathHint from hsms.process.unsigned_spend import UnsignedSpend From f136851dcdcaaf5c5aafe30bce8e64057567b228 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 11 Aug 2023 17:08:40 -0700 Subject: [PATCH 08/40] Forgot a command --- hsms/cmds/hsm_dump_us.py | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 hsms/cmds/hsm_dump_us.py diff --git a/hsms/cmds/hsm_dump_us.py b/hsms/cmds/hsm_dump_us.py new file mode 100644 index 0000000..b8e39da --- /dev/null +++ b/hsms/cmds/hsm_dump_us.py @@ -0,0 +1,63 @@ +import argparse + +from hsms.clvm.disasm import disassemble +from hsms.cmds.hsms import summarize_unsigned_spend +from hsms.process.unsigned_spend import UnsignedSpend + + +def file_or_string(p) -> str: + try: + with open(p) as f: + text = f.read().strip() + except Exception: + text = p + return text + + +def dump_coin_spend(coin_spend): + coin = coin_spend.coin + print(f"parent coin: {coin.parent_coin_info.hex()}") + print(f" amount: {coin.amount}") + print(" puzzle:", end="") + if coin_spend.puzzle_reveal.tree_hash() == coin.puzzle_hash: + print(f" {disassemble(coin_spend.puzzle_reveal)}") + else: + print(" ** bad puzzle reveal") + print(f" solution: {disassemble(coin_spend.solution)}") + + +def dump_unsigned_spend(unsigned_spend): + count = len(unsigned_spend.coin_spends) + print() + print(f"coin count: {count}") + print() + for coin_spend in unsigned_spend.coin_spends: + dump_coin_spend(coin_spend) + + +def hsms_dump_us(args, parser): + blob = bytes.fromhex(file_or_string(args.unsigned_spend)) + unsigned_spend = UnsignedSpend.from_bytes(blob) + summarize_unsigned_spend(unsigned_spend) + + +def create_parser(): + parser = argparse.ArgumentParser( + description="Dump information about `UnsignedSpend`" + ) + parser.add_argument( + "unsigned_spend", + metavar="hex-encoded-unsigned-spend-or-file", + help="hex-encoded `UnsignedSpend`", + ) + return parser + + +def main(argv=None): + parser = create_parser(argv) + args = parser.parse_args() + return hsms_dump_us(args, parser) + + +if __name__ == "__main__": + main() From e8a20e5b1c566040011a9c6541e294c85b7dbee3 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 11 Aug 2023 17:51:57 -0700 Subject: [PATCH 09/40] setuptools --- README.md | 7 ------- SConstruct | 42 ------------------------------------------ pyproject.toml | 48 ++++++++++++++++++++++-------------------------- setup.py | 7 +++++++ 4 files changed, 29 insertions(+), 75 deletions(-) delete mode 100644 SConstruct create mode 100644 setup.py diff --git a/README.md b/README.md index 7b3994b..e78f5ae 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,3 @@ For testing & debugging: - `hsm_test_spend` - create a simple test `UnsignedSpend` multisig spend - `hsm_dump_sb` - debug utility to dump information about a `SpendBundle` - - -enscons -------- - -This package uses [enscons](https://github.com/dholth/enscons) -which uses [SCons](https://scons.org/) to build rather than the commonly used `setuptools`. diff --git a/SConstruct b/SConstruct deleted file mode 100644 index c2b4e56..0000000 --- a/SConstruct +++ /dev/null @@ -1,42 +0,0 @@ -# Starter SConstruct for enscons - -import enscons -import setuptools_scm -import pytoml - - -metadata = dict(pytoml.load(open("pyproject.toml")))["project"] -metadata["version"] = setuptools_scm.get_version(local_scheme="no-local-version") - -full_tag = "py3-none-any" # pure Python packages compatible with 2+3 - -env = Environment( - tools=["default", "packaging", enscons.generate], - PACKAGE_METADATA=metadata, - WHEEL_TAG=full_tag, - ROOT_IS_PURELIB=full_tag.endswith("-any"), -) - -# Only *.py is included automatically by setup2toml. -# Add extra 'purelib' files or package_data here. -py_source = ( - Glob("hsms/*.py") - + Glob("hsms/*/*.py") - + Glob("hsms/puzzles/*cl") - + Glob("hsms/puzzles/*clvm") -) - -source = env.Whl("purelib", py_source, root="") -whl = env.WhlFile(source=source) - -# It's easier to just use Glob() instead of FindSourceFiles() since we have -# so few installed files.. -sdist_source = ( - File(["PKG-INFO", "README.md", "SConstruct", "pyproject.toml"]) + py_source -) -sdist = env.SDist(source=sdist_source) -env.NoClean(sdist) -env.Alias("sdist", sdist) - -# needed for pep517 (enscons.api) to work -env.Default(whl, sdist) diff --git a/pyproject.toml b/pyproject.toml index dd851e6..49f39b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,30 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=6.2", "chialisp_builder==0.1.0"] +build-backend = "setuptools.build_meta" + [project] name = "hsms" description = "Hardware security module simulator for chia bls12_381 signatures" -authors = ["Richard Kiss "] -license = "Apache-2.0" -repository = "https://github.com/chia-network/hsms.git" +authors = [{name = "Richard Kiss", email = "him@richardkiss.com"}] +license = {file = "LICENSE"} readme = "README.md" -src_root = "." dependencies = ["segno==1.4.1", "chia_base==0.1.3", "chialisp_puzzles==0.1.0"] -packages = ["hsms"] -# version is defined with `setuptools_scm`. See `SConstruct` file. +# version is defined with `setuptools_scm` +dynamic = ["version"] -[project.optional_dependencies] -test = ["nose", "coverage"] -dev = ["flake8>=4.0.1", "black>=22.6"] +[project.optional-dependencies] +dev = ["flake8>=4.0.1", "black>=22.6", "pytest"] -[project.entry_points] -console_scripts = [ - "hsms = hsms.cmds.hsms:main", - "hsmpk = hsms.cmds.hsmpk:main", - "hsmgen = hsms.cmds.hsmgen:main", - "hsmmerge = hsms.cmds.hsmmerge:main", - "hsm_test_spend = hsms.cmds.hsm_test_spend:main", - "hsm_dump_sb = hsms.cmds.hsm_dump_sb:main", - "hsm_dump_us = hsms.cmds.hsm_dump_us:main", - "qrint = hsms.cmds.qrint:main", - "hsmwizard = hsms.cmds.hsmwizard:main", - "poser_gen = hsms.cmds.poser_gen:main", - "poser_verify = hsms.cmds.poser_verify:main" -] +[project.scripts] +hsms = "hsms.cmds.hsms:main" +hsmpk = "hsms.cmds.hsmpk:main" +hsmgen = "hsms.cmds.hsmgen:main" +hsmmerge = "hsms.cmds.hsmmerge:main" +hsm_test_spend = "hsms.cmds.hsm_test_spend:main" +hsm_dump_sb = "hsms.cmds.hsm_dump_sb:main" +qrint = "hsms.cmds.qrint:main" +hsmwizard = "hsms.cmds.hsmwizard:main" -[build-system] -requires = ["pytoml>=0.1", "enscons", "setuptools_scm>=6.2"] -build-backend = "enscons.api" +# version is defined with `setuptools_scm` +[tool.setuptools_scm] +local_scheme = "no-local-version" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7595453 --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, find_packages + +packages = find_packages(where=".", include=["hsms*"]) + +setup( + packages=packages, +) From f2f2d9b2bab9ed5bf3b04a7a1141ff6bf3905cb2 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 14 Aug 2023 16:22:34 -0700 Subject: [PATCH 10/40] Some minor improvements --- hsms/cmds/hsm_test_spend.py | 3 +-- hsms/cmds/hsms.py | 8 ++++---- hsms/debug/debug_spend_bundle.py | 8 +++++--- hsms/process/sign.py | 4 ++-- hsms/process/unsigned_spend.py | 4 ++-- hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py | 4 +++- tests/generate.py | 2 +- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index 05cb6a0..f7d326d 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -8,14 +8,13 @@ from chia_base.core import Coin, CoinSpend from hsms.process.signing_hints import SumHint, PathHint - +from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE, puzzle_for_public_key_and_hidden_puzzle, solution_for_conditions, calculate_synthetic_offset, ) -from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.conlang import CREATE_COIN from hsms.util.byte_chunks import ( create_chunks_for_blob, diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index c490ab1..f248064 100644 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -8,14 +8,14 @@ import sys import zlib -from clvm_rs import Program - -import segno - from chia_base.atoms import bytes32 from chia_base.bls12_381 import BLSSecretExponent, BLSSignature from chia_base.util.bech32 import bech32_encode +from clvm_rs import Program + +import segno + from hsms.consensus.conditions import conditions_by_opcode from hsms.process.sign import conditions_for_coin_spend, sign from hsms.process.unsigned_spend import UnsignedSpend diff --git a/hsms/debug/debug_spend_bundle.py b/hsms/debug/debug_spend_bundle.py index 5c85e77..f0f05dc 100644 --- a/hsms/debug/debug_spend_bundle.py +++ b/hsms/debug/debug_spend_bundle.py @@ -1,10 +1,10 @@ from typing import List -from clvm_rs import Program - from chia_base.core import Coin from chia_base.util.std_hash import std_hash +from clvm_rs import Program + from hsms.clvm.disasm import disassemble as bu_disassemble, KEYWORD_FROM_ATOM from hsms.consensus.conditions import conditions_by_opcode from hsms.process.sign import generate_verify_pairs @@ -16,6 +16,8 @@ "ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" ) +MAX_COST = 1 << 34 + # information needed to spend a cc # if we ever support more genesis conditions, like a re-issuable coin, @@ -82,7 +84,7 @@ def debug_spend_bundle( f"\nbrun -y main.sym '{bu_disassemble(puzzle_reveal)}'" f" '{bu_disassemble(solution)}'" ) - cost, r = puzzle_reveal.run_with_cost(solution, max_cost=1<<34) + cost, r = puzzle_reveal.run_with_cost(solution, max_cost=MAX_COST) conditions = conditions_by_opcode(r) error = None if error: diff --git a/hsms/process/sign.py b/hsms/process/sign.py index 2b3d943..6be269f 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -2,12 +2,12 @@ from typing import Dict, Iterable, List, Optional, Tuple from weakref import WeakKeyDictionary -from clvm_rs import Program - from chia_base.atoms import bytes32, hexbytes from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent from chia_base.core import CoinSpend +from clvm_rs import Program + from hsms.consensus.conditions import conditions_by_opcode from hsms.puzzles.conlang import AGG_SIG_ME, AGG_SIG_UNSAFE diff --git a/hsms/process/unsigned_spend.py b/hsms/process/unsigned_spend.py index b51714b..7bb988b 100644 --- a/hsms/process/unsigned_spend.py +++ b/hsms/process/unsigned_spend.py @@ -3,12 +3,12 @@ import zlib -from clvm_rs import Program - from chia_base.atoms import bytes32 from chia_base.bls12_381 import BLSPublicKey, BLSSignature from chia_base.core import Coin, CoinSpend +from clvm_rs import Program + from hsms.process.signing_hints import SumHint, PathHint from hsms.util.byte_chunks import assemble_chunks, create_chunks_for_blob from hsms.util.clvm_serialization import ( diff --git a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py index 4d66cd0..3411ce1 100644 --- a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py +++ b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py @@ -78,6 +78,8 @@ SYNTHETIC_MOD = load_puzzle("calculate_synthetic_public_key") +MAX_COST = 1 << 24 + def calculate_synthetic_offset( public_key: BLSPublicKey, hidden_puzzle_hash: bytes32 @@ -92,7 +94,7 @@ def calculate_synthetic_public_key( public_key: BLSPublicKey, hidden_puzzle_hash: bytes32 ) -> BLSPublicKey: _cost, r = SYNTHETIC_MOD.run_with_cost( - [bytes(public_key), hidden_puzzle_hash], max_cost=1 << 32 + [bytes(public_key), hidden_puzzle_hash], max_cost=MAX_COST ) return BLSPublicKey.from_bytes(r.atom) diff --git a/tests/generate.py b/tests/generate.py index 4f59896..3a6078e 100644 --- a/tests/generate.py +++ b/tests/generate.py @@ -1,8 +1,8 @@ import hashlib import hmac -from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent from chia_base.atoms import bytes32 +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent def bytes32_generate(nonce: int, namespace: str = "bytes32") -> bytes32: From 832149090e86f789b4e8bef892985c5aa2fa141e Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 15 Aug 2023 17:20:15 -0700 Subject: [PATCH 11/40] Get `hsm_dump_us` working --- README.md | 1 + hsms/cmds/hsm_dump_us.py | 7 ++++--- hsms/cmds/hsm_test_spend.py | 30 ++++++++++++++++++++---------- hsms/cmds/hsmpk.py | 4 +--- hsms/cmds/hsms.py | 29 +++++++++++++++-------------- pyproject.toml | 1 + tests/cmds/hsm_dump_us_1.txt | 7 +++++++ 7 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 tests/cmds/hsm_dump_us_1.txt diff --git a/README.md b/README.md index e78f5ae..7acdb0d 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,4 @@ For testing & debugging: - `hsm_test_spend` - create a simple test `UnsignedSpend` multisig spend - `hsm_dump_sb` - debug utility to dump information about a `SpendBundle` +- `hsm_dump_us` - debug utility to dump information about an `UnsignedSpend` diff --git a/hsms/cmds/hsm_dump_us.py b/hsms/cmds/hsm_dump_us.py index b8e39da..5aa2789 100644 --- a/hsms/cmds/hsm_dump_us.py +++ b/hsms/cmds/hsm_dump_us.py @@ -1,4 +1,5 @@ import argparse +import sys from hsms.clvm.disasm import disassemble from hsms.cmds.hsms import summarize_unsigned_spend @@ -53,9 +54,9 @@ def create_parser(): return parser -def main(argv=None): - parser = create_parser(argv) - args = parser.parse_args() +def main(argv=sys.argv[1:]): + parser = create_parser() + args = parser.parse_args(argv) return hsms_dump_us(args, parser) diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index f7d326d..8239648 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -1,5 +1,6 @@ import argparse import hashlib +import sys import zlib from clvm_rs import Program @@ -78,16 +79,19 @@ def hsm_test_spend(args, parser): ) b = bytes(unsigned_spend) - if args.no_chunks: - chunks = [b] + if args.hex: + print(b.hex()) else: - cb = zlib.compress(b) - optimal_size = optimal_chunk_size_for_max_chunk_size( - len(cb), args.max_chunk_size - ) - chunks = create_chunks_for_blob(cb, optimal_size) - for chunk in chunks: - print(b2a_qrint(chunk)) + if args.no_chunks: + chunks = [b] + else: + cb = zlib.compress(b) + optimal_size = optimal_chunk_size_for_max_chunk_size( + len(cb), args.max_chunk_size + ) + chunks = create_chunks_for_blob(cb, optimal_size) + for chunk in chunks: + print(b2a_qrint(chunk)) us = UnsignedSpend.from_bytes(b) assert bytes(us) == b @@ -105,6 +109,12 @@ def create_parser(): help="maximum number of bytes encoded into each chunk", type=int, ) + parser.add_argument( + "-H", + "--hex", + action="store_true", + help="hex output", + ) parser.add_argument( "-n", "--no-chunks", @@ -121,7 +131,7 @@ def create_parser(): return parser -def main(argv=None): +def main(argv=sys.argv[1:]): parser = create_parser() args = parser.parse_args(argv) return hsm_test_spend(args, parser) diff --git a/hsms/cmds/hsmpk.py b/hsms/cmds/hsmpk.py index b028557..0fbc8e7 100644 --- a/hsms/cmds/hsmpk.py +++ b/hsms/cmds/hsmpk.py @@ -3,9 +3,7 @@ from chia_base.bls12_381 import BLSSecretExponent -def main(argv=None): - if argv is None: - argv = sys.argv[1:] +def main(argv=sys.argv[1:]): for arg in argv: secret_exponent = BLSSecretExponent.from_bech32m(arg) print(secret_exponent.public_key().as_bech32m()) diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index f248064..48e3b8d 100644 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -37,12 +37,14 @@ def unsigned_spend_from_blob(blob: bytes) -> UnsignedSpend: return UnsignedSpend.from_program(program) -def create_unsigned_spend_pipeline(nochunks: bool) -> Iterable[UnsignedSpend]: - print("waiting for qrint-encoded signing requests", file=sys.stderr) +def create_unsigned_spend_pipeline( + nochunks: bool, f=sys.stdout +) -> Iterable[UnsignedSpend]: + print("waiting for qrint-encoded signing requests", file=f) partial_encodings = {} while True: try: - print("> ", end="", file=sys.stderr) + print("> ", end="", file=f) line = input("").strip() if len(line) == 0: break @@ -64,7 +66,7 @@ def create_unsigned_spend_pipeline(nochunks: bool) -> Iterable[UnsignedSpend]: except EOFError: break except Exception as ex: - print(ex, file=sys.stderr) + print(ex, file=f) def replace_with_gpg_pipe(args, f: BinaryIO) -> TextIO: @@ -92,17 +94,15 @@ def parse_private_key_file(args) -> List[BLSSecretExponent]: return secret_exponents -def summarize_unsigned_spend(unsigned_spend: UnsignedSpend): - print(file=sys.stderr) +def summarize_unsigned_spend(unsigned_spend: UnsignedSpend, f=sys.stdout): + print(file=f) for coin_spend in unsigned_spend.coin_spends: xch_amount = Decimal(coin_spend.coin.amount) / XCH_PER_MOJO address = address_for_puzzle_hash(coin_spend.coin.puzzle_hash) - print( - f"COIN SPENT: {xch_amount:0.12f} xch at address {address}", file=sys.stderr - ) + print(f"COIN SPENT: {xch_amount:0.12f} xch at address {address}", file=f) conditions = conditions_for_coin_spend(coin_spend) - print(file=sys.stderr) + print(file=f) for coin_spend in unsigned_spend.coin_spends: conditions = conditions_for_coin_spend(coin_spend) conditions_lookup = conditions_by_opcode(conditions) @@ -111,8 +111,8 @@ def summarize_unsigned_spend(unsigned_spend: UnsignedSpend): address = address_for_puzzle_hash(puzzle_hash) amount = int(create_coin.at("rrf")) xch_amount = Decimal(amount) / XCH_PER_MOJO - print(f"COIN CREATED: {xch_amount:0.12f} xch to {address}", file=sys.stderr) - print(file=sys.stderr) + print(f"COIN CREATED: {xch_amount:0.12f} xch to {address}", file=f) + print(file=f) def address_for_puzzle_hash(puzzle_hash: bytes32) -> str: @@ -126,10 +126,11 @@ def check_ok(): def hsms(args, parser): wallet = parse_private_key_file(args) - unsigned_spend_pipeline = create_unsigned_spend_pipeline(args.nochunks) + f = sys.stderr + unsigned_spend_pipeline = create_unsigned_spend_pipeline(args.nochunks, f) for unsigned_spend in unsigned_spend_pipeline: if not args.yes: - summarize_unsigned_spend(unsigned_spend) + summarize_unsigned_spend(unsigned_spend, f) if not check_ok(): continue signature_info = sign(unsigned_spend, wallet) diff --git a/pyproject.toml b/pyproject.toml index 49f39b4..59f367a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ hsmgen = "hsms.cmds.hsmgen:main" hsmmerge = "hsms.cmds.hsmmerge:main" hsm_test_spend = "hsms.cmds.hsm_test_spend:main" hsm_dump_sb = "hsms.cmds.hsm_dump_sb:main" +hsm_dump_us = "hsms.cmds.hsm_dump_us:main" qrint = "hsms.cmds.qrint:main" hsmwizard = "hsms.cmds.hsmwizard:main" diff --git a/tests/cmds/hsm_dump_us_1.txt b/tests/cmds/hsm_dump_us_1.txt new file mode 100644 index 0000000..a6a1f4e --- /dev/null +++ b/tests/cmds/hsm_dump_us_1.txt @@ -0,0 +1,7 @@ +hsm_dump_us ffff61a0ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbbffff63ffffa0e47125968b3b71049fbc4802d1e40a71ea1359decfabacf70b34588037d4ff0cffff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0845bd56585419b672a0dc78613617e1f2913393ee240b872b6c6b2fe01ef0567e78cc1562a6f415594b537947dac65f0ff018080ff01ffff80ffff01ffff33ffa0f6152f2ad8a93dc0f8f825f2a8d162d6da46e81f5fe481ff76b4f8384a677886ff8602ba7def300080ffff33ffa0991e4b5f669e57fb49aa4632b0eb0bec0a684d0ab5edac4da47c7a504c6a62abff8601d1a94a20008080ff80808080ffff73ffffffb0874c122c61f9f98aebc4f675ff81dc78c63b5bc648fd6a2771ff4f947571ff2988739c2ae460767574b4aae7b5eebc58ffb0b73f2e3ae3c56191e2d86a8d564c6aa1724e838fed188f3143675d1cd65a94d4a9b28028d113abbe1c9b07088f17c54180a02ba303ccb924dc7f352a871743762b3b9d22d114e8a35092238d39d2996e851c80ffff70ffffb08645bf4b31899847295762e390594caaea0464dd11579b997ad067177b4043ce20ba53ba40371cb40ebdf8eed67ad07eff80ff0180ffffb0b3dd2c23c2251a6203df5fd11984c935726363dcdc8a8ede102302fa6b4655ad59dfa403760c272fdb316b7bf23e7fd8ff01ff02808080 + +COIN SPENT: 0.000000000001 xch at address xch1nhnfkl9flan2y30h7cvth8awytlw3m8c3s9u940vkdn36q4ajw7s7v758m + +COIN CREATED: 3.000000000000 xch to xch17c2j72kc4y7up78cyhe235tz6mdyd6qltljgrlmkknursjn80zrqaqrzge +COIN CREATED: 2.000000000000 xch to xch1ny0ykhmxnetlkjd2gcetp6ctas9xsng2khk6cndy03a9qnr2v24s3xpffn + From ca26079373aac3599a55cf3ab6f419ef72f8947c Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 15 Aug 2023 17:44:40 -0700 Subject: [PATCH 12/40] Remove `UnsignedSpend.from_chunks` --- hsms/process/unsigned_spend.py | 4 ---- tests/test_lifecycle.py | 6 +++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hsms/process/unsigned_spend.py b/hsms/process/unsigned_spend.py index 7bb988b..f9071e2 100644 --- a/hsms/process/unsigned_spend.py +++ b/hsms/process/unsigned_spend.py @@ -67,10 +67,6 @@ def chunk(self, bytes_per_chunk: int) -> List[bytes]: bundle_bytes = zlib.compress(bytes(self), level=9) return create_chunks_for_blob(bundle_bytes, bytes_per_chunk) - @classmethod - def from_chunks(cls, chunks: List[bytes]) -> "UnsignedSpend": - return UnsignedSpend.from_bytes(zlib.decompress(assemble_chunks(chunks))) - def coin_spend_from_program(program: Program) -> CoinSpend: struct = transform_as_struct(program, as_atom, no_op, as_int, no_op) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 95468a1..67448e1 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1,3 +1,5 @@ +import zlib + from tests.generate import se_generate, bytes32_generate, uint256_generate from chia_base.core import Coin, CoinSpend, SpendBundle @@ -115,7 +117,9 @@ def test_lifecycle(): coin_spends, sum_hints, path_hints, AGG_SIG_ME_ADDITIONAL_DATA ) - assert unsigned_spend == UnsignedSpend.from_chunks(unsigned_spend.chunk(500)) + assert unsigned_spend == UnsignedSpend.from_bytes( + zlib.decompress(ChunkAssembler(unsigned_spend.chunk(500)).assemble()) + ) spend_chunks = create_chunks_for_blob(bytes(unsigned_spend), 250) assembler = ChunkAssembler() From 3dea622fe4c12f8ce452439bab77a168372c9ed9 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 17 Aug 2023 17:04:19 -0700 Subject: [PATCH 13/40] Handle hex or qrint encoding, zlib okay too --- hsms/cmds/hsm_dump_us.py | 19 ++++++++++++++++++- hsms/cmds/poser_gen.py | 4 +++- hsms/process/unsigned_spend.py | 7 ------- hsms/util/byte_chunks.py | 20 ++++++++++++++++++-- tests/test_lifecycle.py | 10 ++++++++-- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/hsms/cmds/hsm_dump_us.py b/hsms/cmds/hsm_dump_us.py index 5aa2789..ab75fe2 100644 --- a/hsms/cmds/hsm_dump_us.py +++ b/hsms/cmds/hsm_dump_us.py @@ -1,9 +1,11 @@ import argparse import sys +import zlib from hsms.clvm.disasm import disassemble from hsms.cmds.hsms import summarize_unsigned_spend from hsms.process.unsigned_spend import UnsignedSpend +from hsms.util.qrint_encoding import a2b_qrint def file_or_string(p) -> str: @@ -36,8 +38,23 @@ def dump_unsigned_spend(unsigned_spend): dump_coin_spend(coin_spend) +def fromhex_or_qrint(s: str) -> bytes: + try: + return a2b_qrint(s) + except ValueError: + pass + return bytes.fromhex(s) + + def hsms_dump_us(args, parser): - blob = bytes.fromhex(file_or_string(args.unsigned_spend)) + """ + Try to handle input in qrint or hex, with or without zlib compression + """ + blob = fromhex_or_qrint(file_or_string(args.unsigned_spend)) + try: + blob = zlib.decompress(blob) + except zlib.error: + pass unsigned_spend = UnsignedSpend.from_bytes(blob) summarize_unsigned_spend(unsigned_spend) diff --git a/hsms/cmds/poser_gen.py b/hsms/cmds/poser_gen.py index 129f1a6..f5f1523 100644 --- a/hsms/cmds/poser_gen.py +++ b/hsms/cmds/poser_gen.py @@ -12,6 +12,7 @@ puzzle_for_synthetic_public_key, solution_for_conditions, ) +from hsms.util.byte_chunks import chunks_for_zlib_blob from hsms.util.qrint_encoding import b2a_qrint @@ -82,7 +83,8 @@ def main(): print(f"challenge coin id: {coin.name().hex()}\n") - chunks = [b2a_qrint(_) for _ in unsigned_spend.chunk(args.chunk_size)] + blob = bytes(unsigned_spend) + chunks = [b2a_qrint(_) for _ in chunks_for_zlib_blob(blob, args.chunk_size)] if verbose: print(f"chunk count: {len(chunks)}\n") diff --git a/hsms/process/unsigned_spend.py b/hsms/process/unsigned_spend.py index f9071e2..7b0735a 100644 --- a/hsms/process/unsigned_spend.py +++ b/hsms/process/unsigned_spend.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import List -import zlib - from chia_base.atoms import bytes32 from chia_base.bls12_381 import BLSPublicKey, BLSSignature from chia_base.core import Coin, CoinSpend @@ -10,7 +8,6 @@ from clvm_rs import Program from hsms.process.signing_hints import SumHint, PathHint -from hsms.util.byte_chunks import assemble_chunks, create_chunks_for_blob from hsms.util.clvm_serialization import ( as_atom, as_int, @@ -63,10 +60,6 @@ def __bytes__(self): def from_bytes(cls, blob) -> "UnsignedSpend": return cls.from_program(Program.from_bytes(blob)) - def chunk(self, bytes_per_chunk: int) -> List[bytes]: - bundle_bytes = zlib.compress(bytes(self), level=9) - return create_chunks_for_blob(bundle_bytes, bytes_per_chunk) - def coin_spend_from_program(program: Program) -> CoinSpend: struct = transform_as_struct(program, as_atom, no_op, as_int, no_op) diff --git a/hsms/util/byte_chunks.py b/hsms/util/byte_chunks.py index 79cd687..07f6d35 100644 --- a/hsms/util/byte_chunks.py +++ b/hsms/util/byte_chunks.py @@ -1,4 +1,5 @@ import math +import zlib from typing import List, Tuple @@ -30,6 +31,10 @@ def create_chunks_for_blob(blob: bytes, bytes_per_chunk: int) -> List[bytes]: return bundle_chunks +def chunks_for_zlib_blob(blob: bytes, bytes_per_chunk: int) -> List[bytes]: + return create_chunks_for_blob(zlib.compress(blob, level=9), bytes_per_chunk) + + class ChunkAssembler: chunks: List[bytes] @@ -60,11 +65,22 @@ def status(self) -> Tuple[int, int]: else: return len(self.chunks), self.chunks[0][-1] + 1 - def assemble(self) -> bytes: + def __bytes__(self) -> bytes: sorted_chunks = sorted(self.chunks, key=lambda c: c[-2]) bare_chunks = [c[:-2] for c in sorted_chunks] return b"".join(bare_chunks) + def assemble(self) -> bytes: + return bytes(self) + -def assemble_chunks(chunks: List[bytes]) -> bytes: + +def blob_for_chunks(chunks: List[bytes]) -> bytes: return ChunkAssembler(chunks).assemble() + + +def blob_for_zlib_chunks(chunks: List[bytes]) -> bytes: + return zlib.decompress(blob_for_chunks(chunks)) + + +assemble_chunks = blob_for_chunks diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 67448e1..45096a4 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -15,7 +15,11 @@ from hsms.process.signing_hints import SumHint, PathHint from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.conlang import CREATE_COIN -from hsms.util.byte_chunks import ChunkAssembler, create_chunks_for_blob +from hsms.util.byte_chunks import ( + ChunkAssembler, + chunks_for_zlib_blob, + create_chunks_for_blob, +) AGG_SIG_ME_ADDITIONAL_DATA = bytes.fromhex( @@ -117,8 +121,10 @@ def test_lifecycle(): coin_spends, sum_hints, path_hints, AGG_SIG_ME_ADDITIONAL_DATA ) + chunks = chunks_for_zlib_blob(bytes(unsigned_spend), 500) + assert unsigned_spend == UnsignedSpend.from_bytes( - zlib.decompress(ChunkAssembler(unsigned_spend.chunk(500)).assemble()) + zlib.decompress(ChunkAssembler(chunks).assemble()) ) spend_chunks = create_chunks_for_blob(bytes(unsigned_spend), 250) From 8b7b8d91942c86e8a7d6e602360905f6bd95f506 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 2 Oct 2023 14:46:13 -0700 Subject: [PATCH 14/40] standard dataclasses --- hsms/util/clvm_serde.py | 203 +++++++++++++++++++++++++++++++++++++++ hsms/util/type_tree.py | 44 +++++++++ tests/test_clvm_serde.py | 66 +++++++++++++ 3 files changed, 313 insertions(+) create mode 100644 hsms/util/clvm_serde.py create mode 100644 hsms/util/type_tree.py create mode 100644 tests/test_clvm_serde.py diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py new file mode 100644 index 0000000..1a3327d --- /dev/null +++ b/hsms/util/clvm_serde.py @@ -0,0 +1,203 @@ +from dataclasses import is_dataclass +from types import UnionType +from typing import Any, BinaryIO, Callable, Union, TypeVar, get_type_hints + +from chia_base.atoms import uint8, uint32 + +from clvm_rs import Program + +from .type_tree import type_tree + + +# some helper methods to implement chia serialization +# +def read_bytes(p: Program) -> bytes: + if p.atom is None: + raise EncodingError("expected atom for str") + return p.atom + + +def read_str(p: Program) -> str: + return read_bytes(p).decode() + + +def read_int(p: Program) -> int: + return Program.int_from_bytes(read_bytes(p)) + + +def serialize_for_list(origin, args, *etc): + write_item = type_tree(args[0], *etc) + + def serialize_list(items): + return Program.to([write_item(_) for _ in items]) + + return serialize_list + + +def serialize_for_tuple(origin, args, *etc): + write_items = [type_tree(_, *etc) for _ in args] + + def serialize_tuple(items): + return Program.to([write_f(item) for write_f, item in zip(write_items, items)]) + + return serialize_tuple + + +def ser_for_union(origin, args, *etc): + item_type = optional_from_union(args) + if item_type is not None: + write_item = type_tree(item_type, *etc) + + def serialize_optional(f, item): + c = 0 if item is None else 1 + f.write(bytes([c])) + if item is not None: + write_item(f, item) + + return serialize_optional + + +SERIALIZER_COMPOUND_TYPE_LOOKUP = { + list: serialize_for_list, + tuple: serialize_for_tuple, + # Union: to_program_for_union, + # UnionType: to_program_for_union, +} + + +def make_ser_for_dcf(f_name: str, f_type: type, *args): + ser_for_field = type_tree(f_type, *args) + + def f(obj): + return ser_for_field(getattr(obj, f_name)) + + return f + + +def fail_ser(t, *args): + if t in [str, bytes, int]: + return Program.to + + if is_dataclass(t): + # TODO: handle key-based data class + # handle regular dataclass + fields = get_type_hints(t) + + if hasattr(t, "_use_keys"): + pass + + streaming_calls = [] + for f_name, f_type in fields.items(): + if f_name.startswith("_"): + continue + streaming_calls.append(make_ser_for_dcf(f_name, f_type, *args)) + + def serialize_dataclass(item): + return Program.to([sc(item) for sc in streaming_calls]) + + return serialize_dataclass + + raise TypeError(f"can't process {t}") + + +def fail_deser(t, *args): + if is_dataclass(t): + # TODO: handle key-based data class + # handle regular dataclass + fields = get_type_hints(t) + + tuple_type = tuple[ + *list( + f_type + for f_name, f_type in fields.items() + if not f_name.startswith("_") + ) + ] + des_f = type_tree(tuple_type, *args) + + def des_dataclass(p: Program): + values = des_f(p) + return t(*values) + + return des_dataclass + + raise TypeError(f"can't process {t}") + + +def to_program_for_type(t: type) -> Callable[[dict[str, Any]], Program]: + return type_tree( + t, + {}, + SERIALIZER_COMPOUND_TYPE_LOOKUP, + fail_ser, + ) + + +def deser_for_list(origin, args, *etc): + read_item = type_tree(args[0], *etc) + + def deserialize_list(p: Program) -> tuple[int, Any]: + return [read_item(_) for _ in p.as_iter()] + + return deserialize_list + + +def deser_for_tuple(origin, args, *etc): + read_items = [type_tree(_, *etc) for _ in args] + + def deserialize_tuple(p: Program) -> tuple[int, Any]: + items = list(p.as_iter()) + if len(items) != len(read_items): + raise EncodingError("wrong size program") + return tuple(f(_) for f, _ in zip(read_items, items)) + + return deserialize_tuple + + +def deser_for_union(origin, args, *etc): + item_type = optional_from_union(args) + if item_type is not None: + read_item = type_tree(item_type, *etc) + + def deserialize_optional(f: BinaryIO) -> tuple[int, Any]: + v = uint8.parse(f) + if v == 0: + return None + return read_item(f) + + return deserialize_optional + raise TypeError("can't handle unions not of the form `A | None`") + + +DESERIALIZER_COMPOUND_TYPE_LOOKUP = { + list: deser_for_list, + tuple: deser_for_tuple, + # Union: deser_for_union, + # UnionType: deser_for_union, +} + + +def optional_from_union(args: type) -> Callable[[dict[str, Any]], bytes] | None: + tn = type(None) + if len(args) == 2 and tn in args: + return args[0 if args[1] is tn else 1] + return None + + +def from_program_for_type(t: type) -> Callable[[memoryview, int], tuple[int, Any]]: + return type_tree( + t, + {bytes: read_bytes, str: read_str, int: read_int}, + DESERIALIZER_COMPOUND_TYPE_LOOKUP, + fail_deser, + ) + + +def merging_function_for_callable_parameters(f: Callable) -> Callable: + parameter_names = [k for k in f.__annotations__.keys() if k != "return"] + + def merging_function(*args, **kwargs) -> tuple[Any]: + merged_args = args + tuple(kwargs[_] for _ in parameter_names[len(args):]) + return merged_args + + return merging_function diff --git a/hsms/util/type_tree.py b/hsms/util/type_tree.py new file mode 100644 index 0000000..d42f187 --- /dev/null +++ b/hsms/util/type_tree.py @@ -0,0 +1,44 @@ +from typing import Callable, get_origin, get_args, TypeVar + + +_T = TypeVar("_T") +SimpleTypeLookup = dict[type, _T] +CompoundLookup = dict[ + type, + Callable[ + [type, type, SimpleTypeLookup[_T], "CompoundLookup[_T]", "OtherHandler[_T]"], _T + ], +] +OtherHandler = Callable[ + [type, SimpleTypeLookup[_T], CompoundLookup[_T], "OtherHandler[_T]"], _T +] + + +def type_tree( + t: type, + simple_type_lookup: SimpleTypeLookup[_T], + compound_type_lookup: CompoundLookup[ + Callable[ + [type, type, SimpleTypeLookup[_T], CompoundLookup[_T], OtherHandler[_T]], _T + ] + ], + other_f: OtherHandler[_T], +) -> _T: + """ + Recursively descend a "type tree" invoking the appropriate functions. + + `simple_type_lookup`: a type to callable look-up. Must return a `_T` value. + `compound_type_lookup`: recursively handle compound types like `list` and `tuple`. + `other_f`: a function to take a type and return + + This function is helpful for run-time building a complex function that operates + on a complex type out of simpler functions that operate on base types. + """ + origin = get_origin(t) + f = compound_type_lookup.get(origin) + if f: + return f(origin, get_args(t), simple_type_lookup, compound_type_lookup, other_f) + f = simple_type_lookup.get(t) + if f: + return f + return other_f(t, simple_type_lookup, compound_type_lookup, other_f) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py new file mode 100644 index 0000000..0f19712 --- /dev/null +++ b/tests/test_clvm_serde.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass + +from clvm_rs import Program + +from hsms.util.clvm_serde import to_program_for_type, from_program_for_type + + +def test_ser(): + tpb = to_program_for_type(bytes) + fpb = from_program_for_type(bytes) + tps = to_program_for_type(str) + fps = from_program_for_type(str) + for s in ["", "foo", "1" * 1000]: + p = tps(s) + assert p == Program.to(s) + assert fps(p) == s + + b = s.encode() + p = tpb(b) + assert p == Program.to(b) + assert fpb(p) == b + + tt = tuple[str, bytes, int] + tp = to_program_for_type(tt) + fp = from_program_for_type(tt) + for t in [ + ("foo", b"bar", 100), + ("this is a test", bytes([5, 6, 7]), -94817), + ]: + assert tp(t) == Program.to(list(t)) + + tt = list[tuple[int, str]] + tp = to_program_for_type(tt) + fp = from_program_for_type(tt) + for t in [ + [(100, "hundred"), (200, "two hundred"), (30, "thirty")], + ]: + rhs = list(list(_) for _ in t) + prhs = Program.to(rhs) + assert tp(t) == Program.to(rhs) + assert fp(tp(t)) == t + + @dataclass + class Foo: + a: int + b: str + + tp = to_program_for_type(Foo) + foo = Foo(100, "boss") + assert tp(foo) == Program.to([100, "boss"]) + fp = from_program_for_type(Foo) + assert foo == fp(tp(foo)) + + @dataclass + class Nested: + a: list[Foo] + b: int + + tp = to_program_for_type(Nested) + nested = Nested([foo, Foo(200, "worker")], 5000) + rhs = Program.to([[[100, "boss"], [200, "worker"]], 5000]) + assert tp(nested) == rhs + fp = from_program_for_type(Nested) + assert nested == fp(tp(nested)) + + Foo._use_keys = 1 From 932db423f8fd9d191390aafeda1c1f61eef605e3 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Wed, 4 Oct 2023 17:43:00 -0700 Subject: [PATCH 15/40] First crack at interop tests --- hsms/util/clvm_serde.py | 166 ++++++++++++++++++++++++++-------- tests/test_clvm_serde.py | 191 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 315 insertions(+), 42 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 1a3327d..660b096 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -1,14 +1,18 @@ -from dataclasses import is_dataclass -from types import UnionType -from typing import Any, BinaryIO, Callable, Union, TypeVar, get_type_hints +from dataclasses import is_dataclass, fields +from typing import Any, BinaryIO, Callable, get_type_hints -from chia_base.atoms import uint8, uint32 +from chia_base.atoms import bytes32, uint8 from clvm_rs import Program from .type_tree import type_tree +class PairTuple(tuple): + def __new__(cls, a, b): + return super().__new__(cls, (a, b)) + + # some helper methods to implement chia serialization # def read_bytes(p: Program) -> bytes: @@ -38,7 +42,25 @@ def serialize_for_tuple(origin, args, *etc): write_items = [type_tree(_, *etc) for _ in args] def serialize_tuple(items): - return Program.to([write_f(item) for write_f, item in zip(write_items, items)]) + return Program.to( + [ + write_f(item) + for write_f, item in zip( + write_items, + items, + ) + ] + ) + + return serialize_tuple + + +def serialize_for_pair_tuple(origin, args, *etc): + write_items = [type_tree(_, *etc) for _ in args] + + def serialize_tuple(items): + as_tuple = tuple(wi(_) for wi, _ in zip(write_items, items)) + return Program.to(as_tuple) return serialize_tuple @@ -60,6 +82,7 @@ def serialize_optional(f, item): SERIALIZER_COMPOUND_TYPE_LOOKUP = { list: serialize_for_list, tuple: serialize_for_tuple, + PairTuple: serialize_for_pair_tuple, # Union: to_program_for_union, # UnionType: to_program_for_union, } @@ -74,52 +97,103 @@ def f(obj): return f +def ser_dataclass(t: type, *etc): + # handle regular dataclass + streaming_calls = [] + for field in fields(t): + streaming_calls.append(make_ser_for_dcf(field.name, field.type, *etc)) + + def serialize_dataclass(item): + return Program.to([sc(item) for sc in streaming_calls]) + + return serialize_dataclass + + +def ser_dataclass_with_keys(t: type, *etc): + # handle key-based data class + field_ser_key_tuples = [ + ( + f, + type_tree(f.type, *etc), + f.metadata.get("key", f.name), + ) + for f in fields(t) + ] + + def s_dataclass_with_keys(obj: t) -> Program: + pairs = [] + for field, ser, key in field_ser_key_tuples: + v = getattr(obj, field.name) + if v == field.default: + continue + pt = PairTuple(key, ser(v)) + pairs.append(pt) + return Program.to(pairs) + + return s_dataclass_with_keys + + def fail_ser(t, *args): if t in [str, bytes, int]: return Program.to - if is_dataclass(t): - # TODO: handle key-based data class - # handle regular dataclass - fields = get_type_hints(t) + if hasattr(t, "__bytes__"): + return lambda x: Program.to(bytes(x)) + if is_dataclass(t): if hasattr(t, "_use_keys"): - pass + return ser_dataclass_with_keys(t, *args) + else: + return ser_dataclass(t, *args) - streaming_calls = [] - for f_name, f_type in fields.items(): - if f_name.startswith("_"): - continue - streaming_calls.append(make_ser_for_dcf(f_name, f_type, *args)) + raise TypeError(f"can't process {t}") - def serialize_dataclass(item): - return Program.to([sc(item) for sc in streaming_calls]) - return serialize_dataclass +def deser_dataclass_with_keys(t: type, *args): + # handle key-based data class - raise TypeError(f"can't process {t}") + field_des_key_tuples = [ + ( + f, + type_tree(f.type, *args), + f.metadata.get("key", f.name), + ) + for f in fields(t) + ] + def des_dataclass_with_keys(p: Program): + d = dict((k.atom.decode(), v) for k, v in (_.pair for _ in p.as_iter())) + kwargs = {} + for field, des, key in field_des_key_tuples: + if key in d: + kwargs[field.name] = des(d[key]) + return t(**kwargs) -def fail_deser(t, *args): - if is_dataclass(t): - # TODO: handle key-based data class - # handle regular dataclass - fields = get_type_hints(t) + return des_dataclass_with_keys + + +def deser_dataclass(t: type, *args): + # handle regular dataclass + + tuple_type = tuple[*list(field.type for field in fields(t))] + des_f = type_tree(tuple_type, *args) + + def des_dataclass(p: Program): + values = des_f(p) + return t(*values) + + return des_dataclass - tuple_type = tuple[ - *list( - f_type - for f_name, f_type in fields.items() - if not f_name.startswith("_") - ) - ] - des_f = type_tree(tuple_type, *args) - def des_dataclass(p: Program): - values = des_f(p) - return t(*values) +def fail_deser(t, *args): + if is_dataclass(t): + if hasattr(t, "_use_keys"): + return deser_dataclass_with_keys(t, *args) + else: + return deser_dataclass(t, *args) - return des_dataclass + if hasattr(t, "from_bytes"): + return lambda p: t.from_bytes(p.atom) raise TypeError(f"can't process {t}") @@ -127,7 +201,7 @@ def des_dataclass(p: Program): def to_program_for_type(t: type) -> Callable[[dict[str, Any]], Program]: return type_tree( t, - {}, + {Program: lambda x: x}, SERIALIZER_COMPOUND_TYPE_LOOKUP, fail_ser, ) @@ -154,6 +228,15 @@ def deserialize_tuple(p: Program) -> tuple[int, Any]: return deserialize_tuple +def deser_for_pair_tuple(origin, args, *etc): + read_items = [type_tree(_, *etc) for _ in args] + + def deserialize_tuple(p: Program) -> tuple[int, Any]: + return PairTuple(*[f(_) for f, _ in zip(read_items, p.pair)]) + + return deserialize_tuple + + def deser_for_union(origin, args, *etc): item_type = optional_from_union(args) if item_type is not None: @@ -172,6 +255,7 @@ def deserialize_optional(f: BinaryIO) -> tuple[int, Any]: DESERIALIZER_COMPOUND_TYPE_LOOKUP = { list: deser_for_list, tuple: deser_for_tuple, + PairTuple: deser_for_pair_tuple, # Union: deser_for_union, # UnionType: deser_for_union, } @@ -187,7 +271,13 @@ def optional_from_union(args: type) -> Callable[[dict[str, Any]], bytes] | None: def from_program_for_type(t: type) -> Callable[[memoryview, int], tuple[int, Any]]: return type_tree( t, - {bytes: read_bytes, str: read_str, int: read_int}, + { + bytes: read_bytes, + bytes32: read_bytes, + str: read_str, + int: read_int, + Program: lambda x: x, + }, DESERIALIZER_COMPOUND_TYPE_LOOKUP, fail_deser, ) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 0f19712..96b7c74 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -1,8 +1,21 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field + +import random + +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.core import Coin, CoinSpend from clvm_rs import Program -from hsms.util.clvm_serde import to_program_for_type, from_program_for_type +from hsms.process.signing_hints import ( + SumHint as LegacySH, + PathHint as LegacyPH, +) +from hsms.util.clvm_serde import ( + to_program_for_type, + from_program_for_type, + PairTuple, +) def test_ser(): @@ -37,13 +50,26 @@ def test_ser(): ]: rhs = list(list(_) for _ in t) prhs = Program.to(rhs) - assert tp(t) == Program.to(rhs) + assert tp(t) == prhs + assert fp(tp(t)) == t + + tt = PairTuple[int, str] + tp = to_program_for_type(tt) + fp = from_program_for_type(tt) + for v in [ + (100, "hundred"), + (200, "two hundred"), + (30, "thirty"), + ]: + t = PairTuple(*v) + rhs = Program.to(t) + assert tp(t) == rhs assert fp(tp(t)) == t @dataclass class Foo: a: int - b: str + b: str = field(default="foo", metadata=dict(key="bob")) tp = to_program_for_type(Foo) foo = Foo(100, "boss") @@ -64,3 +90,160 @@ class Nested: assert nested == fp(tp(nested)) Foo._use_keys = 1 + tp = to_program_for_type(Foo) + fp = from_program_for_type(Foo) + p = Program.to([("a", 1000), ("bob", "hello")]) + foo = fp(p) + assert foo.a == 1000 + assert foo.b == "hello" + p1 = tp(foo) + assert p1 == p + + for foo in [Foo(20, "foo"), Foo(999, "bar"), Foo(-294, "baz")]: + p = tp(foo) + f1 = fp(p) + assert f1 == foo + + +def rnd_coin_spend(seed: int) -> CoinSpend: + r = random.Random(seed) + parent = r.randbytes(32) + puzzle = Program.to(f"puz: {r.randbytes(5).hex()}") + amount = r.randint(1, 1000) * int(1e3) + solution = Program.to(f"sol: {r.randbytes(10).hex()}") + coin = Coin(parent, puzzle.tree_hash(), amount) + return CoinSpend(coin, puzzle, solution) + + +SumHint = PairTuple[list[BLSPublicKey], BLSSecretExponent] + + +def test_interop_sum_hint(): + pks = [BLSSecretExponent.from_int(_).public_key() for _ in range(5)] + synthetic_offset = BLSSecretExponent.from_int(3**70 & ((1 << 256) - 1)) + sum_hint = SumHint(pks, synthetic_offset) + tp = to_program_for_type(SumHint) + fp = from_program_for_type(SumHint) + p = tp(sum_hint) + print(bytes(p).hex()) + lsh = LegacySH.from_program(p) + print(lsh) + assert lsh.public_keys == sum_hint[0] + assert lsh.synthetic_offset == sum_hint[1] + sh1 = fp(p) + assert sum_hint == sh1 + + +CoinSpendTuple = tuple[bytes, Program, int, Program] + + +PathHint = PairTuple[BLSPublicKey, list[int]] + + +def test_interop_path_hint(): + public_key = BLSSecretExponent.from_int(1).public_key() + ints = [1, 5, 91, 29484, -99] + path_hint = PathHint(public_key, ints) + tp = to_program_for_type(PathHint) + fp = from_program_for_type(PathHint) + p = tp(path_hint) + print(bytes(p).hex()) + lph = LegacyPH.from_program(p) + print(lph) + assert lph.root_public_key == path_hint[0] + assert lph.path == path_hint[1] + ph1 = fp(p) + assert path_hint == ph1 + + +def test_interop_coin_spend(): + for _ in range(10): + cs = rnd_coin_spend(_) + cst = coin_spend_to_tuple(cs) + tp = to_program_for_type(CoinSpendTuple) + fp = from_program_for_type(CoinSpendTuple) + p = tp(cst) + print(bytes(p).hex()) + cst1 = fp(p) + assert cst == cst1 + + +@dataclass +class UnsignedSpend: + coin_spend_tuples: list[CoinSpendTuple] = field( + default_factory=list, metadata=dict(key="c") + ) + sum_hints: list[SumHint] = field( + default_factory=list, + metadata=dict(key="s"), + ) + path_hints: list[PathHint] = field( + default_factory=list, + metadata=dict(key="p"), + ) + agg_sig_me_network_suffix: bytes = field( + default=b"", + metadata=dict(key="a"), + ) + + def coin_spends(self): + return [coin_spend_from_tuple(_) for _ in self.coin_spend_tuples] + + +UnsignedSpend._use_keys = True + + +def coin_spend_from_tuple(cst: CoinSpendTuple) -> CoinSpend: + coin = Coin(cst[0], cst[1].tree_hash(), cst[2]) + return CoinSpend(coin, cst[1], cst[3]) + + +def coin_spend_to_tuple(coin_spend: CoinSpend) -> CoinSpendTuple: + coin = coin_spend.coin + return ( + coin.parent_coin_info, + coin_spend.puzzle_reveal, + coin.amount, + coin_spend.solution, + ) + + +def sum_hint_as_tuple(sum_hint: SumHint) -> tuple: + return tuple(getattr(sum_hint, _) for _ in "public_keys synthetic_offset".split()) + + +def path_hint_as_tuple(path_hint: PathHint) -> tuple: + return tuple(getattr(path_hint, _) for _ in "root_public_key path".split()) + + +def test_interop_unsigned_spend(): + from hsms.process.unsigned_spend import UnsignedSpend as LegacyUS + + cs_list = [rnd_coin_spend(_) for _ in range(10)] + cst_list = [coin_spend_to_tuple(_) for _ in cs_list] + + public_keys = [BLSSecretExponent.from_int(_).public_key() for _ in range(5)] + synthetic_offset = BLSSecretExponent.from_int(3**70 & ((1 << 256) - 1)) + sum_hint = SumHint(public_keys, synthetic_offset) + lsh = LegacySH(public_keys, synthetic_offset) + pubkey = BLSSecretExponent.from_int(9).public_key() + ints = [2, 5, 17] + path_hint = PathHint(pubkey, ints) + lph = LegacyPH(pubkey, ints) + + suffix = b"a" * 32 + us = UnsignedSpend(cst_list[0:3], [sum_hint], [path_hint], suffix) + + lus = LegacyUS(cs_list[0:3], [lsh], [lph], suffix) + print(bytes(lus.as_program()).hex()) + tp = to_program_for_type(UnsignedSpend) + fp = from_program_for_type(UnsignedSpend) + p = tp(us) + print(bytes(p).hex()) + lus = LegacyUS.from_program(p) + assert lus.coin_spends == us.coin_spends() + assert [sum_hint_as_tuple(_) for _ in lus.sum_hints] == us.sum_hints + assert [path_hint_as_tuple(_) for _ in lus.path_hints] == us.path_hints + assert lus.agg_sig_me_network_suffix == us.agg_sig_me_network_suffix + us1 = fp(p) + assert us == us1 From 11350e118008f99477b12771153ae4005053420a Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 5 Oct 2023 14:46:16 -0700 Subject: [PATCH 16/40] Pay attention to `Field.default_factory` --- hsms/util/clvm_serde.py | 45 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 660b096..26992ec 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -1,4 +1,4 @@ -from dataclasses import is_dataclass, fields +from dataclasses import is_dataclass, fields, MISSING from typing import Any, BinaryIO, Callable, get_type_hints from chia_base.atoms import bytes32, uint8 @@ -109,22 +109,32 @@ def serialize_dataclass(item): return serialize_dataclass -def ser_dataclass_with_keys(t: type, *etc): - # handle key-based data class - field_ser_key_tuples = [ - ( - f, +def field_tuples_for_type(t: type, *etc): + field_tuples = [] + for f in fields(t): + default_value = ( + f.default if f.default_factory is MISSING else f.default_factory() + ) + t = ( + default_value, + f.name, type_tree(f.type, *etc), f.metadata.get("key", f.name), ) - for f in fields(t) - ] + field_tuples.append(t) + return field_tuples + + +def ser_dataclass_with_keys(t: type, *etc): + # handle key-based data class + + field_tuples = field_tuples_for_type(t, *etc) def s_dataclass_with_keys(obj: t) -> Program: pairs = [] - for field, ser, key in field_ser_key_tuples: - v = getattr(obj, field.name) - if v == field.default: + for default_value, name, ser, key in field_tuples: + v = getattr(obj, name) + if v == default_value: continue pt = PairTuple(key, ser(v)) pairs.append(pt) @@ -152,21 +162,14 @@ def fail_ser(t, *args): def deser_dataclass_with_keys(t: type, *args): # handle key-based data class - field_des_key_tuples = [ - ( - f, - type_tree(f.type, *args), - f.metadata.get("key", f.name), - ) - for f in fields(t) - ] + field_tuples = field_tuples_for_type(t, *args) def des_dataclass_with_keys(p: Program): d = dict((k.atom.decode(), v) for k, v in (_.pair for _ in p.as_iter())) kwargs = {} - for field, des, key in field_des_key_tuples: + for default_value, name, des, key in field_tuples: if key in d: - kwargs[field.name] = des(d[key]) + kwargs[name] = des(d[key]) return t(**kwargs) return des_dataclass_with_keys From e9e6c5a877d964f1c2fe4643610c10756bf0bc39 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 6 Oct 2023 23:56:46 -0700 Subject: [PATCH 17/40] Allow hybrid expandable `dataclass` objects --- hsms/util/clvm_serde.py | 98 +++++++++++++++------------------------- tests/test_clvm_serde.py | 28 +++++++++--- 2 files changed, 58 insertions(+), 68 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 26992ec..d17fa98 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -88,59 +88,51 @@ def serialize_optional(f, item): } -def make_ser_for_dcf(f_name: str, f_type: type, *args): - ser_for_field = type_tree(f_type, *args) - +def make_ser_for_dcf(f_name: str, ser_for_field: Callable): def f(obj): return ser_for_field(getattr(obj, f_name)) return f -def ser_dataclass(t: type, *etc): - # handle regular dataclass - streaming_calls = [] - for field in fields(t): - streaming_calls.append(make_ser_for_dcf(field.name, field.type, *etc)) - - def serialize_dataclass(item): - return Program.to([sc(item) for sc in streaming_calls]) - - return serialize_dataclass - +def field_info_for_type(t: type, *etc): + # split into key-based and location-based -def field_tuples_for_type(t: type, *etc): - field_tuples = [] + key_based = [] + location_based = [] for f in fields(t): default_value = ( f.default if f.default_factory is MISSING else f.default_factory() ) - t = ( - default_value, - f.name, - type_tree(f.type, *etc), - f.metadata.get("key", f.name), - ) - field_tuples.append(t) - return field_tuples + key = f.metadata.get("key") + call = type_tree(f.type, *etc) + if key is None: + location_based.append((f.name, call)) + else: + key_based.append((f.name, call, key, default_value)) + return location_based, key_based -def ser_dataclass_with_keys(t: type, *etc): - # handle key-based data class - field_tuples = field_tuples_for_type(t, *etc) +def ser_dataclass(t: type, *etc): + location_based, key_based = field_info_for_type(t, *etc) + + streaming_calls = [] + for name, ser in location_based: + streaming_calls.append(make_ser_for_dcf(name, ser)) - def s_dataclass_with_keys(obj: t) -> Program: - pairs = [] - for default_value, name, ser, key in field_tuples: - v = getattr(obj, name) + def ser(item): + pairs = [sc(item) for sc in streaming_calls] + + for name, ser, key, default_value in key_based: + v = getattr(item, name) if v == default_value: continue pt = PairTuple(key, ser(v)) pairs.append(pt) return Program.to(pairs) - return s_dataclass_with_keys + return ser def fail_ser(t, *args): @@ -151,49 +143,33 @@ def fail_ser(t, *args): return lambda x: Program.to(bytes(x)) if is_dataclass(t): - if hasattr(t, "_use_keys"): - return ser_dataclass_with_keys(t, *args) - else: - return ser_dataclass(t, *args) + return ser_dataclass(t, *args) raise TypeError(f"can't process {t}") -def deser_dataclass_with_keys(t: type, *args): - # handle key-based data class +def deser_dataclass(t: type, *args): + location_based, key_based = field_info_for_type(t, *args) - field_tuples = field_tuples_for_type(t, *args) + def des(p: Program): + args = [] + for name, des in location_based: + args.append(des(p.pair[0])) + p = p.pair[1] - def des_dataclass_with_keys(p: Program): - d = dict((k.atom.decode(), v) for k, v in (_.pair for _ in p.as_iter())) kwargs = {} - for default_value, name, des, key in field_tuples: + for name, des, key, default_value in key_based: + d = dict((k.atom.decode(), v) for k, v in (_.pair for _ in p.as_iter())) if key in d: kwargs[name] = des(d[key]) - return t(**kwargs) - - return des_dataclass_with_keys - + return t(*args, **kwargs) -def deser_dataclass(t: type, *args): - # handle regular dataclass - - tuple_type = tuple[*list(field.type for field in fields(t))] - des_f = type_tree(tuple_type, *args) - - def des_dataclass(p: Program): - values = des_f(p) - return t(*values) - - return des_dataclass + return des def fail_deser(t, *args): if is_dataclass(t): - if hasattr(t, "_use_keys"): - return deser_dataclass_with_keys(t, *args) - else: - return deser_dataclass(t, *args) + return deser_dataclass(t, *args) if hasattr(t, "from_bytes"): return lambda p: t.from_bytes(p.atom) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 96b7c74..ab6d1c9 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -69,13 +69,14 @@ def test_ser(): @dataclass class Foo: a: int - b: str = field(default="foo", metadata=dict(key="bob")) + b: str tp = to_program_for_type(Foo) foo = Foo(100, "boss") - assert tp(foo) == Program.to([100, "boss"]) + rhs = Program.to([100, "boss"]) + assert tp(foo) == rhs fp = from_program_for_type(Foo) - assert foo == fp(tp(foo)) + assert foo == fp(rhs) @dataclass class Nested: @@ -89,7 +90,23 @@ class Nested: fp = from_program_for_type(Nested) assert nested == fp(tp(nested)) - Foo._use_keys = 1 + @dataclass + class Foo: + a: int + b: str = field(default="foo", metadata=dict(key="bob")) + + tp = to_program_for_type(Foo) + foo = Foo(100, "boss") + rhs = Program.to([100, ("bob", "boss")]) + assert tp(foo) == rhs + fp = from_program_for_type(Foo) + assert foo == fp(rhs) + + @dataclass + class Foo: + a: int = field(metadata=dict(key="a")) + b: str = field(default="foo", metadata=dict(key="bob")) + tp = to_program_for_type(Foo) fp = from_program_for_type(Foo) p = Program.to([("a", 1000), ("bob", "hello")]) @@ -190,9 +207,6 @@ def coin_spends(self): return [coin_spend_from_tuple(_) for _ in self.coin_spend_tuples] -UnsignedSpend._use_keys = True - - def coin_spend_from_tuple(cst: CoinSpendTuple) -> CoinSpend: coin = Coin(cst[0], cst[1].tree_hash(), cst[2]) return CoinSpend(coin, cst[1], cst[3]) From d6dd3895d490b1d906098cbbc34a93e7a6e2dc09 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 10 Oct 2023 15:02:50 -0700 Subject: [PATCH 18/40] refactor --- tests/core/__init__.py | 0 tests/core/unsigned_spend.py | 61 +++++++++++++++++++++++++ tests/legacy/unsigned_spend.py | 83 ++++++++++++++++++++++++++++++++++ tests/test_clvm_serde.py | 55 ++++------------------ 4 files changed, 153 insertions(+), 46 deletions(-) create mode 100644 tests/core/__init__.py create mode 100644 tests/core/unsigned_spend.py create mode 100644 tests/legacy/unsigned_spend.py diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/unsigned_spend.py b/tests/core/unsigned_spend.py new file mode 100644 index 0000000..acdc765 --- /dev/null +++ b/tests/core/unsigned_spend.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass, field + +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.core import Coin, CoinSpend + +from clvm_rs import Program + +from hsms.util.clvm_serde import PairTuple + + +SumHint = PairTuple[list[BLSPublicKey], BLSSecretExponent] + +CoinSpendTuple = tuple[bytes, Program, int, Program] + +PathHint = PairTuple[BLSPublicKey, list[int]] + + +@dataclass +class UnsignedSpend: + coin_spend_tuples: list[CoinSpendTuple] = field( + default_factory=list, metadata=dict(key="c") + ) + sum_hints: list[SumHint] = field( + default_factory=list, + metadata=dict(key="s"), + ) + path_hints: list[PathHint] = field( + default_factory=list, + metadata=dict(key="p"), + ) + agg_sig_me_network_suffix: bytes = field( + default=b"", + metadata=dict(key="a"), + ) + + def coin_spends(self): + return [coin_spend_from_tuple(_) for _ in self.coin_spend_tuples] + + +def coin_spend_from_tuple(cst: CoinSpendTuple) -> CoinSpend: + coin = Coin(cst[0], cst[1].tree_hash(), cst[2]) + return CoinSpend(coin, cst[1], cst[3]) + + +def coin_spend_to_tuple(coin_spend: CoinSpend) -> CoinSpendTuple: + coin = coin_spend.coin + return ( + coin.parent_coin_info, + coin_spend.puzzle_reveal, + coin.amount, + coin_spend.solution, + ) + + +def sum_hint_as_tuple(sum_hint: SumHint) -> tuple: + keys = "public_keys synthetic_offset".split() + return tuple(getattr(sum_hint, _) for _ in keys) + + +def path_hint_as_tuple(path_hint: PathHint) -> tuple: + return tuple(getattr(path_hint, _) for _ in "root_public_key path".split()) diff --git a/tests/legacy/unsigned_spend.py b/tests/legacy/unsigned_spend.py new file mode 100644 index 0000000..7b0735a --- /dev/null +++ b/tests/legacy/unsigned_spend.py @@ -0,0 +1,83 @@ +from dataclasses import dataclass +from typing import List + +from chia_base.atoms import bytes32 +from chia_base.bls12_381 import BLSPublicKey, BLSSignature +from chia_base.core import Coin, CoinSpend + +from clvm_rs import Program + +from hsms.process.signing_hints import SumHint, PathHint +from hsms.util.clvm_serialization import ( + as_atom, + as_int, + clvm_to_list, + no_op, + transform_dict, + transform_dict_by_key, + transform_as_struct, +) + + +@dataclass +class SignatureInfo: + signature: BLSSignature + partial_public_key: BLSPublicKey + final_public_key: BLSPublicKey + message: bytes + + +@dataclass +class UnsignedSpend: + coin_spends: List[CoinSpend] + sum_hints: List[SumHint] + path_hints: List[PathHint] + agg_sig_me_network_suffix: bytes32 + + def as_program(self): + as_clvm = [("a", self.agg_sig_me_network_suffix)] + cs_as_clvm = [ + [_.coin.parent_coin_info, _.puzzle_reveal, _.coin.amount, _.solution] + for _ in self.coin_spends + ] + as_clvm.append(("c", cs_as_clvm)) + sh_as_clvm = [_.as_program() for _ in self.sum_hints] + as_clvm.append(("s", sh_as_clvm)) + ph_as_clvm = [_.as_program() for _ in self.path_hints] + as_clvm.append(("p", ph_as_clvm)) + self_as_program = Program.to(as_clvm) + return self_as_program + + @classmethod + def from_program(cls, program) -> "UnsignedSpend": + d = transform_dict(program, transform_dict_by_key(UNSIGNED_SPEND_TRANSFORMER)) + return cls(d["c"], d.get("s", []), d.get("p", []), d["a"]) + + def __bytes__(self): + return bytes(self.as_program()) + + @classmethod + def from_bytes(cls, blob) -> "UnsignedSpend": + return cls.from_program(Program.from_bytes(blob)) + + +def coin_spend_from_program(program: Program) -> CoinSpend: + struct = transform_as_struct(program, as_atom, no_op, as_int, no_op) + parent_coin_info, puzzle_reveal, amount, solution = struct + return CoinSpend( + Coin( + parent_coin_info, + puzzle_reveal.tree_hash(), + amount, + ), + puzzle_reveal, + solution, + ) + + +UNSIGNED_SPEND_TRANSFORMER = { + "c": lambda x: clvm_to_list(x, coin_spend_from_program), + "s": lambda x: clvm_to_list(x, SumHint.from_program), + "p": lambda x: clvm_to_list(x, PathHint.from_program), + "a": lambda x: x.atom, +} diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index ab6d1c9..c3855e3 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -17,6 +17,13 @@ PairTuple, ) +from .core.unsigned_spend import ( + UnsignedSpend, + coin_spend_to_tuple, + sum_hint_as_tuple, + path_hint_as_tuple, +) + def test_ser(): tpb = to_program_for_type(bytes) @@ -185,58 +192,14 @@ def test_interop_coin_spend(): assert cst == cst1 -@dataclass -class UnsignedSpend: - coin_spend_tuples: list[CoinSpendTuple] = field( - default_factory=list, metadata=dict(key="c") - ) - sum_hints: list[SumHint] = field( - default_factory=list, - metadata=dict(key="s"), - ) - path_hints: list[PathHint] = field( - default_factory=list, - metadata=dict(key="p"), - ) - agg_sig_me_network_suffix: bytes = field( - default=b"", - metadata=dict(key="a"), - ) - - def coin_spends(self): - return [coin_spend_from_tuple(_) for _ in self.coin_spend_tuples] - - -def coin_spend_from_tuple(cst: CoinSpendTuple) -> CoinSpend: - coin = Coin(cst[0], cst[1].tree_hash(), cst[2]) - return CoinSpend(coin, cst[1], cst[3]) - - -def coin_spend_to_tuple(coin_spend: CoinSpend) -> CoinSpendTuple: - coin = coin_spend.coin - return ( - coin.parent_coin_info, - coin_spend.puzzle_reveal, - coin.amount, - coin_spend.solution, - ) - - -def sum_hint_as_tuple(sum_hint: SumHint) -> tuple: - return tuple(getattr(sum_hint, _) for _ in "public_keys synthetic_offset".split()) - - -def path_hint_as_tuple(path_hint: PathHint) -> tuple: - return tuple(getattr(path_hint, _) for _ in "root_public_key path".split()) - - def test_interop_unsigned_spend(): from hsms.process.unsigned_spend import UnsignedSpend as LegacyUS cs_list = [rnd_coin_spend(_) for _ in range(10)] cst_list = [coin_spend_to_tuple(_) for _ in cs_list] - public_keys = [BLSSecretExponent.from_int(_).public_key() for _ in range(5)] + secret_keys = [BLSSecretExponent.from_int(_) for _ in range(5)] + public_keys = [_.public_key() for _ in secret_keys] synthetic_offset = BLSSecretExponent.from_int(3**70 & ((1 << 256) - 1)) sum_hint = SumHint(public_keys, synthetic_offset) lsh = LegacySH(public_keys, synthetic_offset) From 3a1ccf6e66f1890eda6518a38f251dd237ba350a Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 10 Oct 2023 15:53:43 -0700 Subject: [PATCH 19/40] `Nonexandable`. May rename --- hsms/util/clvm_serde.py | 58 ++++++++++++++++++++++++++++++++++++++++ tests/test_clvm_serde.py | 32 ++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index d17fa98..4a5d335 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -13,6 +13,15 @@ def __new__(cls, a, b): return super().__new__(cls, (a, b)) +class Nonexpandable: + """ + This is a tag. Subclasses, when serialized, don't use a nil terminator when + serialized. + """ + + pass + + # some helper methods to implement chia serialization # def read_bytes(p: Program) -> bytes: @@ -114,6 +123,24 @@ def field_info_for_type(t: type, *etc): return location_based, key_based +def ser_nonexpandable(t: type, *etc): + location_based, key_based = field_info_for_type(t, *etc) + if key_based: + raise ValueError("key-based fields not support for `Nonexpandable`") + + streaming_calls = [] + for name, ser in location_based: + streaming_calls.append(make_ser_for_dcf(name, ser)) + + def ser(item): + values = [sc(item) for sc in streaming_calls] + t = values.pop() + while values: + t = (values.pop(), t) + return Program.to(t) + return ser + + def ser_dataclass(t: type, *etc): location_based, key_based = field_info_for_type(t, *etc) @@ -142,12 +169,40 @@ def fail_ser(t, *args): if hasattr(t, "__bytes__"): return lambda x: Program.to(bytes(x)) + if issubclass(t, Nonexpandable): + return ser_nonexpandable(t, *args) + if is_dataclass(t): return ser_dataclass(t, *args) raise TypeError(f"can't process {t}") +def deser_nonexpandable(t: type, *etc): + location_based, key_based = field_info_for_type(t, *etc) + if key_based: + raise ValueError("key-based fields not support for `Nonexpandable`") + + streaming_calls = [] + for name, ser in location_based: + streaming_calls.append(make_ser_for_dcf(name, ser)) + + def des(p: Program): + args = [] + todo = list(reversed(location_based)) + while todo: + name, des = todo.pop() + if todo: + v = p.pair[0] + p = p.pair[1] + else: + v = p + args.append(des(v)) + return t(*args) + + return des + + def deser_dataclass(t: type, *args): location_based, key_based = field_info_for_type(t, *args) @@ -168,6 +223,9 @@ def des(p: Program): def fail_deser(t, *args): + if issubclass(t, Nonexpandable): + return deser_nonexpandable(t, *args) + if is_dataclass(t): return deser_dataclass(t, *args) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index c3855e3..d1b3778 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -15,6 +15,7 @@ to_program_for_type, from_program_for_type, PairTuple, + Nonexpandable, ) from .core.unsigned_spend import ( @@ -129,6 +130,37 @@ class Foo: assert f1 == foo +def test_serde_nonexpandable(): + @dataclass + class Foo(Nonexpandable): + a: int + b: str + + tp = to_program_for_type(Foo) + fp = from_program_for_type(Foo) + p = Program.to((1000, "hello")) + foo = fp(p) + assert foo.a == 1000 + assert foo.b == "hello" + p1 = tp(foo) + assert p1 == p + + @dataclass + class Bar(Nonexpandable): + a: int + b: str + c: list[int] + + tp = to_program_for_type(Bar) + fp = from_program_for_type(Bar) + p = Program.to((1000, ("hello", [5, 19, 220]))) + foo = fp(p) + assert foo.a == 1000 + assert foo.b == "hello" + assert foo.c == [5, 19, 220] + p1 = tp(foo) + assert p1 == p + def rnd_coin_spend(seed: int) -> CoinSpend: r = random.Random(seed) parent = r.randbytes(32) From 07c39a2310b44a3b12766319a1c85af55306db1c Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 10 Oct 2023 16:17:37 -0700 Subject: [PATCH 20/40] Simplify `SumHint` and `PathHint`. --- tests/test_clvm_serde.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index d1b3778..3526e9a 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -161,6 +161,7 @@ class Bar(Nonexpandable): p1 = tp(foo) assert p1 == p + def rnd_coin_spend(seed: int) -> CoinSpend: r = random.Random(seed) parent = r.randbytes(32) @@ -171,7 +172,10 @@ def rnd_coin_spend(seed: int) -> CoinSpend: return CoinSpend(coin, puzzle, solution) -SumHint = PairTuple[list[BLSPublicKey], BLSSecretExponent] +@dataclass +class SumHint(Nonexpandable): + public_keys: list[BLSPublicKey] + synthetic_offset: BLSSecretExponent def test_interop_sum_hint(): @@ -184,8 +188,8 @@ def test_interop_sum_hint(): print(bytes(p).hex()) lsh = LegacySH.from_program(p) print(lsh) - assert lsh.public_keys == sum_hint[0] - assert lsh.synthetic_offset == sum_hint[1] + assert lsh.public_keys == sum_hint.public_keys + assert lsh.synthetic_offset == sum_hint.synthetic_offset sh1 = fp(p) assert sum_hint == sh1 @@ -193,7 +197,10 @@ def test_interop_sum_hint(): CoinSpendTuple = tuple[bytes, Program, int, Program] -PathHint = PairTuple[BLSPublicKey, list[int]] +@dataclass +class PathHint(Nonexpandable): + root_public_key: BLSPublicKey + path: list[int] def test_interop_path_hint(): @@ -206,8 +213,8 @@ def test_interop_path_hint(): print(bytes(p).hex()) lph = LegacyPH.from_program(p) print(lph) - assert lph.root_public_key == path_hint[0] - assert lph.path == path_hint[1] + assert lph.root_public_key == path_hint.root_public_key + assert lph.path == path_hint.path ph1 = fp(p) assert path_hint == ph1 From 90b4f6613439fac8b32027ec51ef09947f75002e Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 10 Oct 2023 16:33:46 -0700 Subject: [PATCH 21/40] Move `SumHint` and `PathHint` --- tests/core/signing_hints.py | 17 +++++++++++++++++ tests/core/unsigned_spend.py | 13 +------------ tests/test_clvm_serde.py | 25 ++++++++----------------- 3 files changed, 26 insertions(+), 29 deletions(-) create mode 100644 tests/core/signing_hints.py diff --git a/tests/core/signing_hints.py b/tests/core/signing_hints.py new file mode 100644 index 0000000..839e960 --- /dev/null +++ b/tests/core/signing_hints.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent + +from hsms.util.clvm_serde import Nonexpandable + + +@dataclass +class SumHint(Nonexpandable): + public_keys: list[BLSPublicKey] + synthetic_offset: BLSSecretExponent + + +@dataclass +class PathHint(Nonexpandable): + root_public_key: BLSPublicKey + path: list[int] diff --git a/tests/core/unsigned_spend.py b/tests/core/unsigned_spend.py index acdc765..4d79ec6 100644 --- a/tests/core/unsigned_spend.py +++ b/tests/core/unsigned_spend.py @@ -7,13 +7,11 @@ from hsms.util.clvm_serde import PairTuple +from .signing_hints import PathHint, SumHint -SumHint = PairTuple[list[BLSPublicKey], BLSSecretExponent] CoinSpendTuple = tuple[bytes, Program, int, Program] -PathHint = PairTuple[BLSPublicKey, list[int]] - @dataclass class UnsignedSpend: @@ -50,12 +48,3 @@ def coin_spend_to_tuple(coin_spend: CoinSpend) -> CoinSpendTuple: coin.amount, coin_spend.solution, ) - - -def sum_hint_as_tuple(sum_hint: SumHint) -> tuple: - keys = "public_keys synthetic_offset".split() - return tuple(getattr(sum_hint, _) for _ in keys) - - -def path_hint_as_tuple(path_hint: PathHint) -> tuple: - return tuple(getattr(path_hint, _) for _ in "root_public_key path".split()) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 3526e9a..7f649c3 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -17,12 +17,11 @@ PairTuple, Nonexpandable, ) - +from .core.signing_hints import SumHint, PathHint from .core.unsigned_spend import ( UnsignedSpend, coin_spend_to_tuple, - sum_hint_as_tuple, - path_hint_as_tuple, + coin_spend_from_tuple, ) @@ -172,12 +171,6 @@ def rnd_coin_spend(seed: int) -> CoinSpend: return CoinSpend(coin, puzzle, solution) -@dataclass -class SumHint(Nonexpandable): - public_keys: list[BLSPublicKey] - synthetic_offset: BLSSecretExponent - - def test_interop_sum_hint(): pks = [BLSSecretExponent.from_int(_).public_key() for _ in range(5)] synthetic_offset = BLSSecretExponent.from_int(3**70 & ((1 << 256) - 1)) @@ -197,12 +190,6 @@ def test_interop_sum_hint(): CoinSpendTuple = tuple[bytes, Program, int, Program] -@dataclass -class PathHint(Nonexpandable): - root_public_key: BLSPublicKey - path: list[int] - - def test_interop_path_hint(): public_key = BLSSecretExponent.from_int(1).public_key() ints = [1, 5, 91, 29484, -99] @@ -258,8 +245,12 @@ def test_interop_unsigned_spend(): print(bytes(p).hex()) lus = LegacyUS.from_program(p) assert lus.coin_spends == us.coin_spends() - assert [sum_hint_as_tuple(_) for _ in lus.sum_hints] == us.sum_hints - assert [path_hint_as_tuple(_) for _ in lus.path_hints] == us.path_hints + for k in "public_keys synthetic_offset".split(): + for lh, rh in zip(lus.sum_hints, us.sum_hints): + assert getattr(lh, k) == getattr(rh, k) + for k in "root_public_key path".split(): + for lh, rh in zip(lus.path_hints, us.path_hints): + assert getattr(lh, k) == getattr(rh, k) assert lus.agg_sig_me_network_suffix == us.agg_sig_me_network_suffix us1 = fp(p) assert us == us1 From 8a04d2de8709581af812acc91c287cc26764edf4 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Wed, 11 Oct 2023 18:20:23 -0700 Subject: [PATCH 22/40] nonexpandable tuple --- hsms/util/clvm_serde.py | 48 ++++++++++++++++++++++++++++++++++++++++ tests/test_clvm_serde.py | 16 ++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 4a5d335..360aa05 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -8,11 +8,19 @@ from .type_tree import type_tree +class EncodingError(BaseException): + pass + + class PairTuple(tuple): def __new__(cls, a, b): return super().__new__(cls, (a, b)) +class tuple_nonexpandable(tuple): + pass + + class Nonexpandable: """ This is a tag. Subclasses, when serialized, don't use a nil terminator when @@ -88,9 +96,28 @@ def serialize_optional(f, item): return serialize_optional +def ser_for_tuple_nonexpandable(origin, args, *etc): + streaming_calls = [type_tree(_, *etc) for _ in args] + + def ser(item): + if len(item) != len(streaming_calls): + raise EncodingError("incorrect number of items in tuple") + + values = list(zip(streaming_calls, item)) + sc, v = values.pop() + t = sc(v) + while values: + sc, v = values.pop() + t = (sc(v), t) + return Program.to(t) + + return ser + + SERIALIZER_COMPOUND_TYPE_LOOKUP = { list: serialize_for_list, tuple: serialize_for_tuple, + tuple_nonexpandable: ser_for_tuple_nonexpandable, PairTuple: serialize_for_pair_tuple, # Union: to_program_for_union, # UnionType: to_program_for_union, @@ -138,6 +165,7 @@ def ser(item): while values: t = (values.pop(), t) return Program.to(t) + return ser @@ -265,6 +293,25 @@ def deserialize_tuple(p: Program) -> tuple[int, Any]: return deserialize_tuple +def de_for_tuple_nonexpandable(origin, args, *etc): + read_items = [type_tree(_, *etc) for _ in args] + + def de(p: Program) -> tuple[int, Any]: + args = [] + todo = list(reversed(read_items)) + while todo: + des = todo.pop() + if todo: + v = p.pair[0] + p = p.pair[1] + else: + v = p + args.append(des(v)) + return tuple(args) + + return de + + def deser_for_pair_tuple(origin, args, *etc): read_items = [type_tree(_, *etc) for _ in args] @@ -292,6 +339,7 @@ def deserialize_optional(f: BinaryIO) -> tuple[int, Any]: DESERIALIZER_COMPOUND_TYPE_LOOKUP = { list: deser_for_list, tuple: deser_for_tuple, + tuple_nonexpandable: de_for_tuple_nonexpandable, PairTuple: deser_for_pair_tuple, # Union: deser_for_union, # UnionType: deser_for_union, diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 7f649c3..e9b83c3 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -16,6 +16,7 @@ from_program_for_type, PairTuple, Nonexpandable, + tuple_nonexpandable, ) from .core.signing_hints import SumHint, PathHint from .core.unsigned_spend import ( @@ -254,3 +255,18 @@ def test_interop_unsigned_spend(): assert lus.agg_sig_me_network_suffix == us.agg_sig_me_network_suffix us1 = fp(p) assert us == us1 + + +def test_tuple_nonexpandable(): + Foo = tuple_nonexpandable[int, str, bytes] + + tp = to_program_for_type(Foo) + fp = from_program_for_type(Foo) + p = Program.to((1000, ("hello", b"bob"))) + foo = fp(p) + assert foo[0] == 1000 + assert foo[1] == "hello" + assert foo[2] == b"bob" + p1 = tp(foo) + assert p1 == p + From 311a44ea92eee0e60d6ce8256c4dade60a751c2b Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Wed, 11 Oct 2023 19:06:41 -0700 Subject: [PATCH 23/40] Rewrite `Nonexpandable` in terms of `tuple_nonexpandable` --- hsms/util/clvm_serde.py | 61 +++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 360aa05..faf1e2a 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -1,5 +1,5 @@ from dataclasses import is_dataclass, fields, MISSING -from typing import Any, BinaryIO, Callable, get_type_hints +from typing import Any, BinaryIO, Callable, GenericAlias, get_type_hints from chia_base.atoms import bytes32, uint8 @@ -150,21 +150,37 @@ def field_info_for_type(t: type, *etc): return location_based, key_based +def types_for_fields(t: type, *etc): + # split into key-based and location-based + + key_based = [] + location_based = [] + for f in fields(t): + default_value = ( + f.default if f.default_factory is MISSING else f.default_factory() + ) + key = f.metadata.get("key") + if key is None: + location_based.append(f) + else: + key_based.append((key, f.name, f.type, default_value)) + + return location_based, key_based + + def ser_nonexpandable(t: type, *etc): - location_based, key_based = field_info_for_type(t, *etc) + location_based, key_based = types_for_fields(t, *etc) if key_based: raise ValueError("key-based fields not support for `Nonexpandable`") - streaming_calls = [] - for name, ser in location_based: - streaming_calls.append(make_ser_for_dcf(name, ser)) + names = [f.name for f in location_based] + types = tuple(f.type for f in location_based) + tuple_type = GenericAlias(tuple_nonexpandable, types) + ser_tuple = type_tree(tuple_type, *etc) def ser(item): - values = [sc(item) for sc in streaming_calls] - t = values.pop() - while values: - t = (values.pop(), t) - return Program.to(t) + t = tuple_nonexpandable(getattr(item, attr) for attr in names) + return Program.to(ser_tuple(t)) return ser @@ -207,28 +223,19 @@ def fail_ser(t, *args): def deser_nonexpandable(t: type, *etc): - location_based, key_based = field_info_for_type(t, *etc) + location_based, key_based = types_for_fields(t, *etc) if key_based: raise ValueError("key-based fields not support for `Nonexpandable`") - streaming_calls = [] - for name, ser in location_based: - streaming_calls.append(make_ser_for_dcf(name, ser)) + types = tuple(f.type for f in location_based) + tuple_type = GenericAlias(tuple_nonexpandable, types) + de_tuple = type_tree(tuple_type, *etc) - def des(p: Program): - args = [] - todo = list(reversed(location_based)) - while todo: - name, des = todo.pop() - if todo: - v = p.pair[0] - p = p.pair[1] - else: - v = p - args.append(des(v)) - return t(*args) + def de(p: Program): + the_tuple = de_tuple(p) + return t(*the_tuple) - return des + return de def deser_dataclass(t: type, *args): From 11c28d184889066baeeaa32e5e764ce8836df4fb Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 12 Oct 2023 12:45:33 -0700 Subject: [PATCH 24/40] Serde of dataclasses now leverages tuple --- hsms/util/clvm_serde.py | 106 +++++++++++++++++++++++++-------------- tests/test_clvm_serde.py | 6 +-- 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index faf1e2a..d7e8d9e 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -1,5 +1,5 @@ from dataclasses import is_dataclass, fields, MISSING -from typing import Any, BinaryIO, Callable, GenericAlias, get_type_hints +from typing import Any, BinaryIO, Callable, GenericAlias from chia_base.atoms import bytes32, uint8 @@ -17,7 +17,7 @@ def __new__(cls, a, b): return super().__new__(cls, (a, b)) -class tuple_nonexpandable(tuple): +class tuple_frugal(tuple): pass @@ -96,7 +96,7 @@ def serialize_optional(f, item): return serialize_optional -def ser_for_tuple_nonexpandable(origin, args, *etc): +def ser_for_tuple_frugal(origin, args, *etc): streaming_calls = [type_tree(_, *etc) for _ in args] def ser(item): @@ -117,7 +117,7 @@ def ser(item): SERIALIZER_COMPOUND_TYPE_LOOKUP = { list: serialize_for_list, tuple: serialize_for_tuple, - tuple_nonexpandable: ser_for_tuple_nonexpandable, + tuple_frugal: ser_for_tuple_frugal, PairTuple: serialize_for_pair_tuple, # Union: to_program_for_union, # UnionType: to_program_for_union, @@ -163,7 +163,8 @@ def types_for_fields(t: type, *etc): if key is None: location_based.append(f) else: - key_based.append((key, f.name, f.type, default_value)) + call = type_tree(f.type, *etc) + key_based.append((key, f.name, call, default_value)) return location_based, key_based @@ -175,33 +176,44 @@ def ser_nonexpandable(t: type, *etc): names = [f.name for f in location_based] types = tuple(f.type for f in location_based) - tuple_type = GenericAlias(tuple_nonexpandable, types) + tuple_type = GenericAlias(tuple_frugal, types) ser_tuple = type_tree(tuple_type, *etc) def ser(item): - t = tuple_nonexpandable(getattr(item, attr) for attr in names) + t = tuple_frugal(getattr(item, attr) for attr in names) return Program.to(ser_tuple(t)) return ser def ser_dataclass(t: type, *etc): - location_based, key_based = field_info_for_type(t, *etc) + location_based, key_based = types_for_fields(t, *etc) - streaming_calls = [] - for name, ser in location_based: - streaming_calls.append(make_ser_for_dcf(name, ser)) + types = tuple(f.type for f in location_based) + tuple_type = GenericAlias(tuple, types) + if key_based: + types = types + (list[tuple_frugal[str, Program]],) + tuple_type = GenericAlias(tuple_frugal, types) - def ser(item): - pairs = [sc(item) for sc in streaming_calls] + names = tuple(f.name for f in location_based) - for name, ser, key, default_value in key_based: - v = getattr(item, name) - if v == default_value: - continue - pt = PairTuple(key, ser(v)) - pairs.append(pt) - return Program.to(pairs) + ser_tuple = type_tree(tuple_type, *etc) + + def ser(item): + # convert to a tuple + v = [] + for name in names: + v.append(getattr(item, name)) + if key_based: + d = [] + for key, name, call, default_value in key_based: + a = getattr(item, name) + if a == default_value: + continue + d.append((key, call(a))) + v.append(d) + + return ser_tuple(tuple(v)) return ser @@ -228,7 +240,7 @@ def deser_nonexpandable(t: type, *etc): raise ValueError("key-based fields not support for `Nonexpandable`") types = tuple(f.type for f in location_based) - tuple_type = GenericAlias(tuple_nonexpandable, types) + tuple_type = GenericAlias(tuple_frugal, types) de_tuple = type_tree(tuple_type, *etc) def de(p: Program): @@ -238,23 +250,43 @@ def de(p: Program): return de -def deser_dataclass(t: type, *args): - location_based, key_based = field_info_for_type(t, *args) +def deser_dataclass(t: type, *etc): + location_based, key_based = types_for_fields(t, *etc) + + types = tuple(f.type for f in location_based) + tuple_type = GenericAlias(tuple, types) + if key_based: + types = types + (list[tuple_frugal[str, Program]],) + tuple_type = GenericAlias(tuple_frugal, types) - def des(p: Program): - args = [] - for name, des in location_based: - args.append(des(p.pair[0])) - p = p.pair[1] + de_tuple = type_tree(tuple_type, *etc) + + if key_based: - kwargs = {} - for name, des, key, default_value in key_based: - d = dict((k.atom.decode(), v) for k, v in (_.pair for _ in p.as_iter())) - if key in d: - kwargs[name] = des(d[key]) - return t(*args, **kwargs) + def de(p: Program): + the_tuple = de_tuple(p) + d = dict((k, v) for k, v in the_tuple[-1]) + args = the_tuple[:-1] + kwargs = {} + for key, name, call, default_value in key_based: + if key in d: + kwargs[name] = call(d[key]) + else: + if default_value == MISSING: + raise EncodingError( + f"missing required field for {name} with key {key}" + ) + kwargs[name] = default_value + + return t(*args, **kwargs) + + else: + + def de(p: Program): + the_tuple = de_tuple(p) + return t(*the_tuple) - return des + return de def fail_deser(t, *args): @@ -300,7 +332,7 @@ def deserialize_tuple(p: Program) -> tuple[int, Any]: return deserialize_tuple -def de_for_tuple_nonexpandable(origin, args, *etc): +def de_for_tuple_frugal(origin, args, *etc): read_items = [type_tree(_, *etc) for _ in args] def de(p: Program) -> tuple[int, Any]: @@ -346,7 +378,7 @@ def deserialize_optional(f: BinaryIO) -> tuple[int, Any]: DESERIALIZER_COMPOUND_TYPE_LOOKUP = { list: deser_for_list, tuple: deser_for_tuple, - tuple_nonexpandable: de_for_tuple_nonexpandable, + tuple_frugal: de_for_tuple_frugal, PairTuple: deser_for_pair_tuple, # Union: deser_for_union, # UnionType: deser_for_union, diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index e9b83c3..f7a743b 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -16,7 +16,7 @@ from_program_for_type, PairTuple, Nonexpandable, - tuple_nonexpandable, + tuple_frugal, ) from .core.signing_hints import SumHint, PathHint from .core.unsigned_spend import ( @@ -257,8 +257,8 @@ def test_interop_unsigned_spend(): assert us == us1 -def test_tuple_nonexpandable(): - Foo = tuple_nonexpandable[int, str, bytes] +def test_tuple_frugal(): + Foo = tuple_frugal[int, str, bytes] tp = to_program_for_type(Foo) fp = from_program_for_type(Foo) From 2819a45b99f8c514a0608d7beedacbabfece2bc4 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 12 Oct 2023 12:50:45 -0700 Subject: [PATCH 25/40] Ditch `PairTuple` and use `Frugal` --- hsms/util/clvm_serde.py | 26 +++++--------------------- tests/core/signing_hints.py | 6 +++--- tests/core/unsigned_spend.py | 3 --- tests/test_clvm_serde.py | 11 +++++------ 4 files changed, 13 insertions(+), 33 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index d7e8d9e..0c8c3d9 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -12,16 +12,11 @@ class EncodingError(BaseException): pass -class PairTuple(tuple): - def __new__(cls, a, b): - return super().__new__(cls, (a, b)) - - class tuple_frugal(tuple): pass -class Nonexpandable: +class Frugal: """ This is a tag. Subclasses, when serialized, don't use a nil terminator when serialized. @@ -118,7 +113,6 @@ def ser(item): list: serialize_for_list, tuple: serialize_for_tuple, tuple_frugal: ser_for_tuple_frugal, - PairTuple: serialize_for_pair_tuple, # Union: to_program_for_union, # UnionType: to_program_for_union, } @@ -172,7 +166,7 @@ def types_for_fields(t: type, *etc): def ser_nonexpandable(t: type, *etc): location_based, key_based = types_for_fields(t, *etc) if key_based: - raise ValueError("key-based fields not support for `Nonexpandable`") + raise ValueError("key-based fields not support for `Frugal`") names = [f.name for f in location_based] types = tuple(f.type for f in location_based) @@ -225,7 +219,7 @@ def fail_ser(t, *args): if hasattr(t, "__bytes__"): return lambda x: Program.to(bytes(x)) - if issubclass(t, Nonexpandable): + if issubclass(t, Frugal): return ser_nonexpandable(t, *args) if is_dataclass(t): @@ -237,7 +231,7 @@ def fail_ser(t, *args): def deser_nonexpandable(t: type, *etc): location_based, key_based = types_for_fields(t, *etc) if key_based: - raise ValueError("key-based fields not support for `Nonexpandable`") + raise ValueError("key-based fields not support for `Frugal`") types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple_frugal, types) @@ -290,7 +284,7 @@ def de(p: Program): def fail_deser(t, *args): - if issubclass(t, Nonexpandable): + if issubclass(t, Frugal): return deser_nonexpandable(t, *args) if is_dataclass(t): @@ -351,15 +345,6 @@ def de(p: Program) -> tuple[int, Any]: return de -def deser_for_pair_tuple(origin, args, *etc): - read_items = [type_tree(_, *etc) for _ in args] - - def deserialize_tuple(p: Program) -> tuple[int, Any]: - return PairTuple(*[f(_) for f, _ in zip(read_items, p.pair)]) - - return deserialize_tuple - - def deser_for_union(origin, args, *etc): item_type = optional_from_union(args) if item_type is not None: @@ -379,7 +364,6 @@ def deserialize_optional(f: BinaryIO) -> tuple[int, Any]: list: deser_for_list, tuple: deser_for_tuple, tuple_frugal: de_for_tuple_frugal, - PairTuple: deser_for_pair_tuple, # Union: deser_for_union, # UnionType: deser_for_union, } diff --git a/tests/core/signing_hints.py b/tests/core/signing_hints.py index 839e960..6721bf4 100644 --- a/tests/core/signing_hints.py +++ b/tests/core/signing_hints.py @@ -2,16 +2,16 @@ from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent -from hsms.util.clvm_serde import Nonexpandable +from hsms.util.clvm_serde import Frugal @dataclass -class SumHint(Nonexpandable): +class SumHint(Frugal): public_keys: list[BLSPublicKey] synthetic_offset: BLSSecretExponent @dataclass -class PathHint(Nonexpandable): +class PathHint(Frugal): root_public_key: BLSPublicKey path: list[int] diff --git a/tests/core/unsigned_spend.py b/tests/core/unsigned_spend.py index 4d79ec6..2e5d408 100644 --- a/tests/core/unsigned_spend.py +++ b/tests/core/unsigned_spend.py @@ -1,12 +1,9 @@ from dataclasses import dataclass, field -from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent from chia_base.core import Coin, CoinSpend from clvm_rs import Program -from hsms.util.clvm_serde import PairTuple - from .signing_hints import PathHint, SumHint diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index f7a743b..b361ec5 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -14,8 +14,7 @@ from hsms.util.clvm_serde import ( to_program_for_type, from_program_for_type, - PairTuple, - Nonexpandable, + Frugal, tuple_frugal, ) from .core.signing_hints import SumHint, PathHint @@ -61,7 +60,7 @@ def test_ser(): assert tp(t) == prhs assert fp(tp(t)) == t - tt = PairTuple[int, str] + tt = tuple_frugal[int, str] tp = to_program_for_type(tt) fp = from_program_for_type(tt) for v in [ @@ -69,7 +68,7 @@ def test_ser(): (200, "two hundred"), (30, "thirty"), ]: - t = PairTuple(*v) + t = tuple_frugal(v) rhs = Program.to(t) assert tp(t) == rhs assert fp(tp(t)) == t @@ -132,7 +131,7 @@ class Foo: def test_serde_nonexpandable(): @dataclass - class Foo(Nonexpandable): + class Foo(Frugal): a: int b: str @@ -146,7 +145,7 @@ class Foo(Nonexpandable): assert p1 == p @dataclass - class Bar(Nonexpandable): + class Bar(Frugal): a: int b: str c: list[int] From ddfd8ed2bae30ea3eac8a6224d024dfa5e429ba7 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 12 Oct 2023 14:04:58 -0700 Subject: [PATCH 26/40] Eliminate special case for `Frugal` --- hsms/util/clvm_serde.py | 72 +++++++++++++++++----------------------- tests/test_clvm_serde.py | 19 ++++++++++- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 0c8c3d9..efdb552 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -144,7 +144,7 @@ def field_info_for_type(t: type, *etc): return location_based, key_based -def types_for_fields(t: type, *etc): +def types_for_fields(t: type, call_morpher, *etc): # split into key-based and location-based key_based = [] @@ -153,40 +153,39 @@ def types_for_fields(t: type, *etc): default_value = ( f.default if f.default_factory is MISSING else f.default_factory() ) - key = f.metadata.get("key") + m = f.metadata + key = m.get("key") if key is None: location_based.append(f) else: - call = type_tree(f.type, *etc) - key_based.append((key, f.name, call, default_value)) + alt_serde_type = m.get("alt_serde_type") + storage_type = alt_serde_type[0] if alt_serde_type else f.type + call = type_tree(storage_type, *etc) + key_based.append( + (key, f.name, call_morpher(call, alt_serde_type), default_value) + ) return location_based, key_based -def ser_nonexpandable(t: type, *etc): - location_based, key_based = types_for_fields(t, *etc) - if key_based: - raise ValueError("key-based fields not support for `Frugal`") - - names = [f.name for f in location_based] - types = tuple(f.type for f in location_based) - tuple_type = GenericAlias(tuple_frugal, types) - ser_tuple = type_tree(tuple_type, *etc) - - def ser(item): - t = tuple_frugal(getattr(item, attr) for attr in names) - return Program.to(ser_tuple(t)) +def ser_dataclass(t: type, *etc): + def make_new_call(call, alt_serde_type): + if alt_serde_type: + _type, from_storage, _to_storage = alt_serde_type - return ser + def f(x): + return call(from_storage(x)) + return f + return call -def ser_dataclass(t: type, *etc): - location_based, key_based = types_for_fields(t, *etc) + location_based, key_based = types_for_fields(t, make_new_call, *etc) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) if key_based: types = types + (list[tuple_frugal[str, Program]],) + if key_based or issubclass(t, Frugal): tuple_type = GenericAlias(tuple_frugal, types) names = tuple(f.name for f in location_based) @@ -219,38 +218,30 @@ def fail_ser(t, *args): if hasattr(t, "__bytes__"): return lambda x: Program.to(bytes(x)) - if issubclass(t, Frugal): - return ser_nonexpandable(t, *args) - if is_dataclass(t): return ser_dataclass(t, *args) raise TypeError(f"can't process {t}") -def deser_nonexpandable(t: type, *etc): - location_based, key_based = types_for_fields(t, *etc) - if key_based: - raise ValueError("key-based fields not support for `Frugal`") - - types = tuple(f.type for f in location_based) - tuple_type = GenericAlias(tuple_frugal, types) - de_tuple = type_tree(tuple_type, *etc) +def deser_dataclass(t: type, *etc): + def make_new_call(call, alt_serde_type): + if alt_serde_type: + _type, _from_storage, to_storage = alt_serde_type - def de(p: Program): - the_tuple = de_tuple(p) - return t(*the_tuple) + def f(x): + return to_storage(call(x)) - return de + return f + return call - -def deser_dataclass(t: type, *etc): - location_based, key_based = types_for_fields(t, *etc) + location_based, key_based = types_for_fields(t, make_new_call, *etc) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) if key_based: types = types + (list[tuple_frugal[str, Program]],) + if key_based or issubclass(t, Frugal): tuple_type = GenericAlias(tuple_frugal, types) de_tuple = type_tree(tuple_type, *etc) @@ -259,8 +250,8 @@ def deser_dataclass(t: type, *etc): def de(p: Program): the_tuple = de_tuple(p) - d = dict((k, v) for k, v in the_tuple[-1]) args = the_tuple[:-1] + d = dict((k, v) for k, v in the_tuple[-1]) kwargs = {} for key, name, call, default_value in key_based: if key in d: @@ -284,9 +275,6 @@ def de(p: Program): def fail_deser(t, *args): - if issubclass(t, Frugal): - return deser_nonexpandable(t, *args) - if is_dataclass(t): return deser_dataclass(t, *args) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index b361ec5..3a8ef88 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -129,7 +129,7 @@ class Foo: assert f1 == foo -def test_serde_nonexpandable(): +def test_serde_frugal(): @dataclass class Foo(Frugal): a: int @@ -269,3 +269,20 @@ def test_tuple_frugal(): p1 = tp(foo) assert p1 == p + +def test_dataclasses_transform(): + @dataclass + class Foo: + a: int = field( + metadata=dict(alt_serde_type=(str, str, int), key="a"), + ) + b: str = field(default="foo", metadata=dict(key="bob")) + + tp = to_program_for_type(Foo) + fp = from_program_for_type(Foo) + p = Program.to([("a", "1000"), ("bob", "hello")]) + foo = fp(p) + assert foo.a == 1000 + assert foo.b == "hello" + p1 = tp(foo) + assert p1 == p From 509e113f06e1818f7bca693fde885f25e293be03 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 12 Oct 2023 14:51:12 -0700 Subject: [PATCH 27/40] Make `UnsignedSpend` more like original version --- tests/core/unsigned_spend.py | 56 ++++++++++++++++++++++-------------- tests/test_clvm_serde.py | 26 ++++++----------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/tests/core/unsigned_spend.py b/tests/core/unsigned_spend.py index 2e5d408..d309983 100644 --- a/tests/core/unsigned_spend.py +++ b/tests/core/unsigned_spend.py @@ -7,13 +7,45 @@ from .signing_hints import PathHint, SumHint -CoinSpendTuple = tuple[bytes, Program, int, Program] +CSTuple = tuple[bytes, Program, int, Program] +SerdeCoinSpends = list[CSTuple] + + +def to_storage( + coin_spend_tuples: SerdeCoinSpends, +) -> list[CoinSpend]: + return [ + CoinSpend(Coin(_[0], _[1].tree_hash(), _[2]), _[1], _[3]) + for _ in coin_spend_tuples + ] + + +def from_storage( + coin_spends: list[CoinSpend], +) -> SerdeCoinSpends: + return [ + ( + _.coin.parent_coin_info, + _.puzzle_reveal, + _.coin.amount, + _.solution, + ) + for _ in coin_spends + ] @dataclass class UnsignedSpend: - coin_spend_tuples: list[CoinSpendTuple] = field( - default_factory=list, metadata=dict(key="c") + coin_spends: list[CoinSpend] = field( + default_factory=list, + metadata=dict( + key="c", + alt_serde_type=( + SerdeCoinSpends, + from_storage, + to_storage, + ), + ), ) sum_hints: list[SumHint] = field( default_factory=list, @@ -27,21 +59,3 @@ class UnsignedSpend: default=b"", metadata=dict(key="a"), ) - - def coin_spends(self): - return [coin_spend_from_tuple(_) for _ in self.coin_spend_tuples] - - -def coin_spend_from_tuple(cst: CoinSpendTuple) -> CoinSpend: - coin = Coin(cst[0], cst[1].tree_hash(), cst[2]) - return CoinSpend(coin, cst[1], cst[3]) - - -def coin_spend_to_tuple(coin_spend: CoinSpend) -> CoinSpendTuple: - coin = coin_spend.coin - return ( - coin.parent_coin_info, - coin_spend.puzzle_reveal, - coin.amount, - coin_spend.solution, - ) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 3a8ef88..6d4dbfa 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -2,7 +2,7 @@ import random -from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.bls12_381 import BLSSecretExponent from chia_base.core import Coin, CoinSpend from clvm_rs import Program @@ -18,11 +18,7 @@ tuple_frugal, ) from .core.signing_hints import SumHint, PathHint -from .core.unsigned_spend import ( - UnsignedSpend, - coin_spend_to_tuple, - coin_spend_from_tuple, -) +from .core.unsigned_spend import UnsignedSpend, from_storage, to_storage def test_ser(): @@ -207,22 +203,16 @@ def test_interop_path_hint(): def test_interop_coin_spend(): - for _ in range(10): - cs = rnd_coin_spend(_) - cst = coin_spend_to_tuple(cs) - tp = to_program_for_type(CoinSpendTuple) - fp = from_program_for_type(CoinSpendTuple) - p = tp(cst) - print(bytes(p).hex()) - cst1 = fp(p) - assert cst == cst1 + cs_list = [rnd_coin_spend(_) for _ in range(10)] + cst = from_storage(cs_list) + cs_list_1 = to_storage(cst) + assert cs_list == cs_list_1 def test_interop_unsigned_spend(): from hsms.process.unsigned_spend import UnsignedSpend as LegacyUS cs_list = [rnd_coin_spend(_) for _ in range(10)] - cst_list = [coin_spend_to_tuple(_) for _ in cs_list] secret_keys = [BLSSecretExponent.from_int(_) for _ in range(5)] public_keys = [_.public_key() for _ in secret_keys] @@ -235,7 +225,7 @@ def test_interop_unsigned_spend(): lph = LegacyPH(pubkey, ints) suffix = b"a" * 32 - us = UnsignedSpend(cst_list[0:3], [sum_hint], [path_hint], suffix) + us = UnsignedSpend(cs_list[0:3], [sum_hint], [path_hint], suffix) lus = LegacyUS(cs_list[0:3], [lsh], [lph], suffix) print(bytes(lus.as_program()).hex()) @@ -244,7 +234,7 @@ def test_interop_unsigned_spend(): p = tp(us) print(bytes(p).hex()) lus = LegacyUS.from_program(p) - assert lus.coin_spends == us.coin_spends() + assert lus.coin_spends == us.coin_spends for k in "public_keys synthetic_offset".split(): for lh, rh in zip(lus.sum_hints, us.sum_hints): assert getattr(lh, k) == getattr(rh, k) From 47a361d5d7327e69eb55434ec7e150535c6ac22e Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 12 Oct 2023 15:04:58 -0700 Subject: [PATCH 28/40] Delete obsolete, rename --- hsms/util/clvm_serde.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index efdb552..114d492 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -125,25 +125,6 @@ def f(obj): return f -def field_info_for_type(t: type, *etc): - # split into key-based and location-based - - key_based = [] - location_based = [] - for f in fields(t): - default_value = ( - f.default if f.default_factory is MISSING else f.default_factory() - ) - key = f.metadata.get("key") - call = type_tree(f.type, *etc) - if key is None: - location_based.append((f.name, call)) - else: - key_based.append((f.name, call, key, default_value)) - - return location_based, key_based - - def types_for_fields(t: type, call_morpher, *etc): # split into key-based and location-based @@ -169,7 +150,7 @@ def types_for_fields(t: type, call_morpher, *etc): def ser_dataclass(t: type, *etc): - def make_new_call(call, alt_serde_type): + def morph_call(call, alt_serde_type): if alt_serde_type: _type, from_storage, _to_storage = alt_serde_type @@ -179,7 +160,7 @@ def f(x): return f return call - location_based, key_based = types_for_fields(t, make_new_call, *etc) + location_based, key_based = types_for_fields(t, morph_call, *etc) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) @@ -225,7 +206,7 @@ def fail_ser(t, *args): def deser_dataclass(t: type, *etc): - def make_new_call(call, alt_serde_type): + def morph_call(call, alt_serde_type): if alt_serde_type: _type, _from_storage, to_storage = alt_serde_type @@ -235,7 +216,7 @@ def f(x): return f return call - location_based, key_based = types_for_fields(t, make_new_call, *etc) + location_based, key_based = types_for_fields(t, morph_call, *etc) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) From 29cd02c82acb8d58ad178e9885a3eda82ea29940 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 16 Oct 2023 13:38:47 -0700 Subject: [PATCH 29/40] Add `hsms.core` --- {tests => hsms}/core/__init__.py | 0 {tests => hsms}/core/signing_hints.py | 0 {tests => hsms}/core/unsigned_spend.py | 0 hsms/util/clvm_serde.py | 17 +++++------------ 4 files changed, 5 insertions(+), 12 deletions(-) rename {tests => hsms}/core/__init__.py (100%) rename {tests => hsms}/core/signing_hints.py (100%) rename {tests => hsms}/core/unsigned_spend.py (100%) diff --git a/tests/core/__init__.py b/hsms/core/__init__.py similarity index 100% rename from tests/core/__init__.py rename to hsms/core/__init__.py diff --git a/tests/core/signing_hints.py b/hsms/core/signing_hints.py similarity index 100% rename from tests/core/signing_hints.py rename to hsms/core/signing_hints.py diff --git a/tests/core/unsigned_spend.py b/hsms/core/unsigned_spend.py similarity index 100% rename from tests/core/unsigned_spend.py rename to hsms/core/unsigned_spend.py diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 114d492..8ef9b51 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -118,13 +118,6 @@ def ser(item): } -def make_ser_for_dcf(f_name: str, ser_for_field: Callable): - def f(obj): - return ser_for_field(getattr(obj, f_name)) - - return f - - def types_for_fields(t: type, call_morpher, *etc): # split into key-based and location-based @@ -142,15 +135,14 @@ def types_for_fields(t: type, call_morpher, *etc): alt_serde_type = m.get("alt_serde_type") storage_type = alt_serde_type[0] if alt_serde_type else f.type call = type_tree(storage_type, *etc) - key_based.append( - (key, f.name, call_morpher(call, alt_serde_type), default_value) - ) + key_based.append((key, f.name, call_morpher(call, f), default_value)) return location_based, key_based def ser_dataclass(t: type, *etc): - def morph_call(call, alt_serde_type): + def morph_call(call, f): + alt_serde_type = f.metadata.get("alt_serde_type") if alt_serde_type: _type, from_storage, _to_storage = alt_serde_type @@ -206,7 +198,8 @@ def fail_ser(t, *args): def deser_dataclass(t: type, *etc): - def morph_call(call, alt_serde_type): + def morph_call(call, f): + alt_serde_type = f.metadata.get("alt_serde_type") if alt_serde_type: _type, _from_storage, to_storage = alt_serde_type From f14bc239d1088e584295cdeea3f78f24fc17f04c Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 17 Oct 2023 16:05:54 -0700 Subject: [PATCH 30/40] Use new serialization rather than legacy. --- hsms/cmds/hsm_dump_us.py | 2 +- hsms/cmds/hsm_test_spend.py | 4 +- hsms/cmds/hsmmerge.py | 4 +- hsms/cmds/hsms.py | 2 +- hsms/cmds/poser_gen.py | 2 +- hsms/core/signing_hints.py | 10 +++ hsms/core/unsigned_spend.py | 22 ++++- hsms/process/sign.py | 5 +- hsms/process/unsigned_spend.py | 83 ------------------- hsms/util/clvm_serde.py | 12 +-- .../legacy}/clvm_serialization.py | 0 .../process => tests/legacy}/signing_hints.py | 2 +- tests/legacy/unsigned_spend.py | 4 +- tests/test_clvm_serde.py | 22 +++-- tests/test_lifecycle.py | 4 +- 15 files changed, 61 insertions(+), 117 deletions(-) delete mode 100644 hsms/process/unsigned_spend.py rename {hsms/util => tests/legacy}/clvm_serialization.py (100%) rename {hsms/process => tests/legacy}/signing_hints.py (97%) diff --git a/hsms/cmds/hsm_dump_us.py b/hsms/cmds/hsm_dump_us.py index ab75fe2..c4599ac 100644 --- a/hsms/cmds/hsm_dump_us.py +++ b/hsms/cmds/hsm_dump_us.py @@ -4,7 +4,7 @@ from hsms.clvm.disasm import disassemble from hsms.cmds.hsms import summarize_unsigned_spend -from hsms.process.unsigned_spend import UnsignedSpend +from hsms.core.unsigned_spend import UnsignedSpend from hsms.util.qrint_encoding import a2b_qrint diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index 8239648..11cf12e 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -8,8 +8,8 @@ from chia_base.bls12_381 import BLSPublicKey from chia_base.core import Coin, CoinSpend -from hsms.process.signing_hints import SumHint, PathHint -from hsms.process.unsigned_spend import UnsignedSpend +from hsms.core.signing_hints import SumHint, PathHint +from hsms.core.unsigned_spend import UnsignedSpend from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE, puzzle_for_public_key_and_hidden_puzzle, diff --git a/hsms/cmds/hsmmerge.py b/hsms/cmds/hsmmerge.py index 1447a59..b276a0b 100644 --- a/hsms/cmds/hsmmerge.py +++ b/hsms/cmds/hsmmerge.py @@ -5,8 +5,8 @@ from chia_base.bls12_381 import BLSSignature from chia_base.core import SpendBundle -from hsms.process.unsigned_spend import UnsignedSpend -from hsms.process.sign import generate_synthetic_offset_signatures +from hsms.core.unsigned_spend import UnsignedSpend +from hsms.core.sign import generate_synthetic_offset_signatures from hsms.util.qrint_encoding import a2b_qrint diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index 48e3b8d..300f408 100644 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -17,8 +17,8 @@ import segno from hsms.consensus.conditions import conditions_by_opcode +from hsms.core.unsigned_spend import UnsignedSpend from hsms.process.sign import conditions_for_coin_spend, sign -from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles import conlang from hsms.util.byte_chunks import ChunkAssembler from hsms.util.qrint_encoding import a2b_qrint, b2a_qrint diff --git a/hsms/cmds/poser_gen.py b/hsms/cmds/poser_gen.py index f5f1523..5f3b5f8 100644 --- a/hsms/cmds/poser_gen.py +++ b/hsms/cmds/poser_gen.py @@ -6,8 +6,8 @@ from chia_base.bls12_381 import BLSPublicKey from chia_base.core.coin import Coin +from hsms.core.unsigned_spend import UnsignedSpend from hsms.streamables.coin_spend import CoinSpend -from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( puzzle_for_synthetic_public_key, solution_for_conditions, diff --git a/hsms/core/signing_hints.py b/hsms/core/signing_hints.py index 6721bf4..bc8d809 100644 --- a/hsms/core/signing_hints.py +++ b/hsms/core/signing_hints.py @@ -10,8 +10,18 @@ class SumHint(Frugal): public_keys: list[BLSPublicKey] synthetic_offset: BLSSecretExponent + def final_public_key(self) -> BLSPublicKey: + return sum(self.public_keys, start=self.synthetic_offset.public_key()) + @dataclass class PathHint(Frugal): root_public_key: BLSPublicKey path: list[int] + + def public_key(self) -> BLSPublicKey: + return self.root_public_key.child_for_path(self.path) + + +PathHints = dict[BLSPublicKey, PathHint] +SumHints = dict[BLSPublicKey, SumHint] diff --git a/hsms/core/unsigned_spend.py b/hsms/core/unsigned_spend.py index d309983..3ffe80d 100644 --- a/hsms/core/unsigned_spend.py +++ b/hsms/core/unsigned_spend.py @@ -1,9 +1,14 @@ from dataclasses import dataclass, field +from chia_base.bls12_381 import BLSPublicKey, BLSSignature from chia_base.core import Coin, CoinSpend from clvm_rs import Program +from hsms.util.clvm_serde import ( + to_program_for_type, + from_program_for_type, +) from .signing_hints import PathHint, SumHint @@ -34,10 +39,17 @@ def from_storage( ] +@dataclass +class SignatureInfo: + signature: BLSSignature + partial_public_key: BLSPublicKey + final_public_key: BLSPublicKey + message: bytes + + @dataclass class UnsignedSpend: coin_spends: list[CoinSpend] = field( - default_factory=list, metadata=dict( key="c", alt_serde_type=( @@ -59,3 +71,11 @@ class UnsignedSpend: default=b"", metadata=dict(key="a"), ) + + +TO_PROGRAM = to_program_for_type(UnsignedSpend) +FROM_PROGRAM = from_program_for_type(UnsignedSpend) + + +UnsignedSpend.__bytes__ = lambda self: bytes(TO_PROGRAM(self)) +UnsignedSpend.from_bytes = lambda blob: FROM_PROGRAM(Program.from_bytes(blob)) diff --git a/hsms/process/sign.py b/hsms/process/sign.py index 6be269f..7edfdb1 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -8,12 +8,11 @@ from clvm_rs import Program +from hsms.core.signing_hints import SumHint, SumHints, PathHint, PathHints +from hsms.core.unsigned_spend import SignatureInfo, UnsignedSpend from hsms.consensus.conditions import conditions_by_opcode from hsms.puzzles.conlang import AGG_SIG_ME, AGG_SIG_UNSAFE -from .signing_hints import SumHint, SumHints, PathHint, PathHints -from .unsigned_spend import SignatureInfo, UnsignedSpend - MAX_COST = 1 << 34 diff --git a/hsms/process/unsigned_spend.py b/hsms/process/unsigned_spend.py deleted file mode 100644 index 7b0735a..0000000 --- a/hsms/process/unsigned_spend.py +++ /dev/null @@ -1,83 +0,0 @@ -from dataclasses import dataclass -from typing import List - -from chia_base.atoms import bytes32 -from chia_base.bls12_381 import BLSPublicKey, BLSSignature -from chia_base.core import Coin, CoinSpend - -from clvm_rs import Program - -from hsms.process.signing_hints import SumHint, PathHint -from hsms.util.clvm_serialization import ( - as_atom, - as_int, - clvm_to_list, - no_op, - transform_dict, - transform_dict_by_key, - transform_as_struct, -) - - -@dataclass -class SignatureInfo: - signature: BLSSignature - partial_public_key: BLSPublicKey - final_public_key: BLSPublicKey - message: bytes - - -@dataclass -class UnsignedSpend: - coin_spends: List[CoinSpend] - sum_hints: List[SumHint] - path_hints: List[PathHint] - agg_sig_me_network_suffix: bytes32 - - def as_program(self): - as_clvm = [("a", self.agg_sig_me_network_suffix)] - cs_as_clvm = [ - [_.coin.parent_coin_info, _.puzzle_reveal, _.coin.amount, _.solution] - for _ in self.coin_spends - ] - as_clvm.append(("c", cs_as_clvm)) - sh_as_clvm = [_.as_program() for _ in self.sum_hints] - as_clvm.append(("s", sh_as_clvm)) - ph_as_clvm = [_.as_program() for _ in self.path_hints] - as_clvm.append(("p", ph_as_clvm)) - self_as_program = Program.to(as_clvm) - return self_as_program - - @classmethod - def from_program(cls, program) -> "UnsignedSpend": - d = transform_dict(program, transform_dict_by_key(UNSIGNED_SPEND_TRANSFORMER)) - return cls(d["c"], d.get("s", []), d.get("p", []), d["a"]) - - def __bytes__(self): - return bytes(self.as_program()) - - @classmethod - def from_bytes(cls, blob) -> "UnsignedSpend": - return cls.from_program(Program.from_bytes(blob)) - - -def coin_spend_from_program(program: Program) -> CoinSpend: - struct = transform_as_struct(program, as_atom, no_op, as_int, no_op) - parent_coin_info, puzzle_reveal, amount, solution = struct - return CoinSpend( - Coin( - parent_coin_info, - puzzle_reveal.tree_hash(), - amount, - ), - puzzle_reveal, - solution, - ) - - -UNSIGNED_SPEND_TRANSFORMER = { - "c": lambda x: clvm_to_list(x, coin_spend_from_program), - "s": lambda x: clvm_to_list(x, SumHint.from_program), - "p": lambda x: clvm_to_list(x, PathHint.from_program), - "a": lambda x: x.atom, -} diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 8ef9b51..15b93d4 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -67,16 +67,6 @@ def serialize_tuple(items): return serialize_tuple -def serialize_for_pair_tuple(origin, args, *etc): - write_items = [type_tree(_, *etc) for _ in args] - - def serialize_tuple(items): - as_tuple = tuple(wi(_) for wi, _ in zip(write_items, items)) - return Program.to(as_tuple) - - return serialize_tuple - - def ser_for_union(origin, args, *etc): item_type = optional_from_union(args) if item_type is not None: @@ -179,7 +169,7 @@ def ser(item): d.append((key, call(a))) v.append(d) - return ser_tuple(tuple(v)) + return ser_tuple(v) return ser diff --git a/hsms/util/clvm_serialization.py b/tests/legacy/clvm_serialization.py similarity index 100% rename from hsms/util/clvm_serialization.py rename to tests/legacy/clvm_serialization.py diff --git a/hsms/process/signing_hints.py b/tests/legacy/signing_hints.py similarity index 97% rename from hsms/process/signing_hints.py rename to tests/legacy/signing_hints.py index affe46b..ce98f05 100644 --- a/hsms/process/signing_hints.py +++ b/tests/legacy/signing_hints.py @@ -3,7 +3,7 @@ from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent -from hsms.util.clvm_serialization import ( +from .clvm_serialization import ( clvm_to_list_of_ints, clvm_to_list, ) diff --git a/tests/legacy/unsigned_spend.py b/tests/legacy/unsigned_spend.py index 7b0735a..2c93689 100644 --- a/tests/legacy/unsigned_spend.py +++ b/tests/legacy/unsigned_spend.py @@ -7,8 +7,7 @@ from clvm_rs import Program -from hsms.process.signing_hints import SumHint, PathHint -from hsms.util.clvm_serialization import ( +from .clvm_serialization import ( as_atom, as_int, clvm_to_list, @@ -17,6 +16,7 @@ transform_dict_by_key, transform_as_struct, ) +from .signing_hints import SumHint, PathHint @dataclass diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 6d4dbfa..4f0fcf2 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -7,9 +7,12 @@ from clvm_rs import Program -from hsms.process.signing_hints import ( - SumHint as LegacySH, - PathHint as LegacyPH, +from hsms.core.signing_hints import SumHint, PathHint +from hsms.core.unsigned_spend import ( + UnsignedSpend, + from_storage, + to_storage, + TO_PROGRAM, ) from hsms.util.clvm_serde import ( to_program_for_type, @@ -17,8 +20,11 @@ Frugal, tuple_frugal, ) -from .core.signing_hints import SumHint, PathHint -from .core.unsigned_spend import UnsignedSpend, from_storage, to_storage +from .legacy.signing_hints import ( + SumHint as LegacySH, + PathHint as LegacyPH, +) +from .legacy.unsigned_spend import UnsignedSpend as LegacyUS def test_ser(): @@ -210,8 +216,6 @@ def test_interop_coin_spend(): def test_interop_unsigned_spend(): - from hsms.process.unsigned_spend import UnsignedSpend as LegacyUS - cs_list = [rnd_coin_spend(_) for _ in range(10)] secret_keys = [BLSSecretExponent.from_int(_) for _ in range(5)] @@ -230,6 +234,10 @@ def test_interop_unsigned_spend(): lus = LegacyUS(cs_list[0:3], [lsh], [lph], suffix) print(bytes(lus.as_program()).hex()) tp = to_program_for_type(UnsignedSpend) + # TODO: remove this temporary hack + # we add `__bytes__` to `UnsignedSpend` which changes how `to_program_for_type` + # works + tp = TO_PROGRAM fp = from_program_for_type(UnsignedSpend) p = tp(us) print(bytes(p).hex()) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 45096a4..daf70c1 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -4,6 +4,8 @@ from chia_base.core import Coin, CoinSpend, SpendBundle +from hsms.core.signing_hints import SumHint, PathHint +from hsms.core.unsigned_spend import UnsignedSpend from hsms.debug.debug_spend_bundle import debug_spend_bundle from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE, @@ -12,8 +14,6 @@ calculate_synthetic_offset, ) from hsms.process.sign import sign, generate_synthetic_offset_signatures -from hsms.process.signing_hints import SumHint, PathHint -from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.conlang import CREATE_COIN from hsms.util.byte_chunks import ( ChunkAssembler, From 5dd7540563916faea80c8b490f2038e9d10a0fb7 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 24 Oct 2023 18:11:30 -0700 Subject: [PATCH 31/40] Revamp `TypeTree` --- hsms/util/clvm_serde.py | 64 ++++++++++++++--------------- hsms/util/type_tree.py | 91 ++++++++++++++++++++++------------------- 2 files changed, 81 insertions(+), 74 deletions(-) diff --git a/hsms/util/clvm_serde.py b/hsms/util/clvm_serde.py index 15b93d4..3df9e40 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/util/clvm_serde.py @@ -5,7 +5,7 @@ from clvm_rs import Program -from .type_tree import type_tree +from .type_tree import TypeTree class EncodingError(BaseException): @@ -41,8 +41,8 @@ def read_int(p: Program) -> int: return Program.int_from_bytes(read_bytes(p)) -def serialize_for_list(origin, args, *etc): - write_item = type_tree(args[0], *etc) +def serialize_for_list(origin, args, type_tree: TypeTree): + write_item = type_tree(args[0]) def serialize_list(items): return Program.to([write_item(_) for _ in items]) @@ -50,8 +50,8 @@ def serialize_list(items): return serialize_list -def serialize_for_tuple(origin, args, *etc): - write_items = [type_tree(_, *etc) for _ in args] +def serialize_for_tuple(origin, args, type_tree: TypeTree): + write_items = [type_tree(_) for _ in args] def serialize_tuple(items): return Program.to( @@ -67,10 +67,10 @@ def serialize_tuple(items): return serialize_tuple -def ser_for_union(origin, args, *etc): +def ser_for_union(origin, args, type_tree: TypeTree): item_type = optional_from_union(args) if item_type is not None: - write_item = type_tree(item_type, *etc) + write_item = type_tree(item_type) def serialize_optional(f, item): c = 0 if item is None else 1 @@ -81,8 +81,8 @@ def serialize_optional(f, item): return serialize_optional -def ser_for_tuple_frugal(origin, args, *etc): - streaming_calls = [type_tree(_, *etc) for _ in args] +def ser_for_tuple_frugal(origin, args, type_tree: TypeTree): + streaming_calls = [type_tree(_, ) for _ in args] def ser(item): if len(item) != len(streaming_calls): @@ -108,7 +108,7 @@ def ser(item): } -def types_for_fields(t: type, call_morpher, *etc): +def types_for_fields(t: type, call_morpher, type_tree: TypeTree): # split into key-based and location-based key_based = [] @@ -124,13 +124,13 @@ def types_for_fields(t: type, call_morpher, *etc): else: alt_serde_type = m.get("alt_serde_type") storage_type = alt_serde_type[0] if alt_serde_type else f.type - call = type_tree(storage_type, *etc) + call = type_tree(storage_type) key_based.append((key, f.name, call_morpher(call, f), default_value)) return location_based, key_based -def ser_dataclass(t: type, *etc): +def ser_dataclass(t: type, type_tree: TypeTree): def morph_call(call, f): alt_serde_type = f.metadata.get("alt_serde_type") if alt_serde_type: @@ -142,7 +142,7 @@ def f(x): return f return call - location_based, key_based = types_for_fields(t, morph_call, *etc) + location_based, key_based = types_for_fields(t, morph_call, type_tree) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) @@ -153,7 +153,7 @@ def f(x): names = tuple(f.name for f in location_based) - ser_tuple = type_tree(tuple_type, *etc) + ser_tuple = type_tree(tuple_type) def ser(item): # convert to a tuple @@ -187,7 +187,7 @@ def fail_ser(t, *args): raise TypeError(f"can't process {t}") -def deser_dataclass(t: type, *etc): +def deser_dataclass(t: type, type_tree: TypeTree): def morph_call(call, f): alt_serde_type = f.metadata.get("alt_serde_type") if alt_serde_type: @@ -199,7 +199,7 @@ def f(x): return f return call - location_based, key_based = types_for_fields(t, morph_call, *etc) + location_based, key_based = types_for_fields(t, morph_call, type_tree) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) @@ -208,7 +208,7 @@ def f(x): if key_based or issubclass(t, Frugal): tuple_type = GenericAlias(tuple_frugal, types) - de_tuple = type_tree(tuple_type, *etc) + de_tuple = type_tree(tuple_type) if key_based: @@ -249,27 +249,26 @@ def fail_deser(t, *args): def to_program_for_type(t: type) -> Callable[[dict[str, Any]], Program]: - return type_tree( - t, + return TypeTree( {Program: lambda x: x}, SERIALIZER_COMPOUND_TYPE_LOOKUP, fail_ser, - ) + )(t) -def deser_for_list(origin, args, *etc): - read_item = type_tree(args[0], *etc) +def deser_for_list(origin, args, type_tree: TypeTree): + read_item = type_tree(args[0]) - def deserialize_list(p: Program) -> tuple[int, Any]: + def deserialize_list(p: Program) -> list: return [read_item(_) for _ in p.as_iter()] return deserialize_list -def deser_for_tuple(origin, args, *etc): - read_items = [type_tree(_, *etc) for _ in args] +def deser_for_tuple(origin, args, type_tree: TypeTree): + read_items = [type_tree(_) for _ in args] - def deserialize_tuple(p: Program) -> tuple[int, Any]: + def deserialize_tuple(p: Program) -> tuple[Any, ...]: items = list(p.as_iter()) if len(items) != len(read_items): raise EncodingError("wrong size program") @@ -278,8 +277,8 @@ def deserialize_tuple(p: Program) -> tuple[int, Any]: return deserialize_tuple -def de_for_tuple_frugal(origin, args, *etc): - read_items = [type_tree(_, *etc) for _ in args] +def de_for_tuple_frugal(origin, args, type_tree: TypeTree): + read_items = [type_tree(_) for _ in args] def de(p: Program) -> tuple[int, Any]: args = [] @@ -297,10 +296,10 @@ def de(p: Program) -> tuple[int, Any]: return de -def deser_for_union(origin, args, *etc): +def deser_for_union(origin, args, type_tree: TypeTree): item_type = optional_from_union(args) if item_type is not None: - read_item = type_tree(item_type, *etc) + read_item = type_tree(item_type) def deserialize_optional(f: BinaryIO) -> tuple[int, Any]: v = uint8.parse(f) @@ -329,8 +328,7 @@ def optional_from_union(args: type) -> Callable[[dict[str, Any]], bytes] | None: def from_program_for_type(t: type) -> Callable[[memoryview, int], tuple[int, Any]]: - return type_tree( - t, + return TypeTree( { bytes: read_bytes, bytes32: read_bytes, @@ -340,7 +338,7 @@ def from_program_for_type(t: type) -> Callable[[memoryview, int], tuple[int, Any }, DESERIALIZER_COMPOUND_TYPE_LOOKUP, fail_deser, - ) + )(t) def merging_function_for_callable_parameters(f: Callable) -> Callable: diff --git a/hsms/util/type_tree.py b/hsms/util/type_tree.py index d42f187..6b84255 100644 --- a/hsms/util/type_tree.py +++ b/hsms/util/type_tree.py @@ -1,44 +1,53 @@ -from typing import Callable, get_origin, get_args, TypeVar - - -_T = TypeVar("_T") -SimpleTypeLookup = dict[type, _T] -CompoundLookup = dict[ - type, - Callable[ - [type, type, SimpleTypeLookup[_T], "CompoundLookup[_T]", "OtherHandler[_T]"], _T - ], -] -OtherHandler = Callable[ - [type, SimpleTypeLookup[_T], CompoundLookup[_T], "OtherHandler[_T]"], _T -] - - -def type_tree( - t: type, - simple_type_lookup: SimpleTypeLookup[_T], - compound_type_lookup: CompoundLookup[ - Callable[ - [type, type, SimpleTypeLookup[_T], CompoundLookup[_T], OtherHandler[_T]], _T - ] - ], - other_f: OtherHandler[_T], -) -> _T: - """ - Recursively descend a "type tree" invoking the appropriate functions. +from dataclasses import dataclass + +from types import GenericAlias +from typing import ( + Callable, + get_origin, + get_args, + Optional, + Type, + TypeVar, + Generic, +) + + +Gtype = type | GenericAlias +T = TypeVar("T") +SimpleTypeLookup = dict[Type, T] +CompoundLookup = dict[Type, Callable[[Type, tuple[Type, ...], "TypeTree"], T]] +OtherHandler = Callable[[Type, "TypeTree"], Optional[T]] - `simple_type_lookup`: a type to callable look-up. Must return a `_T` value. - `compound_type_lookup`: recursively handle compound types like `list` and `tuple`. - `other_f`: a function to take a type and return - This function is helpful for run-time building a complex function that operates - on a complex type out of simpler functions that operate on base types. +@dataclass +class TypeTree(Generic[T]): """ - origin = get_origin(t) - f = compound_type_lookup.get(origin) - if f: - return f(origin, get_args(t), simple_type_lookup, compound_type_lookup, other_f) - f = simple_type_lookup.get(t) - if f: - return f - return other_f(t, simple_type_lookup, compound_type_lookup, other_f) + `simple_type_lookup`: a type to callable look-up. Must return a `T` value. + `compound_type_lookup`: recursively handle compound types like `list` and `tuple`. + `other_f`: a function to take a type and return a `T` value + """ + + simple_type_lookup: SimpleTypeLookup[T] + compound_lookup: CompoundLookup[T] + other_handler: OtherHandler[T] + + def __call__(self, t: Type) -> T: + """ + Recursively descend a "type tree" invoking the appropriate functions. + + This function is helpful for run-time building a complex function that operates + on a complex type out of simpler functions that operate on base types. + """ + origin: None | Type = get_origin(t) + if origin is not None: + f = self.compound_lookup.get(origin) + if f: + args: tuple[Type, ...] = get_args(t) + return f(origin, args, self) + g = self.simple_type_lookup.get(t) + if g: + return g + r = self.other_handler(t, self) + if r: + return r + raise ValueError(f"unable to handle type {t}") From 19b833a0d01e228a389d4abd0d17f902a9635cc7 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 24 Oct 2023 19:51:20 -0700 Subject: [PATCH 32/40] move `clvm_serde` --- hsms/{util/clvm_serde.py => clvm_serde/__init__.py} | 2 +- hsms/core/signing_hints.py | 2 +- hsms/core/unsigned_spend.py | 2 +- tests/test_clvm_serde.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) rename hsms/{util/clvm_serde.py => clvm_serde/__init__.py} (99%) diff --git a/hsms/util/clvm_serde.py b/hsms/clvm_serde/__init__.py similarity index 99% rename from hsms/util/clvm_serde.py rename to hsms/clvm_serde/__init__.py index 3df9e40..6073fe9 100644 --- a/hsms/util/clvm_serde.py +++ b/hsms/clvm_serde/__init__.py @@ -5,7 +5,7 @@ from clvm_rs import Program -from .type_tree import TypeTree +from hsms.util.type_tree import TypeTree class EncodingError(BaseException): diff --git a/hsms/core/signing_hints.py b/hsms/core/signing_hints.py index bc8d809..d70491f 100644 --- a/hsms/core/signing_hints.py +++ b/hsms/core/signing_hints.py @@ -2,7 +2,7 @@ from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent -from hsms.util.clvm_serde import Frugal +from hsms.clvm_serde import Frugal @dataclass diff --git a/hsms/core/unsigned_spend.py b/hsms/core/unsigned_spend.py index 3ffe80d..add1312 100644 --- a/hsms/core/unsigned_spend.py +++ b/hsms/core/unsigned_spend.py @@ -5,7 +5,7 @@ from clvm_rs import Program -from hsms.util.clvm_serde import ( +from hsms.clvm_serde import ( to_program_for_type, from_program_for_type, ) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 4f0fcf2..651b0bf 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -7,6 +7,12 @@ from clvm_rs import Program +from hsms.clvm_serde import ( + to_program_for_type, + from_program_for_type, + Frugal, + tuple_frugal, +) from hsms.core.signing_hints import SumHint, PathHint from hsms.core.unsigned_spend import ( UnsignedSpend, @@ -14,12 +20,6 @@ to_storage, TO_PROGRAM, ) -from hsms.util.clvm_serde import ( - to_program_for_type, - from_program_for_type, - Frugal, - tuple_frugal, -) from .legacy.signing_hints import ( SumHint as LegacySH, PathHint as LegacyPH, From 6e449bf699fc0829e2d37c1c61e0db8c96b705ba Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 6 Nov 2023 15:30:20 -0800 Subject: [PATCH 33/40] Fix tests, mypy --- hsms/clvm/disasm.py | 2 +- hsms/clvm_serde/__init__.py | 147 ++++++++---------- hsms/cmds/hsm_test_spend.py | 2 +- hsms/cmds/hsmmerge.py | 2 +- hsms/cmds/hsms.py | 8 +- hsms/cmds/poser_gen.py | 3 +- hsms/consensus/conditions.py | 2 +- hsms/core/unsigned_spend.py | 13 +- hsms/debug/debug_spend_bundle.py | 2 +- hsms/process/sign.py | 10 +- hsms/puzzles/p2_conditions.py | 4 +- .../p2_delegated_puzzle_or_hidden_puzzle.py | 4 +- tests/cmds/hsm_test_1.txt | 2 +- tests/test_clvm_serde.py | 2 +- tests/test_lifecycle.py | 5 +- 15 files changed, 92 insertions(+), 116 deletions(-) diff --git a/hsms/clvm/disasm.py b/hsms/clvm/disasm.py index 4a2353e..62d3e3f 100644 --- a/hsms/clvm/disasm.py +++ b/hsms/clvm/disasm.py @@ -1,6 +1,6 @@ import io -from clvm_rs import Program +from clvm_rs import Program # type: ignore # this differs from clvm_tools in that it adds the single quote diff --git a/hsms/clvm_serde/__init__.py b/hsms/clvm_serde/__init__.py index 6073fe9..7e8dd2a 100644 --- a/hsms/clvm_serde/__init__.py +++ b/hsms/clvm_serde/__init__.py @@ -1,11 +1,14 @@ from dataclasses import is_dataclass, fields, MISSING -from typing import Any, BinaryIO, Callable, GenericAlias +from types import GenericAlias +from typing import Any, Callable, Type -from chia_base.atoms import bytes32, uint8 +from chia_base.atoms import bytes32 +from chia_base.meta.type_tree import ArgsType, CompoundLookup, OriginArgsType, TypeTree -from clvm_rs import Program +from clvm_rs import Program # type: ignore -from hsms.util.type_tree import TypeTree +ToProgram = Callable[[Any], Program] +FromProgram = Callable[[Program], Any] class EncodingError(BaseException): @@ -41,7 +44,7 @@ def read_int(p: Program) -> int: return Program.int_from_bytes(read_bytes(p)) -def serialize_for_list(origin, args, type_tree: TypeTree): +def serialize_for_list(origin, args, type_tree: TypeTree) -> Program: write_item = type_tree(args[0]) def serialize_list(items): @@ -50,7 +53,7 @@ def serialize_list(items): return serialize_list -def serialize_for_tuple(origin, args, type_tree: TypeTree): +def serialize_for_tuple(origin, args, type_tree: TypeTree) -> Program: write_items = [type_tree(_) for _ in args] def serialize_tuple(items): @@ -67,22 +70,13 @@ def serialize_tuple(items): return serialize_tuple -def ser_for_union(origin, args, type_tree: TypeTree): - item_type = optional_from_union(args) - if item_type is not None: - write_item = type_tree(item_type) - - def serialize_optional(f, item): - c = 0 if item is None else 1 - f.write(bytes([c])) - if item is not None: - write_item(f, item) - - return serialize_optional - - -def ser_for_tuple_frugal(origin, args, type_tree: TypeTree): - streaming_calls = [type_tree(_, ) for _ in args] +def ser_for_tuple_frugal(origin, args, type_tree: TypeTree) -> Program: + streaming_calls = [ + type_tree( + _, + ) + for _ in args + ] def ser(item): if len(item) != len(streaming_calls): @@ -99,12 +93,10 @@ def ser(item): return ser -SERIALIZER_COMPOUND_TYPE_LOOKUP = { +SERIALIZER_COMPOUND_TYPE_LOOKUP: CompoundLookup[ToProgram] = { list: serialize_for_list, tuple: serialize_for_tuple, tuple_frugal: ser_for_tuple_frugal, - # Union: to_program_for_union, - # UnionType: to_program_for_union, } @@ -130,7 +122,7 @@ def types_for_fields(t: type, call_morpher, type_tree: TypeTree): return location_based, key_based -def ser_dataclass(t: type, type_tree: TypeTree): +def ser_dataclass(origin: Type, args_type: ArgsType, type_tree: TypeTree) -> Program: def morph_call(call, f): alt_serde_type = f.metadata.get("alt_serde_type") if alt_serde_type: @@ -142,13 +134,15 @@ def f(x): return f return call - location_based, key_based = types_for_fields(t, morph_call, type_tree) + location_based, key_based = types_for_fields(origin, morph_call, type_tree) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) if key_based: - types = types + (list[tuple_frugal[str, Program]],) - if key_based or issubclass(t, Frugal): + types = types + ( + GenericAlias(list, (GenericAlias(tuple_frugal, (str, Program)),)), + ) + if key_based or issubclass(origin, Frugal): tuple_type = GenericAlias(tuple_frugal, types) names = tuple(f.name for f in location_based) @@ -174,20 +168,22 @@ def ser(item): return ser -def fail_ser(t, *args): - if t in [str, bytes, int]: +def fail_ser( + origin: Type, args_type: ArgsType, type_tree: TypeTree +) -> None | ToProgram: + if origin in [str, bytes, int]: return Program.to - if hasattr(t, "__bytes__"): - return lambda x: Program.to(bytes(x)) + if is_dataclass(origin): + return ser_dataclass(origin, args_type, type_tree) - if is_dataclass(t): - return ser_dataclass(t, *args) + if hasattr(origin, "__bytes__"): + return lambda x: Program.to(bytes(x)) - raise TypeError(f"can't process {t}") + return None -def deser_dataclass(t: type, type_tree: TypeTree): +def deser_dataclass(origin: Type, args_type: ArgsType, type_tree: TypeTree): def morph_call(call, f): alt_serde_type = f.metadata.get("alt_serde_type") if alt_serde_type: @@ -199,13 +195,15 @@ def f(x): return f return call - location_based, key_based = types_for_fields(t, morph_call, type_tree) + location_based, key_based = types_for_fields(origin, morph_call, type_tree) types = tuple(f.type for f in location_based) tuple_type = GenericAlias(tuple, types) if key_based: - types = types + (list[tuple_frugal[str, Program]],) - if key_based or issubclass(t, Frugal): + types = types + ( + GenericAlias(list, GenericAlias(tuple_frugal, (str, Program))), + ) + if key_based or issubclass(origin, Frugal): tuple_type = GenericAlias(tuple_frugal, types) de_tuple = type_tree(tuple_type) @@ -227,30 +225,30 @@ def de(p: Program): ) kwargs[name] = default_value - return t(*args, **kwargs) + return origin(*args, **kwargs) else: def de(p: Program): the_tuple = de_tuple(p) - return t(*the_tuple) + return origin(*the_tuple) return de -def fail_deser(t, *args): - if is_dataclass(t): - return deser_dataclass(t, *args) +def fail_deser(origin: Type, args_type: ArgsType, type_tree: TypeTree): + if is_dataclass(origin): + return deser_dataclass(origin, args_type, type_tree) - if hasattr(t, "from_bytes"): - return lambda p: t.from_bytes(p.atom) + if hasattr(origin, "from_bytes"): + return lambda p: origin.from_bytes(p.atom) - raise TypeError(f"can't process {t}") + return None -def to_program_for_type(t: type) -> Callable[[dict[str, Any]], Program]: +def to_program_for_type(t: type) -> Callable[[Any], Program]: return TypeTree( - {Program: lambda x: x}, + {(Program, None): lambda x: x}, SERIALIZER_COMPOUND_TYPE_LOOKUP, fail_ser, )(t) @@ -280,7 +278,7 @@ def deserialize_tuple(p: Program) -> tuple[Any, ...]: def de_for_tuple_frugal(origin, args, type_tree: TypeTree): read_items = [type_tree(_) for _ in args] - def de(p: Program) -> tuple[int, Any]: + def de(p: Program) -> tuple[Any, ...]: args = [] todo = list(reversed(read_items)) while todo: @@ -296,46 +294,23 @@ def de(p: Program) -> tuple[int, Any]: return de -def deser_for_union(origin, args, type_tree: TypeTree): - item_type = optional_from_union(args) - if item_type is not None: - read_item = type_tree(item_type) - - def deserialize_optional(f: BinaryIO) -> tuple[int, Any]: - v = uint8.parse(f) - if v == 0: - return None - return read_item(f) - - return deserialize_optional - raise TypeError("can't handle unions not of the form `A | None`") - - -DESERIALIZER_COMPOUND_TYPE_LOOKUP = { +DESERIALIZER_COMPOUND_TYPE_LOOKUP: CompoundLookup[FromProgram] = { list: deser_for_list, tuple: deser_for_tuple, tuple_frugal: de_for_tuple_frugal, - # Union: deser_for_union, - # UnionType: deser_for_union, } -def optional_from_union(args: type) -> Callable[[dict[str, Any]], bytes] | None: - tn = type(None) - if len(args) == 2 and tn in args: - return args[0 if args[1] is tn else 1] - return None - - -def from_program_for_type(t: type) -> Callable[[memoryview, int], tuple[int, Any]]: +def from_program_for_type(t: type) -> FromProgram: + simple_lookup: dict[OriginArgsType, FromProgram] = { + (bytes, None): read_bytes, + (bytes32, None): read_bytes, + (str, None): read_str, + (int, None): read_int, + (Program, None): lambda x: x, + } return TypeTree( - { - bytes: read_bytes, - bytes32: read_bytes, - str: read_str, - int: read_int, - Program: lambda x: x, - }, + simple_lookup, DESERIALIZER_COMPOUND_TYPE_LOOKUP, fail_deser, )(t) @@ -344,8 +319,8 @@ def from_program_for_type(t: type) -> Callable[[memoryview, int], tuple[int, Any def merging_function_for_callable_parameters(f: Callable) -> Callable: parameter_names = [k for k in f.__annotations__.keys() if k != "return"] - def merging_function(*args, **kwargs) -> tuple[Any]: - merged_args = args + tuple(kwargs[_] for _ in parameter_names[len(args):]) + def merging_function(*args, **kwargs) -> tuple[Any, ...]: + merged_args = args + tuple(kwargs[_] for _ in parameter_names[len(args) :]) return merged_args return merging_function diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index 11cf12e..4713395 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -3,7 +3,7 @@ import sys import zlib -from clvm_rs import Program +from clvm_rs import Program # type: ignore from chia_base.bls12_381 import BLSPublicKey from chia_base.core import Coin, CoinSpend diff --git a/hsms/cmds/hsmmerge.py b/hsms/cmds/hsmmerge.py index b276a0b..ecac84b 100644 --- a/hsms/cmds/hsmmerge.py +++ b/hsms/cmds/hsmmerge.py @@ -6,7 +6,7 @@ from chia_base.core import SpendBundle from hsms.core.unsigned_spend import UnsignedSpend -from hsms.core.sign import generate_synthetic_offset_signatures +from hsms.process.sign import generate_synthetic_offset_signatures from hsms.util.qrint_encoding import a2b_qrint diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index 300f408..6e11d67 100644 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -12,7 +12,7 @@ from chia_base.bls12_381 import BLSSecretExponent, BLSSignature from chia_base.util.bech32 import bech32_encode -from clvm_rs import Program +from clvm_rs import Program # type: ignore import segno @@ -30,11 +30,9 @@ def unsigned_spend_from_blob(blob: bytes) -> UnsignedSpend: try: uncompressed_blob = zlib.decompress(blob) - program = Program.from_bytes(uncompressed_blob) - return UnsignedSpend.from_program(program) + return UnsignedSpend.from_program_bytes(uncompressed_blob) except Exception: - program = Program.from_bytes(blob) - return UnsignedSpend.from_program(program) + return UnsignedSpend.from_program_bytes(blob) def create_unsigned_spend_pipeline( diff --git a/hsms/cmds/poser_gen.py b/hsms/cmds/poser_gen.py index 5f3b5f8..912cc36 100644 --- a/hsms/cmds/poser_gen.py +++ b/hsms/cmds/poser_gen.py @@ -4,10 +4,9 @@ from chia_base.bls12_381 import BLSPublicKey -from chia_base.core.coin import Coin +from chia_base.core import Coin, CoinSpend from hsms.core.unsigned_spend import UnsignedSpend -from hsms.streamables.coin_spend import CoinSpend from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( puzzle_for_synthetic_public_key, solution_for_conditions, diff --git a/hsms/consensus/conditions.py b/hsms/consensus/conditions.py index 5cb258e..a937f28 100644 --- a/hsms/consensus/conditions.py +++ b/hsms/consensus/conditions.py @@ -1,6 +1,6 @@ from typing import Dict, List -from clvm_rs import Program +from clvm_rs import Program # type: ignore def conditions_by_opcode(conditions: Program) -> Dict[int, List[Program]]: diff --git a/hsms/core/unsigned_spend.py b/hsms/core/unsigned_spend.py index add1312..ee5fcc4 100644 --- a/hsms/core/unsigned_spend.py +++ b/hsms/core/unsigned_spend.py @@ -3,7 +3,7 @@ from chia_base.bls12_381 import BLSPublicKey, BLSSignature from chia_base.core import Coin, CoinSpend -from clvm_rs import Program +from clvm_rs import Program # type: ignore from hsms.clvm_serde import ( to_program_for_type, @@ -72,10 +72,13 @@ class UnsignedSpend: metadata=dict(key="a"), ) + def __bytes__(self): + return bytes(TO_PROGRAM(self)) -TO_PROGRAM = to_program_for_type(UnsignedSpend) -FROM_PROGRAM = from_program_for_type(UnsignedSpend) + @classmethod + def from_bytes(cls, blob: bytes): + return FROM_PROGRAM(Program.from_bytes(blob)) -UnsignedSpend.__bytes__ = lambda self: bytes(TO_PROGRAM(self)) -UnsignedSpend.from_bytes = lambda blob: FROM_PROGRAM(Program.from_bytes(blob)) +TO_PROGRAM = to_program_for_type(UnsignedSpend) +FROM_PROGRAM = from_program_for_type(UnsignedSpend) diff --git a/hsms/debug/debug_spend_bundle.py b/hsms/debug/debug_spend_bundle.py index f0f05dc..c53aab2 100644 --- a/hsms/debug/debug_spend_bundle.py +++ b/hsms/debug/debug_spend_bundle.py @@ -3,7 +3,7 @@ from chia_base.core import Coin from chia_base.util.std_hash import std_hash -from clvm_rs import Program +from clvm_rs import Program # type: ignore from hsms.clvm.disasm import disassemble as bu_disassemble, KEYWORD_FROM_ATOM from hsms.consensus.conditions import conditions_by_opcode diff --git a/hsms/process/sign.py b/hsms/process/sign.py index 7edfdb1..d7c048e 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -6,7 +6,7 @@ from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent from chia_base.core import CoinSpend -from clvm_rs import Program +from clvm_rs import Program # type: ignore from hsms.core.signing_hints import SumHint, SumHints, PathHint, PathHints from hsms.core.unsigned_spend import SignatureInfo, UnsignedSpend @@ -23,7 +23,7 @@ class SignatureMetadata: message: bytes -CONDITIONS_FOR_COIN_SPEND: Dict[CoinSpend, Program] = WeakKeyDictionary() +CONDITIONS_FOR_COIN_SPEND: WeakKeyDictionary[CoinSpend, Program] = WeakKeyDictionary() def conditions_for_coin_spend(coin_spend: CoinSpend) -> Program: @@ -134,7 +134,7 @@ def verify_pairs_for_conditions( agg_sig_unsafe_conditions = d.get(AGG_SIG_UNSAFE, []) for condition in agg_sig_unsafe_conditions: - yield BLSPublicKey.from_bytes(condition.at("rf"), hexbytes(condition.at("rrf"))) + yield BLSPublicKey.from_bytes(condition.at("rf")), hexbytes(condition.at("rrf")) def secret_key_for_public_key( @@ -152,10 +152,10 @@ def partial_signature_metadata_for_hsm( conditions: Program, sum_hints: SumHints, path_hints: PathHints, - agg_sig_me_network_suffix: bytes32, + agg_sig_me_message_suffix: bytes, ) -> Iterable[SignatureMetadata]: for final_public_key, message in verify_pairs_for_conditions( - conditions, agg_sig_me_network_suffix + conditions, agg_sig_me_message_suffix, ): sum_hint = sum_hints.get(final_public_key) or SumHint( [final_public_key], BLSSecretExponent.zero() diff --git a/hsms/puzzles/p2_conditions.py b/hsms/puzzles/p2_conditions.py index 1fa38ff..708a7e9 100644 --- a/hsms/puzzles/p2_conditions.py +++ b/hsms/puzzles/p2_conditions.py @@ -10,9 +10,9 @@ the doctor ordered. """ -from clvm_rs import Program +from clvm_rs import Program # type: ignore -from chialisp_puzzles import load_puzzle +from chialisp_puzzles import load_puzzle # type: ignore MOD = load_puzzle("p2_conditions") diff --git a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py index 3411ce1..da8d480 100644 --- a/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py +++ b/hsms/puzzles/p2_delegated_puzzle_or_hidden_puzzle.py @@ -59,12 +59,12 @@ import hashlib -from clvm_rs import Program +from clvm_rs import Program # type: ignore from chia_base.atoms import bytes32 from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent -from chialisp_puzzles import load_puzzle +from chialisp_puzzles import load_puzzle # type: ignore from .p2_conditions import puzzle_for_conditions diff --git a/tests/cmds/hsm_test_1.txt b/tests/cmds/hsm_test_1.txt index ac836a2..9feb424 100644 --- a/tests/cmds/hsm_test_1.txt +++ b/tests/cmds/hsm_test_1.txt @@ -1,2 +1,2 @@ hsm_test_spend bls12381jlca8fe3jltegf54vwxyl2dvplpk3rz0ja6tjpdpfcar79cm43vxc40g8luh5xh0lva0qzkmytrtk7l5wds -440471080944257678950586942698633700843411528606405041796537532860668245089043282051850937366804449198522799103833368097055342179256864400369754902247372269393304897873246637209585249171701502818174975837601687680701653118001738367692894771185761748674898168395377524515985896212582336215423798385782601253703651612056400429730016400778599185922362459376649249668907746182592152896640468053613610739695422546963587518862381953895702154968323421174291271502578615538724660615085744621375010673086445263211287957384099273797585073923017814186584483032793013206669704009775863817567881164226447845539164783508526070162180061298339296048192113275829112041735441207977805149278535109579050392830110564366531442537088541926014925577308013521611516823327354736163337843313005293421047844749276080652092077654857098021738132239400149988911858993407505308292892112175755314230215025725024920352154345692459209566694302898377182643728972131451602112628300051371204986367858560375097766473119274813170370898276396632955389837044578312544762982332716703172743330954284806430244972274816864047954121403451333758709845675915640537535224947919679145902075435612852486432225978261514620729547128263967254512269136691250806603720000000000 +34047108094429339442807125285572002104699150339493615642302298331325668207875883183821102455461113748856476876234896576601094508539476824369964397311997709279460852325654110975221758502511249348942334738923244968558468940116612003201409478404982969004110762927806041979432788169363295746361556336686181213582376130655959523667599330485237394041191036332635740772617352553229315713371078363675803598550425903406363417194205726515448440839392128121975028113368054279294040417422067670427318281582457036485693829101878095809205907336619585884516708088747154853234268426821058360238077501402489966158252317212923722642512490421615240512625076442633980498929434964108173909532587390857531815997165592399951504396748773684332447839907840104304127985896393638203795015606284258340736157951427191554750549272307923279215936744060356400561828199118463837059005721517230334159819957566696825314981781340480522127072369137373709196631315368572438111889219544980474680953363825443265705460550074640230202913816129805219059100646177900664268004742921415431662378504584087599800238248741600757420391491213533304786003118930496265741700569822326207203349055073315223840613571013535341092555098972749055937343082691486777995264 diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 651b0bf..5e0782e 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -5,7 +5,7 @@ from chia_base.bls12_381 import BLSSecretExponent from chia_base.core import Coin, CoinSpend -from clvm_rs import Program +from clvm_rs import Program # type: ignore from hsms.clvm_serde import ( to_program_for_type, diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index daf70c1..fba692c 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -3,6 +3,7 @@ from tests.generate import se_generate, bytes32_generate, uint256_generate from chia_base.core import Coin, CoinSpend, SpendBundle +from chia_base.cbincode.util import from_bytes, to_bytes from hsms.core.signing_hints import SumHint, PathHint from hsms.core.unsigned_spend import UnsignedSpend @@ -77,7 +78,7 @@ def test_lifecycle(): ] for coin in coins: - c = Coin.from_bytes(bytes(coin)) + c = from_bytes(Coin, to_bytes(coin)) assert c == coin # the destination puzzle hashes are nonsense, but that's okay @@ -154,7 +155,7 @@ def test_lifecycle(): signatures = signatures_A + signatures_B spend_bundle = create_spend_bundle(unsigned_spend, signatures) - sb2 = SpendBundle.from_bytes(bytes(spend_bundle)) + sb2 = from_bytes(SpendBundle, to_bytes(spend_bundle)) assert sb2 == spend_bundle validates = debug_spend_bundle(spend_bundle) From fd6450aaf1b54ed05bc1dd96f7311e6caa5fc952 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 6 Nov 2023 17:27:35 -0800 Subject: [PATCH 34/40] pin to `chia_base` --- pyproject.toml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 59f367a..23d13f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,14 @@ build-backend = "setuptools.build_meta" [project] name = "hsms" description = "Hardware security module simulator for chia bls12_381 signatures" -authors = [{name = "Richard Kiss", email = "him@richardkiss.com"}] -license = {file = "LICENSE"} +authors = [{ name = "Richard Kiss", email = "him@richardkiss.com" }] +license = { file = "LICENSE" } readme = "README.md" -dependencies = ["segno==1.4.1", "chia_base==0.1.3", "chialisp_puzzles==0.1.0"] +dependencies = [ + "segno==1.4.1", + "chia_base @ git+https://github.com/richardkiss/chia_base@3401836913a0c42e8ad0f8c896ee1ff065843064#egg=chia_base", + "chialisp_puzzles==0.1.0", +] # version is defined with `setuptools_scm` dynamic = ["version"] From 3ee14755ea46676f91085cad09df8a65e1696191 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Tue, 7 Nov 2023 13:45:56 -0800 Subject: [PATCH 35/40] Support `from __future__ import annotations` --- hsms/clvm_serde/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/hsms/clvm_serde/__init__.py b/hsms/clvm_serde/__init__.py index 7e8dd2a..55fda3a 100644 --- a/hsms/clvm_serde/__init__.py +++ b/hsms/clvm_serde/__init__.py @@ -1,6 +1,6 @@ from dataclasses import is_dataclass, fields, MISSING from types import GenericAlias -from typing import Any, Callable, Type +from typing import Any, Callable, Type, get_type_hints from chia_base.atoms import bytes32 from chia_base.meta.type_tree import ArgsType, CompoundLookup, OriginArgsType, TypeTree @@ -105,17 +105,19 @@ def types_for_fields(t: type, call_morpher, type_tree: TypeTree): key_based = [] location_based = [] + type_hints = get_type_hints(t) for f in fields(t): + type_hint = type_hints[f.name] default_value = ( f.default if f.default_factory is MISSING else f.default_factory() ) m = f.metadata key = m.get("key") if key is None: - location_based.append(f) + location_based.append((f.name, type_hint)) else: alt_serde_type = m.get("alt_serde_type") - storage_type = alt_serde_type[0] if alt_serde_type else f.type + storage_type = alt_serde_type[0] if alt_serde_type else type_hint call = type_tree(storage_type) key_based.append((key, f.name, call_morpher(call, f), default_value)) @@ -136,7 +138,7 @@ def f(x): location_based, key_based = types_for_fields(origin, morph_call, type_tree) - types = tuple(f.type for f in location_based) + types = tuple(type_hint for name, type_hint in location_based) tuple_type = GenericAlias(tuple, types) if key_based: types = types + ( @@ -145,7 +147,7 @@ def f(x): if key_based or issubclass(origin, Frugal): tuple_type = GenericAlias(tuple_frugal, types) - names = tuple(f.name for f in location_based) + names = tuple(name for name, type_hint in location_based) ser_tuple = type_tree(tuple_type) @@ -197,7 +199,7 @@ def f(x): location_based, key_based = types_for_fields(origin, morph_call, type_tree) - types = tuple(f.type for f in location_based) + types = tuple(type_hint for name, type_hint in location_based) tuple_type = GenericAlias(tuple, types) if key_based: types = types + ( From c9963d8158a77baeff4095fbc63f288b3a203d4d Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Wed, 8 Nov 2023 18:09:09 -0800 Subject: [PATCH 36/40] Interop with subtypes of `int`, `bytes`, `str` --- hsms/clvm_serde/__init__.py | 19 ++++++++++++------- tests/test_clvm_serde.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/hsms/clvm_serde/__init__.py b/hsms/clvm_serde/__init__.py index 55fda3a..287b989 100644 --- a/hsms/clvm_serde/__init__.py +++ b/hsms/clvm_serde/__init__.py @@ -32,7 +32,7 @@ class Frugal: # def read_bytes(p: Program) -> bytes: if p.atom is None: - raise EncodingError("expected atom for str") + raise EncodingError("expected atom") return p.atom @@ -173,7 +173,7 @@ def ser(item): def fail_ser( origin: Type, args_type: ArgsType, type_tree: TypeTree ) -> None | ToProgram: - if origin in [str, bytes, int]: + if issubclass(origin, (str, bytes, int)): return Program.to if is_dataclass(origin): @@ -239,11 +239,20 @@ def de(p: Program): def fail_deser(origin: Type, args_type: ArgsType, type_tree: TypeTree): + if issubclass(origin, int): + return read_int + + if issubclass(origin, bytes): + return read_bytes + + if issubclass(origin, str): + return read_str + if is_dataclass(origin): return deser_dataclass(origin, args_type, type_tree) if hasattr(origin, "from_bytes"): - return lambda p: origin.from_bytes(p.atom) + return lambda p: origin.from_bytes(read_bytes(p)) return None @@ -305,10 +314,6 @@ def de(p: Program) -> tuple[Any, ...]: def from_program_for_type(t: type) -> FromProgram: simple_lookup: dict[OriginArgsType, FromProgram] = { - (bytes, None): read_bytes, - (bytes32, None): read_bytes, - (str, None): read_str, - (int, None): read_int, (Program, None): lambda x: x, } return TypeTree( diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 5e0782e..6486523 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -163,6 +163,43 @@ class Bar(Frugal): assert p1 == p +def test_subclasses(): + # `chia-blockchain` has several int subclasses + + class Foo(int): + pass + + tp = to_program_for_type(Foo) + fp = from_program_for_type(Foo) + for v in [-1000, -1, 0, 1, 100, 25678]: + foo = Foo(v) + p = Program.to(v) + assert tp(foo) == p + assert fp(p) == foo + + class Bar(bytes): + pass + + tp = to_program_for_type(Bar) + fp = from_program_for_type(Bar) + for v in [b"", b"a", b"hello", b"a84kdhb8" * 500]: + bar = Bar(v) + p = Program.to(v) + assert tp(bar) == p + assert fp(p) == bar + + class Baz(str): + pass + + tp = to_program_for_type(Baz) + fp = from_program_for_type(Baz) + for v in ["", "a", "hello", "a84kdhb8" * 500]: + baz = Baz(v) + p = Program.to(v) + assert tp(baz) == p + assert fp(p) == baz + + def rnd_coin_spend(seed: int) -> CoinSpend: r = random.Random(seed) parent = r.randbytes(32) From 331a3314688c64a63e4443ec2c9a4c11046c61fa Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 9 Nov 2023 11:00:27 -0800 Subject: [PATCH 37/40] coverage --- hsms/clvm_serde/__init__.py | 16 +++------- hsms/cmds/hsm_dump_us.py | 22 -------------- hsms/cmds/hsms.py | 5 ++-- hsms/process/sign.py | 12 ++++---- hsms/util/byte_chunks.py | 1 - pyproject.toml | 2 +- tests/cmds/hsm_test_2.txt | 2 ++ tests/cmds/hsm_test_3.txt | 2 ++ tests/legacy/clvm_serialization.py | 14 --------- tests/legacy/unsigned_spend.py | 7 ----- tests/test_clvm_serde.py | 47 ++++++++++++++++++++++++++++-- tests/test_cmds.py | 1 - 12 files changed, 62 insertions(+), 69 deletions(-) create mode 100644 tests/cmds/hsm_test_2.txt create mode 100644 tests/cmds/hsm_test_3.txt diff --git a/hsms/clvm_serde/__init__.py b/hsms/clvm_serde/__init__.py index 287b989..573cef4 100644 --- a/hsms/clvm_serde/__init__.py +++ b/hsms/clvm_serde/__init__.py @@ -2,7 +2,6 @@ from types import GenericAlias from typing import Any, Callable, Type, get_type_hints -from chia_base.atoms import bytes32 from chia_base.meta.type_tree import ArgsType, CompoundLookup, OriginArgsType, TypeTree from clvm_rs import Program # type: ignore @@ -11,7 +10,7 @@ FromProgram = Callable[[Program], Any] -class EncodingError(BaseException): +class EncodingError(ValueError): pass @@ -57,6 +56,9 @@ def serialize_for_tuple(origin, args, type_tree: TypeTree) -> Program: write_items = [type_tree(_) for _ in args] def serialize_tuple(items): + item_list = list(items) + if len(item_list) != len(write_items): + raise EncodingError("incorrect number of items in tuple") return Program.to( [ write_f(item) @@ -321,13 +323,3 @@ def from_program_for_type(t: type) -> FromProgram: DESERIALIZER_COMPOUND_TYPE_LOOKUP, fail_deser, )(t) - - -def merging_function_for_callable_parameters(f: Callable) -> Callable: - parameter_names = [k for k in f.__annotations__.keys() if k != "return"] - - def merging_function(*args, **kwargs) -> tuple[Any, ...]: - merged_args = args + tuple(kwargs[_] for _ in parameter_names[len(args) :]) - return merged_args - - return merging_function diff --git a/hsms/cmds/hsm_dump_us.py b/hsms/cmds/hsm_dump_us.py index c4599ac..0cf0181 100644 --- a/hsms/cmds/hsm_dump_us.py +++ b/hsms/cmds/hsm_dump_us.py @@ -2,7 +2,6 @@ import sys import zlib -from hsms.clvm.disasm import disassemble from hsms.cmds.hsms import summarize_unsigned_spend from hsms.core.unsigned_spend import UnsignedSpend from hsms.util.qrint_encoding import a2b_qrint @@ -17,27 +16,6 @@ def file_or_string(p) -> str: return text -def dump_coin_spend(coin_spend): - coin = coin_spend.coin - print(f"parent coin: {coin.parent_coin_info.hex()}") - print(f" amount: {coin.amount}") - print(" puzzle:", end="") - if coin_spend.puzzle_reveal.tree_hash() == coin.puzzle_hash: - print(f" {disassemble(coin_spend.puzzle_reveal)}") - else: - print(" ** bad puzzle reveal") - print(f" solution: {disassemble(coin_spend.solution)}") - - -def dump_unsigned_spend(unsigned_spend): - count = len(unsigned_spend.coin_spends) - print() - print(f"coin count: {count}") - print() - for coin_spend in unsigned_spend.coin_spends: - dump_coin_spend(coin_spend) - - def fromhex_or_qrint(s: str) -> bytes: try: return a2b_qrint(s) diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index 6e11d67..c350131 100644 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -12,7 +12,6 @@ from chia_base.bls12_381 import BLSSecretExponent, BLSSignature from chia_base.util.bech32 import bech32_encode -from clvm_rs import Program # type: ignore import segno @@ -185,9 +184,9 @@ def create_parser() -> argparse.ArgumentParser: return parser -def main(): +def main(argv=sys.argv[1:]): parser = create_parser() - args = parser.parse_args() + args = parser.parse_args(argv) return hsms(args, parser) diff --git a/hsms/process/sign.py b/hsms/process/sign.py index d7c048e..4d8616f 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -1,8 +1,8 @@ from dataclasses import dataclass -from typing import Dict, Iterable, List, Optional, Tuple +from typing import Iterable, List, Optional, Tuple from weakref import WeakKeyDictionary -from chia_base.atoms import bytes32, hexbytes +from chia_base.atoms import hexbytes from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent from chia_base.core import CoinSpend @@ -128,8 +128,9 @@ def verify_pairs_for_conditions( agg_sig_me_conditions = d.get(AGG_SIG_ME, []) for condition in agg_sig_me_conditions: - yield BLSPublicKey.from_bytes(condition.at("rf").atom), hexbytes( - condition.at("rrf").atom + agg_sig_me_message_suffix + yield ( + BLSPublicKey.from_bytes(condition.at("rf").atom), + hexbytes(condition.at("rrf").atom + agg_sig_me_message_suffix), ) agg_sig_unsafe_conditions = d.get(AGG_SIG_UNSAFE, []) @@ -155,7 +156,8 @@ def partial_signature_metadata_for_hsm( agg_sig_me_message_suffix: bytes, ) -> Iterable[SignatureMetadata]: for final_public_key, message in verify_pairs_for_conditions( - conditions, agg_sig_me_message_suffix, + conditions, + agg_sig_me_message_suffix, ): sum_hint = sum_hints.get(final_public_key) or SumHint( [final_public_key], BLSSecretExponent.zero() diff --git a/hsms/util/byte_chunks.py b/hsms/util/byte_chunks.py index 07f6d35..0bb24fd 100644 --- a/hsms/util/byte_chunks.py +++ b/hsms/util/byte_chunks.py @@ -74,7 +74,6 @@ def assemble(self) -> bytes: return bytes(self) - def blob_for_chunks(chunks: List[bytes]) -> bytes: return ChunkAssembler(chunks).assemble() diff --git a/pyproject.toml b/pyproject.toml index 23d13f1..0d08a87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ license = { file = "LICENSE" } readme = "README.md" dependencies = [ "segno==1.4.1", - "chia_base @ git+https://github.com/richardkiss/chia_base@3401836913a0c42e8ad0f8c896ee1ff065843064#egg=chia_base", + "chia_base @ git+https://github.com/richardkiss/chia_base@0adbf1fef35451310c37670b01ac9a4d26b8466d#egg=chia_base", "chialisp_puzzles==0.1.0", ] # version is defined with `setuptools_scm` diff --git a/tests/cmds/hsm_test_2.txt b/tests/cmds/hsm_test_2.txt new file mode 100644 index 0000000..639cdc1 --- /dev/null +++ b/tests/cmds/hsm_test_2.txt @@ -0,0 +1,2 @@ +hsm_test_spend -H bls12381jlca8fe3jltegf54vwxyl2dvplpk3rz0ja6tjpdpfcar79cm43vxc40g8luh5xh0lva0qzkmytrtk7l5wds +ffff63ffffa0e47125968b3b71049fbc4802d1e40a71ea1359decfabacf70b34588037d4ff0cffff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a074598a29b394264f997d444687d6e6f38dfe8df4787abbc01181715511caf94ddc118d369917815be6d7bfa151d712ff018080ff01ffff80ffff01ffff33ffa0f6152f2ad8a93dc0f8f825f2a8d162d6da46e81f5fe481ff76b4f8384a677886ff8602ba7def300080ffff33ffa0991e4b5f669e57fb49aa4632b0eb0bec0a684d0ab5edac4da47c7a504c6a62abff8601d1a94a20008080ff80808080ffff73ffffffb0b7de0f748b947fb43ed36c330325144670fa448b6fa0cef1c33da1fc7c28b3482251c4719a6166fb88dd9a676d0d6fba80a0129e8af7687e4a65a2f9343ce7966afbf7a77bb8d51e5919165a53879c9ba86d80ffff70ffffb097f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bbff80ff018080ffff61a0ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb80 diff --git a/tests/cmds/hsm_test_3.txt b/tests/cmds/hsm_test_3.txt new file mode 100644 index 0000000..f053d96 --- /dev/null +++ b/tests/cmds/hsm_test_3.txt @@ -0,0 +1,2 @@ +hsm_test_spend -n bls12381jlca8fe3jltegf54vwxyl2dvplpk3rz0ja6tjpdpfcar79cm43vxc40g8luh5xh0lva0qzkmytrtk7l5wds +685898547198565002692504498428345682513324300881025262527509440165595744144706648430228531008724142198589414392080530230380593551044290969599645919577585734195190335416319853068907185894737918334078143858786198475161767036467616642427976281643118103050033686527845676341583466616240536862783755185260764550666234328457471855703501785896775640805304359858889828837580883517528775617213988140821559052794311810305858967551632207984680135262232053686287985857730046434160639436207591185571015678523349039858941237585888982883758088351752877561721394226562155905279855677287785896775640402651175833617716002694881598585772994858990195285735179528556577278010079539184583382960453642053512158680244556550072720220139387472127847632181654009772549662126959167925281527093822192145920561577956985694278288512855664230300671078478589516797025803646658230803761044253052571111460336620800313217622014796628604913964257838462019495748093441610737287677789645605589552802384318178683401821231591103652028751203247324274506032051012285859371287135174656431187532900336865278516534271850680163282974767518272196827042792205811818092204678697373763448282381181587462183470151086063023937952815617357249501215797417446629399004992699364701125342729485615276293201867117896565275800447561837170687379165065716068973086656277665176725830884088918963659731941742159227256074073830928559320576513635777987511937231024562149051800010695721282147463376343655921708122791677658989851032327496185736763414781812556618676517144155447680000000000 diff --git a/tests/legacy/clvm_serialization.py b/tests/legacy/clvm_serialization.py index 3f90184..46850b9 100644 --- a/tests/legacy/clvm_serialization.py +++ b/tests/legacy/clvm_serialization.py @@ -61,24 +61,10 @@ def clvm_to_list( return r -def clvm_list_of_bytes_to_list( - items: Program, from_bytes_f: Callable[[bytes], T] -) -> List[T]: - return clvm_to_list(items, lambda obj: from_bytes_f(obj.atom)) - - def clvm_to_list_of_ints(items: Program) -> List[int]: return clvm_to_list(items, lambda obj: Program.to(obj).as_int()) -def clvm_list_to_dict( - items: Program, - from_clvm_f_to_kv: Callable[[Program, Program], Tuple[K, V]], -) -> Dict[K, V]: - r = clvm_to_list(items, lambda obj: from_clvm_f_to_kv(obj.pair[0], obj.pair[1])) - return dict(r) - - def no_op(x): return x diff --git a/tests/legacy/unsigned_spend.py b/tests/legacy/unsigned_spend.py index 2c93689..b792ccb 100644 --- a/tests/legacy/unsigned_spend.py +++ b/tests/legacy/unsigned_spend.py @@ -53,13 +53,6 @@ def from_program(cls, program) -> "UnsignedSpend": d = transform_dict(program, transform_dict_by_key(UNSIGNED_SPEND_TRANSFORMER)) return cls(d["c"], d.get("s", []), d.get("p", []), d["a"]) - def __bytes__(self): - return bytes(self.as_program()) - - @classmethod - def from_bytes(cls, blob) -> "UnsignedSpend": - return cls.from_program(Program.from_bytes(blob)) - def coin_spend_from_program(program: Program) -> CoinSpend: struct = transform_as_struct(program, as_atom, no_op, as_int, no_op) diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index 6486523..c0bfae7 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -2,16 +2,19 @@ import random +import pytest + from chia_base.bls12_381 import BLSSecretExponent from chia_base.core import Coin, CoinSpend from clvm_rs import Program # type: ignore from hsms.clvm_serde import ( - to_program_for_type, from_program_for_type, - Frugal, + to_program_for_type, tuple_frugal, + EncodingError, + Frugal, ) from hsms.core.signing_hints import SumHint, PathHint from hsms.core.unsigned_spend import ( @@ -222,6 +225,7 @@ def test_interop_sum_hint(): print(lsh) assert lsh.public_keys == sum_hint.public_keys assert lsh.synthetic_offset == sum_hint.synthetic_offset + assert lsh.final_public_key() == sum_hint.final_public_key() sh1 = fp(p) assert sum_hint == sh1 @@ -231,7 +235,7 @@ def test_interop_sum_hint(): def test_interop_path_hint(): public_key = BLSSecretExponent.from_int(1).public_key() - ints = [1, 5, 91, 29484, -99] + ints = [1, 5, 91, 29484, 399] path_hint = PathHint(public_key, ints) tp = to_program_for_type(PathHint) fp = from_program_for_type(PathHint) @@ -241,6 +245,7 @@ def test_interop_path_hint(): print(lph) assert lph.root_public_key == path_hint.root_public_key assert lph.path == path_hint.path + assert lph.public_key() == path_hint.public_key() ph1 = fp(p) assert path_hint == ph1 @@ -321,3 +326,39 @@ class Foo: assert foo.b == "hello" p1 = tp(foo) assert p1 == p + + +def test_failures(): + fp = from_program_for_type(bytes) + with pytest.raises(EncodingError): + fp(Program.to([1, 2])) + + tp = to_program_for_type(tuple_frugal[int]) + with pytest.raises(EncodingError): + tp((1, 2)) + + fp = from_program_for_type(tuple_frugal[int]) + with pytest.raises(EncodingError): + fp(Program.to((1, 2))) + + tp = to_program_for_type(tuple[int]) + with pytest.raises(EncodingError): + tp((1, 2)) + + fp = from_program_for_type(tuple[int]) + with pytest.raises(EncodingError): + fp(Program.to([1, 2])) + + with pytest.raises(ValueError): + from_program_for_type(object) + + with pytest.raises(ValueError): + to_program_for_type(object) + + @dataclass + class Foo: + a: int = field(metadata=dict(key="a")) + + fp = from_program_for_type(Foo) + with pytest.raises(EncodingError): + fp(Program.to([])) diff --git a/tests/test_cmds.py b/tests/test_cmds.py index 739bf43..8340514 100644 --- a/tests/test_cmds.py +++ b/tests/test_cmds.py @@ -45,7 +45,6 @@ def get_test_cases(path): class TestCmds(unittest.TestCase): def invoke_tool(self, cmd_line): - # capture io stdout_buffer = io.StringIO() stderr_buffer = io.StringIO() From 287b3aad260d50597e60e3fe80d3f0c3dc587554 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 9 Nov 2023 16:02:20 -0800 Subject: [PATCH 38/40] py38 --- hsms/clvm_serde/__init__.py | 10 +++++----- hsms/core/signing_hints.py | 10 ++++++---- hsms/core/unsigned_spend.py | 15 ++++++++------- hsms/process/sign.py | 2 +- pyproject.toml | 4 ++-- tests/test_clvm_serde.py | 38 ++++++++++++++++++++++--------------- 6 files changed, 45 insertions(+), 34 deletions(-) diff --git a/hsms/clvm_serde/__init__.py b/hsms/clvm_serde/__init__.py index 573cef4..4a28759 100644 --- a/hsms/clvm_serde/__init__.py +++ b/hsms/clvm_serde/__init__.py @@ -1,7 +1,7 @@ from dataclasses import is_dataclass, fields, MISSING -from types import GenericAlias -from typing import Any, Callable, Type, get_type_hints +from typing import Any, Callable, List, Optional, Tuple, Type, get_type_hints +from chia_base.meta.py38 import GenericAlias from chia_base.meta.type_tree import ArgsType, CompoundLookup, OriginArgsType, TypeTree from clvm_rs import Program # type: ignore @@ -174,7 +174,7 @@ def ser(item): def fail_ser( origin: Type, args_type: ArgsType, type_tree: TypeTree -) -> None | ToProgram: +) -> Optional[ToProgram]: if issubclass(origin, (str, bytes, int)): return Program.to @@ -279,7 +279,7 @@ def deserialize_list(p: Program) -> list: def deser_for_tuple(origin, args, type_tree: TypeTree): read_items = [type_tree(_) for _ in args] - def deserialize_tuple(p: Program) -> tuple[Any, ...]: + def deserialize_tuple(p: Program) -> Tuple[Any, ...]: items = list(p.as_iter()) if len(items) != len(read_items): raise EncodingError("wrong size program") @@ -291,7 +291,7 @@ def deserialize_tuple(p: Program) -> tuple[Any, ...]: def de_for_tuple_frugal(origin, args, type_tree: TypeTree): read_items = [type_tree(_) for _ in args] - def de(p: Program) -> tuple[Any, ...]: + def de(p: Program) -> Tuple[Any, ...]: args = [] todo = list(reversed(read_items)) while todo: diff --git a/hsms/core/signing_hints.py b/hsms/core/signing_hints.py index d70491f..6582504 100644 --- a/hsms/core/signing_hints.py +++ b/hsms/core/signing_hints.py @@ -1,5 +1,7 @@ from dataclasses import dataclass +from typing import Dict, List + from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent from hsms.clvm_serde import Frugal @@ -7,7 +9,7 @@ @dataclass class SumHint(Frugal): - public_keys: list[BLSPublicKey] + public_keys: List[BLSPublicKey] synthetic_offset: BLSSecretExponent def final_public_key(self) -> BLSPublicKey: @@ -17,11 +19,11 @@ def final_public_key(self) -> BLSPublicKey: @dataclass class PathHint(Frugal): root_public_key: BLSPublicKey - path: list[int] + path: List[int] def public_key(self) -> BLSPublicKey: return self.root_public_key.child_for_path(self.path) -PathHints = dict[BLSPublicKey, PathHint] -SumHints = dict[BLSPublicKey, SumHint] +PathHints = Dict[BLSPublicKey, PathHint] +SumHints = Dict[BLSPublicKey, SumHint] diff --git a/hsms/core/unsigned_spend.py b/hsms/core/unsigned_spend.py index ee5fcc4..c96de38 100644 --- a/hsms/core/unsigned_spend.py +++ b/hsms/core/unsigned_spend.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from typing import List, Tuple from chia_base.bls12_381 import BLSPublicKey, BLSSignature from chia_base.core import Coin, CoinSpend @@ -12,13 +13,13 @@ from .signing_hints import PathHint, SumHint -CSTuple = tuple[bytes, Program, int, Program] -SerdeCoinSpends = list[CSTuple] +CSTuple = Tuple[bytes, Program, int, Program] +SerdeCoinSpends = List[CSTuple] def to_storage( coin_spend_tuples: SerdeCoinSpends, -) -> list[CoinSpend]: +) -> List[CoinSpend]: return [ CoinSpend(Coin(_[0], _[1].tree_hash(), _[2]), _[1], _[3]) for _ in coin_spend_tuples @@ -26,7 +27,7 @@ def to_storage( def from_storage( - coin_spends: list[CoinSpend], + coin_spends: List[CoinSpend], ) -> SerdeCoinSpends: return [ ( @@ -49,7 +50,7 @@ class SignatureInfo: @dataclass class UnsignedSpend: - coin_spends: list[CoinSpend] = field( + coin_spends: List[CoinSpend] = field( metadata=dict( key="c", alt_serde_type=( @@ -59,11 +60,11 @@ class UnsignedSpend: ), ), ) - sum_hints: list[SumHint] = field( + sum_hints: List[SumHint] = field( default_factory=list, metadata=dict(key="s"), ) - path_hints: list[PathHint] = field( + path_hints: List[PathHint] = field( default_factory=list, metadata=dict(key="p"), ) diff --git a/hsms/process/sign.py b/hsms/process/sign.py index 4d8616f..c4a8d48 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -23,7 +23,7 @@ class SignatureMetadata: message: bytes -CONDITIONS_FOR_COIN_SPEND: WeakKeyDictionary[CoinSpend, Program] = WeakKeyDictionary() +CONDITIONS_FOR_COIN_SPEND: WeakKeyDictionary = WeakKeyDictionary() def conditions_for_coin_spend(coin_spend: CoinSpend) -> Program: diff --git a/pyproject.toml b/pyproject.toml index 0d08a87..bb91821 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ license = { file = "LICENSE" } readme = "README.md" dependencies = [ "segno==1.4.1", - "chia_base @ git+https://github.com/richardkiss/chia_base@0adbf1fef35451310c37670b01ac9a4d26b8466d#egg=chia_base", - "chialisp_puzzles==0.1.0", + "chia_base @ git+https://github.com/richardkiss/chia_base@91d9dba09e316714cf4bc86505587437fe442e48#egg=chia_base", + "chialisp_puzzles @ git+https://github.com/richardkiss/chialisp_puzzles@eb97cd844b094e6dcd595b5bd6522d3a78eb692c#egg=chialisp_puzzles", ] # version is defined with `setuptools_scm` dynamic = ["version"] diff --git a/tests/test_clvm_serde.py b/tests/test_clvm_serde.py index c0bfae7..4ed9c4b 100644 --- a/tests/test_clvm_serde.py +++ b/tests/test_clvm_serde.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from typing import List, Tuple import random @@ -6,7 +7,7 @@ from chia_base.bls12_381 import BLSSecretExponent from chia_base.core import Coin, CoinSpend - +from chia_base.meta.py38 import GenericAlias from clvm_rs import Program # type: ignore from hsms.clvm_serde import ( @@ -45,7 +46,7 @@ def test_ser(): assert p == Program.to(b) assert fpb(p) == b - tt = tuple[str, bytes, int] + tt = Tuple[str, bytes, int] tp = to_program_for_type(tt) fp = from_program_for_type(tt) for t in [ @@ -54,7 +55,7 @@ def test_ser(): ]: assert tp(t) == Program.to(list(t)) - tt = list[tuple[int, str]] + tt = List[Tuple[int, str]] tp = to_program_for_type(tt) fp = from_program_for_type(tt) for t in [ @@ -65,7 +66,7 @@ def test_ser(): assert tp(t) == prhs assert fp(tp(t)) == t - tt = tuple_frugal[int, str] + tt = GenericAlias(tuple_frugal, (int, str)) tp = to_program_for_type(tt) fp = from_program_for_type(tt) for v in [ @@ -92,7 +93,7 @@ class Foo: @dataclass class Nested: - a: list[Foo] + a: List[Foo] b: int tp = to_program_for_type(Nested) @@ -153,7 +154,7 @@ class Foo(Frugal): class Bar(Frugal): a: int b: str - c: list[int] + c: List[int] tp = to_program_for_type(Bar) fp = from_program_for_type(Bar) @@ -203,12 +204,18 @@ class Baz(str): assert fp(p) == baz +def randbytes(r: random.Random, count: int) -> bytes: + if hasattr(r, "randbytes"): + return r.randbytes(count) + return bytes([r.randint(0, 255) for _ in range(count)]) + + def rnd_coin_spend(seed: int) -> CoinSpend: r = random.Random(seed) - parent = r.randbytes(32) - puzzle = Program.to(f"puz: {r.randbytes(5).hex()}") + parent = randbytes(r, 32) + puzzle = Program.to(f"puz: {randbytes(r, 5).hex()}") amount = r.randint(1, 1000) * int(1e3) - solution = Program.to(f"sol: {r.randbytes(10).hex()}") + solution = Program.to(f"sol: {randbytes(r, 10).hex()}") coin = Coin(parent, puzzle.tree_hash(), amount) return CoinSpend(coin, puzzle, solution) @@ -230,7 +237,7 @@ def test_interop_sum_hint(): assert sum_hint == sh1 -CoinSpendTuple = tuple[bytes, Program, int, Program] +CoinSpendTuple = Tuple[bytes, Program, int, Program] def test_interop_path_hint(): @@ -297,7 +304,7 @@ def test_interop_unsigned_spend(): def test_tuple_frugal(): - Foo = tuple_frugal[int, str, bytes] + Foo = GenericAlias(tuple_frugal, (int, str, bytes)) tp = to_program_for_type(Foo) fp = from_program_for_type(Foo) @@ -333,19 +340,20 @@ def test_failures(): with pytest.raises(EncodingError): fp(Program.to([1, 2])) - tp = to_program_for_type(tuple_frugal[int]) + tfi = GenericAlias(tuple_frugal, (int,)) + tp = to_program_for_type(tfi) with pytest.raises(EncodingError): tp((1, 2)) - fp = from_program_for_type(tuple_frugal[int]) + fp = from_program_for_type(tfi) with pytest.raises(EncodingError): fp(Program.to((1, 2))) - tp = to_program_for_type(tuple[int]) + tp = to_program_for_type(Tuple[int]) with pytest.raises(EncodingError): tp((1, 2)) - fp = from_program_for_type(tuple[int]) + fp = from_program_for_type(Tuple[int]) with pytest.raises(EncodingError): fp(Program.to([1, 2])) From 41337c583f71a261af940632a9e0194817a9e1d5 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Thu, 9 Nov 2023 16:21:24 -0800 Subject: [PATCH 39/40] `from_bytes` --- hsms/cmds/hsms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hsms/cmds/hsms.py b/hsms/cmds/hsms.py index c350131..b42115f 100644 --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -29,9 +29,9 @@ def unsigned_spend_from_blob(blob: bytes) -> UnsignedSpend: try: uncompressed_blob = zlib.decompress(blob) - return UnsignedSpend.from_program_bytes(uncompressed_blob) + return UnsignedSpend.from_bytes(uncompressed_blob) except Exception: - return UnsignedSpend.from_program_bytes(blob) + return UnsignedSpend.from_bytes(blob) def create_unsigned_spend_pipeline( From 906c92ce5ecb49ac591836bfb8349e412a986175 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Jan 2024 14:10:33 -0800 Subject: [PATCH 40/40] Pin to actual version and small helpful change --- hsms/clvm_serde/__init__.py | 4 ++-- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hsms/clvm_serde/__init__.py b/hsms/clvm_serde/__init__.py index 4a28759..76d5cd5 100644 --- a/hsms/clvm_serde/__init__.py +++ b/hsms/clvm_serde/__init__.py @@ -297,8 +297,8 @@ def de(p: Program) -> Tuple[Any, ...]: while todo: des = todo.pop() if todo: - v = p.pair[0] - p = p.pair[1] + v = Program.to(p.pair[0]) + p = Program.to(p.pair[1]) else: v = p args.append(des(v)) diff --git a/pyproject.toml b/pyproject.toml index bb91821..fc25cfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ license = { file = "LICENSE" } readme = "README.md" dependencies = [ "segno==1.4.1", - "chia_base @ git+https://github.com/richardkiss/chia_base@91d9dba09e316714cf4bc86505587437fe442e48#egg=chia_base", - "chialisp_puzzles @ git+https://github.com/richardkiss/chialisp_puzzles@eb97cd844b094e6dcd595b5bd6522d3a78eb692c#egg=chialisp_puzzles", + "chia_base==0.1.4", + "chialisp_puzzles==0.1.1", ] # version is defined with `setuptools_scm` dynamic = ["version"]