astreum 0.1.13__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 (111) hide show
  1. astreum-0.3.34/PKG-INFO +188 -0
  2. astreum-0.3.34/README.md +170 -0
  3. {astreum-0.1.13 → astreum-0.3.34}/pyproject.toml +5 -4
  4. astreum-0.3.34/src/astreum/__init__.py +19 -0
  5. astreum-0.3.34/src/astreum/communication/__init__.py +13 -0
  6. astreum-0.3.34/src/astreum/communication/disconnect.py +57 -0
  7. astreum-0.3.34/src/astreum/communication/handlers/handshake.py +92 -0
  8. astreum-0.3.34/src/astreum/communication/handlers/object_request.py +185 -0
  9. astreum-0.3.34/src/astreum/communication/handlers/object_response.py +115 -0
  10. astreum-0.3.34/src/astreum/communication/handlers/ping.py +42 -0
  11. astreum-0.3.34/src/astreum/communication/handlers/route_request.py +76 -0
  12. astreum-0.3.34/src/astreum/communication/handlers/route_response.py +53 -0
  13. astreum-0.3.34/src/astreum/communication/models/message.py +124 -0
  14. astreum-0.3.34/src/astreum/communication/models/peer.py +53 -0
  15. astreum-0.3.34/src/astreum/communication/models/ping.py +45 -0
  16. astreum-0.3.34/src/astreum/communication/models/route.py +94 -0
  17. astreum-0.3.34/src/astreum/communication/node.py +36 -0
  18. astreum-0.3.34/src/astreum/communication/outgoing_queue.py +87 -0
  19. astreum-0.3.34/src/astreum/communication/processors/incoming.py +126 -0
  20. astreum-0.3.34/src/astreum/communication/processors/outgoing.py +53 -0
  21. astreum-0.3.34/src/astreum/communication/processors/peer.py +119 -0
  22. astreum-0.3.34/src/astreum/communication/setup.py +273 -0
  23. astreum-0.3.34/src/astreum/communication/util.py +49 -0
  24. astreum-0.3.34/src/astreum/crypto/chacha20poly1305.py +74 -0
  25. astreum-0.3.34/src/astreum/crypto/ed25519.py +50 -0
  26. astreum-0.3.34/src/astreum/crypto/quadratic_form.py +123 -0
  27. astreum-0.3.34/src/astreum/crypto/wesolowski.py +154 -0
  28. astreum-0.3.34/src/astreum/crypto/x25519.py +31 -0
  29. astreum-0.3.34/src/astreum/machine/__init__.py +20 -0
  30. astreum-0.3.34/src/astreum/machine/evaluations/high_evaluation.py +237 -0
  31. astreum-0.3.34/src/astreum/machine/evaluations/low_evaluation.py +281 -0
  32. astreum-0.3.34/src/astreum/machine/evaluations/script_evaluation.py +27 -0
  33. astreum-0.3.34/src/astreum/machine/models/environment.py +31 -0
  34. astreum-0.3.34/src/astreum/machine/models/expression.py +218 -0
  35. astreum-0.3.34/src/astreum/machine/models/meter.py +18 -0
  36. astreum-0.3.34/src/astreum/machine/parser.py +51 -0
  37. astreum-0.3.34/src/astreum/machine/tokenizer.py +90 -0
  38. astreum-0.3.34/src/astreum/node.py +100 -0
  39. astreum-0.3.34/src/astreum/storage/__init__.py +7 -0
  40. astreum-0.3.34/src/astreum/storage/actions/get.py +187 -0
  41. astreum-0.3.34/src/astreum/storage/actions/set.py +179 -0
  42. astreum-0.3.34/src/astreum/storage/models/atom.py +107 -0
  43. astreum-0.3.34/src/astreum/storage/models/trie.py +502 -0
  44. astreum-0.3.34/src/astreum/storage/providers.py +24 -0
  45. astreum-0.3.34/src/astreum/storage/requests.py +28 -0
  46. astreum-0.3.34/src/astreum/storage/setup.py +23 -0
  47. astreum-0.3.34/src/astreum/utils/bytes.py +24 -0
  48. astreum-0.3.34/src/astreum/utils/config.py +215 -0
  49. astreum-0.3.34/src/astreum/utils/integer.py +25 -0
  50. astreum-0.3.34/src/astreum/utils/logging.py +219 -0
  51. astreum-0.3.34/src/astreum/validation/__init__.py +16 -0
  52. astreum-0.3.34/src/astreum/validation/constants.py +2 -0
  53. astreum-0.3.34/src/astreum/validation/genesis.py +64 -0
  54. astreum-0.3.34/src/astreum/validation/models/__init__.py +0 -0
  55. astreum-0.3.34/src/astreum/validation/models/account.py +84 -0
  56. astreum-0.3.34/src/astreum/validation/models/accounts.py +72 -0
  57. astreum-0.3.34/src/astreum/validation/models/block.py +544 -0
  58. astreum-0.3.34/src/astreum/validation/models/fork.py +511 -0
  59. astreum-0.3.34/src/astreum/validation/models/receipt.py +111 -0
  60. astreum-0.3.34/src/astreum/validation/models/transaction.py +255 -0
  61. astreum-0.3.34/src/astreum/validation/node.py +175 -0
  62. astreum-0.3.34/src/astreum/validation/validator.py +104 -0
  63. astreum-0.3.34/src/astreum/validation/workers/__init__.py +8 -0
  64. astreum-0.3.34/src/astreum/validation/workers/validation.py +349 -0
  65. astreum-0.3.34/src/astreum/verification/__init__.py +4 -0
  66. astreum-0.3.34/src/astreum/verification/discover.py +66 -0
  67. astreum-0.3.34/src/astreum/verification/node.py +61 -0
  68. astreum-0.3.34/src/astreum/verification/worker.py +183 -0
  69. astreum-0.3.34/src/astreum.egg-info/PKG-INFO +188 -0
  70. astreum-0.3.34/src/astreum.egg-info/SOURCES.txt +79 -0
  71. {astreum-0.1.13 → astreum-0.3.34}/src/astreum.egg-info/requires.txt +1 -0
  72. astreum-0.1.13/PKG-INFO +0 -88
  73. astreum-0.1.13/README.md +0 -72
  74. astreum-0.1.13/src/astreum/__init__.py +0 -2
  75. astreum-0.1.13/src/astreum/lispeum/expression.py +0 -95
  76. astreum-0.1.13/src/astreum/lispeum/parser.py +0 -40
  77. astreum-0.1.13/src/astreum/lispeum/special/definition.py +0 -27
  78. astreum-0.1.13/src/astreum/lispeum/special/list/all.py +0 -32
  79. astreum-0.1.13/src/astreum/lispeum/special/list/any.py +0 -32
  80. astreum-0.1.13/src/astreum/lispeum/special/list/fold.py +0 -29
  81. astreum-0.1.13/src/astreum/lispeum/special/list/get.py +0 -20
  82. astreum-0.1.13/src/astreum/lispeum/special/list/insert.py +0 -23
  83. astreum-0.1.13/src/astreum/lispeum/special/list/map.py +0 -30
  84. astreum-0.1.13/src/astreum/lispeum/special/list/position.py +0 -33
  85. astreum-0.1.13/src/astreum/lispeum/special/list/remove.py +0 -22
  86. astreum-0.1.13/src/astreum/lispeum/storage.py +0 -410
  87. astreum-0.1.13/src/astreum/lispeum/tokenizer.py +0 -52
  88. astreum-0.1.13/src/astreum/machine/__init__.py +0 -352
  89. astreum-0.1.13/src/astreum/machine/environment.py +0 -29
  90. astreum-0.1.13/src/astreum/machine/error.py +0 -2
  91. astreum-0.1.13/src/astreum/node/__init__.py +0 -592
  92. astreum-0.1.13/src/astreum/node/models.py +0 -309
  93. astreum-0.1.13/src/astreum/node/relay/__init__.py +0 -326
  94. astreum-0.1.13/src/astreum/node/relay/bucket.py +0 -90
  95. astreum-0.1.13/src/astreum/node/relay/envelope.py +0 -280
  96. astreum-0.1.13/src/astreum/node/relay/message.py +0 -110
  97. astreum-0.1.13/src/astreum/node/relay/peer.py +0 -174
  98. astreum-0.1.13/src/astreum/node/relay/route.py +0 -161
  99. astreum-0.1.13/src/astreum/utils/bytes_format.py +0 -75
  100. astreum-0.1.13/src/astreum.egg-info/PKG-INFO +0 -88
  101. astreum-0.1.13/src/astreum.egg-info/SOURCES.txt +0 -40
  102. {astreum-0.1.13 → astreum-0.3.34}/LICENSE +0 -0
  103. {astreum-0.1.13 → astreum-0.3.34}/setup.cfg +0 -0
  104. {astreum-0.1.13/src/astreum/lispeum → astreum-0.3.34/src/astreum/communication/handlers}/__init__.py +0 -0
  105. {astreum-0.1.13/src/astreum/lispeum/special → astreum-0.3.34/src/astreum/communication/models}/__init__.py +0 -0
  106. {astreum-0.1.13/src/astreum/lispeum/special/list → astreum-0.3.34/src/astreum/communication/processors}/__init__.py +0 -0
  107. {astreum-0.1.13/src/astreum/lispeum/special/number → astreum-0.3.34/src/astreum/crypto}/__init__.py +0 -0
  108. {astreum-0.1.13/src/astreum/utils → astreum-0.3.34/src/astreum/machine/evaluations}/__init__.py +0 -0
  109. /astreum-0.1.13/src/astreum/lispeum/special/number/addition.py → /astreum-0.3.34/src/astreum/machine/models/__init__.py +0 -0
  110. {astreum-0.1.13 → astreum-0.3.34}/src/astreum.egg-info/dependency_links.txt +0 -0
  111. {astreum-0.1.13 → astreum-0.3.34}/src/astreum.egg-info/top_level.txt +0 -0
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: astreum
3
+ Version: 0.3.34
4
+ Summary: Python library to interact with the Astreum blockchain and its virtual machine.
5
+ Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
+ Project-URL: Homepage, https://github.com/astreum/lib-py
7
+ Project-URL: Issues, https://github.com/astreum/lib-py/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pycryptodomex==3.21.0
15
+ Requires-Dist: cryptography==44.0.2
16
+ Requires-Dist: blake3==1.0.4
17
+ Dynamic: license-file
18
+
19
+ # lib
20
+
21
+ Python library to interact with the Astreum blockchain and its virtual machine.
22
+
23
+ [View on PyPI](https://pypi.org/project/astreum/)
24
+
25
+ ## Configuration
26
+
27
+ When initializing an `astreum.Node`, pass a dictionary with any of the options below. Only the parameters you want to override need to be present – everything else falls back to its default.
28
+
29
+ ### Core Configuration
30
+
31
+ | Parameter | Type | Default | Description |
32
+ | --------------------------- | ---------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | `hot_storage_limit` | int | `1073741824` | Maximum bytes kept in the hot cache before new atoms are skipped (1 GiB). |
34
+ | `cold_storage_limit` | int | `10737418240` | Cold storage write threshold (10 GiB by default); set to `0` to skip the limit. |
35
+ | `cold_storage_path` | string | `None` | Directory where persisted atoms live; Astreum creates it on startup and skips cold storage when unset. |
36
+ | `logging_retention_days` | int | `90` | Number of days to keep rotated log files (daily gzip). |
37
+ | `chain_id` | int | `0` | Chain identifier used for validation (0 = test, 1 = main). |
38
+ | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
39
+
40
+ ### Communication
41
+
42
+ | Parameter | Type | Default | Description |
43
+ | ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
44
+ | `relay_secret_key` | hex string | Auto-generated | X25519 private key used for the relay route; a new keypair is created when this field is omitted. |
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
+ | `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
47
+ | `incoming_port` | int | `52780` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
48
+ | `default_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`. |
50
+ | `peer_timeout` | int | `900` | Evict peers that have not been seen within this many seconds (15 minutes). |
51
+ | `peer_timeout_interval` | int | `10` | How often (seconds) the peer manager checks for stale peers. |
52
+ | `bootstrap_retry_interval` | int | `30` | How often (seconds) to retry bootstrapping when the peer list is empty. |
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. |
55
+
56
+ > **Note**
57
+ > The peer‑to‑peer *route* used for object discovery is always enabled.
58
+ > If `validation_secret_key` is provided the node automatically joins the validation route too.
59
+
60
+ ### Example
61
+
62
+ ```python
63
+ from astreum.node import Node
64
+
65
+ config = {
66
+ "relay_secret_key": "ab…cd", # optional – hex encoded
67
+ "validation_secret_key": "12…34", # optional – validator
68
+ "hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
69
+ "cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
70
+ "cold_storage_path": "./data/node1",
71
+ "incoming_port": 52780,
72
+ "use_ipv6": False,
73
+ "default_seed": None,
74
+ "additional_seeds": [
75
+ "127.0.0.1:7374"
76
+ ]
77
+ }
78
+
79
+ node = Node(config)
80
+ # … your code …
81
+ ```
82
+
83
+
84
+ ## Astreum Machine Quickstart
85
+
86
+ The Astreum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Astreum script, and the node tokenizes, parses, and evaluates.
87
+
88
+ ```python
89
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
90
+
91
+ import uuid
92
+ from astreum import Node, Env, Expr
93
+
94
+ # 1) Spin‑up a stand‑alone VM
95
+ node = Node()
96
+
97
+ # 2) Create an environment (simple manual setup)
98
+ env_id = uuid.uuid4()
99
+ node.environments[env_id] = Env()
100
+
101
+ # 3) Build a function value using a low‑level stack body via `sk`.
102
+ # Body does: $0 $1 add (i.e., a + b)
103
+ low_body = Expr.ListExpr([
104
+ Expr.Symbol("$0"), # a (first arg)
105
+ Expr.Symbol("$1"), # b (second arg)
106
+ Expr.Symbol("add"),
107
+ ])
108
+
109
+ fn_body = Expr.ListExpr([
110
+ Expr.Symbol("a"),
111
+ Expr.Symbol("b"),
112
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
113
+ ])
114
+
115
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
116
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
117
+
118
+ # 4) Store under the name "int.add"
119
+ node.env_set(env_id, "int.add", int_add_fn)
120
+
121
+ # 5) Retrieve the function and call it with bytes 1 and 2
122
+ bound = node.env_get(env_id, "int.add")
123
+ call = Expr.ListExpr([Expr.Bytes(b"\x01"), Expr.Bytes(b"\x02"), bound])
124
+ res = node.high_eval(env_id, call)
125
+
126
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
127
+ print([int.from_bytes(b.value, 'big', signed=True) for b in res.elements]) # [3]
128
+ ```
129
+
130
+ ### Handling errors
131
+
132
+ Both helpers raise `ParseError` (from `astreum.machine.error`) when something goes wrong:
133
+
134
+ * Unterminated string literals are caught by `tokenize`.
135
+ * Unexpected or missing parentheses are caught by `parse`.
136
+
137
+ Catch the exception to provide developer‑friendly diagnostics:
138
+
139
+ ```python
140
+ try:
141
+ tokens = tokenize(bad_source)
142
+ expr, _ = parse(tokens)
143
+ except ParseError as e:
144
+ print("Parse failed:", e)
145
+ ```
146
+
147
+ ---
148
+
149
+
150
+ ## Logging
151
+
152
+ Every `Node` instance wires up structured logging automatically:
153
+
154
+ - 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.
155
+ - 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"]`.
156
+ - Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
157
+ - Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
158
+ - The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
159
+
160
+ ## Testing
161
+
162
+ ```bash
163
+ python3 -m venv venv
164
+ source venv/bin/activate
165
+ pip install -e .
166
+ ```
167
+
168
+ for all tests
169
+ ```
170
+ python3 -m unittest discover -s tests
171
+ ```
172
+
173
+ for individual tests
174
+ ```
175
+ python3 -m unittest tests.node.test_atom_get
176
+ python3 -m unittest tests.node.test_current_validator
177
+ python3 -m unittest tests.node.test_node_connection
178
+ python3 -m unittest tests.node.test_node_init
179
+ python3 -m unittest tests.node.test_node_validation
180
+ python3 -m unittest tests.node.tokenize
181
+ python3 -m unittest tests.node.parse
182
+ python3 -m unittest tests.node.function
183
+ python3 -m unittest tests.node.stack
184
+ python3 -m unittest tests.models.test_merkle
185
+ python3 -m unittest tests.models.test_patricia
186
+ python3 -m unittest tests.block.atom
187
+ python3 -m unittest tests.block.nonce
188
+ ```
@@ -0,0 +1,170 @@
1
+ # lib
2
+
3
+ Python library to interact with the Astreum blockchain and its virtual machine.
4
+
5
+ [View on PyPI](https://pypi.org/project/astreum/)
6
+
7
+ ## Configuration
8
+
9
+ When initializing an `astreum.Node`, pass a dictionary with any of the options below. Only the parameters you want to override need to be present – everything else falls back to its default.
10
+
11
+ ### Core Configuration
12
+
13
+ | Parameter | Type | Default | Description |
14
+ | --------------------------- | ---------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `hot_storage_limit` | int | `1073741824` | Maximum bytes kept in the hot cache before new atoms are skipped (1 GiB). |
16
+ | `cold_storage_limit` | int | `10737418240` | Cold storage write threshold (10 GiB by default); set to `0` to skip the limit. |
17
+ | `cold_storage_path` | string | `None` | Directory where persisted atoms live; Astreum creates it on startup and skips cold storage when unset. |
18
+ | `logging_retention_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
+ | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
21
+
22
+ ### Communication
23
+
24
+ | Parameter | Type | Default | Description |
25
+ | ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
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_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`. |
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
+ | `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. |
37
+
38
+ > **Note**
39
+ > The peer‑to‑peer *route* used for object discovery is always enabled.
40
+ > If `validation_secret_key` is provided the node automatically joins the validation route too.
41
+
42
+ ### Example
43
+
44
+ ```python
45
+ from astreum.node import Node
46
+
47
+ config = {
48
+ "relay_secret_key": "ab…cd", # optional – hex encoded
49
+ "validation_secret_key": "12…34", # optional – validator
50
+ "hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
51
+ "cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
52
+ "cold_storage_path": "./data/node1",
53
+ "incoming_port": 52780,
54
+ "use_ipv6": False,
55
+ "default_seed": None,
56
+ "additional_seeds": [
57
+ "127.0.0.1:7374"
58
+ ]
59
+ }
60
+
61
+ node = Node(config)
62
+ # … your code …
63
+ ```
64
+
65
+
66
+ ## Astreum Machine Quickstart
67
+
68
+ The Astreum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Astreum script, and the node tokenizes, parses, and evaluates.
69
+
70
+ ```python
71
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
72
+
73
+ import uuid
74
+ from astreum import Node, Env, Expr
75
+
76
+ # 1) Spin‑up a stand‑alone VM
77
+ node = Node()
78
+
79
+ # 2) Create an environment (simple manual setup)
80
+ env_id = uuid.uuid4()
81
+ node.environments[env_id] = Env()
82
+
83
+ # 3) Build a function value using a low‑level stack body via `sk`.
84
+ # Body does: $0 $1 add (i.e., a + b)
85
+ low_body = Expr.ListExpr([
86
+ Expr.Symbol("$0"), # a (first arg)
87
+ Expr.Symbol("$1"), # b (second arg)
88
+ Expr.Symbol("add"),
89
+ ])
90
+
91
+ fn_body = Expr.ListExpr([
92
+ Expr.Symbol("a"),
93
+ Expr.Symbol("b"),
94
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
95
+ ])
96
+
97
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
98
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
99
+
100
+ # 4) Store under the name "int.add"
101
+ node.env_set(env_id, "int.add", int_add_fn)
102
+
103
+ # 5) Retrieve the function and call it with bytes 1 and 2
104
+ bound = node.env_get(env_id, "int.add")
105
+ call = Expr.ListExpr([Expr.Bytes(b"\x01"), Expr.Bytes(b"\x02"), bound])
106
+ res = node.high_eval(env_id, call)
107
+
108
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
109
+ print([int.from_bytes(b.value, 'big', signed=True) for b in res.elements]) # [3]
110
+ ```
111
+
112
+ ### Handling errors
113
+
114
+ Both helpers raise `ParseError` (from `astreum.machine.error`) when something goes wrong:
115
+
116
+ * Unterminated string literals are caught by `tokenize`.
117
+ * Unexpected or missing parentheses are caught by `parse`.
118
+
119
+ Catch the exception to provide developer‑friendly diagnostics:
120
+
121
+ ```python
122
+ try:
123
+ tokens = tokenize(bad_source)
124
+ expr, _ = parse(tokens)
125
+ except ParseError as e:
126
+ print("Parse failed:", e)
127
+ ```
128
+
129
+ ---
130
+
131
+
132
+ ## Logging
133
+
134
+ Every `Node` instance wires up structured logging automatically:
135
+
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.
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"]`.
138
+ - Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
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`.
140
+ - The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
141
+
142
+ ## Testing
143
+
144
+ ```bash
145
+ python3 -m venv venv
146
+ source venv/bin/activate
147
+ pip install -e .
148
+ ```
149
+
150
+ for all tests
151
+ ```
152
+ python3 -m unittest discover -s tests
153
+ ```
154
+
155
+ for individual tests
156
+ ```
157
+ python3 -m unittest tests.node.test_atom_get
158
+ python3 -m unittest tests.node.test_current_validator
159
+ python3 -m unittest tests.node.test_node_connection
160
+ python3 -m unittest tests.node.test_node_init
161
+ python3 -m unittest tests.node.test_node_validation
162
+ python3 -m unittest tests.node.tokenize
163
+ python3 -m unittest tests.node.parse
164
+ python3 -m unittest tests.node.function
165
+ python3 -m unittest tests.node.stack
166
+ python3 -m unittest tests.models.test_merkle
167
+ python3 -m unittest tests.models.test_patricia
168
+ python3 -m unittest tests.block.atom
169
+ python3 -m unittest tests.block.nonce
170
+ ```
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.1.13"
3
+ version = "0.3.34"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
7
- description = "Python library to interact with the Astreum blockchain and its Lispeum virtual machine."
7
+ description = "Python library to interact with the Astreum blockchain and its virtual machine."
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.8"
10
10
  classifiers = [
@@ -15,8 +15,9 @@ classifiers = [
15
15
  dependencies = [
16
16
  "pycryptodomex==3.21.0",
17
17
  "cryptography==44.0.2",
18
+ "blake3==1.0.4"
18
19
  ]
19
20
 
20
21
  [project.urls]
21
- Homepage = "https://github.com/astreum/lib"
22
- Issues = "https://github.com/astreum/lib/issues"
22
+ Homepage = "https://github.com/astreum/lib-py"
23
+ Issues = "https://github.com/astreum/lib-py/issues"
@@ -0,0 +1,19 @@
1
+
2
+ from astreum.validation import Account, Accounts, Block, Fork, Receipt, Transaction
3
+ from astreum.machine import Env, Expr, parse, tokenize
4
+ from astreum.node import Node
5
+
6
+
7
+ __all__: list[str] = [
8
+ "Node",
9
+ "Env",
10
+ "Expr",
11
+ "Block",
12
+ "Fork",
13
+ "Receipt",
14
+ "Transaction",
15
+ "Account",
16
+ "Accounts",
17
+ "parse",
18
+ "tokenize",
19
+ ]
@@ -0,0 +1,13 @@
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
+ ]
@@ -0,0 +1,57 @@
1
+ """Helpers related to disconnecting communication components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from astreum.node import Node
9
+
10
+
11
+ _SOCKET_ATTRS: tuple[str, ...] = ("incoming_socket", "outgoing_socket")
12
+ _THREAD_ATTRS: tuple[str, ...] = (
13
+ "incoming_populate_thread",
14
+ "incoming_process_thread",
15
+ "outgoing_thread",
16
+ "peer_manager_thread",
17
+ "latest_block_discovery_thread",
18
+ "verify_thread",
19
+ "consensus_validation_thread",
20
+ )
21
+
22
+
23
+ def _set_event(node: "Node", attr_name: str) -> None:
24
+ event = getattr(node, attr_name, None)
25
+ if event is not None:
26
+ event.set()
27
+
28
+
29
+ def _close_socket(node: "Node", attr_name: str) -> None:
30
+ sock = getattr(node, attr_name, None)
31
+ if sock is None:
32
+ return
33
+ try:
34
+ sock.close()
35
+ except Exception as exc: # pragma: no cover - defensive logging path
36
+ node.logger.warning("Error closing %s: %s", attr_name, exc)
37
+
38
+
39
+ def disconnect_node(node: "Node") -> None:
40
+ """Gracefully stop worker threads and close communication sockets."""
41
+ node.logger.info("Disconnecting Astreum Node")
42
+
43
+ _set_event(node, "communication_stop_event")
44
+ _set_event(node, "_validation_stop_event")
45
+ _set_event(node, "_verify_stop_event")
46
+
47
+ for sock_name in _SOCKET_ATTRS:
48
+ _close_socket(node, sock_name)
49
+
50
+ for thread_name in _THREAD_ATTRS:
51
+ thread = getattr(node, thread_name, None)
52
+ if thread is None or not thread.is_alive():
53
+ continue
54
+ thread.join(timeout=1.0)
55
+
56
+ node.is_connected = False
57
+ node.logger.info("Node disconnected")
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from cryptography.hazmat.primitives import serialization
6
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
7
+
8
+ from ..models.peer import Peer
9
+ from ..models.message import Message, MessageTopic
10
+ from ..models.ping import Ping
11
+
12
+ if TYPE_CHECKING:
13
+ from .... import Node
14
+
15
+
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
+ )
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
+ 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
66
+
67
+ try:
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
+ )
74
+ except Exception:
75
+ return True
76
+
77
+ node.add_peer(sender_public_key_bytes, peer)
78
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
79
+
80
+ node.logger.info(
81
+ "Handshake accepted from %s:%s; peer added",
82
+ peer_address[0],
83
+ peer_address[1],
84
+ )
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