astreum 0.2.7__py3-none-any.whl → 0.2.9__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.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

@@ -0,0 +1,123 @@
1
+ import math
2
+
3
+
4
+ def extended_gcd(a: int, b: int) -> tuple[int, int, int]:
5
+ """
6
+ Return (g, x, y) such that a*x + b*y = g = gcd(a, b).
7
+ """
8
+ if b == 0:
9
+ return (a, 1, 0)
10
+ g, x1, y1 = extended_gcd(b, a % b)
11
+ return (g, y1, x1 - (a // b) * y1)
12
+
13
+
14
+ def modinv(a: int, m: int) -> int:
15
+ """
16
+ Return modular inverse of a mod m.
17
+ """
18
+ g, x, _ = extended_gcd(a, m)
19
+ if g != 1:
20
+ raise ValueError(f"No modular inverse for {a} modulo {m}")
21
+ return x % m
22
+
23
+
24
+ def is_reduced(q: 'QuadraticForm') -> bool:
25
+ """
26
+ Check if the form q is in reduced (Gauss) form:
27
+ |b| <= a <= c, and if a == c then b >= 0.
28
+ """
29
+ return abs(q.b) <= q.a <= q.c and not (q.a == q.c and q.b < 0)
30
+
31
+
32
+ def is_primitive(a: int, b: int, c: int) -> bool:
33
+ """
34
+ Check if the form coefficients are coprime: gcd(a, b, c) == 1.
35
+ """
36
+ return math.gcd(math.gcd(a, b), c) == 1
37
+
38
+
39
+ class QuadraticForm:
40
+ """
41
+ Represents the binary quadratic form ax^2 + bxy + cy^2 with discriminant D,
42
+ stored in reduced, primitive form.
43
+ """
44
+
45
+ def __init__(self, a: int, b: int, c: int, D: int):
46
+ if b*b - 4*a*c != D:
47
+ raise ValueError(f"Discriminant mismatch: b^2 - 4ac = {b*b - 4*a*c}, expected {D}")
48
+ if not is_primitive(a, b, c):
49
+ raise ValueError("Form coefficients are not coprime (not primitive)")
50
+ self.a = a
51
+ self.b = b
52
+ self.c = c
53
+ self.D = D
54
+
55
+ def reduce(self) -> 'QuadraticForm':
56
+ """
57
+ Perform Gauss reduction until the form is reduced.
58
+ """
59
+ while not is_reduced(self):
60
+ self._gauss_step()
61
+ return self
62
+
63
+ def _gauss_step(self) -> None:
64
+ """
65
+ One iteration of Gauss reduction on the current form.
66
+ """
67
+ a, b, c = self.a, self.b, self.c
68
+ # Compute m = round(b / (2a)) using integer arithmetic
69
+ sign = 1 if b >= 0 else -1
70
+ m = (b + sign * a) // (2 * a)
71
+ # Update b and c
72
+ b_new = b - 2 * m * a
73
+ c_new = m * m * a - m * b + c
74
+ # Assign back
75
+ self.b = b_new
76
+ self.c = c_new
77
+ # Swap if needed to ensure a <= c and proper sign
78
+ if a > self.c or (a == self.c and self.b < 0):
79
+ self.a, self.b, self.c = self.c, -self.b, a
80
+
81
+ def __mul__(self, other: 'QuadraticForm') -> 'QuadraticForm':
82
+ """
83
+ Dirichlet (NUCOMP) composition of two forms of the same discriminant.
84
+ """
85
+ if self.D != other.D:
86
+ raise ValueError("Cannot compose forms with different discriminants")
87
+ a1, b1, c1 = self.a, self.b, self.c
88
+ a2, b2, c2 = other.a, other.b, other.c
89
+ D = self.D
90
+ # Compute g = gcd(a1, a2, (b1 + b2)//2)
91
+ k = (b1 + b2) // 2
92
+ g = math.gcd(math.gcd(a1, a2), k)
93
+ a1p = a1 // g
94
+ a2p = a2 // g
95
+ # Solve m * a1p ≡ (b2 - b1)//2 mod a2p
96
+ diff = (b2 - b1) // 2
97
+ inv = modinv(a1p, a2p)
98
+ m = (diff * inv) % a2p
99
+ # Compute composed coefficients
100
+ b3 = b1 + 2 * m * a1
101
+ a3 = a1 * a2p
102
+ c3 = (b3 * b3 - D) // (4 * a3)
103
+ return QuadraticForm(a3, b3, c3, D).reduce()
104
+
105
+ def to_bytes(self) -> bytes:
106
+ """
107
+ Serialize this form to bytes (a and b, big-endian, fixed width).
108
+ """
109
+ # Width: enough to hold |D| bitlength / 8 rounded up
110
+ width = (self.D.bit_length() + 15) // 8
111
+ return self.a.to_bytes(width, 'big', signed=True) + \
112
+ self.b.to_bytes(width, 'big', signed=True)
113
+
114
+ @classmethod
115
+ def from_bytes(cls, data: bytes, D: int) -> 'QuadraticForm':
116
+ """
117
+ Deserialize bytes back into a QuadraticForm for discriminant D.
118
+ """
119
+ width = len(data) // 2
120
+ a = int.from_bytes(data[:width], 'big', signed=True)
121
+ b = int.from_bytes(data[width:], 'big', signed=True)
122
+ c = (b*b - D) // (4 * a)
123
+ return cls(a, b, c, D).reduce()
@@ -0,0 +1,154 @@
1
+ import hashlib
2
+ from typing import Tuple
3
+ from quadratic_form import QuadraticForm
4
+
5
+ # --- Helper functions ---------------------------------------------------
6
+
7
+ def hash_to_int(*args: bytes) -> int:
8
+ """
9
+ Hash the concatenation of args (bytes) to a large integer using SHA-256.
10
+ """
11
+ h = hashlib.sha256()
12
+ for b in args:
13
+ h.update(b)
14
+ return int.from_bytes(h.digest(), 'big')
15
+
16
+ # --- Class-group VDF functions using QuadraticForm ----------------------
17
+
18
+ def group_mul(x: QuadraticForm, y: QuadraticForm) -> QuadraticForm:
19
+ """
20
+ Compose two class-group elements via QuadraticForm multiplication.
21
+ """
22
+ return (x * y)
23
+
24
+
25
+ def identity(D: int) -> QuadraticForm:
26
+ """
27
+ Return the identity element of the class group for discriminant D.
28
+ """
29
+ # For D ≡ 1 mod 4, identity form is (1, 1, (1-D)//4)
30
+ b0 = 1
31
+ c0 = (b0*b0 - D) // 4
32
+ return QuadraticForm(1, b0, c0, D)
33
+
34
+
35
+ def class_group_square(x: QuadraticForm) -> QuadraticForm:
36
+ """
37
+ One sequential squaring step in the class group.
38
+ """
39
+ return group_mul(x, x)
40
+
41
+
42
+ def group_exp(x: QuadraticForm, exponent: int) -> QuadraticForm:
43
+ """
44
+ Fast exponentiation in the class group by repeated squaring.
45
+ """
46
+ result = identity(x.D)
47
+ base = x
48
+ e = exponent
49
+ while e > 0:
50
+ if e & 1:
51
+ result = group_mul(result, base)
52
+ base = group_mul(base, base)
53
+ e >>= 1
54
+ return result
55
+
56
+ # --- Wesolowski proof and verify ----------------------------------------
57
+
58
+ def compute_wesolowski_proof(
59
+ x0: QuadraticForm,
60
+ y: QuadraticForm,
61
+ T: int
62
+ ) -> QuadraticForm:
63
+ """
64
+ Compute the Wesolowski proof π for VDF evaluation:
65
+ Solve 2^T = c * q + r, where
66
+ c = hash(x0 || y || T)
67
+ Return π = x0^q in the class group.
68
+ """
69
+ # Derive challenge c
70
+ h_bytes = serialize(x0) + serialize(y) + T.to_bytes((T.bit_length()+7)//8, 'big')
71
+ c = hash_to_int(h_bytes)
72
+ # Divide exponent
73
+ two_T = 1 << T
74
+ q, r = divmod(two_T, c)
75
+ # π = x0^q
76
+ return group_exp(x0, q)
77
+
78
+
79
+ def verify_wesolowski_proof(
80
+ x0: QuadraticForm,
81
+ y: QuadraticForm,
82
+ pi: QuadraticForm,
83
+ T: int
84
+ ) -> bool:
85
+ """
86
+ Verify π satisfies: π^c * x0^r == y.
87
+ """
88
+ h_bytes = serialize(x0) + serialize(y) + T.to_bytes((T.bit_length()+7)//8, 'big')
89
+ c = hash_to_int(h_bytes)
90
+ two_T = 1 << T
91
+ q, r = divmod(two_T, c)
92
+ lhs = group_mul(group_exp(pi, c), group_exp(x0, r))
93
+ return lhs == y
94
+
95
+ # --- Serialization helpers ----------------------------------------------
96
+
97
+ def serialize(x: QuadraticForm) -> bytes:
98
+ """
99
+ Serialize a QuadraticForm to bytes.
100
+ """
101
+ return x.to_bytes()
102
+
103
+
104
+ def deserialize(data: bytes, D: int) -> QuadraticForm:
105
+ """
106
+ Deserialize bytes into a QuadraticForm of discriminant D.
107
+ """
108
+ return QuadraticForm.from_bytes(data, D)
109
+
110
+ # --- Public VDF API -----------------------------------------------------
111
+
112
+ def generate(
113
+ old_output: bytes,
114
+ T: int,
115
+ D: int
116
+ ) -> Tuple[bytes, bytes]:
117
+ """
118
+ Evaluate the VDF by sequentially squaring the previous output 'T' times,
119
+ then produce a Wesolowski proof.
120
+
121
+ Returns:
122
+ new_output : serialized new VDF output (y)
123
+ proof : serialized proof (π)
124
+ """
125
+ # Decode previous output
126
+ x0 = deserialize(old_output, D)
127
+ # Sequential squarings
128
+ x = x0
129
+ for _ in range(T):
130
+ x = class_group_square(x)
131
+ # Serialize output
132
+ y_bytes = serialize(x)
133
+ # Compute proof
134
+ pi = compute_wesolowski_proof(x0, x, T)
135
+ proof_bytes = serialize(pi)
136
+ return y_bytes, proof_bytes
137
+
138
+
139
+ def verify(
140
+ old_output: bytes,
141
+ new_output: bytes,
142
+ proof: bytes,
143
+ T: int,
144
+ D: int
145
+ ) -> bool:
146
+ """
147
+ Verify the Wesolowski VDF proof.
148
+
149
+ Returns True if valid, False otherwise.
150
+ """
151
+ x0 = deserialize(old_output, D)
152
+ y = deserialize(new_output, D)
153
+ pi = deserialize(proof, D)
154
+ return verify_wesolowski_proof(x0, y, pi, T)
astreum/node.py CHANGED
@@ -324,6 +324,86 @@ class Env:
324
324
  )
325
325
 
326
326
 
327
+ class Transaction:
328
+ def __init__(
329
+ self,
330
+ sender_pk: bytes,
331
+ recipient_pk: bytes,
332
+ amount: int,
333
+ fee: int,
334
+ nonce: int,
335
+ signature: bytes | None = None,
336
+ ) -> None:
337
+ self.sender_pk = sender_pk
338
+ self.recipient_pk = recipient_pk
339
+ self.amount = amount
340
+ self.fee = fee
341
+ self.nonce = nonce
342
+ self.signature = signature
343
+
344
+ if self.amount < 0 or self.fee < 0:
345
+ raise ValueError("amount and fee must be non-negative")
346
+
347
+ if self.fee % 2 != 0:
348
+ raise ValueError("fee must be divisible by two")
349
+
350
+ self.tx_body_hash: bytes = blake3.blake3(self._body_bytes()).digest()
351
+
352
+ if self.signature is not None:
353
+ self.tx_hash = blake3.blake3(self.tx_body_hash + self.signature).digest()
354
+ else:
355
+ self.tx_hash = None
356
+
357
+ def sign(self, priv_key: ed25519.Ed25519PrivateKey) -> None:
358
+ if self.signature is not None:
359
+ raise ValueError("transaction already signed")
360
+ sig = priv_key.sign(self.tx_body_hash)
361
+ self.signature = sig
362
+ self.tx_hash = blake3.blake3(self.tx_body_hash + sig).digest()
363
+
364
+ def verify_signature(self) -> bool:
365
+ if self.signature is None:
366
+ return False
367
+ try:
368
+ pub = ed25519.Ed25519PublicKey.from_public_bytes(self.sender_pk)
369
+ pub.verify(self.signature, self.tx_body_hash)
370
+ return True
371
+ except Exception:
372
+ return False
373
+
374
+ def to_bytes(self) -> bytes:
375
+ sig = self.signature or b""
376
+ return encode([
377
+ self.sender_pk,
378
+ self.recipient_pk,
379
+ self.amount,
380
+ self.fee,
381
+ self.nonce,
382
+ sig,
383
+ ])
384
+
385
+ @classmethod
386
+ def from_bytes(cls, blob: bytes) -> 'Transaction':
387
+ sender, recipient, amount, fee, nonce, sig = decode(blob)
388
+ return cls(sender, recipient, int(amount), int(fee), int(nonce), sig)
389
+
390
+ def _body_bytes(self) -> bytes:
391
+ return encode([
392
+ self.sender_pk,
393
+ self.recipient_pk,
394
+ self.amount,
395
+ self.fee,
396
+ self.nonce,
397
+ ])
398
+
399
+ def __eq__(self, other: Any) -> bool:
400
+ if not isinstance(other, Transaction):
401
+ return NotImplemented
402
+ return self.tx_hash == other.tx_hash
403
+
404
+ def __hash__(self) -> int:
405
+ return int.from_bytes(self.tx_hash, 'big')
406
+
327
407
  class Node:
328
408
  def __init__(self, config: dict = {}):
329
409
  self._machine_setup()
@@ -331,6 +411,16 @@ class Node:
331
411
  if not machine_only:
332
412
  self._storage_setup(config=config)
333
413
  self._relay_setup(config=config)
414
+ self._validation_setup(config=config)
415
+
416
+ def _validation_setup(self, config: dict):
417
+ if True:
418
+ self.validator_transactions: Dict[bytes, Transaction] = {}
419
+ # validator thread
420
+ pass
421
+
422
+ def _create_block(self):
423
+ pass
334
424
 
335
425
  # STORAGE METHODS
336
426
  def _storage_setup(self, config: dict):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -1,18 +1,20 @@
1
1
  astreum/__init__.py,sha256=y2Ok3EY_FstcmlVASr80lGR_0w-dH-SXDCCQFmL6uwA,28
2
2
  astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
3
- astreum/node.py,sha256=SswYgBq6-iJyD6dWbo3nf0_QrjNl_KifPSJsFMJ2z1Q,45941
3
+ astreum/node.py,sha256=LImdMz2-eeVrR67p_67bVBUaVgo-atoe4fgU42bnXAA,48659
4
4
  astreum/_node/__init__.py,sha256=7yz1YHo0DCUgUQvJf75qdUo_ocl5-XZRU-Vc2NhcvJs,18639
5
5
  astreum/_node/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  astreum/_node/storage/merkle.py,sha256=XCQBrHbwI0FuPTCUwHOy-Kva3uWbvCdw_-13hRPf1UI,10219
7
7
  astreum/_node/storage/patricia.py,sha256=tynxn_qETCU9X7yJdeh_0GHpC8Pzcoq4CWrSZlMUeRc,11546
8
8
  astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
10
+ astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
11
+ astreum/crypto/wesolowski.py,sha256=FDAX82L5cceR6DGTtUO57ZhcxpBNiskGrnLWnd_3BSw,4084
10
12
  astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
11
13
  astreum/lispeum/__init__.py,sha256=K-NDzIjtIsXzC9X7lnYvlvIaVxjFcY7WNsgLIE3DH3U,58
12
14
  astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
13
15
  astreum/lispeum/tokenizer.py,sha256=J-I7MEd0r2ZoVqxvRPlu-Afe2ZdM0tKXXhf1R4SxYTo,1429
14
- astreum-0.2.7.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
15
- astreum-0.2.7.dist-info/METADATA,sha256=XNEj0LtWAJMnTLPgvIfZDawrG7PIrLXiPm6pAv2g4Og,5453
16
- astreum-0.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- astreum-0.2.7.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
18
- astreum-0.2.7.dist-info/RECORD,,
16
+ astreum-0.2.9.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
17
+ astreum-0.2.9.dist-info/METADATA,sha256=XISFbnyLNnvRhitMrPeQVC78T15Ipy9JNgO8IhVbSH8,5453
18
+ astreum-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ astreum-0.2.9.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
20
+ astreum-0.2.9.dist-info/RECORD,,