astreum 0.2.43__tar.gz → 0.2.45__tar.gz

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.

Files changed (62) hide show
  1. {astreum-0.2.43/src/astreum.egg-info → astreum-0.2.45}/PKG-INFO +1 -1
  2. {astreum-0.2.43 → astreum-0.2.45}/pyproject.toml +1 -1
  3. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_communication/message.py +1 -0
  4. astreum-0.2.45/src/astreum/_communication/peer.py +23 -0
  5. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_communication/route.py +40 -3
  6. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_communication/setup.py +72 -4
  7. astreum-0.2.45/src/astreum/_consensus/genesis.py +72 -0
  8. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/setup.py +50 -3
  9. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/workers/validation.py +5 -2
  10. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_node.py +44 -10
  11. astreum-0.2.45/src/astreum/utils/bytes.py +24 -0
  12. {astreum-0.2.43 → astreum-0.2.45/src/astreum.egg-info}/PKG-INFO +1 -1
  13. {astreum-0.2.43 → astreum-0.2.45}/src/astreum.egg-info/SOURCES.txt +1 -0
  14. astreum-0.2.43/src/astreum/_communication/peer.py +0 -11
  15. astreum-0.2.43/src/astreum/_consensus/genesis.py +0 -141
  16. {astreum-0.2.43 → astreum-0.2.45}/LICENSE +0 -0
  17. {astreum-0.2.43 → astreum-0.2.45}/README.md +0 -0
  18. {astreum-0.2.43 → astreum-0.2.45}/setup.cfg +0 -0
  19. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/__init__.py +0 -0
  20. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_communication/__init__.py +0 -0
  21. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_communication/ping.py +0 -0
  22. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_communication/util.py +0 -0
  23. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/__init__.py +0 -0
  24. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/account.py +0 -0
  25. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/accounts.py +0 -0
  26. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/block.py +0 -0
  27. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/chain.py +0 -0
  28. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/fork.py +0 -0
  29. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/receipt.py +0 -0
  30. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/transaction.py +0 -0
  31. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/workers/__init__.py +0 -0
  32. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/workers/discovery.py +0 -0
  33. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_consensus/workers/verify.py +0 -0
  34. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/__init__.py +0 -0
  35. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/environment.py +0 -0
  36. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/expression.py +0 -0
  37. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/high_evaluation.py +0 -0
  38. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/low_evaluation.py +0 -0
  39. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/meter.py +0 -0
  40. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/parser.py +0 -0
  41. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_lispeum/tokenizer.py +0 -0
  42. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_storage/__init__.py +0 -0
  43. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_storage/atom.py +0 -0
  44. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/_storage/patricia.py +0 -0
  45. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/crypto/__init__.py +0 -0
  46. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/crypto/ed25519.py +0 -0
  47. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/crypto/quadratic_form.py +0 -0
  48. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/crypto/wesolowski.py +0 -0
  49. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/crypto/x25519.py +0 -0
  50. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/format.py +0 -0
  51. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/models/__init__.py +0 -0
  52. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/models/block.py +0 -0
  53. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/models/merkle.py +0 -0
  54. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/models/patricia.py +0 -0
  55. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/node.py +0 -0
  56. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/storage/__init__.py +0 -0
  57. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/storage/object.py +0 -0
  58. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/storage/setup.py +0 -0
  59. {astreum-0.2.43 → astreum-0.2.45}/src/astreum/utils/integer.py +0 -0
  60. {astreum-0.2.43 → astreum-0.2.45}/src/astreum.egg-info/dependency_links.txt +0 -0
  61. {astreum-0.2.43 → astreum-0.2.45}/src/astreum.egg-info/requires.txt +0 -0
  62. {astreum-0.2.43 → astreum-0.2.45}/src/astreum.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.43
3
+ Version: 0.2.45
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.2.43"
3
+ version = "0.2.45"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -10,6 +10,7 @@ class MessageTopic(IntEnum):
10
10
  ROUTE_REQUEST = 3
11
11
  ROUTE_RESPONSE = 4
12
12
  TRANSACTION = 5
13
+ STORAGE_REQUEST = 6
13
14
 
14
15
 
15
16
  class Message:
@@ -0,0 +1,23 @@
1
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
2
+ from cryptography.hazmat.primitives import serialization
3
+ from datetime import datetime, timezone
4
+ from typing import Optional, Tuple
5
+
6
+ class Peer:
7
+ shared_key: bytes
8
+ timestamp: datetime
9
+ latest_block: bytes
10
+ address: Optional[Tuple[str, int]]
11
+ public_key: X25519PublicKey
12
+ public_key_bytes: bytes
13
+
14
+ def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
15
+ self.shared_key = my_sec_key.exchange(peer_pub_key)
16
+ self.timestamp = datetime.now(timezone.utc)
17
+ self.latest_block = b""
18
+ self.address = None
19
+ self.public_key = peer_pub_key
20
+ self.public_key_bytes = peer_pub_key.public_bytes(
21
+ encoding=serialization.Encoding.Raw,
22
+ format=serialization.PublicFormat.Raw,
23
+ )
@@ -1,6 +1,7 @@
1
- from typing import Dict, List, Union
1
+ from typing import Dict, List, Optional, Union
2
2
  from cryptography.hazmat.primitives import serialization
3
3
  from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
4
+ from .peer import Peer
4
5
 
5
6
  PeerKey = Union[X25519PublicKey, bytes, bytearray]
6
7
 
@@ -15,7 +16,7 @@ class Route:
15
16
  self.buckets: Dict[int, List[bytes]] = {
16
17
  i: [] for i in range(len(self.relay_public_key_bytes) * 8)
17
18
  }
18
- self.peers = {}
19
+ self.peers: Dict[bytes, Peer] = {}
19
20
 
20
21
  @staticmethod
21
22
  def _matching_leading_bits(a: bytes, b: bytes) -> int:
@@ -38,13 +39,21 @@ class Route:
38
39
  return key_bytes
39
40
  raise TypeError("peer_public_key must be raw bytes or X25519PublicKey")
40
41
 
41
- def add_peer(self, peer_public_key: PeerKey):
42
+ @staticmethod
43
+ def _xor_distance(a: bytes, b: bytes) -> int:
44
+ if len(a) != len(b):
45
+ raise ValueError("xor distance requires equal-length operands")
46
+ return int.from_bytes(bytes(x ^ y for x, y in zip(a, b)), "big", signed=False)
47
+
48
+ def add_peer(self, peer_public_key: PeerKey, peer: Optional[Peer] = None):
42
49
  peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
43
50
  bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
44
51
  if len(self.buckets[bucket_idx]) < self.bucket_size:
45
52
  bucket = self.buckets[bucket_idx]
46
53
  if peer_public_key_bytes not in bucket:
47
54
  bucket.append(peer_public_key_bytes)
55
+ if peer is not None:
56
+ self.peers[peer_public_key_bytes] = peer
48
57
 
49
58
  def remove_peer(self, peer_public_key: PeerKey):
50
59
  peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
@@ -56,3 +65,31 @@ class Route:
56
65
  bucket.remove(peer_public_key_bytes)
57
66
  except ValueError:
58
67
  pass
68
+ self.peers.pop(peer_public_key_bytes, None)
69
+
70
+ def closest_peer_for_hash(self, target_hash: bytes) -> Optional[Peer]:
71
+ """Return the peer with the minimal XOR distance to ``target_hash``."""
72
+ if not isinstance(target_hash, (bytes, bytearray)):
73
+ raise TypeError("target_hash must be bytes-like")
74
+
75
+ target = bytes(target_hash)
76
+ if len(target) != len(self.relay_public_key_bytes):
77
+ raise ValueError("target_hash must match peer key length (32 bytes)")
78
+
79
+ closest_key: Optional[bytes] = None
80
+ closest_distance: Optional[int] = None
81
+
82
+ for bucket in self.buckets.values():
83
+ for peer_key in bucket:
84
+ try:
85
+ distance = self._xor_distance(target, peer_key)
86
+ except ValueError:
87
+ continue
88
+ if closest_distance is None or distance < closest_distance:
89
+ closest_distance = distance
90
+ closest_key = peer_key
91
+
92
+ if closest_key is None:
93
+ return None
94
+ peer = self.peers.get(closest_key)
95
+ return peer
@@ -95,9 +95,10 @@ def process_incoming_messages(node: "Node") -> None:
95
95
  peer = Peer(node.relay_secret_key, sender_key)
96
96
  except Exception:
97
97
  continue
98
-
98
+ peer.address = address_key
99
+
99
100
  node.peers[sender_public_key_bytes] = peer
100
- node.peer_route.add_peer(sender_public_key_bytes)
101
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
101
102
 
102
103
  response = Message(handshake=True, sender=node.relay_public_key)
103
104
  node.outgoing_queue.put((response.to_bytes(), address_key))
@@ -105,17 +106,25 @@ def process_incoming_messages(node: "Node") -> None:
105
106
 
106
107
  elif old_key_bytes == sender_public_key_bytes:
107
108
  # existing mapping with same key -> nothing to change
108
- pass
109
+ peer = node.peers.get(sender_public_key_bytes)
110
+ if peer is not None:
111
+ peer.address = address_key
109
112
 
110
113
  else:
111
114
  # address reused with a different key -> replace peer
112
115
  node.peers.pop(old_key_bytes, None)
116
+ try:
117
+ node.peer_route.remove_peer(old_key_bytes)
118
+ except Exception:
119
+ pass
113
120
  try:
114
121
  peer = Peer(node.relay_secret_key, sender_key)
115
122
  except Exception:
116
123
  continue
117
-
124
+ peer.address = address_key
125
+
118
126
  node.peers[sender_public_key_bytes] = peer
127
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
119
128
 
120
129
  match message.topic:
121
130
  case MessageTopic.PING:
@@ -164,6 +173,65 @@ def process_incoming_messages(node: "Node") -> None:
164
173
  if node.validation_secret_key is None:
165
174
  continue
166
175
  node._validation_transaction_queue.put(message.content)
176
+
177
+ case MessageTopic.STORAGE_REQUEST:
178
+ payload = message.content
179
+ if len(payload) < 32:
180
+ continue
181
+
182
+ atom_id = payload[:32]
183
+ provider_bytes = payload[32:]
184
+ if not provider_bytes:
185
+ continue
186
+
187
+ try:
188
+ provider_str = provider_bytes.decode("utf-8")
189
+ except UnicodeDecodeError:
190
+ continue
191
+
192
+ try:
193
+ host, port = addr[0], int(addr[1])
194
+ except Exception:
195
+ continue
196
+ address_key = (host, port)
197
+ sender_key_bytes = node.addresses.get(address_key)
198
+ if sender_key_bytes is None:
199
+ continue
200
+
201
+ try:
202
+ local_key_bytes = node.relay_public_key.public_bytes(
203
+ encoding=serialization.Encoding.Raw,
204
+ format=serialization.PublicFormat.Raw,
205
+ )
206
+ except Exception:
207
+ continue
208
+
209
+ def xor_distance(target: bytes, key: bytes) -> int:
210
+ return int.from_bytes(
211
+ bytes(a ^ b for a, b in zip(target, key)),
212
+ byteorder="big",
213
+ signed=False,
214
+ )
215
+
216
+ self_distance = xor_distance(atom_id, local_key_bytes)
217
+
218
+ try:
219
+ closest_peer = node.peer_route.closest_peer_for_hash(atom_id)
220
+ except Exception:
221
+ closest_peer = None
222
+
223
+ if (
224
+ closest_peer is not None
225
+ and closest_peer.public_key_bytes != sender_key_bytes
226
+ ):
227
+ closest_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
228
+ if closest_distance < self_distance:
229
+ target_addr = closest_peer.address
230
+ if target_addr is not None and target_addr != addr:
231
+ node.outgoing_queue.put((message.to_bytes(), target_addr))
232
+ continue
233
+
234
+ node.storage_index[atom_id] = provider_str.strip()
167
235
 
168
236
  case _:
169
237
  continue
@@ -0,0 +1,72 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, List
5
+
6
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
+
8
+ from .account import Account
9
+ from .block import Block
10
+ from .._storage.atom import ZERO32
11
+ from .._storage.patricia import PatriciaTrie
12
+ from ..utils.integer import int_to_bytes
13
+
14
+ TREASURY_ADDRESS = b"\x01" * 32
15
+ BURN_ADDRESS = b"\x00" * 32
16
+ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
17
+ validator_pk = bytes(validator_public_key)
18
+
19
+ if len(validator_pk) != 32:
20
+ raise ValueError("validator_public_key must be 32 bytes")
21
+
22
+ # 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
23
+ stake_trie = PatriciaTrie()
24
+ stake_amount = int_to_bytes(1)
25
+ stake_trie.put(storage_node=node, key=validator_pk, value=stake_amount)
26
+ stake_root = stake_trie.root_hash
27
+
28
+ # 2. Account trie with treasury, burn, and validator accounts.
29
+ accounts_trie = PatriciaTrie()
30
+
31
+ treasury_account = Account.create(balance=1, data=stake_root, counter=0)
32
+ accounts_trie.put(storage_node=node, key=TREASURY_ADDRESS, value=treasury_account.hash)
33
+
34
+ burn_account = Account.create(balance=0, data=b"", counter=0)
35
+ accounts_trie.put(storage_node=node, key=BURN_ADDRESS, value=burn_account.hash)
36
+
37
+ validator_account = Account.create(balance=0, data=b"", counter=0)
38
+ accounts_trie.put(storage_node=node, key=validator_pk, value=validator_account.hash)
39
+
40
+ accounts_root = accounts_trie.root_hash
41
+ if accounts_root is None:
42
+ raise ValueError("genesis accounts trie is empty")
43
+
44
+ # 3. Assemble block metadata.
45
+ block = Block()
46
+ block.previous_block_hash = ZERO32
47
+ block.number = 0
48
+ block.timestamp = 0
49
+ block.accounts_hash = accounts_root
50
+ block.accounts = accounts_trie
51
+ block.transactions_total_fees = 0
52
+ block.transactions_hash = ZERO32
53
+ block.receipts_hash = ZERO32
54
+ block.delay_difficulty = 0
55
+ block.delay_output = b""
56
+ block.validator_public_key = validator_pk
57
+ block.transactions = []
58
+ block.receipts = []
59
+
60
+ # 4. Sign the block body with the validator secret key.
61
+ block.signature = b""
62
+ block.to_atom()
63
+
64
+ if block.body_hash is None:
65
+ raise ValueError("failed to materialise genesis block body")
66
+
67
+ secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
68
+ block.signature = secret.sign(block.body_hash)
69
+ block_hash, _ = block.to_atom()
70
+
71
+ block.hash = block_hash
72
+ return block
@@ -2,13 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import threading
4
4
  from queue import Queue
5
- from typing import Any
5
+ from typing import Any, Optional
6
6
 
7
7
  from .workers import (
8
8
  make_discovery_worker,
9
9
  make_validation_worker,
10
10
  make_verify_worker,
11
11
  )
12
+ from .genesis import create_genesis_block
13
+ from ..utils.bytes import hex_to_bytes
12
14
 
13
15
 
14
16
  def current_validator(node: Any) -> bytes:
@@ -16,7 +18,9 @@ def current_validator(node: Any) -> bytes:
16
18
  raise NotImplementedError("current_validator must be implemented by the host node")
17
19
 
18
20
 
19
- def consensus_setup(node: Any) -> None:
21
+ def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
22
+ config = config or {}
23
+
20
24
  # Shared state
21
25
  node.validation_lock = getattr(node, "validation_lock", threading.RLock())
22
26
 
@@ -26,6 +30,12 @@ def consensus_setup(node: Any) -> None:
26
30
  node.chains = getattr(node, "chains", {})
27
31
  node.forks = getattr(node, "forks", {})
28
32
 
33
+ node.latest_block_hash = None
34
+ latest_block_hex = config.get("latest_block_hash")
35
+ if latest_block_hex is not None:
36
+ node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
37
+ node.latest_block = None
38
+
29
39
  # Pending transactions queue (hash-only entries)
30
40
  node._validation_transaction_queue = getattr(
31
41
  node, "_validation_transaction_queue", Queue()
@@ -64,5 +74,42 @@ def consensus_setup(node: Any) -> None:
64
74
  )
65
75
  node.consensus_discovery_thread.start()
66
76
  node.consensus_verify_thread.start()
67
- if getattr(node, "validation_secret_key", None):
77
+
78
+ validator_secret_hex = config.get("validation_secret_key")
79
+ if validator_secret_hex:
80
+ validator_secret_bytes = hex_to_bytes(validator_secret_hex, expected_length=32)
81
+ try:
82
+ from cryptography.hazmat.primitives import serialization
83
+ from cryptography.hazmat.primitives.asymmetric import ed25519
84
+
85
+ validator_private = ed25519.Ed25519PrivateKey.from_private_bytes(
86
+ validator_secret_bytes
87
+ )
88
+ except Exception as exc:
89
+ raise ValueError("invalid validation_secret_key") from exc
90
+
91
+ validator_public_bytes = validator_private.public_key().public_bytes(
92
+ encoding=serialization.Encoding.Raw,
93
+ format=serialization.PublicFormat.Raw,
94
+ )
95
+
96
+ node.validation_secret_key = validator_private
97
+ node.validation_public_key = validator_public_bytes
98
+
99
+ if node.latest_block_hash is None:
100
+ genesis_block = create_genesis_block(
101
+ node,
102
+ validator_public_key=validator_public_bytes,
103
+ validator_secret_key=validator_secret_bytes,
104
+ )
105
+ genesis_hash, genesis_atoms = genesis_block.to_atom()
106
+ if hasattr(node, "_local_set"):
107
+ for atom in genesis_atoms:
108
+ try:
109
+ node._local_set(atom.object_id(), atom)
110
+ except Exception:
111
+ pass
112
+ node.latest_block_hash = genesis_hash
113
+ node.latest_block = genesis_block
114
+
68
115
  node.consensus_validation_thread.start()
@@ -88,7 +88,7 @@ def make_validation_worker(
88
88
  new_block.timestamp = max(int(now), min_allowed)
89
89
 
90
90
  # atomize block
91
- new_block_hash, _ = new_block.to_atom()
91
+ new_block_hash, new_block_atoms = new_block.to_atom()
92
92
  # put as own latest block hash
93
93
  node.latest_block_hash = new_block_hash
94
94
 
@@ -117,6 +117,9 @@ def make_validation_worker(
117
117
  except Exception:
118
118
  pass
119
119
 
120
- # store the new block and receipts
120
+ # upload block atoms
121
+
122
+ # upload receipt atoms
123
+ # upload account atoms
121
124
 
122
125
  return _validation_worker
@@ -15,21 +15,24 @@ class Node:
15
15
  # Storage Setup
16
16
  self.in_memory_storage: Dict[bytes, Atom] = {}
17
17
  self.in_memory_storage_lock = threading.RLock()
18
+ self.storage_index: Dict[bytes, str] = {}
18
19
  # Lispeum Setup
19
20
  self.environments: Dict[uuid.UUID, Env] = {}
20
21
  self.machine_environments_lock = threading.RLock()
21
22
  self.low_eval = low_eval
22
23
  # Communication and Validation Setup (import lazily to avoid heavy deps during parsing tests)
23
- try:
24
- from astreum._communication import communication_setup # type: ignore
25
- communication_setup(node=self, config=config)
26
- except Exception:
27
- pass
28
- try:
29
- from astreum._consensus import consensus_setup # type: ignore
30
- consensus_setup(node=self)
31
- except Exception:
32
- pass
24
+ try:
25
+ from astreum._communication import communication_setup # type: ignore
26
+ communication_setup(node=self, config=config)
27
+ except Exception:
28
+ pass
29
+ try:
30
+ from astreum._consensus import consensus_setup # type: ignore
31
+ consensus_setup(node=self, config=config)
32
+ except Exception:
33
+ pass
34
+
35
+
33
36
 
34
37
  # ---- Env helpers ----
35
38
  def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
@@ -68,3 +71,34 @@ class Node:
68
71
  if atom is not None:
69
72
  return atom
70
73
  return self._network_get(key)
74
+
75
+ def _network_set(self, atom: Atom) -> None:
76
+ """Advertise an atom to the closest known peer so they can fetch it from us."""
77
+ try:
78
+ from src.astreum._communication.message import Message, MessageTopic
79
+ except Exception:
80
+ return
81
+
82
+ atom_id = atom.object_id()
83
+ try:
84
+ closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
85
+ except Exception:
86
+ return
87
+ if closest_peer is None or closest_peer.address is None:
88
+ return
89
+ target_addr = closest_peer.address
90
+
91
+ try:
92
+ provider_ip, provider_port = self.incoming_socket.getsockname()[:2]
93
+ except Exception:
94
+ return
95
+
96
+ provider_str = f"{provider_ip}:{int(provider_port)}"
97
+ try:
98
+ provider_bytes = provider_str.encode("utf-8")
99
+ except Exception:
100
+ return
101
+
102
+ payload = atom_id + provider_bytes
103
+ message = Message(topic=MessageTopic.STORAGE_REQUEST, content=payload)
104
+ self.outgoing_queue.put((message.to_bytes(), target_addr))
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+
4
+ def hex_to_bytes(value: str, *, expected_length: Optional[int] = None) -> bytes:
5
+ """Convert a 0x-prefixed hex string into raw bytes."""
6
+ if not isinstance(value, str):
7
+ raise TypeError("hex value must be provided as a string")
8
+
9
+ if not value.startswith(("0x", "0X")):
10
+ raise ValueError("hex value must start with '0x'")
11
+
12
+ hex_digits = value[2:]
13
+ if len(hex_digits) % 2:
14
+ raise ValueError("hex value must have an even number of digits")
15
+
16
+ try:
17
+ result = bytes.fromhex(hex_digits)
18
+ except ValueError as exc:
19
+ raise ValueError("hex value contains non-hexadecimal characters") from exc
20
+
21
+ if expected_length is not None and len(result) != expected_length:
22
+ raise ValueError(f"hex value must decode to exactly {expected_length} bytes")
23
+
24
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.43
3
+ Version: 0.2.45
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
@@ -54,4 +54,5 @@ src/astreum/models/patricia.py
54
54
  src/astreum/storage/__init__.py
55
55
  src/astreum/storage/object.py
56
56
  src/astreum/storage/setup.py
57
+ src/astreum/utils/bytes.py
57
58
  src/astreum/utils/integer.py
@@ -1,11 +0,0 @@
1
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
2
- from datetime import datetime, timezone
3
-
4
- class Peer:
5
- shared_key: bytes
6
- timestamp: datetime
7
- latest_block: bytes
8
-
9
- def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
10
- self.shared_key = my_sec_key.exchange(peer_pub_key)
11
- self.timestamp = datetime.now(timezone.utc)
@@ -1,141 +0,0 @@
1
-
2
- from __future__ import annotations
3
-
4
- from typing import Any, Iterable, List, Optional, Tuple
5
-
6
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
-
8
- from .account import Account
9
- from .block import Block
10
- from .._storage.atom import Atom, ZERO32
11
- from .._storage.patricia import PatriciaTrie, PatriciaNode
12
-
13
- TREASURY_ADDRESS = b"\x01" * 32
14
- BURN_ADDRESS = b"\x00" * 32
15
-
16
-
17
- def _int_to_be_bytes(value: int) -> bytes:
18
- if value < 0:
19
- raise ValueError("integer fields in genesis must be non-negative")
20
- if value == 0:
21
- return b"\x00"
22
- length = (value.bit_length() + 7) // 8
23
- return value.to_bytes(length, "big")
24
-
25
-
26
- def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
27
- next_hash = ZERO32
28
- chain: List[Atom] = []
29
- for child_id in reversed(child_ids):
30
- elem = Atom.from_data(data=child_id, next_hash=next_hash)
31
- next_hash = elem.object_id()
32
- chain.append(elem)
33
- chain.reverse()
34
-
35
- value_atom = Atom.from_data(
36
- data=len(child_ids).to_bytes(8, "little"),
37
- next_hash=next_hash,
38
- )
39
- type_atom = Atom.from_data(data=b"list", next_hash=value_atom.object_id())
40
- atoms = chain + [value_atom, type_atom]
41
- return type_atom.object_id(), atoms
42
-
43
-
44
- def _store_atoms(node: Any, atoms: Iterable[Atom]) -> None:
45
- setter = getattr(node, "_local_set", None)
46
- if not callable(setter):
47
- raise TypeError("node must expose '_local_set(object_id, atom)'")
48
- for atom in atoms:
49
- setter(atom.object_id(), atom)
50
-
51
-
52
- def _persist_trie(trie: PatriciaTrie, node: Any) -> None:
53
- for patricia_node in trie.nodes.values():
54
- _, atoms = patricia_node.to_atoms()
55
- _store_atoms(node, atoms)
56
-
57
-
58
- if not hasattr(PatriciaNode, "to_bytes"):
59
- def _patricia_node_to_bytes(self: PatriciaNode) -> bytes: # type: ignore[no-redef]
60
- fields = [
61
- bytes([self.key_len]) + self.key,
62
- self.child_0 or ZERO32,
63
- self.child_1 or ZERO32,
64
- self.value or b"",
65
- ]
66
- encoded: List[bytes] = []
67
- for field in fields:
68
- encoded.append(len(field).to_bytes(4, "big"))
69
- encoded.append(field)
70
- return b"".join(encoded)
71
-
72
- PatriciaNode.to_bytes = _patricia_node_to_bytes # type: ignore[attr-defined]
73
-
74
-
75
- def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
76
- validator_pk = bytes(validator_public_key)
77
-
78
- if len(validator_pk) != 32:
79
- raise ValueError("validator_public_key must be 32 bytes")
80
-
81
- # 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
82
- stake_trie = PatriciaTrie()
83
- stake_amount = (1).to_bytes(32, "big")
84
- stake_trie.put(node, validator_pk, stake_amount)
85
- _persist_trie(stake_trie, node)
86
- stake_root = stake_trie.root_hash or ZERO32
87
-
88
- # 2. Account trie with treasury, burn, and validator accounts.
89
- accounts_trie = PatriciaTrie()
90
-
91
- treasury_account = Account.create(balance=1, data=stake_root, counter=0)
92
- treasury_account_id, treasury_atoms = treasury_account.to_atom()
93
- _store_atoms(node, treasury_atoms)
94
- accounts_trie.put(node, TREASURY_ADDRESS, treasury_account_id)
95
-
96
- burn_account = Account.create(balance=0, data=b"", counter=0)
97
- burn_account_id, burn_atoms = burn_account.to_atom()
98
- _store_atoms(node, burn_atoms)
99
- accounts_trie.put(node, BURN_ADDRESS, burn_account_id)
100
-
101
- validator_account = Account.create(balance=0, data=b"", counter=0)
102
- validator_account_id, validator_atoms = validator_account.to_atom()
103
- _store_atoms(node, validator_atoms)
104
- accounts_trie.put(node, validator_pk, validator_account_id)
105
-
106
- _persist_trie(accounts_trie, node)
107
-
108
- accounts_root = accounts_trie.root_hash
109
- if accounts_root is None:
110
- raise ValueError("genesis accounts trie is empty")
111
-
112
- # 3. Assemble block metadata.
113
- block = Block()
114
- block.previous_block_hash = ZERO32
115
- block.number = 0
116
- block.timestamp = 0
117
- block.accounts_hash = accounts_root
118
- block.accounts = accounts_trie
119
- block.transactions_total_fees = 0
120
- block.transactions_hash = ZERO32
121
- block.receipts_hash = ZERO32
122
- block.delay_difficulty = 0
123
- block.delay_output = b""
124
- block.validator_public_key = validator_pk
125
- block.transactions = []
126
- block.receipts = []
127
-
128
- # 4. Sign the block body with the validator secret key.
129
- block.signature = b""
130
- block.to_atom()
131
-
132
- if block.body_hash is None:
133
- raise ValueError("failed to materialise genesis block body")
134
-
135
- secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
136
- block.signature = secret.sign(block.body_hash)
137
- block_hash, block_atoms = block.to_atom()
138
- _store_atoms(node, block_atoms)
139
-
140
- block.hash = block_hash
141
- return block
File without changes
File without changes
File without changes
File without changes
File without changes