astreum 0.3.21__tar.gz → 0.3.34__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.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

Files changed (86) hide show
  1. {astreum-0.3.21/src/astreum.egg-info → astreum-0.3.34}/PKG-INFO +8 -4
  2. {astreum-0.3.21 → astreum-0.3.34}/README.md +12 -8
  3. {astreum-0.3.21 → astreum-0.3.34}/pyproject.toml +1 -1
  4. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/__init__.py +1 -2
  5. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/__init__.py +13 -11
  6. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/handlers/handshake.py +64 -34
  7. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/handlers/object_request.py +185 -176
  8. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/handlers/ping.py +8 -0
  9. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/models/peer.py +2 -0
  10. astreum-0.3.34/src/astreum/communication/models/ping.py +45 -0
  11. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/models/route.py +4 -0
  12. astreum-0.3.34/src/astreum/communication/outgoing_queue.py +87 -0
  13. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/processors/incoming.py +114 -105
  14. astreum-0.3.34/src/astreum/communication/processors/outgoing.py +53 -0
  15. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/processors/peer.py +119 -63
  16. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/setup.py +129 -27
  17. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/node.py +100 -100
  18. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/storage/actions/get.py +41 -37
  19. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/storage/actions/set.py +179 -176
  20. astreum-0.3.34/src/astreum/storage/providers.py +24 -0
  21. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/storage/setup.py +23 -22
  22. astreum-0.3.34/src/astreum/utils/config.py +215 -0
  23. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/__init__.py +0 -2
  24. astreum-0.3.34/src/astreum/validation/constants.py +2 -0
  25. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/genesis.py +1 -4
  26. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/models/block.py +208 -48
  27. astreum-0.3.34/src/astreum/validation/models/fork.py +511 -0
  28. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/models/transaction.py +1 -1
  29. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/node.py +65 -17
  30. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/validator.py +1 -1
  31. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/workers/validation.py +44 -50
  32. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/verification/worker.py +145 -66
  33. {astreum-0.3.21 → astreum-0.3.34/src/astreum.egg-info}/PKG-INFO +8 -4
  34. {astreum-0.3.21 → astreum-0.3.34}/src/astreum.egg-info/SOURCES.txt +3 -1
  35. astreum-0.3.21/src/astreum/communication/models/ping.py +0 -33
  36. astreum-0.3.21/src/astreum/communication/processors/outgoing.py +0 -29
  37. astreum-0.3.21/src/astreum/utils/config.py +0 -109
  38. astreum-0.3.21/src/astreum/validation/models/chain.py +0 -66
  39. astreum-0.3.21/src/astreum/validation/models/fork.py +0 -100
  40. {astreum-0.3.21 → astreum-0.3.34}/LICENSE +0 -0
  41. {astreum-0.3.21 → astreum-0.3.34}/setup.cfg +0 -0
  42. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/disconnect.py +0 -0
  43. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/handlers/__init__.py +0 -0
  44. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/handlers/object_response.py +0 -0
  45. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/handlers/route_request.py +0 -0
  46. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/handlers/route_response.py +0 -0
  47. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/models/__init__.py +0 -0
  48. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/models/message.py +0 -0
  49. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/node.py +0 -0
  50. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/processors/__init__.py +0 -0
  51. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/communication/util.py +0 -0
  52. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/crypto/__init__.py +0 -0
  53. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/crypto/chacha20poly1305.py +0 -0
  54. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/crypto/ed25519.py +0 -0
  55. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/crypto/quadratic_form.py +0 -0
  56. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/crypto/wesolowski.py +0 -0
  57. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/crypto/x25519.py +0 -0
  58. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/__init__.py +0 -0
  59. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/evaluations/__init__.py +0 -0
  60. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/evaluations/high_evaluation.py +0 -0
  61. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/evaluations/low_evaluation.py +0 -0
  62. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/evaluations/script_evaluation.py +0 -0
  63. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/models/__init__.py +0 -0
  64. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/models/environment.py +0 -0
  65. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/models/expression.py +0 -0
  66. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/models/meter.py +0 -0
  67. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/parser.py +0 -0
  68. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/machine/tokenizer.py +0 -0
  69. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/storage/__init__.py +0 -0
  70. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/storage/models/atom.py +0 -0
  71. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/storage/models/trie.py +0 -0
  72. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/storage/requests.py +0 -0
  73. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/utils/bytes.py +0 -0
  74. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/utils/integer.py +0 -0
  75. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/utils/logging.py +0 -0
  76. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/models/__init__.py +0 -0
  77. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/models/account.py +0 -0
  78. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/models/accounts.py +0 -0
  79. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/models/receipt.py +0 -0
  80. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/validation/workers/__init__.py +0 -0
  81. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/verification/__init__.py +0 -0
  82. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/verification/discover.py +0 -0
  83. {astreum-0.3.21 → astreum-0.3.34}/src/astreum/verification/node.py +0 -0
  84. {astreum-0.3.21 → astreum-0.3.34}/src/astreum.egg-info/dependency_links.txt +0 -0
  85. {astreum-0.3.21 → astreum-0.3.34}/src/astreum.egg-info/requires.txt +0 -0
  86. {astreum-0.3.21 → astreum-0.3.34}/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.21
3
+ Version: 0.3.34
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
@@ -45,9 +45,13 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
45
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. |
46
46
  | `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
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
- | `bootstrap` | list\[str\] | `[]` | Addresses to ping with a handshake before joining; each must look like `host:port` or `[ipv6]:port`. |
48
+ | `default_seed` | string | `"bootstrap.astreum.org:52780"` | Default address to ping before joining; set to `None` to disable the built-in default. |
49
+ | `additional_seeds` | list\[str\] | `[]` | Extra addresses appended to the bootstrap list; each must look like `host:port` or `[ipv6]:port`. |
49
50
  | `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
50
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. |
53
+ | `storage_index_interval` | int | `600` | How often (seconds) to re-advertise cold storage atoms to the closest known peer. |
54
+ | `outgoing_queue_size_limit` | int | `67108864` | Soft cap (bytes) for `enqueue_outgoing`-tracked outgoing queue usage; set to `0` to disable. |
51
55
 
52
56
  > **Note**
53
57
  > The peer‑to‑peer *route* used for object discovery is always enabled.
@@ -66,8 +70,8 @@ config = {
66
70
  "cold_storage_path": "./data/node1",
67
71
  "incoming_port": 52780,
68
72
  "use_ipv6": False,
69
- "bootstrap": [
70
- "bootstrap.astreum.org:52780",
73
+ "default_seed": None,
74
+ "additional_seeds": [
71
75
  "127.0.0.1:7374"
72
76
  ]
73
77
  }
@@ -15,8 +15,8 @@ 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
- | `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). |
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). |
20
20
  | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
21
21
 
22
22
  ### Communication
@@ -25,11 +25,15 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
25
25
  | ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
26
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
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
- | `bootstrap` | list\[str\] | `[]` | Addresses to ping with a handshake before joining; each must look like `host:port` or `[ipv6]:port`. |
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_seed` | string | `"bootstrap.astreum.org:52780"` | Default address to ping before joining; set to `None` to disable the built-in default. |
31
+ | `additional_seeds` | list\[str\] | `[]` | Extra addresses appended to the bootstrap list; each must look like `host:port` or `[ipv6]:port`. |
31
32
  | `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
32
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. |
35
+ | `storage_index_interval` | int | `600` | How often (seconds) to re-advertise cold storage atoms to the closest known peer. |
36
+ | `outgoing_queue_size_limit` | int | `67108864` | Soft cap (bytes) for `enqueue_outgoing`-tracked outgoing queue usage; set to `0` to disable. |
33
37
 
34
38
  > **Note**
35
39
  > The peer‑to‑peer *route* used for object discovery is always enabled.
@@ -48,8 +52,8 @@ config = {
48
52
  "cold_storage_path": "./data/node1",
49
53
  "incoming_port": 52780,
50
54
  "use_ipv6": False,
51
- "bootstrap": [
52
- "bootstrap.astreum.org:52780",
55
+ "default_seed": None,
56
+ "additional_seeds": [
53
57
  "127.0.0.1:7374"
54
58
  ]
55
59
  }
@@ -130,7 +134,7 @@ except ParseError as e:
130
134
  Every `Node` instance wires up structured logging automatically:
131
135
 
132
136
  - 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.
133
- - 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"]`.
137
+ - 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"]`.
134
138
  - Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
135
139
  - Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
136
140
  - The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.3.21"
3
+ version = "0.3.34"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -1,5 +1,5 @@
1
1
 
2
- from astreum.validation import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
2
+ from astreum.validation import Account, Accounts, Block, Fork, Receipt, Transaction
3
3
  from astreum.machine import Env, Expr, parse, tokenize
4
4
  from astreum.node import Node
5
5
 
@@ -9,7 +9,6 @@ __all__: list[str] = [
9
9
  "Env",
10
10
  "Expr",
11
11
  "Block",
12
- "Chain",
13
12
  "Fork",
14
13
  "Receipt",
15
14
  "Transaction",
@@ -1,11 +1,13 @@
1
- from .models.message import Message
2
- from .models.peer import Peer
3
- from .models.route import Route
4
- from .setup import communication_setup
5
-
6
- __all__ = [
7
- "Message",
8
- "Peer",
9
- "Route",
10
- "communication_setup",
11
- ]
1
+ from .models.message import Message
2
+ from .models.peer import Peer
3
+ from .models.route import Route
4
+ from .outgoing_queue import enqueue_outgoing
5
+ from .setup import communication_setup
6
+
7
+ __all__ = [
8
+ "Message",
9
+ "Peer",
10
+ "Route",
11
+ "enqueue_outgoing",
12
+ "communication_setup",
13
+ ]
@@ -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)
@@ -24,39 +48,45 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
24
48
  node.logger.warning("Error extracting sender key bytes: %s", exc)
25
49
  return True
26
50
 
27
- try:
28
- host = addr[0]
29
- port = int.from_bytes(message.content[:2], "big", signed=False)
30
- except Exception:
31
- return True
32
- peer_address = (host, port)
33
-
34
- existing_peer = node.get_peer(sender_public_key_bytes)
35
- if existing_peer is not None:
36
- existing_peer.address = peer_address
37
- return False
51
+ try:
52
+ host = addr[0]
53
+ port = int.from_bytes(message.content[:2], "big", signed=False)
54
+ except Exception:
55
+ return True
56
+ peer_address = (host, port)
57
+ default_seed_ips = getattr(node, "default_seed_ips", None)
58
+ is_default_seed = bool(default_seed_ips) and host in default_seed_ips
59
+
60
+ existing_peer = node.get_peer(sender_public_key_bytes)
61
+ if existing_peer is not None:
62
+ existing_peer.address = peer_address
63
+ existing_peer.is_default_seed = is_default_seed
64
+ _queue_handshake_ping(existing_peer, peer_address)
65
+ return False
38
66
 
39
67
  try:
40
- peer = Peer(
41
- node_secret_key=node.relay_secret_key,
42
- peer_public_key=sender_key,
43
- address=peer_address,
44
- )
68
+ peer = Peer(
69
+ node_secret_key=node.relay_secret_key,
70
+ peer_public_key=sender_key,
71
+ address=peer_address,
72
+ is_default_seed=is_default_seed,
73
+ )
45
74
  except Exception:
46
75
  return True
47
76
 
48
77
  node.add_peer(sender_public_key_bytes, peer)
49
78
  node.peer_route.add_peer(sender_public_key_bytes, peer)
50
79
 
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),
80
+ node.logger.info(
81
+ "Handshake accepted from %s:%s; peer added",
82
+ peer_address[0],
83
+ peer_address[1],
60
84
  )
61
- node.outgoing_queue.put((response.to_bytes(), peer_address))
62
- return True
85
+ response = Message(
86
+ handshake=True,
87
+ sender=node.relay_public_key,
88
+ content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
89
+ )
90
+ node.outgoing_queue.put((response.to_bytes(), peer_address))
91
+ _queue_handshake_ping(peer, peer_address)
92
+ return True
@@ -1,176 +1,185 @@
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
- from ..util import xor_distance
9
-
10
- if TYPE_CHECKING:
11
- from .. import Node
12
- from ..models.peer import Peer
13
-
14
-
15
- class ObjectRequestType(IntEnum):
16
- OBJECT_GET = 0
17
- OBJECT_PUT = 1
18
-
19
-
20
- class ObjectRequest:
21
- type: ObjectRequestType
22
- data: bytes
23
- atom_id: bytes
24
-
25
- def __init__(self, type: ObjectRequestType, data: bytes, atom_id: bytes = None):
26
- self.type = type
27
- self.data = data
28
- self.atom_id = atom_id
29
-
30
- def to_bytes(self):
31
- return bytes([self.type.value]) + self.atom_id + self.data
32
-
33
- @classmethod
34
- def from_bytes(cls, data: bytes) -> "ObjectRequest":
35
- # need at least 1 byte for type + 32 bytes for hash
36
- if len(data) < 1 + 32:
37
- raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
38
-
39
- type_val = data[0]
40
- try:
41
- req_type = ObjectRequestType(type_val)
42
- except ValueError:
43
- raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
44
-
45
- atom_id_bytes = data[1:33]
46
- payload = data[33:]
47
- return cls(req_type, payload, atom_id_bytes)
48
-
49
-
50
- def encode_peer_contact_bytes(peer: "Peer") -> bytes:
51
- """Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
52
- host, port = peer.address
53
- key_bytes = peer.public_key_bytes
54
- try:
55
- ip_bytes = socket.inet_aton(host)
56
- except OSError as exc: # pragma: no cover - inet_aton raises for invalid hosts
57
- raise ValueError(f"invalid IPv4 address: {host}") from exc
58
- if not (0 <= port <= 0xFFFF):
59
- raise ValueError(f"port out of range (0-65535): {port}")
60
- port_bytes = int(port).to_bytes(2, "big", signed=False)
61
- return key_bytes + ip_bytes + port_bytes
62
-
63
-
64
- def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
65
- if message.content is None:
66
- node.logger.warning("OBJECT_REQUEST from %s missing content", peer.address)
67
- return
68
-
69
- try:
70
- object_request = ObjectRequest.from_bytes(message.content)
71
- except Exception as exc:
72
- node.logger.warning("Error decoding OBJECT_REQUEST from %s: %s", peer.address, exc)
73
- return
74
-
75
- match object_request.type:
76
- case ObjectRequestType.OBJECT_GET:
77
- atom_id = object_request.atom_id
78
- node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
79
-
80
- local_atom = node.local_get(atom_id)
81
- if local_atom is not None:
82
- node.logger.debug("Object %s found locally; returning to %s", atom_id.hex(), peer.address)
83
- resp = ObjectResponse(
84
- type=ObjectResponseType.OBJECT_FOUND,
85
- data=local_atom.to_bytes(),
86
- atom_id=atom_id
87
- )
88
- obj_res_msg = Message(
89
- topic=MessageTopic.OBJECT_RESPONSE,
90
- body=resp.to_bytes(),
91
- sender=node.relay_public_key,
92
- )
93
- obj_res_msg.encrypt(peer.shared_key_bytes)
94
- node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
95
- return
96
-
97
- if atom_id in node.storage_index:
98
- node.logger.debug("Known provider for %s; informing %s", atom_id.hex(), peer.address)
99
- provider_bytes = node.storage_index[atom_id]
100
- resp = ObjectResponse(
101
- type=ObjectResponseType.OBJECT_PROVIDER,
102
- data=provider_bytes,
103
- atom_id=atom_id
104
- )
105
- obj_res_msg = Message(
106
- topic=MessageTopic.OBJECT_RESPONSE,
107
- body=resp.to_bytes(),
108
- sender=node.relay_public_key,
109
- )
110
- obj_res_msg.encrypt(peer.shared_key_bytes)
111
- node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
112
- return
113
-
114
- nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
115
- if nearest_peer:
116
- node.logger.debug("Forwarding requester %s to nearest peer for %s", peer.address, atom_id.hex())
117
- peer_info = encode_peer_contact_bytes(nearest_peer)
118
- resp = ObjectResponse(
119
- type=ObjectResponseType.OBJECT_PROVIDER,
120
- # type=ObjectResponseType.OBJECT_NEAREST_PEER,
121
- data=peer_info,
122
- atom_id=atom_id
123
- )
124
- obj_res_msg = Message(
125
- topic=MessageTopic.OBJECT_RESPONSE,
126
- body=resp.to_bytes(),
127
- sender=node.relay_public_key,
128
- )
129
- obj_res_msg.encrypt(nearest_peer.shared_key_bytes)
130
- node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
131
-
132
- case ObjectRequestType.OBJECT_PUT:
133
- node.logger.debug("Handling OBJECT_PUT for %s from %s", object_request.atom_id.hex(), peer.address)
134
-
135
- nearest_peer = node.peer_route.closest_peer_for_hash(object_request.atom_id)
136
- is_self_closest = False
137
- if nearest_peer is None or nearest_peer.address is None:
138
- is_self_closest = True
139
- else:
140
- try:
141
- self_distance = xor_distance(object_request.atom_id, node.relay_public_key_bytes)
142
- peer_distance = xor_distance(object_request.atom_id, nearest_peer.public_key_bytes)
143
- except Exception as exc:
144
- node.logger.warning(
145
- "Failed distance comparison for OBJECT_PUT %s: %s",
146
- object_request.atom_id.hex(),
147
- exc,
148
- )
149
- is_self_closest = True
150
- else:
151
- is_self_closest = self_distance <= peer_distance
152
-
153
- if is_self_closest:
154
- node.logger.debug("Storing provider info for %s locally", object_request.atom_id.hex())
155
- node.storage_index[object_request.atom_id] = object_request.data
156
- else:
157
- node.logger.debug(
158
- "Forwarding OBJECT_PUT for %s to nearer peer %s",
159
- object_request.atom_id.hex(),
160
- nearest_peer.address,
161
- )
162
- fwd_req = ObjectRequest(
163
- type=ObjectRequestType.OBJECT_PUT,
164
- data=object_request.data,
165
- atom_id=object_request.atom_id,
166
- )
167
- obj_req_msg = Message(
168
- topic=MessageTopic.OBJECT_REQUEST,
169
- body=fwd_req.to_bytes(),
170
- sender=node.relay_public_key,
171
- )
172
- obj_req_msg.encrypt(nearest_peer.shared_key_bytes)
173
- node.outgoing_queue.put((obj_req_msg.to_bytes(), nearest_peer.address))
174
-
175
- case _:
176
- node.logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, peer.address)
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
+ from ..util import xor_distance
9
+ from ...storage.providers import provider_id_for_payload, provider_payload_for_id
10
+
11
+ if TYPE_CHECKING:
12
+ from .. import Node
13
+ from ..models.peer import Peer
14
+
15
+
16
+ class ObjectRequestType(IntEnum):
17
+ OBJECT_GET = 0
18
+ OBJECT_PUT = 1
19
+
20
+
21
+ class ObjectRequest:
22
+ type: ObjectRequestType
23
+ data: bytes
24
+ atom_id: bytes
25
+
26
+ def __init__(self, type: ObjectRequestType, data: bytes, atom_id: bytes = None):
27
+ self.type = type
28
+ self.data = data
29
+ self.atom_id = atom_id
30
+
31
+ def to_bytes(self):
32
+ return bytes([self.type.value]) + self.atom_id + self.data
33
+
34
+ @classmethod
35
+ def from_bytes(cls, data: bytes) -> "ObjectRequest":
36
+ # need at least 1 byte for type + 32 bytes for hash
37
+ if len(data) < 1 + 32:
38
+ raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
39
+
40
+ type_val = data[0]
41
+ try:
42
+ req_type = ObjectRequestType(type_val)
43
+ except ValueError:
44
+ raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
45
+
46
+ atom_id_bytes = data[1:33]
47
+ payload = data[33:]
48
+ return cls(req_type, payload, atom_id_bytes)
49
+
50
+
51
+ def encode_peer_contact_bytes(peer: "Peer") -> bytes:
52
+ """Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
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", peer: "Peer", message: Message) -> None:
66
+ if message.content is None:
67
+ node.logger.warning("OBJECT_REQUEST from %s missing content", peer.address)
68
+ return
69
+
70
+ try:
71
+ object_request = ObjectRequest.from_bytes(message.content)
72
+ except Exception as exc:
73
+ node.logger.warning("Error decoding OBJECT_REQUEST from %s: %s", peer.address, exc)
74
+ return
75
+
76
+ match object_request.type:
77
+ case ObjectRequestType.OBJECT_GET:
78
+ atom_id = object_request.atom_id
79
+ node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
80
+
81
+ local_atom = node.local_get(atom_id)
82
+ if local_atom is not None:
83
+ node.logger.debug("Object %s found locally; returning to %s", atom_id.hex(), peer.address)
84
+ resp = ObjectResponse(
85
+ type=ObjectResponseType.OBJECT_FOUND,
86
+ data=local_atom.to_bytes(),
87
+ atom_id=atom_id
88
+ )
89
+ obj_res_msg = Message(
90
+ topic=MessageTopic.OBJECT_RESPONSE,
91
+ body=resp.to_bytes(),
92
+ sender=node.relay_public_key,
93
+ )
94
+ obj_res_msg.encrypt(peer.shared_key_bytes)
95
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
96
+ return
97
+
98
+ if atom_id in node.storage_index:
99
+ provider_id = node.storage_index[atom_id]
100
+ provider_bytes = provider_payload_for_id(node, provider_id)
101
+ if provider_bytes is not None:
102
+ node.logger.debug("Known provider for %s; informing %s", atom_id.hex(), peer.address)
103
+ resp = ObjectResponse(
104
+ type=ObjectResponseType.OBJECT_PROVIDER,
105
+ data=provider_bytes,
106
+ atom_id=atom_id
107
+ )
108
+ obj_res_msg = Message(
109
+ topic=MessageTopic.OBJECT_RESPONSE,
110
+ body=resp.to_bytes(),
111
+ sender=node.relay_public_key,
112
+ )
113
+ obj_res_msg.encrypt(peer.shared_key_bytes)
114
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
115
+ return
116
+ node.logger.warning(
117
+ "Unknown provider id %s for %s",
118
+ provider_id,
119
+ atom_id.hex(),
120
+ )
121
+
122
+ nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
123
+ if nearest_peer:
124
+ node.logger.debug("Forwarding requester %s to nearest peer for %s", peer.address, atom_id.hex())
125
+ peer_info = encode_peer_contact_bytes(nearest_peer)
126
+ resp = ObjectResponse(
127
+ type=ObjectResponseType.OBJECT_PROVIDER,
128
+ # type=ObjectResponseType.OBJECT_NEAREST_PEER,
129
+ data=peer_info,
130
+ atom_id=atom_id
131
+ )
132
+ obj_res_msg = Message(
133
+ topic=MessageTopic.OBJECT_RESPONSE,
134
+ body=resp.to_bytes(),
135
+ sender=node.relay_public_key,
136
+ )
137
+ obj_res_msg.encrypt(nearest_peer.shared_key_bytes)
138
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
139
+
140
+ case ObjectRequestType.OBJECT_PUT:
141
+ node.logger.debug("Handling OBJECT_PUT for %s from %s", object_request.atom_id.hex(), peer.address)
142
+
143
+ nearest_peer = node.peer_route.closest_peer_for_hash(object_request.atom_id)
144
+ is_self_closest = False
145
+ if nearest_peer is None or nearest_peer.address is None:
146
+ is_self_closest = True
147
+ else:
148
+ try:
149
+ self_distance = xor_distance(object_request.atom_id, node.relay_public_key_bytes)
150
+ peer_distance = xor_distance(object_request.atom_id, nearest_peer.public_key_bytes)
151
+ except Exception as exc:
152
+ node.logger.warning(
153
+ "Failed distance comparison for OBJECT_PUT %s: %s",
154
+ object_request.atom_id.hex(),
155
+ exc,
156
+ )
157
+ is_self_closest = True
158
+ else:
159
+ is_self_closest = self_distance <= peer_distance
160
+
161
+ if is_self_closest:
162
+ node.logger.debug("Storing provider info for %s locally", object_request.atom_id.hex())
163
+ provider_id = provider_id_for_payload(node, object_request.data)
164
+ node.storage_index[object_request.atom_id] = provider_id
165
+ else:
166
+ node.logger.debug(
167
+ "Forwarding OBJECT_PUT for %s to nearer peer %s",
168
+ object_request.atom_id.hex(),
169
+ nearest_peer.address,
170
+ )
171
+ fwd_req = ObjectRequest(
172
+ type=ObjectRequestType.OBJECT_PUT,
173
+ data=object_request.data,
174
+ atom_id=object_request.atom_id,
175
+ )
176
+ obj_req_msg = Message(
177
+ topic=MessageTopic.OBJECT_REQUEST,
178
+ body=fwd_req.to_bytes(),
179
+ sender=node.relay_public_key,
180
+ )
181
+ obj_req_msg.encrypt(nearest_peer.shared_key_bytes)
182
+ node.outgoing_queue.put((obj_req_msg.to_bytes(), nearest_peer.address))
183
+
184
+ case _:
185
+ node.logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, peer.address)