astreum 0.2.42__tar.gz → 0.3.5__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.3.5/PKG-INFO +160 -0
- astreum-0.3.5/README.md +142 -0
- {astreum-0.2.42 → astreum-0.3.5}/pyproject.toml +4 -4
- astreum-0.3.5/src/astreum/__init__.py +18 -0
- {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication}/__init__.py +3 -3
- astreum-0.3.5/src/astreum/communication/handlers/handshake.py +81 -0
- astreum-0.3.5/src/astreum/communication/handlers/object_request.py +153 -0
- astreum-0.3.5/src/astreum/communication/handlers/object_response.py +37 -0
- astreum-0.3.5/src/astreum/communication/handlers/ping.py +48 -0
- astreum-0.3.5/src/astreum/communication/handlers/route_request.py +78 -0
- astreum-0.3.5/src/astreum/communication/handlers/route_response.py +52 -0
- astreum-0.3.5/src/astreum/communication/models/message.py +105 -0
- astreum-0.3.5/src/astreum/communication/models/peer.py +23 -0
- {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication/models}/route.py +40 -8
- {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication}/setup.py +105 -97
- astreum-0.3.5/src/astreum/communication/start.py +38 -0
- {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication}/util.py +7 -0
- astreum-0.3.5/src/astreum/consensus/__init__.py +20 -0
- astreum-0.3.5/src/astreum/consensus/genesis.py +66 -0
- astreum-0.3.5/src/astreum/consensus/models/account.py +84 -0
- astreum-0.3.5/src/astreum/consensus/models/accounts.py +72 -0
- astreum-0.3.5/src/astreum/consensus/models/block.py +364 -0
- {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus/models}/chain.py +7 -7
- {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus/models}/fork.py +8 -8
- astreum-0.3.5/src/astreum/consensus/models/receipt.py +98 -0
- astreum-0.3.5/src/astreum/consensus/models/transaction.py +213 -0
- {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/setup.py +26 -11
- astreum-0.3.5/src/astreum/consensus/start.py +68 -0
- astreum-0.3.5/src/astreum/consensus/validator.py +95 -0
- {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/workers/discovery.py +20 -1
- astreum-0.3.5/src/astreum/consensus/workers/validation.py +292 -0
- {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/workers/verify.py +32 -3
- astreum-0.3.5/src/astreum/crypto/__init__.py +0 -0
- astreum-0.3.5/src/astreum/machine/__init__.py +20 -0
- astreum-0.3.5/src/astreum/machine/evaluations/__init__.py +0 -0
- astreum-0.3.5/src/astreum/machine/evaluations/high_evaluation.py +237 -0
- astreum-0.3.5/src/astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum-0.3.5/src/astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum-0.3.5/src/astreum/machine/models/__init__.py +0 -0
- astreum-0.3.5/src/astreum/machine/models/environment.py +31 -0
- astreum-0.3.5/src/astreum/machine/models/expression.py +218 -0
- {astreum-0.2.42/src/astreum/_lispeum → astreum-0.3.5/src/astreum/machine}/parser.py +26 -31
- astreum-0.3.5/src/astreum/machine/tokenizer.py +90 -0
- astreum-0.3.5/src/astreum/node.py +75 -0
- astreum-0.3.5/src/astreum/storage/__init__.py +7 -0
- astreum-0.3.5/src/astreum/storage/actions/get.py +85 -0
- astreum-0.3.5/src/astreum/storage/actions/set.py +138 -0
- astreum-0.3.5/src/astreum/storage/models/atom.py +107 -0
- astreum-0.2.42/src/astreum/_storage/patricia.py → astreum-0.3.5/src/astreum/storage/models/trie.py +236 -177
- astreum-0.3.5/src/astreum/storage/setup.py +22 -0
- astreum-0.3.5/src/astreum/utils/bytes.py +24 -0
- astreum-0.3.5/src/astreum/utils/config.py +48 -0
- astreum-0.3.5/src/astreum/utils/logging.py +219 -0
- astreum-0.3.5/src/astreum.egg-info/PKG-INFO +160 -0
- astreum-0.3.5/src/astreum.egg-info/SOURCES.txt +69 -0
- astreum-0.2.42/PKG-INFO +0 -146
- astreum-0.2.42/README.md +0 -128
- astreum-0.2.42/src/astreum/__init__.py +0 -9
- astreum-0.2.42/src/astreum/_communication/message.py +0 -100
- astreum-0.2.42/src/astreum/_communication/peer.py +0 -11
- astreum-0.2.42/src/astreum/_consensus/__init__.py +0 -20
- astreum-0.2.42/src/astreum/_consensus/account.py +0 -95
- astreum-0.2.42/src/astreum/_consensus/accounts.py +0 -38
- astreum-0.2.42/src/astreum/_consensus/block.py +0 -328
- astreum-0.2.42/src/astreum/_consensus/genesis.py +0 -141
- astreum-0.2.42/src/astreum/_consensus/receipt.py +0 -177
- astreum-0.2.42/src/astreum/_consensus/transaction.py +0 -216
- astreum-0.2.42/src/astreum/_consensus/workers/validation.py +0 -122
- astreum-0.2.42/src/astreum/_lispeum/__init__.py +0 -16
- astreum-0.2.42/src/astreum/_lispeum/environment.py +0 -13
- astreum-0.2.42/src/astreum/_lispeum/expression.py +0 -37
- astreum-0.2.42/src/astreum/_lispeum/high_evaluation.py +0 -177
- astreum-0.2.42/src/astreum/_lispeum/low_evaluation.py +0 -123
- astreum-0.2.42/src/astreum/_lispeum/tokenizer.py +0 -22
- 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/src/astreum/format.py +0 -75
- astreum-0.2.42/src/astreum/models/block.py +0 -441
- astreum-0.2.42/src/astreum/models/merkle.py +0 -205
- astreum-0.2.42/src/astreum/models/patricia.py +0 -393
- astreum-0.2.42/src/astreum/node.py +0 -781
- astreum-0.2.42/src/astreum/storage/object.py +0 -68
- astreum-0.2.42/src/astreum/storage/setup.py +0 -15
- astreum-0.2.42/src/astreum.egg-info/PKG-INFO +0 -146
- astreum-0.2.42/src/astreum.egg-info/SOURCES.txt +0 -57
- {astreum-0.2.42 → astreum-0.3.5}/LICENSE +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/setup.cfg +0 -0
- {astreum-0.2.42/src/astreum/crypto → astreum-0.3.5/src/astreum/communication/handlers}/__init__.py +0 -0
- {astreum-0.2.42/src/astreum → astreum-0.3.5/src/astreum/communication}/models/__init__.py +0 -0
- {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication/models}/ping.py +0 -0
- {astreum-0.2.42/src/astreum/storage → astreum-0.3.5/src/astreum/consensus/models}/__init__.py +0 -0
- {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/workers/__init__.py +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/wesolowski.py +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.2.42/src/astreum/_lispeum → astreum-0.3.5/src/astreum/machine/models}/meter.py +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum/utils/integer.py +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.2.42 → astreum-0.3.5}/src/astreum.egg-info/top_level.txt +0 -0
astreum-0.3.5/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astreum
|
|
3
|
+
Version: 0.3.5
|
|
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
|
+
```
|
astreum-0.3.5/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# lib
|
|
2
|
+
|
|
3
|
+
Python library to interact with the Astreum blockchain and its virtual machine.
|
|
4
|
+
|
|
5
|
+
[View on PyPI](https://pypi.org/project/astreum/)
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Core Configuration
|
|
12
|
+
|
|
13
|
+
| Parameter | Type | Default | Description |
|
|
14
|
+
| --------------------------- | ---------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `hot_storage_limit` | int | `1073741824` | Maximum bytes kept in the hot cache before new atoms are skipped (1 GiB). |
|
|
16
|
+
| `cold_storage_limit` | int | `10737418240` | Cold storage write threshold (10 GiB by default); set to `0` to skip the limit. |
|
|
17
|
+
| `cold_storage_path` | string | `None` | Directory where persisted atoms live; Astreum creates it on startup and skips cold storage when unset. |
|
|
18
|
+
| `logging_retention` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
19
|
+
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
20
|
+
|
|
21
|
+
### Networking
|
|
22
|
+
|
|
23
|
+
| Parameter | Type | Default | Description |
|
|
24
|
+
| ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `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. |
|
|
26
|
+
| `validation_secret_key` | hex string | `None` | Optional Ed25519 key that lets the node join the validation route; leave blank to opt out of validation. |
|
|
27
|
+
| `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
|
|
28
|
+
| `incoming_port` | int | `7373` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
|
|
29
|
+
| `bootstrap` | list\[str\] | `[]` | Addresses to ping with a handshake before joining; each must look like `host:port` or `[ipv6]:port`. |
|
|
30
|
+
|
|
31
|
+
> **Note**
|
|
32
|
+
> The peer‑to‑peer *route* used for object discovery is always enabled.
|
|
33
|
+
> If `validation_secret_key` is provided the node automatically joins the validation route too.
|
|
34
|
+
|
|
35
|
+
### Example
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from astreum.node import Node
|
|
39
|
+
|
|
40
|
+
config = {
|
|
41
|
+
"relay_secret_key": "ab…cd", # optional – hex encoded
|
|
42
|
+
"validation_secret_key": "12…34", # optional – validator
|
|
43
|
+
"hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
|
|
44
|
+
"cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
|
|
45
|
+
"cold_storage_path": "./data/node1",
|
|
46
|
+
"incoming_port": 7373,
|
|
47
|
+
"use_ipv6": False,
|
|
48
|
+
"bootstrap": [
|
|
49
|
+
"bootstrap.astreum.org:7373",
|
|
50
|
+
"127.0.0.1:7374"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
node = Node(config)
|
|
55
|
+
# … your code …
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Astreum Machine Quickstart
|
|
60
|
+
|
|
61
|
+
The Astreum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Astreum script, and the node tokenizes, parses, and evaluates.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Define a named function int.add (stack body) and call it with bytes 1 and 2
|
|
65
|
+
|
|
66
|
+
import uuid
|
|
67
|
+
from astreum import Node, Env, Expr
|
|
68
|
+
|
|
69
|
+
# 1) Spin‑up a stand‑alone VM
|
|
70
|
+
node = Node()
|
|
71
|
+
|
|
72
|
+
# 2) Create an environment (simple manual setup)
|
|
73
|
+
env_id = uuid.uuid4()
|
|
74
|
+
node.environments[env_id] = Env()
|
|
75
|
+
|
|
76
|
+
# 3) Build a function value using a low‑level stack body via `sk`.
|
|
77
|
+
# Body does: $0 $1 add (i.e., a + b)
|
|
78
|
+
low_body = Expr.ListExpr([
|
|
79
|
+
Expr.Symbol("$0"), # a (first arg)
|
|
80
|
+
Expr.Symbol("$1"), # b (second arg)
|
|
81
|
+
Expr.Symbol("add"),
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
fn_body = Expr.ListExpr([
|
|
85
|
+
Expr.Symbol("a"),
|
|
86
|
+
Expr.Symbol("b"),
|
|
87
|
+
Expr.ListExpr([low_body, Expr.Symbol("sk")]),
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
|
|
91
|
+
int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
|
|
92
|
+
|
|
93
|
+
# 4) Store under the name "int.add"
|
|
94
|
+
node.env_set(env_id, "int.add", int_add_fn)
|
|
95
|
+
|
|
96
|
+
# 5) Retrieve the function and call it with bytes 1 and 2
|
|
97
|
+
bound = node.env_get(env_id, "int.add")
|
|
98
|
+
call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
|
|
99
|
+
res = node.high_eval(env_id, call)
|
|
100
|
+
|
|
101
|
+
# sk returns a list of bytes; for 1+2 expect a single byte with value 3
|
|
102
|
+
print([b.value for b in res.elements]) # [3]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Handling errors
|
|
106
|
+
|
|
107
|
+
Both helpers raise `ParseError` (from `astreum.machine.error`) when something goes wrong:
|
|
108
|
+
|
|
109
|
+
* Unterminated string literals are caught by `tokenize`.
|
|
110
|
+
* Unexpected or missing parentheses are caught by `parse`.
|
|
111
|
+
|
|
112
|
+
Catch the exception to provide developer‑friendly diagnostics:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
try:
|
|
116
|
+
tokens = tokenize(bad_source)
|
|
117
|
+
expr, _ = parse(tokens)
|
|
118
|
+
except ParseError as e:
|
|
119
|
+
print("Parse failed:", e)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Logging
|
|
126
|
+
|
|
127
|
+
Every `Node` instance wires up structured logging automatically:
|
|
128
|
+
|
|
129
|
+
- 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.
|
|
130
|
+
- 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"]`.
|
|
131
|
+
- Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
|
|
132
|
+
- Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
|
|
133
|
+
- The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
|
|
134
|
+
|
|
135
|
+
## Testing
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python3 -m venv venv
|
|
139
|
+
source venv/bin/activate
|
|
140
|
+
pip install -e .
|
|
141
|
+
python3 -m unittest discover -s tests
|
|
142
|
+
```
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "astreum"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.5"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Roy R. O. Okello", email="roy@stelar.xyz" },
|
|
6
6
|
]
|
|
7
|
-
description = "Python library to interact with the Astreum blockchain and its
|
|
7
|
+
description = "Python library to interact with the Astreum blockchain and its virtual machine."
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.8"
|
|
10
10
|
classifiers = [
|
|
@@ -19,5 +19,5 @@ dependencies = [
|
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
[project.urls]
|
|
22
|
-
Homepage = "https://github.com/astreum/lib"
|
|
23
|
-
Issues = "https://github.com/astreum/lib/issues"
|
|
22
|
+
Homepage = "https://github.com/astreum/lib-py"
|
|
23
|
+
Issues = "https://github.com/astreum/lib-py/issues"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
from astreum.consensus import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
|
|
3
|
+
from astreum.machine import Env, Expr
|
|
4
|
+
from astreum.node import Node
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__: list[str] = [
|
|
8
|
+
"Node",
|
|
9
|
+
"Env",
|
|
10
|
+
"Expr",
|
|
11
|
+
"Block",
|
|
12
|
+
"Chain",
|
|
13
|
+
"Fork",
|
|
14
|
+
"Receipt",
|
|
15
|
+
"Transaction",
|
|
16
|
+
"Account",
|
|
17
|
+
"Accounts",
|
|
18
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Sequence
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
7
|
+
|
|
8
|
+
from ..models.peer import Peer
|
|
9
|
+
from ..models.message import Message
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .... import Node
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_handshake(node: "Node", addr: Sequence[object], message: Message) -> bool:
|
|
16
|
+
"""Handle incoming handshake messages.
|
|
17
|
+
|
|
18
|
+
Returns True if the outer loop should `continue`, False otherwise.
|
|
19
|
+
"""
|
|
20
|
+
logger = node.logger
|
|
21
|
+
|
|
22
|
+
sender_public_key_bytes = message.sender_bytes
|
|
23
|
+
try:
|
|
24
|
+
sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
|
|
25
|
+
except Exception as exc:
|
|
26
|
+
logger.warning("Error extracting sender key bytes: %s", exc)
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
host, port = addr[0], int(addr[1])
|
|
31
|
+
except Exception:
|
|
32
|
+
return True
|
|
33
|
+
address_key = (host, port)
|
|
34
|
+
|
|
35
|
+
old_key_bytes = node.addresses.get(address_key)
|
|
36
|
+
node.addresses[address_key] = sender_public_key_bytes
|
|
37
|
+
|
|
38
|
+
if old_key_bytes is None:
|
|
39
|
+
try:
|
|
40
|
+
peer = Peer(node.relay_secret_key, sender_key)
|
|
41
|
+
except Exception:
|
|
42
|
+
return True
|
|
43
|
+
peer.address = address_key
|
|
44
|
+
|
|
45
|
+
node.peers[sender_public_key_bytes] = peer
|
|
46
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
47
|
+
|
|
48
|
+
logger.info(
|
|
49
|
+
"Handshake accepted from %s:%s; peer added",
|
|
50
|
+
address_key[0],
|
|
51
|
+
address_key[1],
|
|
52
|
+
)
|
|
53
|
+
response = Message(handshake=True, sender=node.relay_public_key)
|
|
54
|
+
node.outgoing_queue.put((response.to_bytes(), address_key))
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
if old_key_bytes == sender_public_key_bytes:
|
|
58
|
+
peer = node.peers.get(sender_public_key_bytes)
|
|
59
|
+
if peer is not None:
|
|
60
|
+
peer.address = address_key
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
node.peers.pop(old_key_bytes, None)
|
|
64
|
+
try:
|
|
65
|
+
node.peer_route.remove_peer(old_key_bytes)
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
try:
|
|
69
|
+
peer = Peer(node.relay_secret_key, sender_key)
|
|
70
|
+
except Exception:
|
|
71
|
+
return True
|
|
72
|
+
peer.address = address_key
|
|
73
|
+
|
|
74
|
+
node.peers[sender_public_key_bytes] = peer
|
|
75
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
76
|
+
logger.info(
|
|
77
|
+
"Peer at %s:%s replaced due to key change",
|
|
78
|
+
address_key[0],
|
|
79
|
+
address_key[1],
|
|
80
|
+
)
|
|
81
|
+
return False
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import socket
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from typing import TYPE_CHECKING, Tuple
|
|
5
|
+
|
|
6
|
+
from .object_response import ObjectResponse, ObjectResponseType
|
|
7
|
+
from ..models.message import Message, MessageTopic
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .. import Node
|
|
11
|
+
from ..models.peer import Peer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ObjectRequestType(IntEnum):
|
|
15
|
+
OBJECT_GET = 0
|
|
16
|
+
OBJECT_PUT = 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ObjectRequest:
|
|
20
|
+
type: ObjectRequestType
|
|
21
|
+
data: bytes
|
|
22
|
+
atom_id: bytes
|
|
23
|
+
|
|
24
|
+
def __init__(self, type: ObjectRequestType, data: bytes, atom_id: bytes = None):
|
|
25
|
+
self.type = type
|
|
26
|
+
self.data = data
|
|
27
|
+
self.atom_id = atom_id
|
|
28
|
+
|
|
29
|
+
def to_bytes(self):
|
|
30
|
+
return [self.type.value] + self.atom_id + self.data
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_bytes(cls, data: bytes) -> "ObjectRequest":
|
|
34
|
+
# need at least 1 byte for type + 32 bytes for hash
|
|
35
|
+
if len(data) < 1 + 32:
|
|
36
|
+
raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
|
|
37
|
+
|
|
38
|
+
type_val = data[0]
|
|
39
|
+
try:
|
|
40
|
+
req_type = ObjectRequestType(type_val)
|
|
41
|
+
except ValueError:
|
|
42
|
+
raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
|
|
43
|
+
|
|
44
|
+
atom_id_bytes = data[1:33]
|
|
45
|
+
payload = data[33:]
|
|
46
|
+
return cls(req_type, payload, atom_id_bytes)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def encode_peer_contact_bytes(peer: "Peer") -> bytes:
|
|
50
|
+
"""Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
|
|
51
|
+
if not peer.address:
|
|
52
|
+
raise ValueError("peer address is required for encoding peer info")
|
|
53
|
+
host, port = peer.address
|
|
54
|
+
key_bytes = peer.public_key_bytes
|
|
55
|
+
try:
|
|
56
|
+
ip_bytes = socket.inet_aton(host)
|
|
57
|
+
except OSError as exc: # pragma: no cover - inet_aton raises for invalid hosts
|
|
58
|
+
raise ValueError(f"invalid IPv4 address: {host}") from exc
|
|
59
|
+
if not (0 <= port <= 0xFFFF):
|
|
60
|
+
raise ValueError(f"port out of range (0-65535): {port}")
|
|
61
|
+
port_bytes = int(port).to_bytes(2, "big", signed=False)
|
|
62
|
+
return key_bytes + ip_bytes + port_bytes
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def handle_object_request(node: "Node", addr: Tuple[str, int], message: Message) -> None:
|
|
66
|
+
node_logger = getattr(node, "logger", logging.getLogger(__name__))
|
|
67
|
+
try:
|
|
68
|
+
object_request = ObjectRequest.from_bytes(message.body)
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
node_logger.warning("Error decoding OBJECT_REQUEST from %s: %s", addr, exc)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
match object_request.type:
|
|
74
|
+
case ObjectRequestType.OBJECT_GET:
|
|
75
|
+
atom_id = object_request.atom_id
|
|
76
|
+
node_logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), addr)
|
|
77
|
+
|
|
78
|
+
local_atom = node.local_get(atom_id)
|
|
79
|
+
if local_atom is not None:
|
|
80
|
+
node_logger.debug("Object %s found locally; returning to %s", atom_id.hex(), addr)
|
|
81
|
+
resp = ObjectResponse(
|
|
82
|
+
type=ObjectResponseType.OBJECT_FOUND,
|
|
83
|
+
data=local_atom.to_bytes(),
|
|
84
|
+
atom_id=atom_id
|
|
85
|
+
)
|
|
86
|
+
obj_res_msg = Message(
|
|
87
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
88
|
+
body=resp.to_bytes(),
|
|
89
|
+
sender=node.relay_public_key,
|
|
90
|
+
)
|
|
91
|
+
node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
storage_index = getattr(node, "storage_index", None) or {}
|
|
95
|
+
if atom_id in storage_index:
|
|
96
|
+
node_logger.debug("Known provider for %s; informing %s", atom_id.hex(), addr)
|
|
97
|
+
provider_bytes = storage_index[atom_id]
|
|
98
|
+
resp = ObjectResponse(
|
|
99
|
+
type=ObjectResponseType.OBJECT_PROVIDER,
|
|
100
|
+
data=provider_bytes,
|
|
101
|
+
atom_id=atom_id
|
|
102
|
+
)
|
|
103
|
+
obj_res_msg = Message(
|
|
104
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
105
|
+
body=resp.to_bytes(),
|
|
106
|
+
sender=node.relay_public_key,
|
|
107
|
+
)
|
|
108
|
+
node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
|
|
112
|
+
if nearest_peer:
|
|
113
|
+
node_logger.debug("Forwarding requester %s to nearest peer for %s", addr, atom_id.hex())
|
|
114
|
+
peer_info = encode_peer_contact_bytes(nearest_peer)
|
|
115
|
+
resp = ObjectResponse(
|
|
116
|
+
type=ObjectResponseType.OBJECT_PROVIDER,
|
|
117
|
+
# type=ObjectResponseType.OBJECT_NEAREST_PEER,
|
|
118
|
+
data=peer_info,
|
|
119
|
+
atom_id=atom_id
|
|
120
|
+
)
|
|
121
|
+
obj_res_msg = Message(
|
|
122
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
123
|
+
body=resp.to_bytes(),
|
|
124
|
+
sender=node.relay_public_key,
|
|
125
|
+
)
|
|
126
|
+
node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
127
|
+
|
|
128
|
+
case ObjectRequestType.OBJECT_PUT:
|
|
129
|
+
atom_hash = object_request.data[:32]
|
|
130
|
+
node_logger.debug("Handling OBJECT_PUT for %s from %s", atom_hash.hex(), addr)
|
|
131
|
+
|
|
132
|
+
nearest_peer = node.peer_route.closest_peer_for_hash(atom_hash)
|
|
133
|
+
nearest = (nearest_peer.public_key, nearest_peer) if nearest_peer else None
|
|
134
|
+
if nearest:
|
|
135
|
+
node_logger.debug("Forwarding OBJECT_PUT for %s to nearer peer %s", atom_hash.hex(), nearest[1].address)
|
|
136
|
+
fwd_req = ObjectRequest(
|
|
137
|
+
type=ObjectRequestType.OBJECT_PUT,
|
|
138
|
+
data=object_request.data,
|
|
139
|
+
)
|
|
140
|
+
obj_req_msg = Message(
|
|
141
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
142
|
+
body=fwd_req.to_bytes(),
|
|
143
|
+
sender=node.relay_public_key,
|
|
144
|
+
)
|
|
145
|
+
node.outgoing_queue.put((obj_req_msg.to_bytes(), nearest[1].address))
|
|
146
|
+
else:
|
|
147
|
+
node_logger.debug("Storing provider info for %s locally", atom_hash.hex())
|
|
148
|
+
if not hasattr(node, "storage_index") or not isinstance(node.storage_index, dict):
|
|
149
|
+
node.storage_index = {}
|
|
150
|
+
node.storage_index[atom_hash] = object_request.data[32:]
|
|
151
|
+
|
|
152
|
+
case _:
|
|
153
|
+
node_logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, addr)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ObjectResponseType(IntEnum):
|
|
5
|
+
OBJECT_FOUND = 0
|
|
6
|
+
OBJECT_PROVIDER = 1
|
|
7
|
+
OBJECT_NEAREST_PEER = 2
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ObjectResponse:
|
|
11
|
+
type: ObjectResponseType
|
|
12
|
+
data: bytes
|
|
13
|
+
atom_id: bytes
|
|
14
|
+
|
|
15
|
+
def __init__(self, type: ObjectResponseType, data: bytes, atom_id: bytes = None):
|
|
16
|
+
self.type = type
|
|
17
|
+
self.data = data
|
|
18
|
+
self.atom_id = atom_id
|
|
19
|
+
|
|
20
|
+
def to_bytes(self):
|
|
21
|
+
return [self.type.value] + self.atom_id + self.data
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_bytes(cls, data: bytes) -> "ObjectResponse":
|
|
25
|
+
# need at least 1 byte for type + 32 bytes for atom id
|
|
26
|
+
if len(data) < 1 + 32:
|
|
27
|
+
raise ValueError(f"Too short to be a valid ObjectResponse ({len(data)} bytes)")
|
|
28
|
+
|
|
29
|
+
type_val = data[0]
|
|
30
|
+
try:
|
|
31
|
+
resp_type = ObjectResponseType(type_val)
|
|
32
|
+
except ValueError:
|
|
33
|
+
raise ValueError(f"Unknown ObjectResponseType: {type_val}")
|
|
34
|
+
|
|
35
|
+
atom_id = data[1:33]
|
|
36
|
+
payload = data[33:]
|
|
37
|
+
return cls(resp_type, payload, atom_id)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import TYPE_CHECKING, Sequence
|
|
5
|
+
|
|
6
|
+
from ..models.ping import Ping
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .... import Node
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_ping(node: "Node", addr: Sequence[object], payload: bytes) -> None:
|
|
13
|
+
"""Update peer and validation state based on an incoming ping message."""
|
|
14
|
+
logger = node.logger
|
|
15
|
+
try:
|
|
16
|
+
host, port = addr[0], int(addr[1])
|
|
17
|
+
except Exception:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
address_key = (host, port)
|
|
21
|
+
sender_public_key_bytes = node.addresses.get(address_key)
|
|
22
|
+
if sender_public_key_bytes is None:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
peer = node.peers.get(sender_public_key_bytes)
|
|
26
|
+
if peer is None:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
ping = Ping.from_bytes(payload)
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
logger.warning("Error decoding ping: %s", exc)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
peer.timestamp = datetime.now(timezone.utc)
|
|
36
|
+
peer.latest_block = ping.latest_block
|
|
37
|
+
|
|
38
|
+
validation_route = node.validation_route
|
|
39
|
+
if validation_route is None:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
if ping.is_validator:
|
|
44
|
+
validation_route.add_peer(sender_public_key_bytes)
|
|
45
|
+
else:
|
|
46
|
+
validation_route.remove_peer(sender_public_key_bytes)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|