diff --git a/README.md b/README.md index 7b3994b..7acdb0d 100644 --- a/README.md +++ b/README.md @@ -28,10 +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` - - -enscons -------- - -This package uses [enscons](https://github.com/dholth/enscons) -which uses [SCons](https://scons.org/) to build rather than the commonly used `setuptools`. +- `hsm_dump_us` - debug utility to dump information about an `UnsignedSpend` diff --git a/SConstruct b/SConstruct deleted file mode 100644 index d7adb29..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")))["tool"]["enscons"] -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/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/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 new file mode 100644 index 0000000..76d5cd5 --- /dev/null +++ b/hsms/clvm_serde/__init__.py @@ -0,0 +1,325 @@ +from dataclasses import is_dataclass, fields, MISSING +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 + +ToProgram = Callable[[Any], Program] +FromProgram = Callable[[Program], Any] + + +class EncodingError(ValueError): + pass + + +class tuple_frugal(tuple): + pass + + +class Frugal: + """ + 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: + if p.atom is None: + raise EncodingError("expected atom") + 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, type_tree: TypeTree) -> Program: + write_item = type_tree(args[0]) + + def serialize_list(items): + return Program.to([write_item(_) for _ in items]) + + return serialize_list + + +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) + for write_f, item in zip( + write_items, + items, + ) + ] + ) + + return serialize_tuple + + +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): + 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: CompoundLookup[ToProgram] = { + list: serialize_for_list, + tuple: serialize_for_tuple, + tuple_frugal: ser_for_tuple_frugal, +} + + +def types_for_fields(t: type, call_morpher, type_tree: TypeTree): + # split into key-based and location-based + + 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.name, type_hint)) + else: + alt_serde_type = m.get("alt_serde_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)) + + return location_based, key_based + + +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: + _type, from_storage, _to_storage = alt_serde_type + + def f(x): + return call(from_storage(x)) + + return f + return call + + location_based, key_based = types_for_fields(origin, morph_call, type_tree) + + types = tuple(type_hint for name, type_hint in location_based) + tuple_type = GenericAlias(tuple, types) + if key_based: + types = types + ( + GenericAlias(list, (GenericAlias(tuple_frugal, (str, Program)),)), + ) + if key_based or issubclass(origin, Frugal): + tuple_type = GenericAlias(tuple_frugal, types) + + names = tuple(name for name, type_hint in location_based) + + ser_tuple = type_tree(tuple_type) + + 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(v) + + return ser + + +def fail_ser( + origin: Type, args_type: ArgsType, type_tree: TypeTree +) -> Optional[ToProgram]: + if issubclass(origin, (str, bytes, int)): + return Program.to + + if is_dataclass(origin): + return ser_dataclass(origin, args_type, type_tree) + + if hasattr(origin, "__bytes__"): + return lambda x: Program.to(bytes(x)) + + return None + + +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: + _type, _from_storage, to_storage = alt_serde_type + + def f(x): + return to_storage(call(x)) + + return f + return call + + location_based, key_based = types_for_fields(origin, morph_call, type_tree) + + types = tuple(type_hint for name, type_hint in location_based) + tuple_type = GenericAlias(tuple, types) + if key_based: + 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) + + if key_based: + + def de(p: Program): + the_tuple = de_tuple(p) + 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: + 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 origin(*args, **kwargs) + + else: + + def de(p: Program): + the_tuple = de_tuple(p) + return origin(*the_tuple) + + return de + + +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(read_bytes(p)) + + return None + + +def to_program_for_type(t: type) -> Callable[[Any], Program]: + return TypeTree( + {(Program, None): lambda x: x}, + SERIALIZER_COMPOUND_TYPE_LOOKUP, + fail_ser, + )(t) + + +def deser_for_list(origin, args, type_tree: TypeTree): + read_item = type_tree(args[0]) + + def deserialize_list(p: Program) -> list: + return [read_item(_) for _ in p.as_iter()] + + return deserialize_list + + +def deser_for_tuple(origin, args, type_tree: TypeTree): + read_items = [type_tree(_) for _ in args] + + def deserialize_tuple(p: Program) -> Tuple[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 de_for_tuple_frugal(origin, args, type_tree: TypeTree): + read_items = [type_tree(_) for _ in args] + + def de(p: Program) -> Tuple[Any, ...]: + args = [] + todo = list(reversed(read_items)) + while todo: + des = todo.pop() + if todo: + v = Program.to(p.pair[0]) + p = Program.to(p.pair[1]) + else: + v = p + args.append(des(v)) + return tuple(args) + + return de + + +DESERIALIZER_COMPOUND_TYPE_LOOKUP: CompoundLookup[FromProgram] = { + list: deser_for_list, + tuple: deser_for_tuple, + tuple_frugal: de_for_tuple_frugal, +} + + +def from_program_for_type(t: type) -> FromProgram: + simple_lookup: dict[OriginArgsType, FromProgram] = { + (Program, None): lambda x: x, + } + return TypeTree( + simple_lookup, + DESERIALIZER_COMPOUND_TYPE_LOOKUP, + fail_deser, + )(t) diff --git a/hsms/cmds/hsm_dump_sb.py b/hsms/cmds/hsm_dump_sb.py old mode 100755 new mode 100644 index 4c53093..7cb6b77 --- a/hsms/cmds/hsm_dump_sb.py +++ b/hsms/cmds/hsm_dump_sb.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python - - 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_dump_us.py b/hsms/cmds/hsm_dump_us.py new file mode 100644 index 0000000..0cf0181 --- /dev/null +++ b/hsms/cmds/hsm_dump_us.py @@ -0,0 +1,59 @@ +import argparse +import sys +import zlib + +from hsms.cmds.hsms import summarize_unsigned_spend +from hsms.core.unsigned_spend import UnsignedSpend +from hsms.util.qrint_encoding import a2b_qrint + + +def file_or_string(p) -> str: + try: + with open(p) as f: + text = f.read().strip() + except Exception: + text = p + return text + + +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): + """ + 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) + + +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=sys.argv[1:]): + parser = create_parser() + args = parser.parse_args(argv) + return hsms_dump_us(args, parser) + + +if __name__ == "__main__": + main() diff --git a/hsms/cmds/hsm_test_spend.py b/hsms/cmds/hsm_test_spend.py index 2b168e8..4713395 100644 --- a/hsms/cmds/hsm_test_spend.py +++ b/hsms/cmds/hsm_test_spend.py @@ -1,21 +1,21 @@ import argparse import hashlib +import sys import zlib -from clvm_rs import Program +from clvm_rs import Program # type: ignore -from hsms.bls12_381 import BLSPublicKey - -from hsms.process.signing_hints import SumHint, PathHint +from chia_base.bls12_381 import BLSPublicKey +from chia_base.core import Coin, CoinSpend +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, solution_for_conditions, calculate_synthetic_offset, ) -from hsms.streamables import Coin, CoinSpend -from hsms.process.unsigned_spend import UnsignedSpend from hsms.puzzles.conlang import CREATE_COIN from hsms.util.byte_chunks import ( create_chunks_for_blob, @@ -32,9 +32,7 @@ def hsm_test_spend(args, parser): - root_public_keys = [ - BLSPublicKey.from_bech32m(_.readline()[:-1]) for _ in args.public_key_file - ] + root_public_keys = [BLSPublicKey.from_bech32m(_) for _ in args.public_key] paths = [[index, index + 1] for index in range(len(root_public_keys))] @@ -81,11 +79,19 @@ 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) - for chunk in chunks: - print(b2a_qrint(chunk)) + if args.hex: + print(b.hex()) + else: + 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 @@ -104,18 +110,30 @@ def create_parser(): type=int, ) parser.add_argument( - "public_key_file", - metavar="path-to-public-key", + "-H", + "--hex", + action="store_true", + help="hex output", + ) + parser.add_argument( + "-n", + "--no-chunks", + action="store_true", + help="don't compress or chunk output", + ) + parser.add_argument( + "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=sys.argv[1:]): 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 old mode 100755 new mode 100644 index bf1658a..ecac84b --- a/hsms/cmds/hsmmerge.py +++ b/hsms/cmds/hsmmerge.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python - from typing import List import argparse -from hsms.bls12_381 import BLSSignature -from hsms.process.unsigned_spend import UnsignedSpend +from chia_base.bls12_381 import BLSSignature +from chia_base.core import SpendBundle + +from hsms.core.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 +18,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..0fbc8e7 100644 --- a/hsms/cmds/hsmpk.py +++ b/hsms/cmds/hsmpk.py @@ -1,10 +1,10 @@ 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=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 old mode 100755 new mode 100644 index a39dffb..b42115f --- a/hsms/cmds/hsms.py +++ b/hsms/cmds/hsms.py @@ -1,26 +1,24 @@ -#!/usr/bin/env python - from decimal import Decimal from typing import BinaryIO, Iterable, List, TextIO 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 -from clvm_rs import Program +from chia_base.atoms import bytes32 +from chia_base.bls12_381 import BLSSecretExponent, BLSSignature +from chia_base.util.bech32 import bech32_encode + import segno -from hsms.bls12_381 import BLSSecretExponent, BLSSignature 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.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 @@ -31,19 +29,19 @@ 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_bytes(uncompressed_blob) except Exception: - program = Program.from_bytes(blob) - return UnsignedSpend.from_program(program) + return UnsignedSpend.from_bytes(blob) -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 @@ -65,7 +63,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: @@ -93,17 +91,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) @@ -112,8 +108,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: @@ -127,10 +123,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) @@ -187,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/cmds/hsmwizard.py b/hsms/cmds/hsmwizard.py old mode 100755 new mode 100644 index 1fad28e..a1b435c --- a/hsms/cmds/hsmwizard.py +++ b/hsms/cmds/hsmwizard.py @@ -1,6 +1,3 @@ -#!/bin/env python3 - - from pathlib import Path import argparse @@ -10,7 +7,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..912cc36 100644 --- a/hsms/cmds/poser_gen.py +++ b/hsms/cmds/poser_gen.py @@ -3,14 +3,15 @@ import argparse -from hsms.bls12_381 import BLSPublicKey -from hsms.streamables.coin import Coin -from hsms.streamables.coin_spend import CoinSpend -from hsms.process.unsigned_spend import UnsignedSpend +from chia_base.bls12_381 import BLSPublicKey +from chia_base.core import Coin, CoinSpend + +from hsms.core.unsigned_spend import UnsignedSpend from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( 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 @@ -81,7 +82,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/cmds/poser_verify.py b/hsms/cmds/poser_verify.py index fd77d7d..68354cb 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() @@ -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 old mode 100755 new mode 100644 index e8d6dac..bfad404 --- a/hsms/cmds/qrint.py +++ b/hsms/cmds/qrint.py @@ -1,24 +1,9 @@ -#!/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/hsms/consensus/conditions.py b/hsms/consensus/conditions.py index 8a58b16..a937f28 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] +from clvm_rs import Program # type: ignore 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/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/contrib/__init__.py b/hsms/core/__init__.py similarity index 100% rename from hsms/contrib/__init__.py rename to hsms/core/__init__.py diff --git a/hsms/core/signing_hints.py b/hsms/core/signing_hints.py new file mode 100644 index 0000000..6582504 --- /dev/null +++ b/hsms/core/signing_hints.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + +from typing import Dict, List + +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent + +from hsms.clvm_serde import Frugal + + +@dataclass +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 new file mode 100644 index 0000000..c96de38 --- /dev/null +++ b/hsms/core/unsigned_spend.py @@ -0,0 +1,85 @@ +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 + +from clvm_rs import Program # type: ignore + +from hsms.clvm_serde import ( + to_program_for_type, + from_program_for_type, +) +from .signing_hints import PathHint, SumHint + + +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 SignatureInfo: + signature: BLSSignature + partial_public_key: BLSPublicKey + final_public_key: BLSPublicKey + message: bytes + + +@dataclass +class UnsignedSpend: + coin_spends: List[CoinSpend] = field( + metadata=dict( + key="c", + alt_serde_type=( + SerdeCoinSpends, + from_storage, + to_storage, + ), + ), + ) + 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 __bytes__(self): + return bytes(TO_PROGRAM(self)) + + @classmethod + def from_bytes(cls, blob: bytes): + return 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 55b27dc..c53aab2 100644 --- a/hsms/debug/debug_spend_bundle.py +++ b/hsms/debug/debug_spend_bundle.py @@ -1,14 +1,14 @@ 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 # type: ignore -from hsms.bls12_381 import BLSSignature 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"} @@ -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: @@ -219,7 +221,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 +229,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..c4a8d48 100644 --- a/hsms/process/sign.py +++ b/hsms/process/sign.py @@ -1,17 +1,19 @@ from dataclasses import dataclass -from typing import Dict, Iterable, List, Optional, Tuple +from typing import Iterable, List, Optional, Tuple from weakref import WeakKeyDictionary -from clvm_rs import Program +from chia_base.atoms import hexbytes +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent +from chia_base.core import CoinSpend -from hsms.atoms import hexbytes -from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent +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 from hsms.consensus.conditions import conditions_by_opcode -from hsms.streamables import bytes32, CoinSpend 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 @dataclass @@ -21,14 +23,15 @@ class SignatureMetadata: message: bytes -CONDITIONS_FOR_COIN_SPEND: Dict[CoinSpend, Program] = WeakKeyDictionary() +CONDITIONS_FOR_COIN_SPEND: WeakKeyDictionary = WeakKeyDictionary() 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] @@ -125,13 +128,14 @@ 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, []) 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( @@ -149,10 +153,11 @@ 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/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/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.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_conditions.py b/hsms/puzzles/p2_conditions.py index 9abcda1..708a7e9 100644 --- a/hsms/puzzles/p2_conditions.py +++ b/hsms/puzzles/p2_conditions.py @@ -10,11 +10,11 @@ the doctor ordered. """ -from clvm_rs import Program +from clvm_rs import Program # type: ignore -from .load_clvm import load_clvm +from chialisp_puzzles import load_puzzle # type: ignore -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.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 47a9a4b..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,13 @@ import hashlib -from clvm_rs import Program +from clvm_rs import Program # type: ignore -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 # type: ignore -from .load_clvm import load_clvm from .p2_conditions import puzzle_for_conditions DEFAULT_HIDDEN_PUZZLE = Program.from_bytes( @@ -73,9 +74,11 @@ 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_puzzle("calculate_synthetic_public_key") -SYNTHETIC_MOD = load_clvm("calculate_synthetic_public_key.cl") +MAX_COST = 1 << 24 def calculate_synthetic_offset( @@ -91,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/hsms/streamables/__init__.py b/hsms/streamables/__init__.py deleted file mode 100644 index ba4142b..0000000 --- a/hsms/streamables/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from hsms.atoms.sized_bytes import bytes32, bytes48, bytes96 # noqa - -from .coin import Coin # noqa -from .coin_spend import CoinSpend # noqa - -from .spend_bundle import SpendBundle # noqa 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 deleted file mode 100644 index 5a16241..0000000 --- a/hsms/streamables/coin_spend.py +++ /dev/null @@ -1,46 +0,0 @@ -from clvm_rs import Program - -from .coin import Coin - -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. - """ - - coin: Coin - puzzle_reveal: Program - solution: Program - - 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, - ) 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/byte_chunks.py b/hsms/util/byte_chunks.py index 79cd687..0bb24fd 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,21 @@ 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/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/hsms/util/type_tree.py b/hsms/util/type_tree.py new file mode 100644 index 0000000..6b84255 --- /dev/null +++ b/hsms/util/type_tree.py @@ -0,0 +1,53 @@ +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]] + + +@dataclass +class TypeTree(Generic[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 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}") diff --git a/pyproject.toml b/pyproject.toml index 993ec4a..fc25cfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,33 +1,35 @@ -[tool.enscons] +[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 = ["blspy==1.0.16", "segno==1.4.1", "clvm_rs==0.2.5", "clvm_tools_rs==0.1.30"] -packages = ["hsms"] -# version is defined with `setuptools_scm`. See `SConstruct` file. +dependencies = [ + "segno==1.4.1", + "chia_base==0.1.4", + "chialisp_puzzles==0.1.1", +] +# version is defined with `setuptools_scm` +dynamic = ["version"] -[tool.enscons.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"] -[tool.enscons.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", - "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" +hsm_dump_us = "hsms.cmds.hsm_dump_us: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, +) 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 + diff --git a/tests/cmds/hsm_test_1.txt b/tests/cmds/hsm_test_1.txt new file mode 100644 index 0000000..9feb424 --- /dev/null +++ b/tests/cmds/hsm_test_1.txt @@ -0,0 +1,2 @@ +hsm_test_spend bls12381jlca8fe3jltegf54vwxyl2dvplpk3rz0ja6tjpdpfcar79cm43vxc40g8luh5xh0lva0qzkmytrtk7l5wds +34047108094429339442807125285572002104699150339493615642302298331325668207875883183821102455461113748856476876234896576601094508539476824369964397311997709279460852325654110975221758502511249348942334738923244968558468940116612003201409478404982969004110762927806041979432788169363295746361556336686181213582376130655959523667599330485237394041191036332635740772617352553229315713371078363675803598550425903406363417194205726515448440839392128121975028113368054279294040417422067670427318281582457036485693829101878095809205907336619585884516708088747154853234268426821058360238077501402489966158252317212923722642512490421615240512625076442633980498929434964108173909532587390857531815997165592399951504396748773684332447839907840104304127985896393638203795015606284258340736157951427191554750549272307923279215936744060356400561828199118463837059005721517230334159819957566696825314981781340480522127072369137373709196631315368572438111889219544980474680953363825443265705460550074640230202913816129805219059100646177900664268004742921415431662378504584087599800238248741600757420391491213533304786003118930496265741700569822326207203349055073315223840613571013535341092555098972749055937343082691486777995264 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/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..8025c90 100755 --- a/tests/full_life_cycle.sh +++ b/tests/full_life_cycle.sh @@ -13,12 +13,13 @@ 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 +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 diff --git a/tests/generate.py b/tests/generate.py index 9c3d33e..3a6078e 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.atoms import bytes32 +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent def bytes32_generate(nonce: int, namespace: str = "bytes32") -> bytes32: diff --git a/hsms/util/clvm_serialization.py b/tests/legacy/clvm_serialization.py similarity index 81% rename from hsms/util/clvm_serialization.py rename to tests/legacy/clvm_serialization.py index 96ff030..46850b9 100644 --- a/hsms/util/clvm_serialization.py +++ b/tests/legacy/clvm_serialization.py @@ -61,19 +61,17 @@ 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 + + +def as_atom(x): + return x.atom + + +def as_int(x): + return x.as_int() diff --git a/hsms/process/signing_hints.py b/tests/legacy/signing_hints.py similarity index 92% rename from hsms/process/signing_hints.py rename to tests/legacy/signing_hints.py index 7864af8..ce98f05 100644 --- a/hsms/process/signing_hints.py +++ b/tests/legacy/signing_hints.py @@ -1,8 +1,9 @@ from dataclasses import dataclass from typing import Dict, List -from hsms.bls12_381 import BLSPublicKey, BLSSecretExponent -from hsms.util.clvm_serialization import ( +from chia_base.bls12_381 import BLSPublicKey, BLSSecretExponent + +from .clvm_serialization import ( clvm_to_list_of_ints, clvm_to_list, ) diff --git a/hsms/process/unsigned_spend.py b/tests/legacy/unsigned_spend.py similarity index 61% rename from hsms/process/unsigned_spend.py rename to tests/legacy/unsigned_spend.py index c96a235..b792ccb 100644 --- a/hsms/process/unsigned_spend.py +++ b/tests/legacy/unsigned_spend.py @@ -1,19 +1,22 @@ -import zlib - 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.bls12_381 import BLSPublicKey, BLSSignature -from hsms.process.signing_hints import SumHint, PathHint -from hsms.streamables import bytes32, CoinSpend -from hsms.util.byte_chunks import assemble_chunks, create_chunks_for_blob -from hsms.util.clvm_serialization import ( +from .clvm_serialization import ( + as_atom, + as_int, + clvm_to_list, + no_op, transform_dict, transform_dict_by_key, - clvm_to_list, + transform_as_struct, ) +from .signing_hints import SumHint, PathHint @dataclass @@ -50,24 +53,23 @@ 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 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) + 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/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_clvm_serde.py b/tests/test_clvm_serde.py new file mode 100644 index 0000000..4ed9c4b --- /dev/null +++ b/tests/test_clvm_serde.py @@ -0,0 +1,372 @@ +from dataclasses import dataclass, field +from typing import List, Tuple + +import random + +import pytest + +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 ( + from_program_for_type, + to_program_for_type, + tuple_frugal, + EncodingError, + Frugal, +) +from hsms.core.signing_hints import SumHint, PathHint +from hsms.core.unsigned_spend import ( + UnsignedSpend, + from_storage, + to_storage, + TO_PROGRAM, +) +from .legacy.signing_hints import ( + SumHint as LegacySH, + PathHint as LegacyPH, +) +from .legacy.unsigned_spend import UnsignedSpend as LegacyUS + + +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) == prhs + assert fp(tp(t)) == t + + tt = GenericAlias(tuple_frugal, (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 = tuple_frugal(v) + rhs = Program.to(t) + assert tp(t) == rhs + assert fp(tp(t)) == t + + @dataclass + class Foo: + a: int + b: str + + tp = to_program_for_type(Foo) + foo = Foo(100, "boss") + rhs = Program.to([100, "boss"]) + assert tp(foo) == rhs + fp = from_program_for_type(Foo) + assert foo == fp(rhs) + + @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)) + + @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")]) + 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 test_serde_frugal(): + @dataclass + class Foo(Frugal): + 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(Frugal): + 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 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 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 = randbytes(r, 32) + puzzle = Program.to(f"puz: {randbytes(r, 5).hex()}") + amount = r.randint(1, 1000) * int(1e3) + solution = Program.to(f"sol: {randbytes(r, 10).hex()}") + coin = Coin(parent, puzzle.tree_hash(), amount) + return CoinSpend(coin, puzzle, solution) + + +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.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 + + +CoinSpendTuple = Tuple[bytes, Program, int, Program] + + +def test_interop_path_hint(): + public_key = BLSSecretExponent.from_int(1).public_key() + ints = [1, 5, 91, 29484, 399] + 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.root_public_key + assert lph.path == path_hint.path + assert lph.public_key() == path_hint.public_key() + ph1 = fp(p) + assert path_hint == ph1 + + +def test_interop_coin_spend(): + 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(): + cs_list = [rnd_coin_spend(_) for _ in range(10)] + + 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) + 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(cs_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) + # 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()) + lus = LegacyUS.from_program(p) + 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) + 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 + + +def test_tuple_frugal(): + Foo = GenericAlias(tuple_frugal, (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 + + +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 + + +def test_failures(): + fp = from_program_for_type(bytes) + with pytest.raises(EncodingError): + fp(Program.to([1, 2])) + + tfi = GenericAlias(tuple_frugal, (int,)) + tp = to_program_for_type(tfi) + with pytest.raises(EncodingError): + tp((1, 2)) + + fp = from_program_for_type(tfi) + 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 new file mode 100644 index 0000000..8340514 --- /dev/null +++ b/tests/test_cmds.py @@ -0,0 +1,125 @@ +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..fba692c 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1,5 +1,12 @@ +import zlib + 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 from hsms.debug.debug_spend_bundle import debug_spend_bundle from hsms.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE, @@ -7,12 +14,13 @@ solution_for_conditions, calculate_synthetic_offset, ) -from hsms.streamables import bytes96, Coin, CoinSpend, SpendBundle 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, 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( @@ -70,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 @@ -114,7 +122,11 @@ def test_lifecycle(): coin_spends, sum_hints, path_hints, AGG_SIG_ME_ADDITIONAL_DATA ) - assert unsigned_spend == UnsignedSpend.from_chunks(unsigned_spend.chunk(500)) + chunks = chunks_for_zlib_blob(bytes(unsigned_spend), 500) + + assert unsigned_spend == UnsignedSpend.from_bytes( + zlib.decompress(ChunkAssembler(chunks).assemble()) + ) spend_chunks = create_chunks_for_blob(bytes(unsigned_spend), 250) assembler = ChunkAssembler() @@ -143,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) @@ -158,4 +170,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)