astreum 0.3.14__tar.gz → 0.3.25__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.14/src/astreum.egg-info → astreum-0.3.25}/PKG-INFO +35 -9
- {astreum-0.3.14 → astreum-0.3.25}/README.md +42 -16
- {astreum-0.3.14 → astreum-0.3.25}/pyproject.toml +1 -1
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/__init__.py +1 -1
- astreum-0.3.25/src/astreum/communication/disconnect.py +57 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/handlers/handshake.py +48 -22
- astreum-0.3.25/src/astreum/communication/models/ping.py +45 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/models/route.py +4 -0
- astreum-0.3.14/src/astreum/communication/start.py → astreum-0.3.25/src/astreum/communication/node.py +10 -11
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/processors/incoming.py +23 -4
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/processors/outgoing.py +11 -2
- astreum-0.3.25/src/astreum/communication/processors/peer.py +117 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/setup.py +201 -148
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/node.py +10 -5
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/storage/actions/set.py +6 -7
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/storage/setup.py +1 -1
- astreum-0.3.25/src/astreum/utils/config.py +140 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/utils/logging.py +1 -1
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/__init__.py +0 -2
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/genesis.py +3 -2
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/models/block.py +99 -79
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/models/receipt.py +17 -4
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/models/transaction.py +44 -2
- astreum-0.3.25/src/astreum/validation/node.py +127 -0
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/validator.py +2 -0
- astreum-0.3.25/src/astreum/validation/workers/__init__.py +8 -0
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/workers/validation.py +107 -92
- astreum-0.3.25/src/astreum/verification/__init__.py +4 -0
- astreum-0.3.14/src/astreum/consensus/workers/discovery.py → astreum-0.3.25/src/astreum/verification/discover.py +1 -1
- astreum-0.3.25/src/astreum/verification/node.py +61 -0
- astreum-0.3.14/src/astreum/consensus/workers/verify.py → astreum-0.3.25/src/astreum/verification/worker.py +23 -9
- {astreum-0.3.14 → astreum-0.3.25/src/astreum.egg-info}/PKG-INFO +35 -9
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum.egg-info/SOURCES.txt +21 -19
- astreum-0.3.14/src/astreum/communication/models/ping.py +0 -33
- astreum-0.3.14/src/astreum/communication/processors/peer.py +0 -59
- astreum-0.3.14/src/astreum/consensus/setup.py +0 -83
- astreum-0.3.14/src/astreum/consensus/start.py +0 -67
- astreum-0.3.14/src/astreum/consensus/workers/__init__.py +0 -9
- astreum-0.3.14/src/astreum/utils/config.py +0 -76
- {astreum-0.3.14 → astreum-0.3.25}/LICENSE +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/setup.cfg +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/handlers/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/handlers/object_request.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/handlers/object_response.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/handlers/ping.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/handlers/route_request.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/handlers/route_response.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/models/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/models/message.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/models/peer.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/processors/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/communication/util.py +0 -0
- {astreum-0.3.14/src/astreum/consensus/models → astreum-0.3.25/src/astreum/crypto}/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/crypto/chacha20poly1305.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/crypto/wesolowski.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/__init__.py +0 -0
- {astreum-0.3.14/src/astreum/crypto → astreum-0.3.25/src/astreum/machine/evaluations}/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/evaluations/high_evaluation.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/evaluations/low_evaluation.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/evaluations/script_evaluation.py +0 -0
- {astreum-0.3.14/src/astreum/machine/evaluations → astreum-0.3.25/src/astreum/machine/models}/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/models/environment.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/models/expression.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/models/meter.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/parser.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/machine/tokenizer.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/storage/__init__.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/storage/actions/get.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/storage/models/atom.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/storage/models/trie.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/storage/requests.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/utils/bytes.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum/utils/integer.py +0 -0
- {astreum-0.3.14/src/astreum/machine → astreum-0.3.25/src/astreum/validation}/models/__init__.py +0 -0
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/models/account.py +0 -0
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/models/accounts.py +0 -0
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/models/chain.py +0 -0
- {astreum-0.3.14/src/astreum/consensus → astreum-0.3.25/src/astreum/validation}/models/fork.py +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.3.14 → astreum-0.3.25}/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.3.
|
|
3
|
+
Version: 0.3.25
|
|
4
4
|
Summary: Python library to interact with the Astreum blockchain and its virtual machine.
|
|
5
5
|
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
6
|
Project-URL: Homepage, https://github.com/astreum/lib-py
|
|
@@ -33,18 +33,23 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
33
33
|
| `hot_storage_limit` | int | `1073741824` | Maximum bytes kept in the hot cache before new atoms are skipped (1 GiB). |
|
|
34
34
|
| `cold_storage_limit` | int | `10737418240` | Cold storage write threshold (10 GiB by default); set to `0` to skip the limit. |
|
|
35
35
|
| `cold_storage_path` | string | `None` | Directory where persisted atoms live; Astreum creates it on startup and skips cold storage when unset. |
|
|
36
|
-
| `
|
|
36
|
+
| `logging_retention_days` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
37
|
+
| `chain_id` | int | `0` | Chain identifier used for validation (0 = test, 1 = main). |
|
|
37
38
|
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
38
39
|
|
|
39
|
-
###
|
|
40
|
+
### Communication
|
|
40
41
|
|
|
41
42
|
| Parameter | Type | Default | Description |
|
|
42
43
|
| ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
43
44
|
| `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
45
|
| `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
46
|
| `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 | `
|
|
47
|
-
| `
|
|
47
|
+
| `incoming_port` | int | `52780` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
|
|
48
|
+
| `default_seeds` | list\[str\] | `["bootstrap.astreum.org:52780"]` | Default addresses to ping before joining; pass `[]` to disable the built-in default. |
|
|
49
|
+
| `additional_seeds` | list\[str\] | `[]` | Extra addresses appended to `default_seeds`; each must look like `host:port` or `[ipv6]:port`. |
|
|
50
|
+
| `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
|
|
51
|
+
| `peer_timeout_interval` | int | `10` | How often (seconds) the peer manager checks for stale peers. |
|
|
52
|
+
| `bootstrap_retry_interval` | int | `30` | How often (seconds) to retry bootstrapping when the peer list is empty. |
|
|
48
53
|
|
|
49
54
|
> **Note**
|
|
50
55
|
> The peer‑to‑peer *route* used for object discovery is always enabled.
|
|
@@ -61,10 +66,10 @@ config = {
|
|
|
61
66
|
"hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
|
|
62
67
|
"cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
|
|
63
68
|
"cold_storage_path": "./data/node1",
|
|
64
|
-
"incoming_port":
|
|
69
|
+
"incoming_port": 52780,
|
|
65
70
|
"use_ipv6": False,
|
|
66
|
-
"
|
|
67
|
-
|
|
71
|
+
"default_seeds": [],
|
|
72
|
+
"additional_seeds": [
|
|
68
73
|
"127.0.0.1:7374"
|
|
69
74
|
]
|
|
70
75
|
}
|
|
@@ -145,7 +150,7 @@ except ParseError as e:
|
|
|
145
150
|
Every `Node` instance wires up structured logging automatically:
|
|
146
151
|
|
|
147
152
|
- 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["
|
|
153
|
+
- 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_days"]`.
|
|
149
154
|
- Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
|
|
150
155
|
- 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
156
|
- The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
|
|
@@ -156,5 +161,26 @@ Every `Node` instance wires up structured logging automatically:
|
|
|
156
161
|
python3 -m venv venv
|
|
157
162
|
source venv/bin/activate
|
|
158
163
|
pip install -e .
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
for all tests
|
|
167
|
+
```
|
|
159
168
|
python3 -m unittest discover -s tests
|
|
160
169
|
```
|
|
170
|
+
|
|
171
|
+
for individual tests
|
|
172
|
+
```
|
|
173
|
+
python3 -m unittest tests.node.test_atom_get
|
|
174
|
+
python3 -m unittest tests.node.test_current_validator
|
|
175
|
+
python3 -m unittest tests.node.test_node_connection
|
|
176
|
+
python3 -m unittest tests.node.test_node_init
|
|
177
|
+
python3 -m unittest tests.node.test_node_validation
|
|
178
|
+
python3 -m unittest tests.node.tokenize
|
|
179
|
+
python3 -m unittest tests.node.parse
|
|
180
|
+
python3 -m unittest tests.node.function
|
|
181
|
+
python3 -m unittest tests.node.stack
|
|
182
|
+
python3 -m unittest tests.models.test_merkle
|
|
183
|
+
python3 -m unittest tests.models.test_patricia
|
|
184
|
+
python3 -m unittest tests.block.atom
|
|
185
|
+
python3 -m unittest tests.block.nonce
|
|
186
|
+
```
|
|
@@ -15,18 +15,23 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
15
15
|
| `hot_storage_limit` | int | `1073741824` | Maximum bytes kept in the hot cache before new atoms are skipped (1 GiB). |
|
|
16
16
|
| `cold_storage_limit` | int | `10737418240` | Cold storage write threshold (10 GiB by default); set to `0` to skip the limit. |
|
|
17
17
|
| `cold_storage_path` | string | `None` | Directory where persisted atoms live; Astreum creates it on startup and skips cold storage when unset. |
|
|
18
|
-
| `
|
|
18
|
+
| `logging_retention_days` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
19
|
+
| `chain_id` | int | `0` | Chain identifier used for validation (0 = test, 1 = main). |
|
|
19
20
|
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
20
21
|
|
|
21
|
-
###
|
|
22
|
+
### Communication
|
|
22
23
|
|
|
23
24
|
| Parameter | Type | Default | Description |
|
|
24
25
|
| ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
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 | `
|
|
29
|
-
| `
|
|
26
|
+
| `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. |
|
|
27
|
+
| `validation_secret_key` | hex string | `None` | Optional Ed25519 key that lets the node join the validation route; leave blank to opt out of validation. |
|
|
28
|
+
| `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
|
|
29
|
+
| `incoming_port` | int | `52780` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
|
|
30
|
+
| `default_seeds` | list\[str\] | `["bootstrap.astreum.org:52780"]` | Default addresses to ping before joining; pass `[]` to disable the built-in default. |
|
|
31
|
+
| `additional_seeds` | list\[str\] | `[]` | Extra addresses appended to `default_seeds`; each must look like `host:port` or `[ipv6]:port`. |
|
|
32
|
+
| `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
|
|
33
|
+
| `peer_timeout_interval` | int | `10` | How often (seconds) the peer manager checks for stale peers. |
|
|
34
|
+
| `bootstrap_retry_interval` | int | `30` | How often (seconds) to retry bootstrapping when the peer list is empty. |
|
|
30
35
|
|
|
31
36
|
> **Note**
|
|
32
37
|
> The peer‑to‑peer *route* used for object discovery is always enabled.
|
|
@@ -42,14 +47,14 @@ config = {
|
|
|
42
47
|
"validation_secret_key": "12…34", # optional – validator
|
|
43
48
|
"hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
|
|
44
49
|
"cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
|
|
45
|
-
"cold_storage_path": "./data/node1",
|
|
46
|
-
"incoming_port":
|
|
47
|
-
"use_ipv6": False,
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
"127.0.0.1:7374"
|
|
51
|
-
]
|
|
52
|
-
}
|
|
50
|
+
"cold_storage_path": "./data/node1",
|
|
51
|
+
"incoming_port": 52780,
|
|
52
|
+
"use_ipv6": False,
|
|
53
|
+
"default_seeds": [],
|
|
54
|
+
"additional_seeds": [
|
|
55
|
+
"127.0.0.1:7374"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
53
58
|
|
|
54
59
|
node = Node(config)
|
|
55
60
|
# … your code …
|
|
@@ -127,7 +132,7 @@ except ParseError as e:
|
|
|
127
132
|
Every `Node` instance wires up structured logging automatically:
|
|
128
133
|
|
|
129
134
|
- 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["
|
|
135
|
+
- 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_days"]`.
|
|
131
136
|
- Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
|
|
132
137
|
- 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
138
|
- The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
|
|
@@ -138,5 +143,26 @@ Every `Node` instance wires up structured logging automatically:
|
|
|
138
143
|
python3 -m venv venv
|
|
139
144
|
source venv/bin/activate
|
|
140
145
|
pip install -e .
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
for all tests
|
|
149
|
+
```
|
|
141
150
|
python3 -m unittest discover -s tests
|
|
142
151
|
```
|
|
152
|
+
|
|
153
|
+
for individual tests
|
|
154
|
+
```
|
|
155
|
+
python3 -m unittest tests.node.test_atom_get
|
|
156
|
+
python3 -m unittest tests.node.test_current_validator
|
|
157
|
+
python3 -m unittest tests.node.test_node_connection
|
|
158
|
+
python3 -m unittest tests.node.test_node_init
|
|
159
|
+
python3 -m unittest tests.node.test_node_validation
|
|
160
|
+
python3 -m unittest tests.node.tokenize
|
|
161
|
+
python3 -m unittest tests.node.parse
|
|
162
|
+
python3 -m unittest tests.node.function
|
|
163
|
+
python3 -m unittest tests.node.stack
|
|
164
|
+
python3 -m unittest tests.models.test_merkle
|
|
165
|
+
python3 -m unittest tests.models.test_patricia
|
|
166
|
+
python3 -m unittest tests.block.atom
|
|
167
|
+
python3 -m unittest tests.block.nonce
|
|
168
|
+
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
from astreum.
|
|
2
|
+
from astreum.validation import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
|
|
3
3
|
from astreum.machine import Env, Expr, parse, tokenize
|
|
4
4
|
from astreum.node import Node
|
|
5
5
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Helpers related to disconnecting communication components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from astreum.node import Node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_SOCKET_ATTRS: tuple[str, ...] = ("incoming_socket", "outgoing_socket")
|
|
12
|
+
_THREAD_ATTRS: tuple[str, ...] = (
|
|
13
|
+
"incoming_populate_thread",
|
|
14
|
+
"incoming_process_thread",
|
|
15
|
+
"outgoing_thread",
|
|
16
|
+
"peer_manager_thread",
|
|
17
|
+
"latest_block_discovery_thread",
|
|
18
|
+
"verify_thread",
|
|
19
|
+
"consensus_validation_thread",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _set_event(node: "Node", attr_name: str) -> None:
|
|
24
|
+
event = getattr(node, attr_name, None)
|
|
25
|
+
if event is not None:
|
|
26
|
+
event.set()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _close_socket(node: "Node", attr_name: str) -> None:
|
|
30
|
+
sock = getattr(node, attr_name, None)
|
|
31
|
+
if sock is None:
|
|
32
|
+
return
|
|
33
|
+
try:
|
|
34
|
+
sock.close()
|
|
35
|
+
except Exception as exc: # pragma: no cover - defensive logging path
|
|
36
|
+
node.logger.warning("Error closing %s: %s", attr_name, exc)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def disconnect_node(node: "Node") -> None:
|
|
40
|
+
"""Gracefully stop worker threads and close communication sockets."""
|
|
41
|
+
node.logger.info("Disconnecting Astreum Node")
|
|
42
|
+
|
|
43
|
+
_set_event(node, "communication_stop_event")
|
|
44
|
+
_set_event(node, "_validation_stop_event")
|
|
45
|
+
_set_event(node, "_verify_stop_event")
|
|
46
|
+
|
|
47
|
+
for sock_name in _SOCKET_ATTRS:
|
|
48
|
+
_close_socket(node, sock_name)
|
|
49
|
+
|
|
50
|
+
for thread_name in _THREAD_ATTRS:
|
|
51
|
+
thread = getattr(node, thread_name, None)
|
|
52
|
+
if thread is None or not thread.is_alive():
|
|
53
|
+
continue
|
|
54
|
+
thread.join(timeout=1.0)
|
|
55
|
+
|
|
56
|
+
node.is_connected = False
|
|
57
|
+
node.logger.info("Node disconnected")
|
|
@@ -5,18 +5,42 @@ from typing import TYPE_CHECKING, Sequence
|
|
|
5
5
|
from cryptography.hazmat.primitives import serialization
|
|
6
6
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
7
7
|
|
|
8
|
-
from ..models.peer import Peer
|
|
9
|
-
from ..models.message import Message
|
|
8
|
+
from ..models.peer import Peer
|
|
9
|
+
from ..models.message import Message, MessageTopic
|
|
10
|
+
from ..models.ping import Ping
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from .... import Node
|
|
13
14
|
|
|
14
15
|
|
|
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
|
-
"""
|
|
16
|
+
def handle_handshake(node: "Node", addr: Sequence[object], message: Message) -> bool:
|
|
17
|
+
"""Handle incoming handshake messages.
|
|
18
|
+
|
|
19
|
+
Returns True if the outer loop should `continue`, False otherwise.
|
|
20
|
+
"""
|
|
21
|
+
def _queue_handshake_ping(peer: Peer, peer_address: tuple[str, int]) -> None:
|
|
22
|
+
latest_block = getattr(node, "latest_block_hash", None)
|
|
23
|
+
if not isinstance(latest_block, (bytes, bytearray)) or len(latest_block) != 32:
|
|
24
|
+
latest_block = None
|
|
25
|
+
try:
|
|
26
|
+
ping_payload = Ping(
|
|
27
|
+
is_validator=bool(getattr(node, "validation_public_key", None)),
|
|
28
|
+
latest_block=latest_block,
|
|
29
|
+
).to_bytes()
|
|
30
|
+
ping_msg = Message(
|
|
31
|
+
topic=MessageTopic.PING,
|
|
32
|
+
content=ping_payload,
|
|
33
|
+
sender=node.relay_public_key,
|
|
34
|
+
)
|
|
35
|
+
ping_msg.encrypt(peer.shared_key_bytes)
|
|
36
|
+
node.outgoing_queue.put((ping_msg.to_bytes(), peer_address))
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
node.logger.debug(
|
|
39
|
+
"Failed sending handshake ping to %s:%s: %s",
|
|
40
|
+
peer_address[0],
|
|
41
|
+
peer_address[1],
|
|
42
|
+
exc,
|
|
43
|
+
)
|
|
20
44
|
sender_public_key_bytes = message.sender_bytes
|
|
21
45
|
try:
|
|
22
46
|
sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
|
|
@@ -31,10 +55,11 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
|
|
|
31
55
|
return True
|
|
32
56
|
peer_address = (host, port)
|
|
33
57
|
|
|
34
|
-
existing_peer = node.get_peer(sender_public_key_bytes)
|
|
35
|
-
if existing_peer is not None:
|
|
36
|
-
existing_peer.address = peer_address
|
|
37
|
-
|
|
58
|
+
existing_peer = node.get_peer(sender_public_key_bytes)
|
|
59
|
+
if existing_peer is not None:
|
|
60
|
+
existing_peer.address = peer_address
|
|
61
|
+
_queue_handshake_ping(existing_peer, peer_address)
|
|
62
|
+
return False
|
|
38
63
|
|
|
39
64
|
try:
|
|
40
65
|
peer = Peer(
|
|
@@ -48,15 +73,16 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
|
|
|
48
73
|
node.add_peer(sender_public_key_bytes, peer)
|
|
49
74
|
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
50
75
|
|
|
51
|
-
node.logger.info(
|
|
52
|
-
"Handshake accepted from %s:%s; peer added",
|
|
53
|
-
peer_address[0],
|
|
54
|
-
peer_address[1],
|
|
55
|
-
)
|
|
56
|
-
response = Message(
|
|
57
|
-
handshake=True,
|
|
58
|
-
sender=node.relay_public_key,
|
|
59
|
-
content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
|
|
76
|
+
node.logger.info(
|
|
77
|
+
"Handshake accepted from %s:%s; peer added",
|
|
78
|
+
peer_address[0],
|
|
79
|
+
peer_address[1],
|
|
60
80
|
)
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
response = Message(
|
|
82
|
+
handshake=True,
|
|
83
|
+
sender=node.relay_public_key,
|
|
84
|
+
content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
|
|
85
|
+
)
|
|
86
|
+
node.outgoing_queue.put((response.to_bytes(), peer_address))
|
|
87
|
+
_queue_handshake_ping(peer, peer_address)
|
|
88
|
+
return True
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PingFormatError(ValueError):
|
|
8
|
+
"""Raised when ping payload bytes are invalid."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Ping:
|
|
13
|
+
is_validator: bool
|
|
14
|
+
latest_block: Optional[bytes]
|
|
15
|
+
|
|
16
|
+
MIN_PAYLOAD_SIZE = 1
|
|
17
|
+
FULL_PAYLOAD_SIZE = 33
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
if self.latest_block is None:
|
|
21
|
+
return
|
|
22
|
+
lb = bytes(self.latest_block)
|
|
23
|
+
if len(lb) != 32:
|
|
24
|
+
raise ValueError("latest_block must be exactly 32 bytes")
|
|
25
|
+
self.latest_block = lb
|
|
26
|
+
|
|
27
|
+
def to_bytes(self) -> bytes:
|
|
28
|
+
flag = b"\x01" if self.is_validator else b"\x00"
|
|
29
|
+
if self.latest_block is None:
|
|
30
|
+
return flag
|
|
31
|
+
return flag + self.latest_block
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_bytes(cls, data: bytes) -> "Ping":
|
|
35
|
+
if len(data) == cls.MIN_PAYLOAD_SIZE:
|
|
36
|
+
flag = data[0]
|
|
37
|
+
if flag not in (0, 1):
|
|
38
|
+
raise PingFormatError("ping validator flag must be 0 or 1")
|
|
39
|
+
return cls(is_validator=bool(flag), latest_block=None)
|
|
40
|
+
if len(data) != cls.FULL_PAYLOAD_SIZE:
|
|
41
|
+
raise PingFormatError("ping payload must be 1 or 33 bytes")
|
|
42
|
+
flag = data[0]
|
|
43
|
+
if flag not in (0, 1):
|
|
44
|
+
raise PingFormatError("ping validator flag must be 0 or 1")
|
|
45
|
+
return cls(is_validator=bool(flag), latest_block=data[1:])
|
|
@@ -42,6 +42,8 @@ class Route:
|
|
|
42
42
|
|
|
43
43
|
def add_peer(self, peer_public_key: PeerKey, peer: Optional[Peer] = None):
|
|
44
44
|
peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
|
|
45
|
+
if peer_public_key_bytes == self.relay_public_key_bytes:
|
|
46
|
+
return
|
|
45
47
|
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
46
48
|
if len(self.buckets[bucket_idx]) < self.bucket_size:
|
|
47
49
|
bucket = self.buckets[bucket_idx]
|
|
@@ -52,6 +54,8 @@ class Route:
|
|
|
52
54
|
|
|
53
55
|
def remove_peer(self, peer_public_key: PeerKey):
|
|
54
56
|
peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
|
|
57
|
+
if peer_public_key_bytes == self.relay_public_key_bytes:
|
|
58
|
+
return
|
|
55
59
|
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
56
60
|
bucket = self.buckets.get(bucket_idx)
|
|
57
61
|
if not bucket:
|
astreum-0.3.14/src/astreum/communication/start.py → astreum-0.3.25/src/astreum/communication/node.py
RENAMED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
def connect_node(self):
|
|
2
3
|
"""Initialize communication and consensus components, then load latest block state."""
|
|
4
|
+
if self.is_connected:
|
|
5
|
+
self.logger.debug("Node already connected; skipping communication setup")
|
|
6
|
+
return
|
|
7
|
+
|
|
3
8
|
self.logger.info("Starting communication and consensus setup")
|
|
4
9
|
try:
|
|
5
10
|
from astreum.communication import communication_setup # type: ignore
|
|
6
11
|
communication_setup(node=self, config=self.config)
|
|
7
12
|
self.logger.info("Communication setup completed")
|
|
8
|
-
except Exception:
|
|
9
|
-
self.logger.exception("Communication setup failed")
|
|
10
|
-
|
|
11
|
-
try:
|
|
12
|
-
from astreum.consensus import consensus_setup # type: ignore
|
|
13
|
-
consensus_setup(node=self, config=self.config)
|
|
14
|
-
self.logger.info("Consensus setup completed")
|
|
15
|
-
except Exception:
|
|
16
|
-
self.logger.exception("Consensus setup failed")
|
|
13
|
+
except Exception as exc:
|
|
14
|
+
self.logger.exception("Communication setup failed: %s", exc)
|
|
15
|
+
return exc
|
|
17
16
|
|
|
18
17
|
# Load latest_block_hash from config
|
|
19
18
|
self.latest_block_hash = getattr(self, "latest_block_hash", None)
|
|
@@ -30,7 +29,7 @@ def connect_to_network_and_verify(self):
|
|
|
30
29
|
|
|
31
30
|
if self.latest_block_hash and self.latest_block is None:
|
|
32
31
|
try:
|
|
33
|
-
from astreum.
|
|
32
|
+
from astreum.validation.models.block import Block
|
|
34
33
|
self.latest_block = Block.from_atom(self, self.latest_block_hash)
|
|
35
34
|
self.logger.info("Loaded latest block %s from storage", self.latest_block_hash.hex())
|
|
36
35
|
except Exception as exc:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import socket
|
|
4
|
+
from queue import Empty
|
|
3
5
|
from typing import TYPE_CHECKING
|
|
4
6
|
|
|
5
7
|
from ..handlers.handshake import handle_handshake
|
|
@@ -18,13 +20,19 @@ if TYPE_CHECKING:
|
|
|
18
20
|
|
|
19
21
|
def process_incoming_messages(node: "Node") -> None:
|
|
20
22
|
"""Process incoming messages (placeholder)."""
|
|
21
|
-
|
|
23
|
+
stop = getattr(node, "communication_stop_event", None)
|
|
24
|
+
while stop is None or not stop.is_set():
|
|
22
25
|
try:
|
|
23
|
-
data, addr = node.incoming_queue.get()
|
|
24
|
-
except
|
|
26
|
+
data, addr = node.incoming_queue.get(timeout=0.5)
|
|
27
|
+
except Empty:
|
|
28
|
+
continue
|
|
29
|
+
except Exception:
|
|
25
30
|
node.logger.exception("Error taking from incoming queue")
|
|
26
31
|
continue
|
|
27
32
|
|
|
33
|
+
if stop is not None and stop.is_set():
|
|
34
|
+
break
|
|
35
|
+
|
|
28
36
|
try:
|
|
29
37
|
message = Message.from_bytes(data)
|
|
30
38
|
except Exception as exc:
|
|
@@ -87,12 +95,23 @@ def process_incoming_messages(node: "Node") -> None:
|
|
|
87
95
|
case _:
|
|
88
96
|
continue
|
|
89
97
|
|
|
98
|
+
node.logger.info("Incoming message processor stopped")
|
|
99
|
+
|
|
90
100
|
|
|
91
101
|
def populate_incoming_messages(node: "Node") -> None:
|
|
92
102
|
"""Receive UDP packets and feed the incoming queue."""
|
|
93
|
-
|
|
103
|
+
stop = getattr(node, "communication_stop_event", None)
|
|
104
|
+
while stop is None or not stop.is_set():
|
|
94
105
|
try:
|
|
95
106
|
data, addr = node.incoming_socket.recvfrom(4096)
|
|
96
107
|
node.incoming_queue.put((data, addr))
|
|
108
|
+
except socket.timeout:
|
|
109
|
+
continue
|
|
110
|
+
except OSError:
|
|
111
|
+
if stop is not None and stop.is_set():
|
|
112
|
+
break
|
|
113
|
+
node.logger.warning("Error populating incoming queue: socket closed")
|
|
97
114
|
except Exception as exc:
|
|
98
115
|
node.logger.warning("Error populating incoming queue: %s", exc)
|
|
116
|
+
|
|
117
|
+
node.logger.info("Incoming message populator stopped")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from queue import Empty
|
|
3
4
|
from typing import TYPE_CHECKING, Tuple
|
|
4
5
|
|
|
5
6
|
if TYPE_CHECKING:
|
|
@@ -7,14 +8,22 @@ if TYPE_CHECKING:
|
|
|
7
8
|
|
|
8
9
|
def process_outgoing_messages(node: "Node") -> None:
|
|
9
10
|
"""Send queued outbound packets."""
|
|
10
|
-
|
|
11
|
+
stop = getattr(node, "communication_stop_event", None)
|
|
12
|
+
while stop is None or not stop.is_set():
|
|
11
13
|
try:
|
|
12
|
-
payload, addr = node.outgoing_queue.get()
|
|
14
|
+
payload, addr = node.outgoing_queue.get(timeout=0.5)
|
|
15
|
+
except Empty:
|
|
16
|
+
continue
|
|
13
17
|
except Exception:
|
|
14
18
|
node.logger.exception("Error taking from outgoing queue")
|
|
15
19
|
continue
|
|
16
20
|
|
|
21
|
+
if stop is not None and stop.is_set():
|
|
22
|
+
break
|
|
23
|
+
|
|
17
24
|
try:
|
|
18
25
|
node.outgoing_socket.sendto(payload, addr)
|
|
19
26
|
except Exception as exc:
|
|
20
27
|
node.logger.warning("Error sending message to %s: %s", addr, exc)
|
|
28
|
+
|
|
29
|
+
node.logger.info("Outgoing message processor stopped")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from ..models.message import Message
|
|
8
|
+
from ..util import address_str_to_host_and_port
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .. import Node
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _queue_bootstrap_handshakes(node: "Node") -> int:
|
|
15
|
+
outgoing_queue = getattr(node, "outgoing_queue", None)
|
|
16
|
+
relay_public_key = getattr(node, "relay_public_key", None)
|
|
17
|
+
if outgoing_queue is None or relay_public_key is None:
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
default_seeds = node.config.get("default_seeds", [])
|
|
21
|
+
additional_seeds = node.config.get("additional_seeds", [])
|
|
22
|
+
bootstrap_peers = list(default_seeds) + list(additional_seeds)
|
|
23
|
+
if not bootstrap_peers:
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
incoming_port = int(node.config.get("incoming_port", 0))
|
|
28
|
+
content = incoming_port.to_bytes(2, "big", signed=False)
|
|
29
|
+
except (TypeError, ValueError, OverflowError):
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
handshake_message = Message(
|
|
33
|
+
handshake=True,
|
|
34
|
+
sender=relay_public_key,
|
|
35
|
+
content=content,
|
|
36
|
+
)
|
|
37
|
+
handshake_bytes = handshake_message.to_bytes()
|
|
38
|
+
sent = 0
|
|
39
|
+
for addr in bootstrap_peers:
|
|
40
|
+
try:
|
|
41
|
+
host, port = address_str_to_host_and_port(addr)
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
node.logger.warning("Invalid bootstrap address %s: %s", addr, exc)
|
|
44
|
+
continue
|
|
45
|
+
outgoing_queue.put((handshake_bytes, (host, port)))
|
|
46
|
+
node.logger.info("Retrying bootstrap handshake to %s:%s", host, port)
|
|
47
|
+
sent += 1
|
|
48
|
+
return sent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def manage_peer(node: "Node") -> None:
|
|
52
|
+
"""Continuously evict peers whose timestamps exceed the configured timeout."""
|
|
53
|
+
node.logger.info(
|
|
54
|
+
"Peer manager started (timeout=%3ds, interval=%3ds)",
|
|
55
|
+
node.config["peer_timeout"],
|
|
56
|
+
node.config["peer_timeout_interval"],
|
|
57
|
+
)
|
|
58
|
+
stop = getattr(node, "communication_stop_event", None)
|
|
59
|
+
while stop is None or not stop.is_set():
|
|
60
|
+
timeout_seconds = node.config["peer_timeout"]
|
|
61
|
+
interval_seconds = node.config["peer_timeout_interval"]
|
|
62
|
+
try:
|
|
63
|
+
peers = getattr(node, "peers", None)
|
|
64
|
+
peer_route = getattr(node, "peer_route", None)
|
|
65
|
+
if not isinstance(peers, dict) or peer_route is None:
|
|
66
|
+
time.sleep(interval_seconds)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
cutoff = datetime.now(timezone.utc) - timedelta(seconds=timeout_seconds)
|
|
70
|
+
stale_keys = []
|
|
71
|
+
with node.peers_lock:
|
|
72
|
+
for peer_key, peer in list(peers.items()):
|
|
73
|
+
if peer.timestamp < cutoff:
|
|
74
|
+
stale_keys.append(peer_key)
|
|
75
|
+
|
|
76
|
+
removed_count = 0
|
|
77
|
+
for peer_key in stale_keys:
|
|
78
|
+
removed = node.remove_peer(peer_key)
|
|
79
|
+
if removed is None:
|
|
80
|
+
continue
|
|
81
|
+
removed_count += 1
|
|
82
|
+
try:
|
|
83
|
+
peer_route.remove_peer(peer_key)
|
|
84
|
+
except Exception:
|
|
85
|
+
node.logger.debug(
|
|
86
|
+
"Unable to remove peer %s from route",
|
|
87
|
+
peer_key.hex(),
|
|
88
|
+
)
|
|
89
|
+
node.logger.debug(
|
|
90
|
+
"Evicted stale peer %s last seen at %s",
|
|
91
|
+
peer_key.hex(),
|
|
92
|
+
getattr(removed, "timestamp", None),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if removed_count:
|
|
96
|
+
node.logger.info("Peer manager removed %s stale peer(s)", removed_count)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
with node.peers_lock:
|
|
100
|
+
peer_count = len(peers)
|
|
101
|
+
except Exception:
|
|
102
|
+
peer_count = len(getattr(node, "peers", {}) or {})
|
|
103
|
+
if peer_count == 0:
|
|
104
|
+
bootstrap_interval = node.config.get("bootstrap_retry_interval", 0)
|
|
105
|
+
now = time.time()
|
|
106
|
+
last_attempt = getattr(node, "_bootstrap_last_attempt", 0.0)
|
|
107
|
+
if bootstrap_interval and (now - last_attempt) >= bootstrap_interval:
|
|
108
|
+
sent = _queue_bootstrap_handshakes(node)
|
|
109
|
+
if sent:
|
|
110
|
+
node._bootstrap_last_attempt = now
|
|
111
|
+
except Exception:
|
|
112
|
+
node.logger.exception("Peer manager iteration failed")
|
|
113
|
+
|
|
114
|
+
if stop is not None and stop.wait(interval_seconds):
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
node.logger.info("Peer manager stopped")
|