astreum 0.3.25__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.
Files changed (96) hide show
  1. {astreum-0.3.25/src/astreum.egg-info → astreum-0.3.50}/PKG-INFO +14 -4
  2. {astreum-0.3.25 → astreum-0.3.50}/README.md +34 -24
  3. {astreum-0.3.25 → astreum-0.3.50}/pyproject.toml +1 -1
  4. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/__init__.py +1 -2
  5. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/__init__.py +15 -11
  6. astreum-0.3.50/src/astreum/communication/difficulty.py +39 -0
  7. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/handlers/handshake.py +59 -42
  8. astreum-0.3.50/src/astreum/communication/handlers/object_request.py +273 -0
  9. astreum-0.3.50/src/astreum/communication/handlers/object_response.py +223 -0
  10. astreum-0.3.50/src/astreum/communication/handlers/ping.py +57 -0
  11. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/handlers/route_request.py +7 -1
  12. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/handlers/route_response.py +7 -1
  13. astreum-0.3.50/src/astreum/communication/incoming_queue.py +96 -0
  14. astreum-0.3.50/src/astreum/communication/message_pow.py +36 -0
  15. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/models/peer.py +4 -0
  16. astreum-0.3.50/src/astreum/communication/models/ping.py +54 -0
  17. astreum-0.3.50/src/astreum/communication/outgoing_queue.py +108 -0
  18. astreum-0.3.50/src/astreum/communication/processors/incoming.py +171 -0
  19. astreum-0.3.50/src/astreum/communication/processors/outgoing.py +53 -0
  20. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/processors/peer.py +122 -105
  21. astreum-0.3.50/src/astreum/communication/setup.py +325 -0
  22. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/util.py +14 -0
  23. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/evaluations/low_evaluation.py +5 -5
  24. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/models/expression.py +5 -5
  25. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/node.py +91 -83
  26. astreum-0.3.50/src/astreum/storage/actions/get.py +285 -0
  27. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/storage/actions/set.py +185 -139
  28. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/storage/models/atom.py +0 -14
  29. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/storage/models/trie.py +2 -2
  30. astreum-0.3.50/src/astreum/storage/providers.py +24 -0
  31. astreum-0.3.50/src/astreum/storage/requests.py +31 -0
  32. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/storage/setup.py +18 -10
  33. astreum-0.3.50/src/astreum/utils/config.py +276 -0
  34. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/__init__.py +0 -2
  35. astreum-0.3.50/src/astreum/validation/constants.py +2 -0
  36. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/genesis.py +1 -4
  37. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/models/account.py +1 -1
  38. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/models/block.py +210 -50
  39. astreum-0.3.50/src/astreum/validation/models/fork.py +511 -0
  40. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/models/receipt.py +1 -1
  41. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/models/transaction.py +8 -8
  42. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/node.py +80 -17
  43. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/validator.py +1 -1
  44. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/workers/validation.py +344 -253
  45. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/verification/worker.py +145 -66
  46. {astreum-0.3.25 → astreum-0.3.50/src/astreum.egg-info}/PKG-INFO +14 -4
  47. {astreum-0.3.25 → astreum-0.3.50}/src/astreum.egg-info/SOURCES.txt +6 -1
  48. astreum-0.3.25/src/astreum/communication/handlers/object_request.py +0 -176
  49. astreum-0.3.25/src/astreum/communication/handlers/object_response.py +0 -115
  50. astreum-0.3.25/src/astreum/communication/handlers/ping.py +0 -34
  51. astreum-0.3.25/src/astreum/communication/models/ping.py +0 -45
  52. astreum-0.3.25/src/astreum/communication/processors/incoming.py +0 -117
  53. astreum-0.3.25/src/astreum/communication/processors/outgoing.py +0 -29
  54. astreum-0.3.25/src/astreum/communication/setup.py +0 -221
  55. astreum-0.3.25/src/astreum/storage/actions/get.py +0 -183
  56. astreum-0.3.25/src/astreum/storage/requests.py +0 -28
  57. astreum-0.3.25/src/astreum/utils/config.py +0 -140
  58. astreum-0.3.25/src/astreum/validation/models/chain.py +0 -66
  59. astreum-0.3.25/src/astreum/validation/models/fork.py +0 -100
  60. {astreum-0.3.25 → astreum-0.3.50}/LICENSE +0 -0
  61. {astreum-0.3.25 → astreum-0.3.50}/setup.cfg +0 -0
  62. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/disconnect.py +0 -0
  63. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/handlers/__init__.py +0 -0
  64. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/models/__init__.py +0 -0
  65. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/models/message.py +0 -0
  66. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/models/route.py +0 -0
  67. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/node.py +0 -0
  68. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/communication/processors/__init__.py +0 -0
  69. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/crypto/__init__.py +0 -0
  70. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/crypto/chacha20poly1305.py +0 -0
  71. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/crypto/ed25519.py +0 -0
  72. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/crypto/quadratic_form.py +0 -0
  73. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/crypto/wesolowski.py +0 -0
  74. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/crypto/x25519.py +0 -0
  75. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/__init__.py +0 -0
  76. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/evaluations/__init__.py +0 -0
  77. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/evaluations/high_evaluation.py +0 -0
  78. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/evaluations/script_evaluation.py +0 -0
  79. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/models/__init__.py +0 -0
  80. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/models/environment.py +0 -0
  81. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/models/meter.py +0 -0
  82. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/parser.py +0 -0
  83. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/machine/tokenizer.py +0 -0
  84. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/storage/__init__.py +0 -0
  85. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/utils/bytes.py +0 -0
  86. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/utils/integer.py +0 -0
  87. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/utils/logging.py +0 -0
  88. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/models/__init__.py +0 -0
  89. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/models/accounts.py +0 -0
  90. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/validation/workers/__init__.py +0 -0
  91. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/verification/__init__.py +0 -0
  92. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/verification/discover.py +0 -0
  93. {astreum-0.3.25 → astreum-0.3.50}/src/astreum/verification/node.py +0 -0
  94. {astreum-0.3.25 → astreum-0.3.50}/src/astreum.egg-info/dependency_links.txt +0 -0
  95. {astreum-0.3.25 → astreum-0.3.50}/src/astreum.egg-info/requires.txt +0 -0
  96. {astreum-0.3.25 → 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.25
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. |
@@ -45,11 +47,18 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
45
47
  | `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
48
  | `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
47
49
  | `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
+ | `default_seed` | string | `"bootstrap.astreum.org:52780"` | Default address to ping before joining; set to `None` to disable the built-in default. |
51
+ | `additional_seeds` | list\[str\] | `[]` | Extra addresses appended to the bootstrap list; each must look like `host:port` or `[ipv6]:port`. |
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. |
55
+ | `storage_index_interval` | int | `600` | How often (seconds) to re-advertise entries in `node.atom_advertisments` to the closest known peer. |
56
+ | `incoming_queue_size_limit` | int | `67108864` | Soft cap (bytes) for inbound queue usage tracked by `enqueue_incoming`; set to `0` to disable. |
57
+ | `incoming_queue_timeout` | float | `1.0` | When > 0, `enqueue_incoming` waits up to this many seconds for space before dropping the payload. |
58
+ | `outgoing_queue_size_limit` | int | `67108864` | Soft cap (bytes) for `enqueue_outgoing`-tracked outgoing queue usage; set to `0` to disable. |
59
+ | `outgoing_queue_timeout` | float | `1.0` | When > 0, `enqueue_outgoing` waits up to this many seconds for space before dropping the payload. |
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.
53
62
 
54
63
  > **Note**
55
64
  > The peer‑to‑peer *route* used for object discovery is always enabled.
@@ -68,7 +77,7 @@ config = {
68
77
  "cold_storage_path": "./data/node1",
69
78
  "incoming_port": 52780,
70
79
  "use_ipv6": False,
71
- "default_seeds": [],
80
+ "default_seed": None,
72
81
  "additional_seeds": [
73
82
  "127.0.0.1:7374"
74
83
  ]
@@ -175,6 +184,7 @@ python3 -m unittest tests.node.test_current_validator
175
184
  python3 -m unittest tests.node.test_node_connection
176
185
  python3 -m unittest tests.node.test_node_init
177
186
  python3 -m unittest tests.node.test_node_validation
187
+ python3 -m unittest tests.node.config.default_seed
178
188
  python3 -m unittest tests.node.tokenize
179
189
  python3 -m unittest tests.node.parse
180
190
  python3 -m unittest tests.node.function
@@ -12,28 +12,37 @@ 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. |
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. |
18
20
  | `logging_retention_days` | int | `90` | Number of days to keep rotated log files (daily gzip). |
19
21
  | `chain_id` | int | `0` | Chain identifier used for validation (0 = test, 1 = main). |
20
- | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
22
+ | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
21
23
 
22
24
  ### Communication
23
25
 
24
26
  | Parameter | Type | Default | Description |
25
27
  | ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
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. |
35
-
36
- > **Note**
28
+ | `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. |
29
+ | `validation_secret_key` | hex string | `None` | Optional Ed25519 key that lets the node join the validation route; leave blank to opt out of validation. |
30
+ | `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
31
+ | `incoming_port` | int | `52780` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
32
+ | `default_seed` | string | `"bootstrap.astreum.org:52780"` | Default address to ping before joining; set to `None` to disable the built-in default. |
33
+ | `additional_seeds` | list\[str\] | `[]` | Extra addresses appended to the bootstrap list; each must look like `host:port` or `[ipv6]:port`. |
34
+ | `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
35
+ | `peer_timeout_interval` | int | `10` | How often (seconds) the peer manager checks for stale peers. |
36
+ | `bootstrap_retry_interval` | int | `30` | How often (seconds) to retry bootstrapping when the peer list is empty. |
37
+ | `storage_index_interval` | int | `600` | How often (seconds) to re-advertise entries in `node.atom_advertisments` to the closest known peer. |
38
+ | `incoming_queue_size_limit` | int | `67108864` | Soft cap (bytes) for inbound queue usage tracked by `enqueue_incoming`; set to `0` to disable. |
39
+ | `incoming_queue_timeout` | float | `1.0` | When > 0, `enqueue_incoming` waits up to this many seconds for space before dropping the payload. |
40
+ | `outgoing_queue_size_limit` | int | `67108864` | Soft cap (bytes) for `enqueue_outgoing`-tracked outgoing queue usage; set to `0` to disable. |
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**
37
46
  > The peer‑to‑peer *route* used for object discovery is always enabled.
38
47
  > If `validation_secret_key` is provided the node automatically joins the validation route too.
39
48
 
@@ -47,14 +56,14 @@ config = {
47
56
  "validation_secret_key": "12…34", # optional – validator
48
57
  "hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
49
58
  "cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
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
- }
59
+ "cold_storage_path": "./data/node1",
60
+ "incoming_port": 52780,
61
+ "use_ipv6": False,
62
+ "default_seed": None,
63
+ "additional_seeds": [
64
+ "127.0.0.1:7374"
65
+ ]
66
+ }
58
67
 
59
68
  node = Node(config)
60
69
  # … your code …
@@ -132,7 +141,7 @@ except ParseError as e:
132
141
  Every `Node` instance wires up structured logging automatically:
133
142
 
134
143
  - 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.
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"]`.
144
+ - 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"]`.
136
145
  - Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
137
146
  - Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
138
147
  - The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
@@ -157,6 +166,7 @@ python3 -m unittest tests.node.test_current_validator
157
166
  python3 -m unittest tests.node.test_node_connection
158
167
  python3 -m unittest tests.node.test_node_init
159
168
  python3 -m unittest tests.node.test_node_validation
169
+ python3 -m unittest tests.node.config.default_seed
160
170
  python3 -m unittest tests.node.tokenize
161
171
  python3 -m unittest tests.node.parse
162
172
  python3 -m unittest tests.node.function
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.3.25"
3
+ version = "0.3.50"
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,15 @@
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 .incoming_queue import enqueue_incoming
5
+ from .outgoing_queue import enqueue_outgoing
6
+ from .setup import communication_setup
7
+
8
+ __all__ = [
9
+ "Message",
10
+ "Peer",
11
+ "Route",
12
+ "enqueue_incoming",
13
+ "enqueue_outgoing",
14
+ "communication_setup",
15
+ ]
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .. import Node
7
+
8
+
9
+ def message_difficulty(node: "Node") -> int:
10
+ """Compute current message difficulty based on incoming queue pressure."""
11
+ size = node.incoming_queue_size
12
+ limit = node.incoming_queue_size_limit
13
+
14
+ if limit <= 0:
15
+ return 1
16
+
17
+ pressure = size / limit
18
+ if pressure < 0.70:
19
+ value = 1
20
+ elif pressure < 0.75:
21
+ value = 3
22
+ elif pressure < 0.80:
23
+ value = 5
24
+ elif pressure < 0.85:
25
+ value = 8
26
+ elif pressure < 0.90:
27
+ value = 12
28
+ elif pressure < 0.93:
29
+ value = 16
30
+ elif pressure < 0.95:
31
+ value = 19
32
+ elif pressure < 0.97:
33
+ value = 22
34
+ elif pressure < 0.98:
35
+ value = 24
36
+ else:
37
+ value = 26
38
+
39
+ return max(1, min(255, value))
@@ -1,18 +1,20 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Sequence
4
-
5
- from cryptography.hazmat.primitives import serialization
6
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
7
-
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from cryptography.hazmat.primitives import serialization
6
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
7
+
8
+ from ..outgoing_queue import enqueue_outgoing
8
9
  from ..models.peer import Peer
9
10
  from ..models.message import Message, MessageTopic
10
11
  from ..models.ping import Ping
11
-
12
- if TYPE_CHECKING:
13
- from .... import Node
14
-
15
-
12
+ from ..difficulty import message_difficulty
13
+
14
+ if TYPE_CHECKING:
15
+ from .... import Node
16
+
17
+
16
18
  def handle_handshake(node: "Node", addr: Sequence[object], message: Message) -> bool:
17
19
  """Handle incoming handshake messages.
18
20
 
@@ -25,6 +27,7 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
25
27
  try:
26
28
  ping_payload = Ping(
27
29
  is_validator=bool(getattr(node, "validation_public_key", None)),
30
+ difficulty=message_difficulty(node),
28
31
  latest_block=latest_block,
29
32
  ).to_bytes()
30
33
  ping_msg = Message(
@@ -33,7 +36,12 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
33
36
  sender=node.relay_public_key,
34
37
  )
35
38
  ping_msg.encrypt(peer.shared_key_bytes)
36
- node.outgoing_queue.put((ping_msg.to_bytes(), peer_address))
39
+ enqueue_outgoing(
40
+ node,
41
+ peer_address,
42
+ message=ping_msg,
43
+ difficulty=peer.difficulty,
44
+ )
37
45
  except Exception as exc:
38
46
  node.logger.debug(
39
47
  "Failed sending handshake ping to %s:%s: %s",
@@ -41,48 +49,57 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
41
49
  peer_address[1],
42
50
  exc,
43
51
  )
44
- sender_public_key_bytes = message.sender_bytes
45
- try:
46
- sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
47
- except Exception as exc:
48
- node.logger.warning("Error extracting sender key bytes: %s", exc)
49
- return True
50
-
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
-
52
+ sender_public_key_bytes = message.sender_bytes
53
+ try:
54
+ sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
55
+ except Exception as exc:
56
+ node.logger.warning("Error extracting sender key bytes: %s", exc)
57
+ return True
58
+
59
+ try:
60
+ host = addr[0]
61
+ port = int.from_bytes(message.content[:2], "big", signed=False)
62
+ except Exception:
63
+ return True
64
+ peer_address = (host, port)
65
+ default_seed_ips = getattr(node, "default_seed_ips", None)
66
+ is_default_seed = bool(default_seed_ips) and host in default_seed_ips
67
+
58
68
  existing_peer = node.get_peer(sender_public_key_bytes)
59
69
  if existing_peer is not None:
60
70
  existing_peer.address = peer_address
71
+ existing_peer.is_default_seed = is_default_seed
61
72
  _queue_handshake_ping(existing_peer, peer_address)
62
73
  return False
63
-
64
- try:
65
- peer = Peer(
66
- node_secret_key=node.relay_secret_key,
67
- peer_public_key=sender_key,
68
- address=peer_address,
69
- )
70
- except Exception:
71
- return True
72
-
73
- node.add_peer(sender_public_key_bytes, peer)
74
- node.peer_route.add_peer(sender_public_key_bytes, peer)
75
-
74
+
75
+ try:
76
+ peer = Peer(
77
+ node_secret_key=node.relay_secret_key,
78
+ peer_public_key=sender_key,
79
+ address=peer_address,
80
+ is_default_seed=is_default_seed,
81
+ )
82
+ except Exception:
83
+ return True
84
+
85
+ node.add_peer(sender_public_key_bytes, peer)
86
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
87
+
76
88
  node.logger.info(
77
89
  "Handshake accepted from %s:%s; peer added",
78
90
  peer_address[0],
79
91
  peer_address[1],
80
- )
92
+ )
81
93
  response = Message(
82
94
  handshake=True,
83
95
  sender=node.relay_public_key,
84
96
  content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
85
97
  )
86
- node.outgoing_queue.put((response.to_bytes(), peer_address))
98
+ enqueue_outgoing(
99
+ node,
100
+ peer_address,
101
+ message=response,
102
+ difficulty=peer.difficulty,
103
+ )
87
104
  _queue_handshake_ping(peer, peer_address)
88
105
  return True
@@ -0,0 +1,273 @@
1
+ import logging
2
+ import socket
3
+ from enum import IntEnum
4
+ from typing import Optional, TYPE_CHECKING, Tuple
5
+
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
+ )
14
+ from ..outgoing_queue import enqueue_outgoing
15
+ from ..models.message import Message, MessageTopic
16
+ from ..util import xor_distance
17
+ from ...storage.providers import provider_id_for_payload, provider_payload_for_id
18
+
19
+ if TYPE_CHECKING:
20
+ from .. import Node
21
+ from ..models.peer import Peer
22
+
23
+
24
+ class ObjectRequestType(IntEnum):
25
+ OBJECT_GET = 0
26
+ OBJECT_PUT = 1
27
+
28
+
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
59
+ if len(data) < 1 + 32:
60
+ raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
61
+
62
+ type_val = data[0]
63
+ try:
64
+ req_type = ObjectRequestType(type_val)
65
+ except ValueError:
66
+ raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
67
+
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)
84
+
85
+
86
+ def encode_peer_contact_bytes(peer: "Peer") -> bytes:
87
+ """Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
88
+ host, port = peer.address
89
+ key_bytes = peer.public_key_bytes
90
+ try:
91
+ ip_bytes = socket.inet_aton(host)
92
+ except OSError as exc: # pragma: no cover - inet_aton raises for invalid hosts
93
+ raise ValueError(f"invalid IPv4 address: {host}") from exc
94
+ if not (0 <= port <= 0xFFFF):
95
+ raise ValueError(f"port out of range (0-65535): {port}")
96
+ port_bytes = int(port).to_bytes(2, "big", signed=False)
97
+ return key_bytes + ip_bytes + port_bytes
98
+
99
+
100
+ def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
101
+ if message.content is None:
102
+ node.logger.warning("OBJECT_REQUEST from %s missing content", peer.address)
103
+ return
104
+
105
+ try:
106
+ object_request = ObjectRequest.from_bytes(message.content)
107
+ except Exception as exc:
108
+ node.logger.warning("Error decoding OBJECT_REQUEST from %s: %s", peer.address, exc)
109
+ return
110
+
111
+ match object_request.type:
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(),
168
+ )
169
+
170
+ if atom_id in node.storage_index:
171
+ provider_id = node.storage_index[atom_id]
172
+ provider_bytes = provider_payload_for_id(node, provider_id)
173
+ if provider_bytes is not None:
174
+ node.logger.debug("Known provider for %s; informing %s", atom_id.hex(), peer.address)
175
+ resp = ObjectResponse(
176
+ type=ObjectResponseType.OBJECT_PROVIDER,
177
+ data=provider_bytes,
178
+ atom_id=atom_id
179
+ )
180
+ obj_res_msg = Message(
181
+ topic=MessageTopic.OBJECT_RESPONSE,
182
+ body=resp.to_bytes(),
183
+ sender=node.relay_public_key,
184
+ )
185
+ obj_res_msg.encrypt(peer.shared_key_bytes)
186
+ enqueue_outgoing(
187
+ node,
188
+ peer.address,
189
+ message=obj_res_msg,
190
+ difficulty=peer.difficulty,
191
+ )
192
+ return
193
+ node.logger.warning(
194
+ "Unknown provider id %s for %s",
195
+ provider_id,
196
+ atom_id.hex(),
197
+ )
198
+
199
+ nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
200
+ if nearest_peer:
201
+ node.logger.debug("Forwarding requester %s to nearest peer for %s", peer.address, atom_id.hex())
202
+ peer_info = encode_peer_contact_bytes(nearest_peer)
203
+ resp = ObjectResponse(
204
+ type=ObjectResponseType.OBJECT_PROVIDER,
205
+ # type=ObjectResponseType.OBJECT_NEAREST_PEER,
206
+ data=peer_info,
207
+ atom_id=atom_id
208
+ )
209
+ obj_res_msg = Message(
210
+ topic=MessageTopic.OBJECT_RESPONSE,
211
+ body=resp.to_bytes(),
212
+ sender=node.relay_public_key,
213
+ )
214
+ obj_res_msg.encrypt(nearest_peer.shared_key_bytes)
215
+ enqueue_outgoing(
216
+ node,
217
+ peer.address,
218
+ message=obj_res_msg,
219
+ difficulty=peer.difficulty,
220
+ )
221
+
222
+ case ObjectRequestType.OBJECT_PUT:
223
+ node.logger.debug("Handling OBJECT_PUT for %s from %s", object_request.atom_id.hex(), peer.address)
224
+
225
+ nearest_peer = node.peer_route.closest_peer_for_hash(object_request.atom_id)
226
+ is_self_closest = False
227
+ if nearest_peer is None or nearest_peer.address is None:
228
+ is_self_closest = True
229
+ else:
230
+ try:
231
+ self_distance = xor_distance(object_request.atom_id, node.relay_public_key_bytes)
232
+ peer_distance = xor_distance(object_request.atom_id, nearest_peer.public_key_bytes)
233
+ except Exception as exc:
234
+ node.logger.warning(
235
+ "Failed distance comparison for OBJECT_PUT %s: %s",
236
+ object_request.atom_id.hex(),
237
+ exc,
238
+ )
239
+ is_self_closest = True
240
+ else:
241
+ is_self_closest = self_distance <= peer_distance
242
+
243
+ if is_self_closest:
244
+ node.logger.debug("Storing provider info for %s locally", object_request.atom_id.hex())
245
+ provider_id = provider_id_for_payload(node, object_request.data)
246
+ node.storage_index[object_request.atom_id] = provider_id
247
+ else:
248
+ node.logger.debug(
249
+ "Forwarding OBJECT_PUT for %s to nearer peer %s",
250
+ object_request.atom_id.hex(),
251
+ nearest_peer.address,
252
+ )
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
+ )
259
+ obj_req_msg = Message(
260
+ topic=MessageTopic.OBJECT_REQUEST,
261
+ body=fwd_req.to_bytes(),
262
+ sender=node.relay_public_key,
263
+ )
264
+ obj_req_msg.encrypt(nearest_peer.shared_key_bytes)
265
+ enqueue_outgoing(
266
+ node,
267
+ nearest_peer.address,
268
+ message=obj_req_msg,
269
+ difficulty=nearest_peer.difficulty,
270
+ )
271
+
272
+ case _:
273
+ node.logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, peer.address)