chik-base 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
chik_base/__init__.py ADDED
File without changes
@@ -0,0 +1,27 @@
1
+ """
2
+ This module defines some useful atomic types for building more complex
3
+ data structures.
4
+
5
+ Except for `hexbytes` (which is just a hex-printing subclass of `bytes`,
6
+ these types are all fixed-sized, simplifying serializing and parsing, like
7
+ those used in `cbincode` (the slight modification to the `bincode` standard that
8
+ chik uses).
9
+ """
10
+
11
+ from .hexbytes import hexbytes
12
+ from .ints import int8, uint8, int16, uint16, int32, uint32, int64, uint64
13
+ from .sized_bytes import bytes32
14
+
15
+
16
+ __all__ = [
17
+ "int8",
18
+ "int16",
19
+ "int32",
20
+ "int64",
21
+ "uint8",
22
+ "uint16",
23
+ "uint32",
24
+ "uint64",
25
+ "hexbytes",
26
+ "bytes32",
27
+ ]
@@ -0,0 +1,11 @@
1
+ class hexbytes(bytes):
2
+ """
3
+ This is a subclass of bytes that prints itself out as hex,
4
+ which is much easier on the eyes for binary data that is very non-ascii .
5
+ """
6
+
7
+ def __str__(self):
8
+ return self.hex()
9
+
10
+ def __repr__(self):
11
+ return "<%s: %s>" % (self.__class__.__name__, str(self))
@@ -0,0 +1,59 @@
1
+ """
2
+ Some ints of fixed size. Their fixed size makes them easier to parse and serialize.
3
+
4
+ Each implements these two class methods:
5
+
6
+ * `.parse(f: BinaryIO)`
7
+ * `._class_stream(obj, f: BinaryIO)`
8
+
9
+ """
10
+
11
+ from .struct_stream import struct_stream
12
+
13
+
14
+ class int8(int, struct_stream):
15
+ "signed 8-bit int"
16
+
17
+ PACK = "!b"
18
+
19
+
20
+ class uint8(int, struct_stream):
21
+ "unsigned 8-bit int"
22
+
23
+ PACK = "!B"
24
+
25
+
26
+ class int16(int, struct_stream):
27
+ "signed 16-bit int"
28
+
29
+ PACK = "!h"
30
+
31
+
32
+ class uint16(int, struct_stream):
33
+ "unsigned 16-bit int"
34
+
35
+ PACK = "!H"
36
+
37
+
38
+ class int32(int, struct_stream):
39
+ "signed 32-bit int"
40
+
41
+ PACK = "!l"
42
+
43
+
44
+ class uint32(int, struct_stream):
45
+ "unsigned 32-bit int"
46
+
47
+ PACK = "!L"
48
+
49
+
50
+ class int64(int, struct_stream):
51
+ "signed 64-bit int"
52
+
53
+ PACK = "!q"
54
+
55
+
56
+ class uint64(int, struct_stream):
57
+ "unsigned 64-bit int"
58
+
59
+ PACK = "!Q"
@@ -0,0 +1,43 @@
1
+ from typing import BinaryIO
2
+ from .hexbytes import hexbytes
3
+
4
+
5
+ class SizedBytes(hexbytes):
6
+ """
7
+ Intended to be subclassed, this base class makes it easy to create
8
+ subclasses of `bytes` that require the length to be a specific value.
9
+
10
+ Having a specific number of bytes means we can easily parse and stream
11
+ """
12
+
13
+ _size: int
14
+
15
+ def __new__(cls, v):
16
+ "`v` must be castable to `bytes`"
17
+ v = bytes(v)
18
+ if not isinstance(v, bytes) or len(v) != cls._size:
19
+ raise ValueError("bad %s initializer %s" % (cls.__name__, v))
20
+ return hexbytes.__new__(cls, v)
21
+
22
+ @classmethod
23
+ def parse(cls, f: BinaryIO) -> bytes:
24
+ b = f.read(cls._size)
25
+ if len(b) != cls._size:
26
+ msg = f"unexpected EOS: {len(b)} bytes read, {cls._size} expected"
27
+ raise ValueError(msg)
28
+ return cls(b)
29
+
30
+ @classmethod
31
+ def _class_stream(cls, obj: bytes, f: BinaryIO) -> None:
32
+ if len(obj) != cls._size:
33
+ msg = f"got {len(obj)} bytes when we expected {cls._size}"
34
+ raise ValueError(msg)
35
+ f.write(obj)
36
+
37
+
38
+ class bytes32(SizedBytes):
39
+ """
40
+ A subclass of `bytes` that requires the length to be 32.
41
+ """
42
+
43
+ _size = 32
@@ -0,0 +1,24 @@
1
+ import struct
2
+
3
+ from typing import BinaryIO, TypeVar, Type
4
+
5
+
6
+ _T = TypeVar("_T", bound="struct_stream")
7
+
8
+
9
+ class struct_stream:
10
+ """
11
+ This is a base class. Subclasses should define `cls.PACK` as a struct.pack
12
+ template string. In return, you get implementations of `parse` and
13
+ `_class_stream`.
14
+ """
15
+
16
+ PACK: str
17
+
18
+ @classmethod
19
+ def parse(cls: Type[_T], f: BinaryIO) -> _T:
20
+ return cls(*struct.unpack(cls.PACK, f.read(struct.calcsize(cls.PACK))))
21
+
22
+ @classmethod
23
+ def _class_stream(cls: Type[_T], obj: _T, f: BinaryIO) -> None:
24
+ f.write(struct.pack(cls.PACK, obj))
@@ -0,0 +1,11 @@
1
+ """
2
+ The `chik_rs` wheel has some rough edges in its api. This module smooths them over
3
+ with a more ergonomic api.
4
+ """
5
+
6
+ from .bls_public_key import BLSPublicKey
7
+ from .bls_secret_exponent import BLSSecretExponent
8
+ from .bls_signature import BLSSignature
9
+
10
+
11
+ __all__ = ["BLSPublicKey", "BLSSecretExponent", "BLSSignature"]
@@ -0,0 +1,129 @@
1
+ from typing import BinaryIO, List
2
+
3
+ import chik_rs # type: ignore
4
+
5
+ from chik_base.atoms import hexbytes
6
+ from chik_base.util.bech32 import bech32_decode, bech32_encode, Encoding
7
+
8
+ from .secret_key_utils import public_key_from_int
9
+
10
+ BECH32M_PUBLIC_KEY_PREFIX = "bls1238"
11
+
12
+
13
+ class BLSPublicKey:
14
+ """
15
+ This corresponds to an element in bls12-381's G1 group, represented
16
+ when serialized by a 48-byte x element (with a few extra bits at the
17
+ beginning for metadata).
18
+ """
19
+ def __init__(self, g1: chik_rs.G1Element):
20
+ assert isinstance(g1, chik_rs.G1Element)
21
+ self._g1 = g1
22
+
23
+ @classmethod
24
+ def from_bytes(cls, blob):
25
+ "parse from a binary blob"
26
+ bls_public_hd_key = chik_rs.G1Element.from_bytes(blob)
27
+ return BLSPublicKey(bls_public_hd_key)
28
+
29
+ @classmethod
30
+ def parse(cls, f: BinaryIO):
31
+ "parse from a stream"
32
+ return cls.from_bytes(f.read(48))
33
+
34
+ @classmethod
35
+ def generator(cls):
36
+ "return the well-known generator"
37
+ return BLSPublicKey(chik_rs.G1Element.generator())
38
+
39
+ @classmethod
40
+ def zero(cls):
41
+ "return the well-known zero"
42
+ return cls(chik_rs.G1Element())
43
+
44
+ def stream(self, f: BinaryIO) -> None:
45
+ "write the serialized version to the file f"
46
+ f.write(bytes(self._g1))
47
+
48
+ def __add__(self, other):
49
+ "add two elements, returning the sum. Use `+`"
50
+ return BLSPublicKey(self._g1 + other._g1)
51
+
52
+ def __mul__(self, other: int):
53
+ "multiply an element by a scalar"
54
+ if other < 0:
55
+ raise ValueError("can't multiply by a negative value")
56
+ if self == self.generator():
57
+ # there is a special method in chik_rs that multiplies
58
+ # the generator by an integer that is not susceptible to
59
+ # timing attacks. Since public keys are generate times integer,
60
+ # using this method with the generator could expose to timing
61
+ # attacks. So instead we use the more clever code specifically
62
+ # for the generator
63
+ return BLSPublicKey(public_key_from_int(other))
64
+ if other == 0:
65
+ return self.zero()
66
+ if other == 1:
67
+ return self
68
+ # use recursion on `__mul__` in a sneaky clever way
69
+ parity = other & 1
70
+ v = self.__mul__(other >> 1)
71
+ v += v
72
+ if parity:
73
+ v += self
74
+ return v
75
+
76
+ def __rmul__(self, other: int):
77
+ return self.__mul__(other)
78
+
79
+ def __eq__(self, other):
80
+ if isinstance(other, type(self)):
81
+ return self._g1 == other._g1
82
+ return False
83
+
84
+ def __bytes__(self) -> bytes:
85
+ return hexbytes(self._g1)
86
+
87
+ def child(self, index: int) -> "BLSPublicKey":
88
+ "unhardened child derivation"
89
+ return BLSPublicKey(
90
+ chik_rs.AugSchemeMPL.derive_child_pk_unhardened(self._g1, index)
91
+ )
92
+
93
+ def child_for_path(self, path: List[int]) -> "BLSPublicKey":
94
+ "A path is a list of child integer derivations"
95
+ r = self
96
+ for index in path:
97
+ r = r.child(index)
98
+ return r
99
+
100
+ def fingerprint(self) -> int:
101
+ "return a 32-bit unsigned integer"
102
+ return self._g1.get_fingerprint()
103
+
104
+ def as_bech32m(self) -> str:
105
+ "convert to a bech32m string"
106
+ return bech32_encode(BECH32M_PUBLIC_KEY_PREFIX, bytes(self), Encoding.BECH32M)
107
+
108
+ @classmethod
109
+ def from_bech32m(cls, text: str) -> "BLSPublicKey":
110
+ "convert from a bech32m string"
111
+ r = bech32_decode(text, max_length=91)
112
+ if r is not None:
113
+ prefix, base8_data, encoding = r
114
+ if (
115
+ encoding == Encoding.BECH32M
116
+ and prefix == BECH32M_PUBLIC_KEY_PREFIX
117
+ and len(base8_data) == 49
118
+ ):
119
+ return cls.from_bytes(base8_data[:48])
120
+ raise ValueError("not bls12_381 bech32m pubkey")
121
+
122
+ def __hash__(self):
123
+ return bytes(self).__hash__()
124
+
125
+ def __str__(self):
126
+ return self.as_bech32m()
127
+
128
+ def __repr__(self):
129
+ return "<%s: %s>" % (self.__class__.__name__, self)
@@ -0,0 +1,138 @@
1
+ from typing import BinaryIO, List, Optional
2
+
3
+ import chik_rs # type: ignore
4
+
5
+ from chik_base.util.bech32 import bech32_decode, bech32_encode, Encoding
6
+
7
+ from .bls_public_key import BLSPublicKey
8
+ from .bls_signature import BLSSignature
9
+ from .secret_key_utils import private_key_from_int
10
+
11
+
12
+ BECH32M_SECRET_EXPONENT_PREFIX = "se"
13
+
14
+
15
+ class BLSSecretExponent:
16
+ """
17
+ This is essentially an `int` with convenience functions.
18
+
19
+ We don't subclass `int` because we have a different implementation of
20
+ `__bytes__` which could cause confusion.
21
+ """
22
+
23
+ def __init__(self, sk: chik_rs.PrivateKey):
24
+ self._sk = sk
25
+
26
+ @classmethod
27
+ def from_seed(cls, seed: bytes) -> "BLSSecretExponent":
28
+ """
29
+ convert from a seed using the specification at
30
+ https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05
31
+ """
32
+ return BLSSecretExponent(chik_rs.AugSchemeMPL.key_gen(seed))
33
+
34
+ @classmethod
35
+ def from_int(cls, secret_exponent) -> "BLSSecretExponent":
36
+ "convert from the given int"
37
+ return cls(private_key_from_int(secret_exponent))
38
+
39
+ @classmethod
40
+ def from_bytes(cls, blob) -> "BLSSecretExponent":
41
+ "deserialize from the given blob. A blob of length 32 bytes is expected"
42
+ return cls(chik_rs.PrivateKey.from_bytes(blob))
43
+
44
+ @classmethod
45
+ def parse(cls, f: BinaryIO):
46
+ "deserialize from the given stream"
47
+ return cls.from_bytes(f.read(32))
48
+
49
+ def stream(self, f: BinaryIO) -> None:
50
+ "serialize to the given stream"
51
+ f.write(bytes(self._sk))
52
+
53
+ def fingerprint(self) -> int:
54
+ "return a 32-bit unsigned integer. The public key fingerprint will match."
55
+ return self._sk.get_g1().get_fingerprint()
56
+
57
+ def sign(
58
+ self, message: bytes, final_public_key: Optional[BLSPublicKey] = None
59
+ ) -> BLSSignature:
60
+ "generate a signature"
61
+ if final_public_key:
62
+ return BLSSignature(
63
+ chik_rs.AugSchemeMPL.sign(self._sk, message, final_public_key._g1)
64
+ )
65
+ return BLSSignature(chik_rs.AugSchemeMPL.sign(self._sk, message))
66
+
67
+ def public_key(self) -> BLSPublicKey:
68
+ "return the corresponding public key"
69
+ return BLSPublicKey(self._sk.get_g1())
70
+
71
+ def secret_exponent(self) -> int:
72
+ "return the exponent as an `int`"
73
+ return int.from_bytes(bytes(self), "big")
74
+
75
+ def hardened_child(self, index: int) -> "BLSSecretExponent":
76
+ "return the hardened child"
77
+ return BLSSecretExponent(chik_rs.AugSchemeMPL.derive_child_sk(self._sk, index))
78
+
79
+ def child(self, index: int) -> "BLSSecretExponent":
80
+ "return the unhardened child. This will match the corresponding public_key child"
81
+ return BLSSecretExponent(
82
+ chik_rs.AugSchemeMPL.derive_child_sk_unhardened(self._sk, index)
83
+ )
84
+
85
+ def child_for_path(self, path: List[int]) -> "BLSSecretExponent":
86
+ "A path is a list of child integer derivations. Unhardened only"
87
+ r = self
88
+ for index in path:
89
+ r = r.child(index)
90
+ return r
91
+
92
+ def as_bech32m(self):
93
+ "convert to a bech32m string"
94
+ return bech32_encode(
95
+ BECH32M_SECRET_EXPONENT_PREFIX, bytes(self), Encoding.BECH32M
96
+ )
97
+
98
+ @classmethod
99
+ def from_bech32m(cls, text: str) -> "BLSSecretExponent":
100
+ "convert from a bech32m string"
101
+ r = bech32_decode(text)
102
+ if r is not None:
103
+ prefix, base8_data, encoding = r
104
+ if (
105
+ encoding == Encoding.BECH32M
106
+ and prefix == BECH32M_SECRET_EXPONENT_PREFIX
107
+ and len(base8_data) == 33
108
+ ):
109
+ return cls.from_bytes(base8_data[:32])
110
+ raise ValueError("not secret exponent")
111
+
112
+ @classmethod
113
+ def zero(cls) -> "BLSSecretExponent":
114
+ "returns the secret exponent corresponding to 0. This shouldn't be used to sign"
115
+ return ZERO
116
+
117
+ def __add__(self, other):
118
+ return self.from_int(int(self) + int(other))
119
+
120
+ def __int__(self):
121
+ return self.secret_exponent()
122
+
123
+ def __eq__(self, other):
124
+ if isinstance(other, int):
125
+ other = BLSSecretExponent.from_int(other)
126
+ return self._sk == other._sk
127
+
128
+ def __bytes__(self):
129
+ return bytes(self._sk)
130
+
131
+ def __str__(self):
132
+ return "<prv for:%s>" % self.public_key()
133
+
134
+ def __repr__(self):
135
+ return "<%s: %s>" % (self.__class__.__name__, self)
136
+
137
+
138
+ ZERO = BLSSecretExponent.from_int(0)
@@ -0,0 +1,81 @@
1
+ from dataclasses import dataclass
2
+ from typing import BinaryIO, List, Sequence, Tuple
3
+
4
+ import chik_rs # type: ignore
5
+
6
+ from chik_base.atoms import bytes32
7
+
8
+ from .bls_public_key import BLSPublicKey
9
+
10
+ ZERO96 = bytes([0] * 96)
11
+
12
+
13
+ class BLSSignature:
14
+ """
15
+ This wraps the chik_rs version and resolves a couple edge cases
16
+ around aggregation and validation.
17
+ """
18
+
19
+ @dataclass
20
+ class aggsig_pair:
21
+ public_key: BLSPublicKey
22
+ message_hash: bytes
23
+
24
+ def __init__(self, g2: chik_rs.G2Element):
25
+ assert isinstance(g2, chik_rs.G2Element)
26
+ self._g2 = g2
27
+
28
+ @classmethod
29
+ def from_bytes(cls, blob):
30
+ "parse from a binary blob"
31
+ bls_public_hd_key = chik_rs.G2Element.from_bytes(blob)
32
+ return cls(bls_public_hd_key)
33
+
34
+ @classmethod
35
+ def parse(cls, f: BinaryIO):
36
+ "parse from a stream"
37
+ return cls.from_bytes(f.read(96))
38
+
39
+ @classmethod
40
+ def generator(cls):
41
+ "return the well-known generator"
42
+ return cls(chik_rs.G2Element.generator())
43
+
44
+ @classmethod
45
+ def zero(cls):
46
+ "returns the g2 element corresponding to 0. This shouldn't be used to sign"
47
+ return cls(chik_rs.G2Element())
48
+
49
+ def stream(self, f):
50
+ "write the serialized version to the file f"
51
+ f.write(bytes(self._g2))
52
+
53
+ def __add__(self, other):
54
+ "add two elements, returning the sum. Use `+`"
55
+ return self.__class__(self._g2 + other._g2)
56
+
57
+ def __eq__(self, other):
58
+ return self._g2 == other._g2
59
+
60
+ def __bytes__(self) -> bytes:
61
+ return bytes(self._g2)
62
+
63
+ def __str__(self):
64
+ return bytes(self._g2).hex()
65
+
66
+ def __repr__(self):
67
+ return "<%s: %s>" % (self.__class__.__name__, self)
68
+
69
+ def validate(self, hash_key_pairs: Sequence[aggsig_pair]) -> bool:
70
+ "check signature"
71
+ return self.verify([(_.public_key, _.message_hash) for _ in hash_key_pairs])
72
+
73
+ def verify(self, hash_key_pairs: Sequence[Tuple[BLSPublicKey, bytes]]) -> bool:
74
+ "check signature"
75
+ hkp = list(hash_key_pairs)
76
+ public_keys: List[chik_rs.G1Element] = [_[0]._g1 for _ in hkp]
77
+ message_hashes: List[bytes32] = [_[1] for _ in hkp]
78
+
79
+ return chik_rs.AugSchemeMPL.aggregate_verify(
80
+ public_keys, message_hashes, self._g2
81
+ )
@@ -0,0 +1,24 @@
1
+ """
2
+ Some secret key utilities that need to know the group order and return `chik_rs`
3
+ structures.
4
+ """
5
+
6
+
7
+ import chik_rs # type: ignore
8
+
9
+
10
+ GROUP_ORDER = (
11
+ 52435875175126190479447740508185965837690552500527637822603658699938581184513
12
+ )
13
+
14
+
15
+ def private_key_from_int(secret_exponent: int) -> chik_rs.PrivateKey:
16
+ "convert an `int` into the `chik_rs.PrivateKey`"
17
+ secret_exponent %= GROUP_ORDER
18
+ blob = secret_exponent.to_bytes(32, "big")
19
+ return chik_rs.PrivateKey.from_bytes(blob)
20
+
21
+
22
+ def public_key_from_int(secret_exponent: int) -> chik_rs.G1Element:
23
+ "convert an `int` into the corresponding `chik_rs.G1Element` multiple of generator"
24
+ return private_key_from_int(secret_exponent).get_g1()
@@ -0,0 +1,43 @@
1
+ """
2
+ Implement `cbincode` streaming and parsing.
3
+
4
+ This is a very simple serialization standard. It's almost identical to the
5
+ bincode standard implemented by a rust crate and described here:
6
+
7
+ https://github.com/bincode-org/bincode/blob/trunk/docs/spec.md
8
+
9
+ It uses `FixintEncoding`, ie. a fixed size for each of `(uint/int)(8|16|32)`.
10
+ The bincode standard uses uint32 for size prefixes (on lists, for example) and
11
+ the cbincode standard uses uint16. Other than that, the encoding has very few
12
+ surprises.
13
+
14
+
15
+ Parsers and streamers are created at runtime based on a type.
16
+
17
+ Supported types include:
18
+
19
+ - `bytes`, `str`
20
+ - any class with a `.parse` class function
21
+ - this includes, `(u)?int(8|16|32)`, `bytes32`
22
+ - `list[T]` where `T` is supported
23
+ - `tuple[T1, T2, ..., TN]` where each `Tn` is supported
24
+ - `Optional[T]` where `T` is supported (also spelled `T | None`)
25
+ - classes decorated with `@dataclass` where each field is of a supported type
26
+
27
+ Transitive closures of the above list are also supported.
28
+ """
29
+
30
+ from .parser import make_parser, ParseFunction
31
+ from .streamer import make_streamer, StreamFunction
32
+ from .util import from_bytes, from_hex, to_bytes, to_hex
33
+
34
+ __all__ = [
35
+ "make_parser",
36
+ "make_streamer",
37
+ "ParseFunction",
38
+ "StreamFunction",
39
+ "from_hex",
40
+ "from_bytes",
41
+ "to_hex",
42
+ "to_bytes",
43
+ ]