astreum 0.2.41__py3-none-any.whl → 0.3.1__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.
- astreum/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +83 -0
- astreum/communication/handlers/ping.py +48 -0
- astreum/communication/handlers/storage_request.py +81 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/{_communication → communication/models}/message.py +1 -0
- astreum/communication/models/peer.py +23 -0
- astreum/{_communication → communication/models}/route.py +45 -8
- astreum/{_communication → communication}/setup.py +46 -95
- astreum/communication/start.py +38 -0
- astreum/consensus/__init__.py +20 -0
- astreum/consensus/genesis.py +66 -0
- astreum/consensus/models/__init__.py +0 -0
- astreum/consensus/models/account.py +84 -0
- astreum/consensus/models/accounts.py +72 -0
- astreum/consensus/models/block.py +364 -0
- astreum/{_consensus → consensus/models}/chain.py +7 -7
- astreum/{_consensus → consensus/models}/fork.py +8 -8
- astreum/consensus/models/receipt.py +98 -0
- astreum/consensus/models/transaction.py +213 -0
- astreum/{_consensus → consensus}/setup.py +26 -11
- astreum/consensus/start.py +68 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +20 -1
- astreum/consensus/workers/validation.py +291 -0
- astreum/{_consensus → consensus}/workers/verify.py +32 -3
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/machine/evaluations/high_evaluation.py +237 -0
- astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum/machine/models/__init__.py +0 -0
- astreum/machine/models/environment.py +31 -0
- astreum/machine/models/expression.py +218 -0
- astreum/{_lispeum → machine}/parser.py +26 -31
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +73 -781
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +69 -0
- astreum/storage/actions/set.py +132 -0
- astreum/storage/models/atom.py +107 -0
- astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
- astreum/storage/setup.py +44 -15
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- astreum-0.3.1.dist-info/METADATA +160 -0
- astreum-0.3.1.dist-info/RECORD +62 -0
- astreum/_communication/peer.py +0 -11
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -170
- astreum/_consensus/accounts.py +0 -67
- astreum/_consensus/block.py +0 -328
- astreum/_consensus/genesis.py +0 -141
- astreum/_consensus/receipt.py +0 -177
- astreum/_consensus/transaction.py +0 -192
- astreum/_consensus/workers/validation.py +0 -122
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/expression.py +0 -37
- astreum/_lispeum/high_evaluation.py +0 -177
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -58
- astreum/_storage/__init__.py +0 -5
- astreum/_storage/atom.py +0 -117
- astreum/format.py +0 -75
- astreum/models/block.py +0 -441
- astreum/models/merkle.py +0 -205
- astreum/models/patricia.py +0 -393
- astreum/storage/object.py +0 -68
- astreum-0.2.41.dist-info/METADATA +0 -146
- astreum-0.2.41.dist-info/RECORD +0 -53
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_communication → communication}/util.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/top_level.txt +0 -0
astreum/utils/logging.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import inspect
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import logging.handlers
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import platform
|
|
12
|
+
import queue
|
|
13
|
+
import shutil
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
from blake3 import blake3
|
|
18
|
+
|
|
19
|
+
# Fixed identity for all loggers in this library
|
|
20
|
+
_ORG_NAME = "Astreum"
|
|
21
|
+
_PRODUCT_NAME = "lib-py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_path(path_str: str) -> Optional[pathlib.Path]:
|
|
25
|
+
try:
|
|
26
|
+
return pathlib.Path(path_str).resolve()
|
|
27
|
+
except Exception:
|
|
28
|
+
try:
|
|
29
|
+
return pathlib.Path(path_str).absolute()
|
|
30
|
+
except Exception:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _hash_path(path: pathlib.Path) -> str:
|
|
35
|
+
try:
|
|
36
|
+
data = str(path).encode("utf-8", errors="ignore")
|
|
37
|
+
except Exception:
|
|
38
|
+
data = repr(path).encode("utf-8", errors="ignore")
|
|
39
|
+
return blake3(data).hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _find_caller_path() -> pathlib.Path:
|
|
43
|
+
stack = inspect.stack()
|
|
44
|
+
candidates: list[pathlib.Path] = []
|
|
45
|
+
for frame_info in stack[2:]:
|
|
46
|
+
filename = frame_info.filename
|
|
47
|
+
if not filename:
|
|
48
|
+
continue
|
|
49
|
+
path = _safe_path(filename)
|
|
50
|
+
if path is None:
|
|
51
|
+
continue
|
|
52
|
+
candidates.append(path)
|
|
53
|
+
if "astreum" not in path.parts:
|
|
54
|
+
return path
|
|
55
|
+
|
|
56
|
+
if candidates:
|
|
57
|
+
return candidates[0]
|
|
58
|
+
return pathlib.Path.cwd()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _derive_instance_id() -> str:
|
|
62
|
+
return _hash_path(_find_caller_path())[:16]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _log_root(org: str, product: str, instance_id: str) -> pathlib.Path:
|
|
66
|
+
"""Resolve the base directory for logs using platform defaults."""
|
|
67
|
+
if platform.system() == "Windows":
|
|
68
|
+
base = os.getenv("LOCALAPPDATA") or str(pathlib.Path.home())
|
|
69
|
+
return pathlib.Path(base) / org / product / "logs" / instance_id
|
|
70
|
+
|
|
71
|
+
xdg_state = os.getenv("XDG_STATE_HOME")
|
|
72
|
+
base_path = pathlib.Path(xdg_state) if xdg_state else pathlib.Path.home() / ".local" / "state"
|
|
73
|
+
return base_path / org / product / "logs" / instance_id
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class JSONFormatter(logging.Formatter):
|
|
77
|
+
"""Log record formatter that emits JSON objects per line."""
|
|
78
|
+
|
|
79
|
+
def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
|
|
80
|
+
payload: Dict[str, Any] = {
|
|
81
|
+
"ts": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
|
|
82
|
+
"level": record.levelname,
|
|
83
|
+
"logger": record.name,
|
|
84
|
+
"msg": record.getMessage(),
|
|
85
|
+
"pid": record.process,
|
|
86
|
+
"thread": record.threadName,
|
|
87
|
+
"module": record.module,
|
|
88
|
+
"func": record.funcName,
|
|
89
|
+
"instance_id": getattr(record, "instance_id", None),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for key, value in record.__dict__.items():
|
|
93
|
+
if key in payload or key.startswith(("_", "msecs", "relativeCreated")):
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
json.dumps(value)
|
|
97
|
+
except Exception:
|
|
98
|
+
continue
|
|
99
|
+
payload[key] = value
|
|
100
|
+
|
|
101
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _gzip_rotator(src: str, dst: str) -> None:
|
|
105
|
+
"""Rotate the log file by gzipping it and removing the original."""
|
|
106
|
+
with open(src, "rb") as source, gzip.open(f"{dst}.gz", "wb") as target:
|
|
107
|
+
shutil.copyfileobj(source, target)
|
|
108
|
+
os.remove(src)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _namer(default_name: str) -> str:
|
|
112
|
+
"""Custom name for rotated logs: node-YYYY-MM-DD.log."""
|
|
113
|
+
path = pathlib.Path(default_name)
|
|
114
|
+
parent = path.parent
|
|
115
|
+
name = path.name
|
|
116
|
+
fragments = name.split(".log.")
|
|
117
|
+
if len(fragments) != 2:
|
|
118
|
+
return default_name
|
|
119
|
+
stem, date_part = fragments
|
|
120
|
+
return str(parent / f"{stem}-{date_part}.log")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _human_line(record: logging.LogRecord) -> str:
|
|
124
|
+
"""Format a record as a concise human-readable line."""
|
|
125
|
+
dt = datetime.fromtimestamp(record.created, tz=timezone.utc)
|
|
126
|
+
stamp = f"{dt:%Y-%m-%d}-{dt:%S}-{dt:%M}"
|
|
127
|
+
return f"[{stamp}] [{record.levelname.lower()}] {record.getMessage()}"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class HumanFormatter(logging.Formatter):
|
|
131
|
+
"""Simple formatter for optional verbose console output."""
|
|
132
|
+
|
|
133
|
+
def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
|
|
134
|
+
return _human_line(record)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _shutdown_listener(listener: logging.handlers.QueueListener, handlers: list[logging.Handler]) -> None:
|
|
138
|
+
"""Stop the queue listener and close handlers on interpreter exit."""
|
|
139
|
+
try:
|
|
140
|
+
listener.stop()
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
finally:
|
|
144
|
+
for handler in handlers:
|
|
145
|
+
try:
|
|
146
|
+
handler.close()
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def logging_setup(config: dict) -> logging.LoggerAdapter:
|
|
152
|
+
"""Configure logging according to the runtime config and return an adapter."""
|
|
153
|
+
if config is None:
|
|
154
|
+
config = {}
|
|
155
|
+
elif not isinstance(config, dict):
|
|
156
|
+
config = dict(config)
|
|
157
|
+
|
|
158
|
+
org = _ORG_NAME
|
|
159
|
+
product = _PRODUCT_NAME
|
|
160
|
+
instance_id = _derive_instance_id()
|
|
161
|
+
|
|
162
|
+
retention_value = config.get("retention_days")
|
|
163
|
+
retention_days = int(retention_value) if retention_value is not None else 90
|
|
164
|
+
|
|
165
|
+
verbose = bool(config.get("verbose", False))
|
|
166
|
+
|
|
167
|
+
log_dir = _log_root(org, product, instance_id)
|
|
168
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
|
|
170
|
+
base_file = log_dir / "node.log"
|
|
171
|
+
file_handler = logging.handlers.TimedRotatingFileHandler(
|
|
172
|
+
filename=str(base_file),
|
|
173
|
+
when="midnight",
|
|
174
|
+
interval=1,
|
|
175
|
+
backupCount=max(retention_days, 0),
|
|
176
|
+
utc=True,
|
|
177
|
+
encoding="utf-8",
|
|
178
|
+
delay=True,
|
|
179
|
+
)
|
|
180
|
+
file_handler.setFormatter(JSONFormatter())
|
|
181
|
+
file_handler.rotator = _gzip_rotator
|
|
182
|
+
file_handler.namer = _namer
|
|
183
|
+
|
|
184
|
+
handler_list: list[logging.Handler] = [file_handler]
|
|
185
|
+
|
|
186
|
+
if verbose:
|
|
187
|
+
console_handler = logging.StreamHandler()
|
|
188
|
+
console_handler.setLevel(logging.INFO)
|
|
189
|
+
console_handler.setFormatter(HumanFormatter())
|
|
190
|
+
handler_list.append(console_handler)
|
|
191
|
+
|
|
192
|
+
log_queue: queue.Queue[logging.LogRecord] = queue.Queue(-1)
|
|
193
|
+
queue_handler = logging.handlers.QueueHandler(log_queue)
|
|
194
|
+
|
|
195
|
+
base_logger = logging.getLogger(f"{product}.{instance_id}")
|
|
196
|
+
base_logger.setLevel(logging.INFO)
|
|
197
|
+
base_logger.handlers.clear()
|
|
198
|
+
base_logger.propagate = False
|
|
199
|
+
base_logger.addHandler(queue_handler)
|
|
200
|
+
|
|
201
|
+
listener = logging.handlers.QueueListener(
|
|
202
|
+
log_queue, *handler_list, respect_handler_level=True
|
|
203
|
+
)
|
|
204
|
+
listener.daemon = True
|
|
205
|
+
listener.start()
|
|
206
|
+
atexit.register(_shutdown_listener, listener, handler_list)
|
|
207
|
+
|
|
208
|
+
adapter = logging.LoggerAdapter(base_logger, {"instance_id": instance_id})
|
|
209
|
+
setattr(adapter, "_queue_listener", listener)
|
|
210
|
+
setattr(adapter, "_handlers", handler_list)
|
|
211
|
+
|
|
212
|
+
return adapter
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
__all__ = [
|
|
216
|
+
"HumanFormatter",
|
|
217
|
+
"JSONFormatter",
|
|
218
|
+
"logging_setup",
|
|
219
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astreum
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Python library to interact with the Astreum blockchain and its virtual machine.
|
|
5
|
+
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
|
+
Project-URL: Homepage, https://github.com/astreum/lib-py
|
|
7
|
+
Project-URL: Issues, https://github.com/astreum/lib-py/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pycryptodomex==3.21.0
|
|
15
|
+
Requires-Dist: cryptography==44.0.2
|
|
16
|
+
Requires-Dist: blake3==1.0.4
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# lib
|
|
20
|
+
|
|
21
|
+
Python library to interact with the Astreum blockchain and its virtual machine.
|
|
22
|
+
|
|
23
|
+
[View on PyPI](https://pypi.org/project/astreum/)
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
When initializing an `astreum.Node`, pass a dictionary with any of the options below. Only the parameters you want to override need to be present – everything else falls back to its default.
|
|
28
|
+
|
|
29
|
+
### Core Configuration
|
|
30
|
+
|
|
31
|
+
| Parameter | Type | Default | Description |
|
|
32
|
+
| --------------------------- | ---------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `hot_storage_limit` | int | `1073741824` | Maximum bytes kept in the hot cache before new atoms are skipped (1 GiB). |
|
|
34
|
+
| `cold_storage_limit` | int | `10737418240` | Cold storage write threshold (10 GiB by default); set to `0` to skip the limit. |
|
|
35
|
+
| `cold_storage_path` | string | `None` | Directory where persisted atoms live; Astreum creates it on startup and skips cold storage when unset. |
|
|
36
|
+
| `logging_retention` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
37
|
+
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
38
|
+
|
|
39
|
+
### Networking
|
|
40
|
+
|
|
41
|
+
| Parameter | Type | Default | Description |
|
|
42
|
+
| ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `relay_secret_key` | hex string | Auto-generated | X25519 private key used for the relay route; a new keypair is created when this field is omitted. |
|
|
44
|
+
| `validation_secret_key` | hex string | `None` | Optional Ed25519 key that lets the node join the validation route; leave blank to opt out of validation. |
|
|
45
|
+
| `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
|
|
46
|
+
| `incoming_port` | int | `7373` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
|
|
47
|
+
| `bootstrap` | list\[str\] | `[]` | Addresses to ping with a handshake before joining; each must look like `host:port` or `[ipv6]:port`. |
|
|
48
|
+
|
|
49
|
+
> **Note**
|
|
50
|
+
> The peer‑to‑peer *route* used for object discovery is always enabled.
|
|
51
|
+
> If `validation_secret_key` is provided the node automatically joins the validation route too.
|
|
52
|
+
|
|
53
|
+
### Example
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from astreum.node import Node
|
|
57
|
+
|
|
58
|
+
config = {
|
|
59
|
+
"relay_secret_key": "ab…cd", # optional – hex encoded
|
|
60
|
+
"validation_secret_key": "12…34", # optional – validator
|
|
61
|
+
"hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
|
|
62
|
+
"cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
|
|
63
|
+
"cold_storage_path": "./data/node1",
|
|
64
|
+
"incoming_port": 7373,
|
|
65
|
+
"use_ipv6": False,
|
|
66
|
+
"bootstrap": [
|
|
67
|
+
"bootstrap.astreum.org:7373",
|
|
68
|
+
"127.0.0.1:7374"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
node = Node(config)
|
|
73
|
+
# … your code …
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Astreum Machine Quickstart
|
|
78
|
+
|
|
79
|
+
The Astreum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Astreum script, and the node tokenizes, parses, and evaluates.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Define a named function int.add (stack body) and call it with bytes 1 and 2
|
|
83
|
+
|
|
84
|
+
import uuid
|
|
85
|
+
from astreum import Node, Env, Expr
|
|
86
|
+
|
|
87
|
+
# 1) Spin‑up a stand‑alone VM
|
|
88
|
+
node = Node()
|
|
89
|
+
|
|
90
|
+
# 2) Create an environment (simple manual setup)
|
|
91
|
+
env_id = uuid.uuid4()
|
|
92
|
+
node.environments[env_id] = Env()
|
|
93
|
+
|
|
94
|
+
# 3) Build a function value using a low‑level stack body via `sk`.
|
|
95
|
+
# Body does: $0 $1 add (i.e., a + b)
|
|
96
|
+
low_body = Expr.ListExpr([
|
|
97
|
+
Expr.Symbol("$0"), # a (first arg)
|
|
98
|
+
Expr.Symbol("$1"), # b (second arg)
|
|
99
|
+
Expr.Symbol("add"),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
fn_body = Expr.ListExpr([
|
|
103
|
+
Expr.Symbol("a"),
|
|
104
|
+
Expr.Symbol("b"),
|
|
105
|
+
Expr.ListExpr([low_body, Expr.Symbol("sk")]),
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
|
|
109
|
+
int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
|
|
110
|
+
|
|
111
|
+
# 4) Store under the name "int.add"
|
|
112
|
+
node.env_set(env_id, "int.add", int_add_fn)
|
|
113
|
+
|
|
114
|
+
# 5) Retrieve the function and call it with bytes 1 and 2
|
|
115
|
+
bound = node.env_get(env_id, "int.add")
|
|
116
|
+
call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
|
|
117
|
+
res = node.high_eval(env_id, call)
|
|
118
|
+
|
|
119
|
+
# sk returns a list of bytes; for 1+2 expect a single byte with value 3
|
|
120
|
+
print([b.value for b in res.elements]) # [3]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Handling errors
|
|
124
|
+
|
|
125
|
+
Both helpers raise `ParseError` (from `astreum.machine.error`) when something goes wrong:
|
|
126
|
+
|
|
127
|
+
* Unterminated string literals are caught by `tokenize`.
|
|
128
|
+
* Unexpected or missing parentheses are caught by `parse`.
|
|
129
|
+
|
|
130
|
+
Catch the exception to provide developer‑friendly diagnostics:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
try:
|
|
134
|
+
tokens = tokenize(bad_source)
|
|
135
|
+
expr, _ = parse(tokens)
|
|
136
|
+
except ParseError as e:
|
|
137
|
+
print("Parse failed:", e)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## Logging
|
|
144
|
+
|
|
145
|
+
Every `Node` instance wires up structured logging automatically:
|
|
146
|
+
|
|
147
|
+
- 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.
|
|
148
|
+
- 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"]`.
|
|
149
|
+
- Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
|
|
150
|
+
- Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
|
|
151
|
+
- The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
|
|
152
|
+
|
|
153
|
+
## Testing
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
python3 -m venv venv
|
|
157
|
+
source venv/bin/activate
|
|
158
|
+
pip install -e .
|
|
159
|
+
python3 -m unittest discover -s tests
|
|
160
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
astreum/__init__.py,sha256=GkEW_ReYore8_0nEOvPnZLUa3lO7CgMWu6LeEjrGXEk,325
|
|
2
|
+
astreum/node.py,sha256=cHZyq9ImhCB9PSROKR5lFsUau6VLCjRIfiJSZhCPFzI,2103
|
|
3
|
+
astreum/communication/__init__.py,sha256=wNxzsAk8Fol9cGMPuVvY4etrrMqn3SjZq1dE82kFrxw,228
|
|
4
|
+
astreum/communication/setup.py,sha256=qliXCj2uHvzullCPSVtUuEG9zdqHewKgYfQLfsM8tao,7236
|
|
5
|
+
astreum/communication/start.py,sha256=lfud8VvLeKFbkF_TwHFODg20RVpClUa4a_zsHB7ynxk,1853
|
|
6
|
+
astreum/communication/util.py,sha256=bJ3td3naDzmCelAJQpLwiDMoRBkijQl9YLROjsWyOrI,1256
|
|
7
|
+
astreum/communication/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
astreum/communication/handlers/handshake.py,sha256=twd18nnfYcyC8hLXZ0EDwUw-2mPQGRf1RYdW21x9CHM,2378
|
|
9
|
+
astreum/communication/handlers/ping.py,sha256=xY-QT0IoeNPKR1hyruRwJa2N8_op7aPOCZUk9X-kZWk,1258
|
|
10
|
+
astreum/communication/handlers/storage_request.py,sha256=rUWhoeOxVZHcvEMxi74hN9XF9SFHe9Uw-9q4pBP-KwE,2406
|
|
11
|
+
astreum/communication/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
astreum/communication/models/message.py,sha256=Wl1IITj7eY9_q0IOT4J7c5gsjS1bF51CH7GcSSuu5OM,3327
|
|
13
|
+
astreum/communication/models/peer.py,sha256=CbqkyCwhFCiC2spd1-KjNdeVGNjjt2ECVs8uHot-ETI,875
|
|
14
|
+
astreum/communication/models/ping.py,sha256=u_DQTZJsbMdYiDDqjdZDsLaN5na2m9WZjVeEM3zq9_Y,955
|
|
15
|
+
astreum/communication/models/route.py,sha256=LRHx0R1MSIln92GQbyDrZpE_hfiHDiSG_3z1Ssq_1n4,4032
|
|
16
|
+
astreum/consensus/__init__.py,sha256=VZR_NyGSD5VvZp3toD2zpdYwFDLBIcckeVZXFPlruuU,425
|
|
17
|
+
astreum/consensus/genesis.py,sha256=RI9AzQFmDTgNFuiiTmW2dDiGcURIUGmThdRpxWrUOBk,1962
|
|
18
|
+
astreum/consensus/setup.py,sha256=lrEapfpJXKqw4iwST11-tqPAI2VW2h3H6Ue4JDAtrP4,3142
|
|
19
|
+
astreum/consensus/start.py,sha256=ZUa77cINmj5AzGR8dnZ1KS0OeDIyesSmrEOx0zo4HBI,2581
|
|
20
|
+
astreum/consensus/validator.py,sha256=cqcmw1WEB8DkznNX_Mn8tmE956rVSNCPv1FicdL8EAQ,3647
|
|
21
|
+
astreum/consensus/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
astreum/consensus/models/account.py,sha256=3QcT59QUZynysLSbiywidFYVzYJ3LR6qia7JwXOwn4I,2690
|
|
23
|
+
astreum/consensus/models/accounts.py,sha256=iUMs6LvmMea-gxd6-ujkFjqhWmuW1cl9XTWGXQkpLys,2388
|
|
24
|
+
astreum/consensus/models/block.py,sha256=nNtw9TbEAF1mIEfgJr1fuswcZ0B63SVnuBANqJ5Zaac,13531
|
|
25
|
+
astreum/consensus/models/chain.py,sha256=SIIDFSYbag76kTUNwnuJ2_zyuhFsvT7n5HgrVTxBrvE,2797
|
|
26
|
+
astreum/consensus/models/fork.py,sha256=IbXRB93bUg2k3q3oQ9dOPzozV-rY-TEDFjYrw-WBymE,3859
|
|
27
|
+
astreum/consensus/models/receipt.py,sha256=KjKKjYp_LnP2zkX1FLIwD_4hqKV1b2TPfp43tY701q4,3336
|
|
28
|
+
astreum/consensus/models/transaction.py,sha256=AYa1Q-BaYW3mkOv1e3WbvDFEsYamKMiFrja-eO2zU_Y,7475
|
|
29
|
+
astreum/consensus/workers/__init__.py,sha256=bS5FjbevbIR5FHbVGnT4Jli17VIld_5auemRw4CaHFU,278
|
|
30
|
+
astreum/consensus/workers/discovery.py,sha256=ckko9286WaK0qAaUpk_pHmQe_N3F87iGZu67OhCdtY8,2487
|
|
31
|
+
astreum/consensus/workers/validation.py,sha256=1jwFUL1zztuzLiYAmi92-KTUq97yraFAhuvhNhFJeLs,12223
|
|
32
|
+
astreum/consensus/workers/verify.py,sha256=eadF27iXOnMife_Pwz65lVwUyTEU8LGIcdGkCT_nzo0,3487
|
|
33
|
+
astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
|
|
35
|
+
astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
|
|
36
|
+
astreum/crypto/wesolowski.py,sha256=SUgGXW3Id07dJtWzDcs4dluIhjqbRWQ8YWjn_mK78AQ,4092
|
|
37
|
+
astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
|
|
38
|
+
astreum/machine/__init__.py,sha256=TjWf9RlGuOGbCqdjJKidh8W4pCzUoLpi3FgutssEGoQ,479
|
|
39
|
+
astreum/machine/parser.py,sha256=Z_Y0Sax0rPh8JcIo19-iNDQoc5GTdGQkmfFyLpCB4bw,1757
|
|
40
|
+
astreum/machine/tokenizer.py,sha256=6wPqR_D3h5BEvR78XKtD45ouy77RZBbz4Yh4jHSmN4o,2394
|
|
41
|
+
astreum/machine/evaluations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
|
+
astreum/machine/evaluations/high_evaluation.py,sha256=0tKOvW8T7EEHrL5pZtMUSnUszYTPWSP2xnEocr1eIOk,9778
|
|
43
|
+
astreum/machine/evaluations/low_evaluation.py,sha256=n3LwHDD889PAoj1XW7D2Eu4WCalx5nl0mKoLrgdoLpo,10337
|
|
44
|
+
astreum/machine/evaluations/script_evaluation.py,sha256=eWouYUwTYzaqUyXqEe-lAJFIluW0gMeCDdXqle88oWw,864
|
|
45
|
+
astreum/machine/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
+
astreum/machine/models/environment.py,sha256=WjP6GRX_8e0-BAhzRLvQ6fYtKQEVR0LZi7DZNZS0TSE,1019
|
|
47
|
+
astreum/machine/models/expression.py,sha256=yYr9ktk-NWPL4EXwHz7ePvr9eNvfSBQe3yzRUz06yas,7675
|
|
48
|
+
astreum/machine/models/meter.py,sha256=5q2PFW7_jmgKVM1-vwE4RRjMfPEthUA4iu1CwR-Axws,505
|
|
49
|
+
astreum/storage/__init__.py,sha256=Flk6WXT2xGFHWWJiZHK3O5OpjoLTOFMqqIiJTtD58kY,111
|
|
50
|
+
astreum/storage/setup.py,sha256=udwLpSCFpneKH9DlxUB40EVjmhjqQQ2hS4dePwQKkL8,1508
|
|
51
|
+
astreum/storage/actions/get.py,sha256=XRNOUzD3OjMpfFPyhQQt2rE5dpS_Hdp9Yf5SYELjm30,2572
|
|
52
|
+
astreum/storage/actions/set.py,sha256=-eyHJW5xPRbkDV8YvPQsp_SEFkCt4HEQ0VK2soYRXvg,4210
|
|
53
|
+
astreum/storage/models/atom.py,sha256=FY_bgtoju59Yo7TL1DTFTr9_pRMNBuH6-u59D6bz2fc,3163
|
|
54
|
+
astreum/storage/models/trie.py,sha256=Bn3ssPGI7YGS4iUH5ESvpG1NE6Ljx2Xo7wkEpQhjKUY,17587
|
|
55
|
+
astreum/utils/bytes.py,sha256=9QTWC2JCdwWLB5R2mPtmjPro0IUzE58DL3uEul4AheE,846
|
|
56
|
+
astreum/utils/integer.py,sha256=iQt-klWOYVghu_NOT341MmHbOle4FDT3by4PNKNXscg,736
|
|
57
|
+
astreum/utils/logging.py,sha256=mRDtWSCj8vKt58WGKLNSkK9Oa0graNVSoS8URby4Q9g,6684
|
|
58
|
+
astreum-0.3.1.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
59
|
+
astreum-0.3.1.dist-info/METADATA,sha256=_fPZGHAf0_YTfkErbQwTEovrBE0x_MbUPp8uXJ04JUE,7716
|
|
60
|
+
astreum-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
61
|
+
astreum-0.3.1.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
62
|
+
astreum-0.3.1.dist-info/RECORD,,
|
astreum/_communication/peer.py
DELETED
|
@@ -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)
|
astreum/_consensus/__init__.py
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from .account import Account
|
|
2
|
-
from .accounts import Accounts
|
|
3
|
-
from .block import Block
|
|
4
|
-
from .chain import Chain
|
|
5
|
-
from .fork import Fork
|
|
6
|
-
from .receipt import Receipt
|
|
7
|
-
from .transaction import Transaction
|
|
8
|
-
from .setup import consensus_setup
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
__all__ = [
|
|
12
|
-
"Block",
|
|
13
|
-
"Chain",
|
|
14
|
-
"Fork",
|
|
15
|
-
"Receipt",
|
|
16
|
-
"Transaction",
|
|
17
|
-
"Account",
|
|
18
|
-
"Accounts",
|
|
19
|
-
"consensus_setup",
|
|
20
|
-
]
|
astreum/_consensus/account.py
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Any, Callable, List, Optional, Tuple
|
|
5
|
-
|
|
6
|
-
from .._storage.atom import Atom, ZERO32
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _int_to_be_bytes(value: int) -> bytes:
|
|
10
|
-
value = int(value)
|
|
11
|
-
if value < 0:
|
|
12
|
-
raise ValueError("account integers must be non-negative")
|
|
13
|
-
if value == 0:
|
|
14
|
-
return b"\x00"
|
|
15
|
-
size = (value.bit_length() + 7) // 8
|
|
16
|
-
return value.to_bytes(size, "big")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _be_bytes_to_int(data: Optional[bytes]) -> int:
|
|
20
|
-
if not data:
|
|
21
|
-
return 0
|
|
22
|
-
return int.from_bytes(data, "big")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
26
|
-
next_hash = ZERO32
|
|
27
|
-
elements: List[Atom] = []
|
|
28
|
-
for child_id in reversed(child_ids):
|
|
29
|
-
elem = Atom.from_data(data=child_id, next_hash=next_hash)
|
|
30
|
-
next_hash = elem.object_id()
|
|
31
|
-
elements.append(elem)
|
|
32
|
-
elements.reverse()
|
|
33
|
-
value_atom = Atom.from_data(
|
|
34
|
-
data=len(child_ids).to_bytes(8, "little"),
|
|
35
|
-
next_hash=next_hash,
|
|
36
|
-
)
|
|
37
|
-
type_atom = Atom.from_data(data=b"list", next_hash=value_atom.object_id())
|
|
38
|
-
atoms = elements + [value_atom, type_atom]
|
|
39
|
-
return type_atom.object_id(), atoms
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _resolve_storage_get(source: Any) -> Callable[[bytes], Optional[Atom]]:
|
|
43
|
-
if callable(source):
|
|
44
|
-
return source
|
|
45
|
-
getter = getattr(source, "_local_get", None)
|
|
46
|
-
if callable(getter):
|
|
47
|
-
return getter
|
|
48
|
-
raise TypeError("Account.from_atom needs a callable storage getter or node with '_local_get'")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _read_list_entries(
|
|
52
|
-
storage_get: Callable[[bytes], Optional[Atom]],
|
|
53
|
-
start: bytes,
|
|
54
|
-
) -> List[bytes]:
|
|
55
|
-
entries: List[bytes] = []
|
|
56
|
-
current = start if start and start != ZERO32 else b""
|
|
57
|
-
while current:
|
|
58
|
-
elem = storage_get(current)
|
|
59
|
-
if elem is None:
|
|
60
|
-
break
|
|
61
|
-
entries.append(elem.data)
|
|
62
|
-
nxt = elem.next
|
|
63
|
-
current = nxt if nxt and nxt != ZERO32 else b""
|
|
64
|
-
return entries
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@dataclass
|
|
68
|
-
class Account:
|
|
69
|
-
_balance: int
|
|
70
|
-
_data: bytes
|
|
71
|
-
_nonce: int
|
|
72
|
-
hash: bytes = ZERO32
|
|
73
|
-
atoms: List[Atom] = field(default_factory=list)
|
|
74
|
-
|
|
75
|
-
@staticmethod
|
|
76
|
-
def _encode(balance: int, data: bytes, nonce: int) -> Tuple[bytes, List[Atom]]:
|
|
77
|
-
balance_atom = Atom.from_data(data=_int_to_be_bytes(balance))
|
|
78
|
-
data_atom = Atom.from_data(data=bytes(data))
|
|
79
|
-
nonce_atom = Atom.from_data(data=_int_to_be_bytes(nonce))
|
|
80
|
-
|
|
81
|
-
field_atoms = [balance_atom, data_atom, nonce_atom]
|
|
82
|
-
field_ids = [a.object_id() for a in field_atoms]
|
|
83
|
-
|
|
84
|
-
body_id, body_atoms = _make_list(field_ids)
|
|
85
|
-
type_atom = Atom.from_data(data=b"account", next_hash=body_id)
|
|
86
|
-
top_id, top_atoms = _make_list([type_atom.object_id(), body_id])
|
|
87
|
-
|
|
88
|
-
atoms = field_atoms + body_atoms + [type_atom] + top_atoms
|
|
89
|
-
return top_id, atoms
|
|
90
|
-
|
|
91
|
-
@classmethod
|
|
92
|
-
def create(cls, balance: int, data: bytes, nonce: int) -> "Account":
|
|
93
|
-
account_hash, atoms = cls._encode(balance, data, nonce)
|
|
94
|
-
return cls(
|
|
95
|
-
_balance=int(balance),
|
|
96
|
-
_data=bytes(data),
|
|
97
|
-
_nonce=int(nonce),
|
|
98
|
-
hash=account_hash,
|
|
99
|
-
atoms=atoms,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
@classmethod
|
|
103
|
-
def from_atom(cls, source: Any, account_id: bytes) -> "Account":
|
|
104
|
-
storage_get = _resolve_storage_get(source)
|
|
105
|
-
|
|
106
|
-
outer_list = storage_get(account_id)
|
|
107
|
-
if outer_list is None or outer_list.data != b"list":
|
|
108
|
-
raise ValueError("not an account (outer list missing)")
|
|
109
|
-
|
|
110
|
-
outer_value = storage_get(outer_list.next)
|
|
111
|
-
if outer_value is None:
|
|
112
|
-
raise ValueError("malformed account (outer value missing)")
|
|
113
|
-
|
|
114
|
-
entries = _read_list_entries(storage_get, outer_value.next)
|
|
115
|
-
if len(entries) < 2:
|
|
116
|
-
raise ValueError("malformed account (type/body missing)")
|
|
117
|
-
|
|
118
|
-
type_atom_id, body_id = entries[0], entries[1]
|
|
119
|
-
type_atom = storage_get(type_atom_id)
|
|
120
|
-
if type_atom is None or type_atom.data != b"account":
|
|
121
|
-
raise ValueError("not an account (type mismatch)")
|
|
122
|
-
|
|
123
|
-
body_list = storage_get(body_id)
|
|
124
|
-
if body_list is None or body_list.data != b"list":
|
|
125
|
-
raise ValueError("malformed account body (type)")
|
|
126
|
-
|
|
127
|
-
body_value = storage_get(body_list.next)
|
|
128
|
-
if body_value is None:
|
|
129
|
-
raise ValueError("malformed account body (value)")
|
|
130
|
-
|
|
131
|
-
field_ids = _read_list_entries(storage_get, body_value.next)
|
|
132
|
-
if len(field_ids) < 3:
|
|
133
|
-
field_ids.extend([ZERO32] * (3 - len(field_ids)))
|
|
134
|
-
|
|
135
|
-
def _read_field(field_id: bytes) -> bytes:
|
|
136
|
-
if not field_id or field_id == ZERO32:
|
|
137
|
-
return b""
|
|
138
|
-
atom = storage_get(field_id)
|
|
139
|
-
return atom.data if atom is not None else b""
|
|
140
|
-
|
|
141
|
-
balance_bytes = _read_field(field_ids[0])
|
|
142
|
-
data_bytes = _read_field(field_ids[1])
|
|
143
|
-
nonce_bytes = _read_field(field_ids[2])
|
|
144
|
-
|
|
145
|
-
account = cls.create(
|
|
146
|
-
balance=_be_bytes_to_int(balance_bytes),
|
|
147
|
-
data=data_bytes,
|
|
148
|
-
nonce=_be_bytes_to_int(nonce_bytes),
|
|
149
|
-
)
|
|
150
|
-
if account.hash != account_id:
|
|
151
|
-
raise ValueError("account hash mismatch while decoding")
|
|
152
|
-
return account
|
|
153
|
-
|
|
154
|
-
def balance(self) -> int:
|
|
155
|
-
return self._balance
|
|
156
|
-
|
|
157
|
-
def data(self) -> bytes:
|
|
158
|
-
return self._data
|
|
159
|
-
|
|
160
|
-
def nonce(self) -> int:
|
|
161
|
-
return self._nonce
|
|
162
|
-
|
|
163
|
-
def body_hash(self) -> bytes:
|
|
164
|
-
return self.hash
|
|
165
|
-
|
|
166
|
-
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
167
|
-
account_hash, atoms = self._encode(self._balance, self._data, self._nonce)
|
|
168
|
-
self.hash = account_hash
|
|
169
|
-
self.atoms = atoms
|
|
170
|
-
return account_hash, list(atoms)
|