astreum 0.3.46__tar.gz → 0.3.50__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.46/src/astreum.egg-info → astreum-0.3.50}/PKG-INFO +6 -4
- {astreum-0.3.46 → astreum-0.3.50}/README.md +14 -12
- {astreum-0.3.46 → astreum-0.3.50}/pyproject.toml +1 -1
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/handlers/object_request.py +117 -50
- astreum-0.3.50/src/astreum/communication/handlers/object_response.py +223 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/handlers/ping.py +57 -43
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/setup.py +76 -78
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/evaluations/low_evaluation.py +5 -5
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/models/expression.py +5 -5
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/node.py +25 -19
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/storage/actions/get.py +257 -186
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/storage/actions/set.py +47 -17
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/storage/models/atom.py +0 -14
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/storage/models/trie.py +2 -2
- astreum-0.3.50/src/astreum/storage/requests.py +31 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/storage/setup.py +21 -14
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/utils/config.py +28 -17
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/models/account.py +1 -1
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/models/block.py +3 -3
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/models/receipt.py +1 -1
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/models/transaction.py +7 -7
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/workers/validation.py +80 -1
- {astreum-0.3.46 → astreum-0.3.50/src/astreum.egg-info}/PKG-INFO +6 -4
- astreum-0.3.46/src/astreum/communication/handlers/object_response.py +0 -121
- astreum-0.3.46/src/astreum/storage/requests.py +0 -28
- {astreum-0.3.46 → astreum-0.3.50}/LICENSE +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/setup.cfg +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/difficulty.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/disconnect.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/handlers/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/handlers/handshake.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/handlers/route_request.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/handlers/route_response.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/incoming_queue.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/message_pow.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/models/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/models/message.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/models/peer.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/models/ping.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/models/route.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/node.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/outgoing_queue.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/processors/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/processors/incoming.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/processors/outgoing.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/processors/peer.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/communication/util.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/crypto/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/crypto/chacha20poly1305.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/crypto/wesolowski.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/evaluations/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/evaluations/high_evaluation.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/evaluations/script_evaluation.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/models/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/models/environment.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/models/meter.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/parser.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/machine/tokenizer.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/storage/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/storage/providers.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/utils/bytes.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/utils/integer.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/utils/logging.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/constants.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/genesis.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/models/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/models/accounts.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/models/fork.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/node.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/validator.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/validation/workers/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/verification/__init__.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/verification/discover.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/verification/node.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum/verification/worker.py +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum.egg-info/SOURCES.txt +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.3.46 → astreum-0.3.50}/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.50
|
|
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,6 +33,8 @@ 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
|
+
| `atom_fetch_interval` | float | `0.25` | Poll interval (seconds) while waiting for missing atoms in `get_atom_list_from_storage`; `0` disables waiting. |
|
|
37
|
+
| `atom_fetch_retries` | int | `8` | Number of poll attempts for missing atoms; max wait is roughly `interval * retries`, `0` disables waiting. |
|
|
36
38
|
| `logging_retention_days` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
37
39
|
| `chain_id` | int | `0` | Chain identifier used for validation (0 = test, 1 = main). |
|
|
38
40
|
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
@@ -50,13 +52,14 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
50
52
|
| `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
|
|
51
53
|
| `peer_timeout_interval` | int | `10` | How often (seconds) the peer manager checks for stale peers. |
|
|
52
54
|
| `bootstrap_retry_interval` | int | `30` | How often (seconds) to retry bootstrapping when the peer list is empty. |
|
|
53
|
-
| `storage_index_interval` | int | `600` | How often (seconds) to re-advertise
|
|
54
|
-
| `cold_storage_advertise_limit` | int | `1000` | Max cold storage atoms advertised per cycle using last-modified time; `-1` unlimited, `0` disable. |
|
|
55
|
+
| `storage_index_interval` | int | `600` | How often (seconds) to re-advertise entries in `node.atom_advertisments` to the closest known peer. |
|
|
55
56
|
| `incoming_queue_size_limit` | int | `67108864` | Soft cap (bytes) for inbound queue usage tracked by `enqueue_incoming`; set to `0` to disable. |
|
|
56
57
|
| `incoming_queue_timeout` | float | `1.0` | When > 0, `enqueue_incoming` waits up to this many seconds for space before dropping the payload. |
|
|
57
58
|
| `outgoing_queue_size_limit` | int | `67108864` | Soft cap (bytes) for `enqueue_outgoing`-tracked outgoing queue usage; set to `0` to disable. |
|
|
58
59
|
| `outgoing_queue_timeout` | float | `1.0` | When > 0, `enqueue_outgoing` waits up to this many seconds for space before dropping the payload. |
|
|
59
60
|
|
|
61
|
+
Advertisements: `node.atom_advertisments` holds `(atom_id, payload_type, expires_at)` tuples. Use `node.add_atom_advertisement` or `node.add_atom_advertisements` to enqueue entries (`expires_at=None` keeps them indefinite). Validators automatically advertise block, transaction (main and detail lists), receipt, and account trie lists for 15 minutes by default.
|
|
62
|
+
|
|
60
63
|
> **Note**
|
|
61
64
|
> The peer‑to‑peer *route* used for object discovery is always enabled.
|
|
62
65
|
> If `validation_secret_key` is provided the node automatically joins the validation route too.
|
|
@@ -72,7 +75,6 @@ config = {
|
|
|
72
75
|
"hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
|
|
73
76
|
"cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
|
|
74
77
|
"cold_storage_path": "./data/node1",
|
|
75
|
-
"cold_storage_advertise_limit": 1000, # -1 unlimited, 0 disable, >0 limit
|
|
76
78
|
"incoming_port": 52780,
|
|
77
79
|
"use_ipv6": False,
|
|
78
80
|
"default_seed": None,
|
|
@@ -12,12 +12,14 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
12
12
|
|
|
13
13
|
| Parameter | Type | Default | Description |
|
|
14
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
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
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
|
+
| `atom_fetch_interval` | float | `0.25` | Poll interval (seconds) while waiting for missing atoms in `get_atom_list_from_storage`; `0` disables waiting. |
|
|
19
|
+
| `atom_fetch_retries` | int | `8` | Number of poll attempts for missing atoms; max wait is roughly `interval * retries`, `0` disables waiting. |
|
|
20
|
+
| `logging_retention_days` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
21
|
+
| `chain_id` | int | `0` | Chain identifier used for validation (0 = test, 1 = main). |
|
|
22
|
+
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
21
23
|
|
|
22
24
|
### Communication
|
|
23
25
|
|
|
@@ -32,14 +34,15 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
32
34
|
| `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
|
|
33
35
|
| `peer_timeout_interval` | int | `10` | How often (seconds) the peer manager checks for stale peers. |
|
|
34
36
|
| `bootstrap_retry_interval` | int | `30` | How often (seconds) to retry bootstrapping when the peer list is empty. |
|
|
35
|
-
| `storage_index_interval` | int | `600` | How often (seconds) to re-advertise
|
|
36
|
-
| `cold_storage_advertise_limit` | int | `1000` | Max cold storage atoms advertised per cycle using last-modified time; `-1` unlimited, `0` disable. |
|
|
37
|
+
| `storage_index_interval` | int | `600` | How often (seconds) to re-advertise entries in `node.atom_advertisments` to the closest known peer. |
|
|
37
38
|
| `incoming_queue_size_limit` | int | `67108864` | Soft cap (bytes) for inbound queue usage tracked by `enqueue_incoming`; set to `0` to disable. |
|
|
38
39
|
| `incoming_queue_timeout` | float | `1.0` | When > 0, `enqueue_incoming` waits up to this many seconds for space before dropping the payload. |
|
|
39
40
|
| `outgoing_queue_size_limit` | int | `67108864` | Soft cap (bytes) for `enqueue_outgoing`-tracked outgoing queue usage; set to `0` to disable. |
|
|
40
|
-
| `outgoing_queue_timeout` | float | `1.0` | When > 0, `enqueue_outgoing` waits up to this many seconds for space before dropping the payload. |
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
| `outgoing_queue_timeout` | float | `1.0` | When > 0, `enqueue_outgoing` waits up to this many seconds for space before dropping the payload. |
|
|
42
|
+
|
|
43
|
+
Advertisements: `node.atom_advertisments` holds `(atom_id, payload_type, expires_at)` tuples. Use `node.add_atom_advertisement` or `node.add_atom_advertisements` to enqueue entries (`expires_at=None` keeps them indefinite). Validators automatically advertise block, transaction (main and detail lists), receipt, and account trie lists for 15 minutes by default.
|
|
44
|
+
|
|
45
|
+
> **Note**
|
|
43
46
|
> The peer‑to‑peer *route* used for object discovery is always enabled.
|
|
44
47
|
> If `validation_secret_key` is provided the node automatically joins the validation route too.
|
|
45
48
|
|
|
@@ -54,7 +57,6 @@ config = {
|
|
|
54
57
|
"hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
|
|
55
58
|
"cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
|
|
56
59
|
"cold_storage_path": "./data/node1",
|
|
57
|
-
"cold_storage_advertise_limit": 1000, # -1 unlimited, 0 disable, >0 limit
|
|
58
60
|
"incoming_port": 52780,
|
|
59
61
|
"use_ipv6": False,
|
|
60
62
|
"default_seed": None,
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import socket
|
|
3
3
|
from enum import IntEnum
|
|
4
|
-
from typing import TYPE_CHECKING, Tuple
|
|
4
|
+
from typing import Optional, TYPE_CHECKING, Tuple
|
|
5
5
|
|
|
6
|
-
from .object_response import
|
|
6
|
+
from .object_response import (
|
|
7
|
+
ObjectResponse,
|
|
8
|
+
ObjectResponseType,
|
|
9
|
+
OBJECT_FOUND_ATOM_PAYLOAD,
|
|
10
|
+
OBJECT_FOUND_LIST_PAYLOAD,
|
|
11
|
+
encode_object_found_atom_payload,
|
|
12
|
+
encode_object_found_list_payload,
|
|
13
|
+
)
|
|
7
14
|
from ..outgoing_queue import enqueue_outgoing
|
|
8
15
|
from ..models.message import Message, MessageTopic
|
|
9
16
|
from ..util import xor_distance
|
|
@@ -19,22 +26,36 @@ class ObjectRequestType(IntEnum):
|
|
|
19
26
|
OBJECT_PUT = 1
|
|
20
27
|
|
|
21
28
|
|
|
22
|
-
class ObjectRequest:
|
|
23
|
-
type: ObjectRequestType
|
|
24
|
-
data: bytes
|
|
25
|
-
atom_id: bytes
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
self
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
class ObjectRequest:
|
|
30
|
+
type: ObjectRequestType
|
|
31
|
+
data: bytes
|
|
32
|
+
atom_id: bytes
|
|
33
|
+
payload_type: Optional[int]
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
type: ObjectRequestType,
|
|
38
|
+
data: bytes = b"",
|
|
39
|
+
atom_id: bytes = None,
|
|
40
|
+
payload_type: Optional[int] = None,
|
|
41
|
+
):
|
|
42
|
+
self.type = type
|
|
43
|
+
self.data = data
|
|
44
|
+
self.atom_id = atom_id
|
|
45
|
+
self.payload_type = payload_type
|
|
46
|
+
|
|
47
|
+
def to_bytes(self):
|
|
48
|
+
if self.type == ObjectRequestType.OBJECT_PUT and self.payload_type is None:
|
|
49
|
+
raise ValueError("OBJECT_PUT requires payload_type")
|
|
50
|
+
if self.payload_type is not None:
|
|
51
|
+
payload = bytes([self.payload_type]) + self.data
|
|
52
|
+
else:
|
|
53
|
+
payload = self.data
|
|
54
|
+
return bytes([self.type.value]) + self.atom_id + payload
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_bytes(cls, data: bytes) -> "ObjectRequest":
|
|
58
|
+
# need at least 1 byte for type + 32 bytes for hash
|
|
38
59
|
if len(data) < 1 + 32:
|
|
39
60
|
raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
|
|
40
61
|
|
|
@@ -44,9 +65,22 @@ class ObjectRequest:
|
|
|
44
65
|
except ValueError:
|
|
45
66
|
raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
|
|
46
67
|
|
|
47
|
-
atom_id_bytes = data[1:33]
|
|
48
|
-
payload
|
|
49
|
-
|
|
68
|
+
atom_id_bytes = data[1:33]
|
|
69
|
+
payload = data[33:]
|
|
70
|
+
if req_type == ObjectRequestType.OBJECT_GET:
|
|
71
|
+
if payload:
|
|
72
|
+
payload_type = payload[0]
|
|
73
|
+
payload = payload[1:]
|
|
74
|
+
else:
|
|
75
|
+
payload_type = None
|
|
76
|
+
return cls(req_type, payload, atom_id_bytes, payload_type=payload_type)
|
|
77
|
+
if req_type == ObjectRequestType.OBJECT_PUT:
|
|
78
|
+
if not payload:
|
|
79
|
+
raise ValueError("OBJECT_PUT missing payload type")
|
|
80
|
+
payload_type = payload[0]
|
|
81
|
+
payload = payload[1:]
|
|
82
|
+
return cls(req_type, payload, atom_id_bytes, payload_type=payload_type)
|
|
83
|
+
return cls(req_type, payload, atom_id_bytes)
|
|
50
84
|
|
|
51
85
|
|
|
52
86
|
def encode_peer_contact_bytes(peer: "Peer") -> bytes:
|
|
@@ -75,31 +109,63 @@ def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
|
|
|
75
109
|
return
|
|
76
110
|
|
|
77
111
|
match object_request.type:
|
|
78
|
-
case ObjectRequestType.OBJECT_GET:
|
|
79
|
-
atom_id = object_request.atom_id
|
|
80
|
-
node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
atom_id
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
case ObjectRequestType.OBJECT_GET:
|
|
113
|
+
atom_id = object_request.atom_id
|
|
114
|
+
node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
|
|
115
|
+
payload_type = object_request.payload_type
|
|
116
|
+
if payload_type is None:
|
|
117
|
+
payload_type = OBJECT_FOUND_ATOM_PAYLOAD
|
|
118
|
+
|
|
119
|
+
if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
|
|
120
|
+
local_atom = node.get_atom_from_local_storage(atom_id=atom_id)
|
|
121
|
+
if local_atom is not None:
|
|
122
|
+
node.logger.debug("Object %s found locally; returning to %s", atom_id.hex(), peer.address)
|
|
123
|
+
resp = ObjectResponse(
|
|
124
|
+
type=ObjectResponseType.OBJECT_FOUND,
|
|
125
|
+
data=encode_object_found_atom_payload(local_atom),
|
|
126
|
+
atom_id=atom_id
|
|
127
|
+
)
|
|
128
|
+
obj_res_msg = Message(
|
|
129
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
130
|
+
body=resp.to_bytes(),
|
|
131
|
+
sender=node.relay_public_key,
|
|
132
|
+
)
|
|
133
|
+
obj_res_msg.encrypt(peer.shared_key_bytes)
|
|
134
|
+
enqueue_outgoing(
|
|
135
|
+
node,
|
|
136
|
+
peer.address,
|
|
137
|
+
message=obj_res_msg,
|
|
138
|
+
difficulty=peer.difficulty,
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
elif payload_type == OBJECT_FOUND_LIST_PAYLOAD:
|
|
142
|
+
local_atoms = node.get_atom_list_from_local_storage(root_hash=atom_id)
|
|
143
|
+
if local_atoms is not None:
|
|
144
|
+
node.logger.debug("Object list %s found locally; returning to %s", atom_id.hex(), peer.address)
|
|
145
|
+
resp = ObjectResponse(
|
|
146
|
+
type=ObjectResponseType.OBJECT_FOUND,
|
|
147
|
+
data=encode_object_found_list_payload(local_atoms),
|
|
148
|
+
atom_id=atom_id
|
|
149
|
+
)
|
|
150
|
+
obj_res_msg = Message(
|
|
151
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
152
|
+
body=resp.to_bytes(),
|
|
153
|
+
sender=node.relay_public_key,
|
|
154
|
+
)
|
|
155
|
+
obj_res_msg.encrypt(peer.shared_key_bytes)
|
|
156
|
+
enqueue_outgoing(
|
|
157
|
+
node,
|
|
158
|
+
peer.address,
|
|
159
|
+
message=obj_res_msg,
|
|
160
|
+
difficulty=peer.difficulty,
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
else:
|
|
164
|
+
node.logger.warning(
|
|
165
|
+
"Unknown OBJECT_GET payload type %s for %s",
|
|
166
|
+
payload_type,
|
|
167
|
+
atom_id.hex(),
|
|
101
168
|
)
|
|
102
|
-
return
|
|
103
169
|
|
|
104
170
|
if atom_id in node.storage_index:
|
|
105
171
|
provider_id = node.storage_index[atom_id]
|
|
@@ -184,11 +250,12 @@ def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
|
|
|
184
250
|
object_request.atom_id.hex(),
|
|
185
251
|
nearest_peer.address,
|
|
186
252
|
)
|
|
187
|
-
fwd_req = ObjectRequest(
|
|
188
|
-
type=ObjectRequestType.OBJECT_PUT,
|
|
189
|
-
data=object_request.data,
|
|
190
|
-
atom_id=object_request.atom_id,
|
|
191
|
-
|
|
253
|
+
fwd_req = ObjectRequest(
|
|
254
|
+
type=ObjectRequestType.OBJECT_PUT,
|
|
255
|
+
data=object_request.data,
|
|
256
|
+
atom_id=object_request.atom_id,
|
|
257
|
+
payload_type=object_request.payload_type,
|
|
258
|
+
)
|
|
192
259
|
obj_req_msg = Message(
|
|
193
260
|
topic=MessageTopic.OBJECT_REQUEST,
|
|
194
261
|
body=fwd_req.to_bytes(),
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from typing import List, Tuple, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..outgoing_queue import enqueue_outgoing
|
|
6
|
+
from ..models.message import Message, MessageTopic
|
|
7
|
+
from ...storage.models.atom import Atom
|
|
8
|
+
from ...storage.requests import get_atom_req_payload
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .. import Node
|
|
12
|
+
from ..models.peer import Peer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ObjectResponseType(IntEnum):
|
|
16
|
+
OBJECT_FOUND = 0
|
|
17
|
+
OBJECT_PROVIDER = 1
|
|
18
|
+
OBJECT_NEAREST_PEER = 2
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
OBJECT_FOUND_ATOM_PAYLOAD = 1
|
|
22
|
+
OBJECT_FOUND_LIST_PAYLOAD = 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ObjectResponse:
|
|
26
|
+
type: ObjectResponseType
|
|
27
|
+
data: bytes
|
|
28
|
+
atom_id: bytes
|
|
29
|
+
|
|
30
|
+
def __init__(self, type: ObjectResponseType, data: bytes, atom_id: bytes = None):
|
|
31
|
+
self.type = type
|
|
32
|
+
self.data = data
|
|
33
|
+
self.atom_id = atom_id
|
|
34
|
+
|
|
35
|
+
def to_bytes(self):
|
|
36
|
+
return bytes([self.type.value]) + self.atom_id + self.data
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_bytes(cls, data: bytes) -> "ObjectResponse":
|
|
40
|
+
# need at least 1 byte for type + 32 bytes for atom id
|
|
41
|
+
if len(data) < 1 + 32:
|
|
42
|
+
raise ValueError(f"Too short to be a valid ObjectResponse ({len(data)} bytes)")
|
|
43
|
+
|
|
44
|
+
type_val = data[0]
|
|
45
|
+
try:
|
|
46
|
+
resp_type = ObjectResponseType(type_val)
|
|
47
|
+
except ValueError:
|
|
48
|
+
raise ValueError(f"Unknown ObjectResponseType: {type_val}")
|
|
49
|
+
|
|
50
|
+
atom_id = data[1:33]
|
|
51
|
+
payload = data[33:]
|
|
52
|
+
return cls(resp_type, payload, atom_id)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def encode_object_found_atom_payload(atom: Atom) -> bytes:
|
|
56
|
+
return bytes([OBJECT_FOUND_ATOM_PAYLOAD]) + atom.to_bytes()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def encode_object_found_list_payload(atoms: List[Atom]) -> bytes:
|
|
60
|
+
parts = [bytes([OBJECT_FOUND_LIST_PAYLOAD])]
|
|
61
|
+
for atom in atoms:
|
|
62
|
+
atom_bytes = atom.to_bytes()
|
|
63
|
+
parts.append(len(atom_bytes).to_bytes(4, "big", signed=False))
|
|
64
|
+
parts.append(atom_bytes)
|
|
65
|
+
return b"".join(parts)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def decode_object_found_list_payload(payload: bytes) -> List[Atom]:
|
|
69
|
+
atoms: List[Atom] = []
|
|
70
|
+
offset = 0
|
|
71
|
+
while offset < len(payload):
|
|
72
|
+
if len(payload) - offset < 4:
|
|
73
|
+
raise ValueError("truncated atom length")
|
|
74
|
+
atom_len = int.from_bytes(payload[offset : offset + 4], "big", signed=False)
|
|
75
|
+
offset += 4
|
|
76
|
+
if atom_len <= 0:
|
|
77
|
+
raise ValueError("invalid atom length")
|
|
78
|
+
end = offset + atom_len
|
|
79
|
+
if end > len(payload):
|
|
80
|
+
raise ValueError("truncated atom payload")
|
|
81
|
+
atoms.append(Atom.from_bytes(payload[offset:end]))
|
|
82
|
+
offset = end
|
|
83
|
+
return atoms
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def decode_object_provider(payload: bytes) -> Tuple[bytes, str, int]:
|
|
87
|
+
expected_len = 32 + 4 + 2
|
|
88
|
+
if len(payload) < expected_len:
|
|
89
|
+
raise ValueError("provider payload too short")
|
|
90
|
+
|
|
91
|
+
provider_public_key = payload[:32]
|
|
92
|
+
provider_ip_bytes = payload[32:36]
|
|
93
|
+
provider_port_bytes = payload[36:38]
|
|
94
|
+
|
|
95
|
+
provider_address = socket.inet_ntoa(provider_ip_bytes)
|
|
96
|
+
provider_port = int.from_bytes(provider_port_bytes, byteorder="big", signed=False)
|
|
97
|
+
return provider_public_key, provider_address, provider_port
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def handle_object_response(node: "Node", peer: "Peer", message: Message) -> None:
|
|
101
|
+
if message.content is None:
|
|
102
|
+
node.logger.warning("OBJECT_RESPONSE from %s missing content", peer.address)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
object_response = ObjectResponse.from_bytes(message.content)
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
node.logger.warning("Error decoding OBJECT_RESPONSE from %s: %s", peer.address, exc)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if not node.has_atom_req(object_response.atom_id):
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
match object_response.type:
|
|
115
|
+
case ObjectResponseType.OBJECT_FOUND:
|
|
116
|
+
payload = object_response.data
|
|
117
|
+
if not payload:
|
|
118
|
+
node.logger.warning(
|
|
119
|
+
"OBJECT_FOUND payload for %s missing content",
|
|
120
|
+
object_response.atom_id.hex(),
|
|
121
|
+
)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
payload_type = payload[0]
|
|
125
|
+
body = payload[1:]
|
|
126
|
+
|
|
127
|
+
if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
|
|
128
|
+
try:
|
|
129
|
+
atom = Atom.from_bytes(body)
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
node.logger.warning(
|
|
132
|
+
"Invalid OBJECT_FOUND atom payload for %s: %s",
|
|
133
|
+
object_response.atom_id.hex(),
|
|
134
|
+
exc,
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
atom_id = atom.object_id()
|
|
139
|
+
if object_response.atom_id != atom_id:
|
|
140
|
+
node.logger.warning(
|
|
141
|
+
"OBJECT_FOUND atom ID mismatch (expected=%s got=%s)",
|
|
142
|
+
object_response.atom_id.hex(),
|
|
143
|
+
atom_id.hex(),
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
node.pop_atom_req(atom_id)
|
|
148
|
+
node._hot_storage_set(atom_id, atom)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if payload_type == OBJECT_FOUND_LIST_PAYLOAD:
|
|
152
|
+
try:
|
|
153
|
+
atoms = decode_object_found_list_payload(body)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
node.logger.warning(
|
|
156
|
+
"Invalid OBJECT_FOUND list payload for %s: %s",
|
|
157
|
+
object_response.atom_id.hex(),
|
|
158
|
+
exc,
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if not atoms:
|
|
163
|
+
node.logger.warning(
|
|
164
|
+
"OBJECT_FOUND list payload for %s contained no atoms",
|
|
165
|
+
object_response.atom_id.hex(),
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
root_id = atoms[0].object_id()
|
|
170
|
+
if object_response.atom_id != root_id:
|
|
171
|
+
node.logger.warning(
|
|
172
|
+
"OBJECT_FOUND list root ID mismatch (expected=%s got=%s)",
|
|
173
|
+
object_response.atom_id.hex(),
|
|
174
|
+
root_id.hex(),
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
node.pop_atom_req(root_id)
|
|
179
|
+
for atom in atoms:
|
|
180
|
+
node._hot_storage_set(atom.object_id(), atom)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
node.logger.warning(
|
|
184
|
+
"Unknown OBJECT_FOUND payload type %s for %s",
|
|
185
|
+
payload_type,
|
|
186
|
+
object_response.atom_id.hex(),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
case ObjectResponseType.OBJECT_PROVIDER:
|
|
190
|
+
try:
|
|
191
|
+
_, provider_address, provider_port = decode_object_provider(object_response.data)
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
node.logger.warning("Invalid OBJECT_PROVIDER payload from %s: %s", peer.address, exc)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
from .object_request import ObjectRequest, ObjectRequestType
|
|
197
|
+
|
|
198
|
+
payload_type = get_atom_req_payload(node, object_response.atom_id)
|
|
199
|
+
if payload_type is None:
|
|
200
|
+
payload_type = OBJECT_FOUND_ATOM_PAYLOAD
|
|
201
|
+
|
|
202
|
+
obj_req = ObjectRequest(
|
|
203
|
+
type=ObjectRequestType.OBJECT_GET,
|
|
204
|
+
data=b"",
|
|
205
|
+
atom_id=object_response.atom_id,
|
|
206
|
+
payload_type=payload_type,
|
|
207
|
+
)
|
|
208
|
+
obj_req_bytes = obj_req.to_bytes()
|
|
209
|
+
obj_req_msg = Message(
|
|
210
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
211
|
+
body=obj_req_bytes,
|
|
212
|
+
sender=node.relay_public_key,
|
|
213
|
+
)
|
|
214
|
+
obj_req_msg.encrypt(peer.shared_key_bytes)
|
|
215
|
+
enqueue_outgoing(
|
|
216
|
+
node,
|
|
217
|
+
(provider_address, provider_port),
|
|
218
|
+
message=obj_req_msg,
|
|
219
|
+
difficulty=1,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
case ObjectResponseType.OBJECT_NEAREST_PEER:
|
|
223
|
+
node.logger.debug("Ignoring OBJECT_NEAREST_PEER response from %s", peer.address)
|
|
@@ -1,43 +1,57 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
from ..models.ping import Ping
|
|
7
|
-
from ..models.peer import Peer
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from .... import Node
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def handle_ping(node: "Node", peer: Peer, payload: bytes) -> None:
|
|
14
|
-
"""Update peer and validation state based on an incoming ping message."""
|
|
15
|
-
try:
|
|
16
|
-
ping = Ping.from_bytes(payload)
|
|
17
|
-
except Exception as exc:
|
|
18
|
-
node.logger.warning("Error decoding ping: %s", exc)
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
peer.timestamp = datetime.now(timezone.utc)
|
|
22
|
-
peer.latest_block = ping.latest_block
|
|
23
|
-
peer.difficulty = ping.difficulty
|
|
24
|
-
if peer.is_default_seed and ping.latest_block:
|
|
25
|
-
if getattr(node, "latest_block_hash", None) != ping.latest_block:
|
|
26
|
-
node.latest_block_hash = ping.latest_block
|
|
27
|
-
node.latest_block = None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from ..models.ping import Ping
|
|
7
|
+
from ..models.peer import Peer
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .... import Node
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_ping(node: "Node", peer: Peer, payload: bytes) -> None:
|
|
14
|
+
"""Update peer and validation state based on an incoming ping message."""
|
|
15
|
+
try:
|
|
16
|
+
ping = Ping.from_bytes(payload)
|
|
17
|
+
except Exception as exc:
|
|
18
|
+
node.logger.warning("Error decoding ping: %s", exc)
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
peer.timestamp = datetime.now(timezone.utc)
|
|
22
|
+
peer.latest_block = ping.latest_block
|
|
23
|
+
peer.difficulty = ping.difficulty
|
|
24
|
+
if peer.is_default_seed and ping.latest_block:
|
|
25
|
+
if getattr(node, "latest_block_hash", None) != ping.latest_block:
|
|
26
|
+
node.latest_block_hash = ping.latest_block
|
|
27
|
+
node.latest_block = None
|
|
28
|
+
try:
|
|
29
|
+
from astreum.validation.models.block import Block
|
|
30
|
+
|
|
31
|
+
node.latest_block = Block.from_atom(node, ping.latest_block)
|
|
32
|
+
node.logger.info(
|
|
33
|
+
"Loaded latest block %s after seed update",
|
|
34
|
+
ping.latest_block.hex(),
|
|
35
|
+
)
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
node.logger.warning(
|
|
38
|
+
"Failed loading latest block %s after seed update: %s",
|
|
39
|
+
ping.latest_block.hex(),
|
|
40
|
+
exc,
|
|
41
|
+
)
|
|
42
|
+
node.logger.info(
|
|
43
|
+
"Updated latest block hash from default seed %s",
|
|
44
|
+
peer.address[0] if peer.address else "unknown",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
validation_route = node.validation_route
|
|
48
|
+
if validation_route is None:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
if ping.is_validator:
|
|
53
|
+
validation_route.add_peer(peer.public_key_bytes)
|
|
54
|
+
else:
|
|
55
|
+
validation_route.remove_peer(peer.public_key_bytes)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|