astreum 0.2.42__tar.gz → 0.2.53__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.
- {astreum-0.2.42/src/astreum.egg-info → astreum-0.2.53}/PKG-INFO +14 -1
- {astreum-0.2.42 → astreum-0.2.53}/README.md +13 -0
- {astreum-0.2.42 → astreum-0.2.53}/pyproject.toml +1 -1
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_communication/message.py +1 -0
- astreum-0.2.53/src/astreum/_communication/peer.py +23 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_communication/route.py +40 -3
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_communication/setup.py +72 -4
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/account.py +1 -1
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/block.py +311 -328
- astreum-0.2.53/src/astreum/_consensus/genesis.py +72 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/setup.py +50 -3
- astreum-0.2.53/src/astreum/_consensus/transaction.py +215 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/workers/validation.py +5 -2
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/workers/verify.py +1 -1
- astreum-0.2.53/src/astreum/_lispeum/expression.py +181 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_lispeum/high_evaluation.py +47 -34
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_lispeum/low_evaluation.py +21 -21
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_lispeum/parser.py +26 -31
- astreum-0.2.53/src/astreum/_node.py +163 -0
- astreum-0.2.53/src/astreum/_storage/__init__.py +7 -0
- astreum-0.2.53/src/astreum/_storage/atom.py +109 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_storage/patricia.py +2 -2
- astreum-0.2.53/src/astreum/_storage/setup.py +35 -0
- astreum-0.2.53/src/astreum/utils/bytes.py +24 -0
- astreum-0.2.53/src/astreum/utils/logging.py +219 -0
- {astreum-0.2.42 → astreum-0.2.53/src/astreum.egg-info}/PKG-INFO +14 -1
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum.egg-info/SOURCES.txt +4 -1
- astreum-0.2.42/src/astreum/_communication/peer.py +0 -11
- astreum-0.2.42/src/astreum/_consensus/genesis.py +0 -141
- astreum-0.2.42/src/astreum/_consensus/transaction.py +0 -216
- astreum-0.2.42/src/astreum/_lispeum/expression.py +0 -37
- astreum-0.2.42/src/astreum/_node.py +0 -58
- astreum-0.2.42/src/astreum/_storage/__init__.py +0 -5
- astreum-0.2.42/src/astreum/_storage/atom.py +0 -117
- {astreum-0.2.42 → astreum-0.2.53}/LICENSE +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/setup.cfg +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_communication/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_communication/ping.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_communication/util.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/accounts.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/chain.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/fork.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/receipt.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/workers/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_consensus/workers/discovery.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_lispeum/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_lispeum/environment.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_lispeum/meter.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/_lispeum/tokenizer.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/crypto/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/crypto/wesolowski.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/format.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/models/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/models/block.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/models/merkle.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/models/patricia.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/node.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/storage/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/storage/object.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/storage/setup.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum/utils/integer.py +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.2.42 → astreum-0.2.53}/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.
|
|
3
|
+
Version: 0.2.53
|
|
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
|
|
@@ -35,6 +35,8 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
35
35
|
| `validation_secret_key` | hex string | `None` | X25519 private key that lets the node participate in the validation route. Leave unset for a non‑validator node. |
|
|
36
36
|
| `storage_path` | string | `None` | Directory where objects are persisted. If *None*, the node uses an in‑memory store. |
|
|
37
37
|
| `storage_get_relay_timeout` | float | `5` | Seconds to wait for an object requested from peers before timing‑out. |
|
|
38
|
+
| `logging_retention` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
39
|
+
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
38
40
|
|
|
39
41
|
### Networking
|
|
40
42
|
|
|
@@ -136,6 +138,17 @@ except ParseError as e:
|
|
|
136
138
|
|
|
137
139
|
---
|
|
138
140
|
|
|
141
|
+
|
|
142
|
+
## Logging
|
|
143
|
+
|
|
144
|
+
Every `Node` instance wires up structured logging automatically:
|
|
145
|
+
|
|
146
|
+
- Logs land in per-instance files named `node.log` under `%LOCALAPPDATA%\Astreum\lib-py\logs/<instance_id>` on Windows and `$XDG_STATE_HOME` (or `~/.local/state`)/`Astreum/lib-py/logs/<instance_id>` on other platforms. The `<instance_id>` is the first 16 hex characters of a BLAKE3 hash of the caller's file path, so running the node from different entry points keeps their logs isolated.
|
|
147
|
+
- Files rotate at midnight UTC with gzip compression (`node-YYYY-MM-DD.log.gz`) and retain 90 days by default. Override via `config["logging_retention"]`.
|
|
148
|
+
- Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
|
|
149
|
+
- Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
|
|
150
|
+
- The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
|
|
151
|
+
|
|
139
152
|
## Testing
|
|
140
153
|
|
|
141
154
|
```bash
|
|
@@ -17,6 +17,8 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
17
17
|
| `validation_secret_key` | hex string | `None` | X25519 private key that lets the node participate in the validation route. Leave unset for a non‑validator node. |
|
|
18
18
|
| `storage_path` | string | `None` | Directory where objects are persisted. If *None*, the node uses an in‑memory store. |
|
|
19
19
|
| `storage_get_relay_timeout` | float | `5` | Seconds to wait for an object requested from peers before timing‑out. |
|
|
20
|
+
| `logging_retention` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
21
|
+
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
20
22
|
|
|
21
23
|
### Networking
|
|
22
24
|
|
|
@@ -118,6 +120,17 @@ except ParseError as e:
|
|
|
118
120
|
|
|
119
121
|
---
|
|
120
122
|
|
|
123
|
+
|
|
124
|
+
## Logging
|
|
125
|
+
|
|
126
|
+
Every `Node` instance wires up structured logging automatically:
|
|
127
|
+
|
|
128
|
+
- Logs land in per-instance files named `node.log` under `%LOCALAPPDATA%\Astreum\lib-py\logs/<instance_id>` on Windows and `$XDG_STATE_HOME` (or `~/.local/state`)/`Astreum/lib-py/logs/<instance_id>` on other platforms. The `<instance_id>` is the first 16 hex characters of a BLAKE3 hash of the caller's file path, so running the node from different entry points keeps their logs isolated.
|
|
129
|
+
- Files rotate at midnight UTC with gzip compression (`node-YYYY-MM-DD.log.gz`) and retain 90 days by default. Override via `config["logging_retention"]`.
|
|
130
|
+
- Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
|
|
131
|
+
- Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
|
|
132
|
+
- The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
|
|
133
|
+
|
|
121
134
|
## Testing
|
|
122
135
|
|
|
123
136
|
```bash
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -33,7 +33,7 @@ class Account:
|
|
|
33
33
|
|
|
34
34
|
@classmethod
|
|
35
35
|
def from_atom(cls, node: Any, account_id: bytes) -> "Account":
|
|
36
|
-
storage_get = node.
|
|
36
|
+
storage_get = node.storage_get
|
|
37
37
|
|
|
38
38
|
type_atom = storage_get(account_id)
|
|
39
39
|
if type_atom is None or type_atom.data != b"account":
|