astreum 0.2.42__tar.gz → 0.3.5__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 (102) hide show
  1. astreum-0.3.5/PKG-INFO +160 -0
  2. astreum-0.3.5/README.md +142 -0
  3. {astreum-0.2.42 → astreum-0.3.5}/pyproject.toml +4 -4
  4. astreum-0.3.5/src/astreum/__init__.py +18 -0
  5. {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication}/__init__.py +3 -3
  6. astreum-0.3.5/src/astreum/communication/handlers/handshake.py +81 -0
  7. astreum-0.3.5/src/astreum/communication/handlers/object_request.py +153 -0
  8. astreum-0.3.5/src/astreum/communication/handlers/object_response.py +37 -0
  9. astreum-0.3.5/src/astreum/communication/handlers/ping.py +48 -0
  10. astreum-0.3.5/src/astreum/communication/handlers/route_request.py +78 -0
  11. astreum-0.3.5/src/astreum/communication/handlers/route_response.py +52 -0
  12. astreum-0.3.5/src/astreum/communication/models/message.py +105 -0
  13. astreum-0.3.5/src/astreum/communication/models/peer.py +23 -0
  14. {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication/models}/route.py +40 -8
  15. {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication}/setup.py +105 -97
  16. astreum-0.3.5/src/astreum/communication/start.py +38 -0
  17. {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication}/util.py +7 -0
  18. astreum-0.3.5/src/astreum/consensus/__init__.py +20 -0
  19. astreum-0.3.5/src/astreum/consensus/genesis.py +66 -0
  20. astreum-0.3.5/src/astreum/consensus/models/account.py +84 -0
  21. astreum-0.3.5/src/astreum/consensus/models/accounts.py +72 -0
  22. astreum-0.3.5/src/astreum/consensus/models/block.py +364 -0
  23. {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus/models}/chain.py +7 -7
  24. {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus/models}/fork.py +8 -8
  25. astreum-0.3.5/src/astreum/consensus/models/receipt.py +98 -0
  26. astreum-0.3.5/src/astreum/consensus/models/transaction.py +213 -0
  27. {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/setup.py +26 -11
  28. astreum-0.3.5/src/astreum/consensus/start.py +68 -0
  29. astreum-0.3.5/src/astreum/consensus/validator.py +95 -0
  30. {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/workers/discovery.py +20 -1
  31. astreum-0.3.5/src/astreum/consensus/workers/validation.py +292 -0
  32. {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/workers/verify.py +32 -3
  33. astreum-0.3.5/src/astreum/crypto/__init__.py +0 -0
  34. astreum-0.3.5/src/astreum/machine/__init__.py +20 -0
  35. astreum-0.3.5/src/astreum/machine/evaluations/__init__.py +0 -0
  36. astreum-0.3.5/src/astreum/machine/evaluations/high_evaluation.py +237 -0
  37. astreum-0.3.5/src/astreum/machine/evaluations/low_evaluation.py +281 -0
  38. astreum-0.3.5/src/astreum/machine/evaluations/script_evaluation.py +27 -0
  39. astreum-0.3.5/src/astreum/machine/models/__init__.py +0 -0
  40. astreum-0.3.5/src/astreum/machine/models/environment.py +31 -0
  41. astreum-0.3.5/src/astreum/machine/models/expression.py +218 -0
  42. {astreum-0.2.42/src/astreum/_lispeum → astreum-0.3.5/src/astreum/machine}/parser.py +26 -31
  43. astreum-0.3.5/src/astreum/machine/tokenizer.py +90 -0
  44. astreum-0.3.5/src/astreum/node.py +75 -0
  45. astreum-0.3.5/src/astreum/storage/__init__.py +7 -0
  46. astreum-0.3.5/src/astreum/storage/actions/get.py +85 -0
  47. astreum-0.3.5/src/astreum/storage/actions/set.py +138 -0
  48. astreum-0.3.5/src/astreum/storage/models/atom.py +107 -0
  49. astreum-0.2.42/src/astreum/_storage/patricia.py → astreum-0.3.5/src/astreum/storage/models/trie.py +236 -177
  50. astreum-0.3.5/src/astreum/storage/setup.py +22 -0
  51. astreum-0.3.5/src/astreum/utils/bytes.py +24 -0
  52. astreum-0.3.5/src/astreum/utils/config.py +48 -0
  53. astreum-0.3.5/src/astreum/utils/logging.py +219 -0
  54. astreum-0.3.5/src/astreum.egg-info/PKG-INFO +160 -0
  55. astreum-0.3.5/src/astreum.egg-info/SOURCES.txt +69 -0
  56. astreum-0.2.42/PKG-INFO +0 -146
  57. astreum-0.2.42/README.md +0 -128
  58. astreum-0.2.42/src/astreum/__init__.py +0 -9
  59. astreum-0.2.42/src/astreum/_communication/message.py +0 -100
  60. astreum-0.2.42/src/astreum/_communication/peer.py +0 -11
  61. astreum-0.2.42/src/astreum/_consensus/__init__.py +0 -20
  62. astreum-0.2.42/src/astreum/_consensus/account.py +0 -95
  63. astreum-0.2.42/src/astreum/_consensus/accounts.py +0 -38
  64. astreum-0.2.42/src/astreum/_consensus/block.py +0 -328
  65. astreum-0.2.42/src/astreum/_consensus/genesis.py +0 -141
  66. astreum-0.2.42/src/astreum/_consensus/receipt.py +0 -177
  67. astreum-0.2.42/src/astreum/_consensus/transaction.py +0 -216
  68. astreum-0.2.42/src/astreum/_consensus/workers/validation.py +0 -122
  69. astreum-0.2.42/src/astreum/_lispeum/__init__.py +0 -16
  70. astreum-0.2.42/src/astreum/_lispeum/environment.py +0 -13
  71. astreum-0.2.42/src/astreum/_lispeum/expression.py +0 -37
  72. astreum-0.2.42/src/astreum/_lispeum/high_evaluation.py +0 -177
  73. astreum-0.2.42/src/astreum/_lispeum/low_evaluation.py +0 -123
  74. astreum-0.2.42/src/astreum/_lispeum/tokenizer.py +0 -22
  75. astreum-0.2.42/src/astreum/_node.py +0 -58
  76. astreum-0.2.42/src/astreum/_storage/__init__.py +0 -5
  77. astreum-0.2.42/src/astreum/_storage/atom.py +0 -117
  78. astreum-0.2.42/src/astreum/format.py +0 -75
  79. astreum-0.2.42/src/astreum/models/block.py +0 -441
  80. astreum-0.2.42/src/astreum/models/merkle.py +0 -205
  81. astreum-0.2.42/src/astreum/models/patricia.py +0 -393
  82. astreum-0.2.42/src/astreum/node.py +0 -781
  83. astreum-0.2.42/src/astreum/storage/object.py +0 -68
  84. astreum-0.2.42/src/astreum/storage/setup.py +0 -15
  85. astreum-0.2.42/src/astreum.egg-info/PKG-INFO +0 -146
  86. astreum-0.2.42/src/astreum.egg-info/SOURCES.txt +0 -57
  87. {astreum-0.2.42 → astreum-0.3.5}/LICENSE +0 -0
  88. {astreum-0.2.42 → astreum-0.3.5}/setup.cfg +0 -0
  89. {astreum-0.2.42/src/astreum/crypto → astreum-0.3.5/src/astreum/communication/handlers}/__init__.py +0 -0
  90. {astreum-0.2.42/src/astreum → astreum-0.3.5/src/astreum/communication}/models/__init__.py +0 -0
  91. {astreum-0.2.42/src/astreum/_communication → astreum-0.3.5/src/astreum/communication/models}/ping.py +0 -0
  92. {astreum-0.2.42/src/astreum/storage → astreum-0.3.5/src/astreum/consensus/models}/__init__.py +0 -0
  93. {astreum-0.2.42/src/astreum/_consensus → astreum-0.3.5/src/astreum/consensus}/workers/__init__.py +0 -0
  94. {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/ed25519.py +0 -0
  95. {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/quadratic_form.py +0 -0
  96. {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/wesolowski.py +0 -0
  97. {astreum-0.2.42 → astreum-0.3.5}/src/astreum/crypto/x25519.py +0 -0
  98. {astreum-0.2.42/src/astreum/_lispeum → astreum-0.3.5/src/astreum/machine/models}/meter.py +0 -0
  99. {astreum-0.2.42 → astreum-0.3.5}/src/astreum/utils/integer.py +0 -0
  100. {astreum-0.2.42 → astreum-0.3.5}/src/astreum.egg-info/dependency_links.txt +0 -0
  101. {astreum-0.2.42 → astreum-0.3.5}/src/astreum.egg-info/requires.txt +0 -0
  102. {astreum-0.2.42 → astreum-0.3.5}/src/astreum.egg-info/top_level.txt +0 -0
astreum-0.3.5/PKG-INFO ADDED
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: astreum
3
+ Version: 0.3.5
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` | int | `90` | Number of days to keep rotated log files (daily gzip). |
37
+ | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
38
+
39
+ ### Networking
40
+
41
+ | Parameter | Type | Default | Description |
42
+ | ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
43
+ | `relay_secret_key` | hex string | Auto-generated | X25519 private key used for the relay route; a new keypair is created when this field is omitted. |
44
+ | `validation_secret_key` | hex string | `None` | Optional Ed25519 key that lets the node join the validation route; leave blank to opt out of validation. |
45
+ | `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
46
+ | `incoming_port` | int | `7373` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
47
+ | `bootstrap` | list\[str\] | `[]` | Addresses to ping with a handshake before joining; each must look like `host:port` or `[ipv6]:port`. |
48
+
49
+ > **Note**
50
+ > The peer‑to‑peer *route* used for object discovery is always enabled.
51
+ > If `validation_secret_key` is provided the node automatically joins the validation route too.
52
+
53
+ ### Example
54
+
55
+ ```python
56
+ from astreum.node import Node
57
+
58
+ config = {
59
+ "relay_secret_key": "ab…cd", # optional – hex encoded
60
+ "validation_secret_key": "12…34", # optional – validator
61
+ "hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
62
+ "cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
63
+ "cold_storage_path": "./data/node1",
64
+ "incoming_port": 7373,
65
+ "use_ipv6": False,
66
+ "bootstrap": [
67
+ "bootstrap.astreum.org:7373",
68
+ "127.0.0.1:7374"
69
+ ]
70
+ }
71
+
72
+ node = Node(config)
73
+ # … your code …
74
+ ```
75
+
76
+
77
+ ## Astreum Machine Quickstart
78
+
79
+ The Astreum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Astreum script, and the node tokenizes, parses, and evaluates.
80
+
81
+ ```python
82
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
83
+
84
+ import uuid
85
+ from astreum import Node, Env, Expr
86
+
87
+ # 1) Spin‑up a stand‑alone VM
88
+ node = Node()
89
+
90
+ # 2) Create an environment (simple manual setup)
91
+ env_id = uuid.uuid4()
92
+ node.environments[env_id] = Env()
93
+
94
+ # 3) Build a function value using a low‑level stack body via `sk`.
95
+ # Body does: $0 $1 add (i.e., a + b)
96
+ low_body = Expr.ListExpr([
97
+ Expr.Symbol("$0"), # a (first arg)
98
+ Expr.Symbol("$1"), # b (second arg)
99
+ Expr.Symbol("add"),
100
+ ])
101
+
102
+ fn_body = Expr.ListExpr([
103
+ Expr.Symbol("a"),
104
+ Expr.Symbol("b"),
105
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
106
+ ])
107
+
108
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
109
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
110
+
111
+ # 4) Store under the name "int.add"
112
+ node.env_set(env_id, "int.add", int_add_fn)
113
+
114
+ # 5) Retrieve the function and call it with bytes 1 and 2
115
+ bound = node.env_get(env_id, "int.add")
116
+ call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
117
+ res = node.high_eval(env_id, call)
118
+
119
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
120
+ print([b.value for b in res.elements]) # [3]
121
+ ```
122
+
123
+ ### Handling errors
124
+
125
+ Both helpers raise `ParseError` (from `astreum.machine.error`) when something goes wrong:
126
+
127
+ * Unterminated string literals are caught by `tokenize`.
128
+ * Unexpected or missing parentheses are caught by `parse`.
129
+
130
+ Catch the exception to provide developer‑friendly diagnostics:
131
+
132
+ ```python
133
+ try:
134
+ tokens = tokenize(bad_source)
135
+ expr, _ = parse(tokens)
136
+ except ParseError as e:
137
+ print("Parse failed:", e)
138
+ ```
139
+
140
+ ---
141
+
142
+
143
+ ## Logging
144
+
145
+ Every `Node` instance wires up structured logging automatically:
146
+
147
+ - Logs land in per-instance files named `node.log` under `%LOCALAPPDATA%\Astreum\lib-py\logs/<instance_id>` on Windows and `$XDG_STATE_HOME` (or `~/.local/state`)/`Astreum/lib-py/logs/<instance_id>` on other platforms. The `<instance_id>` is the first 16 hex characters of a BLAKE3 hash of the caller's file path, so running the node from different entry points keeps their logs isolated.
148
+ - Files rotate at midnight UTC with gzip compression (`node-YYYY-MM-DD.log.gz`) and retain 90 days by default. Override via `config["logging_retention"]`.
149
+ - Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
150
+ - Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
151
+ - The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
152
+
153
+ ## Testing
154
+
155
+ ```bash
156
+ python3 -m venv venv
157
+ source venv/bin/activate
158
+ pip install -e .
159
+ python3 -m unittest discover -s tests
160
+ ```
@@ -0,0 +1,142 @@
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` | int | `90` | Number of days to keep rotated log files (daily gzip). |
19
+ | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
20
+
21
+ ### Networking
22
+
23
+ | Parameter | Type | Default | Description |
24
+ | ------------------------ | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
25
+ | `relay_secret_key` | hex string | Auto-generated | X25519 private key used for the relay route; a new keypair is created when this field is omitted. |
26
+ | `validation_secret_key` | hex string | `None` | Optional Ed25519 key that lets the node join the validation route; leave blank to opt out of validation. |
27
+ | `use_ipv6` | bool | `False` | Bind the incoming/outgoing sockets on IPv6 (the OS still listens on IPv4 if a peer speaks both). |
28
+ | `incoming_port` | int | `7373` | UDP port the relay binds to; pass `0` or omit to let the OS pick an ephemeral port. |
29
+ | `bootstrap` | list\[str\] | `[]` | Addresses to ping with a handshake before joining; each must look like `host:port` or `[ipv6]:port`. |
30
+
31
+ > **Note**
32
+ > The peer‑to‑peer *route* used for object discovery is always enabled.
33
+ > If `validation_secret_key` is provided the node automatically joins the validation route too.
34
+
35
+ ### Example
36
+
37
+ ```python
38
+ from astreum.node import Node
39
+
40
+ config = {
41
+ "relay_secret_key": "ab…cd", # optional – hex encoded
42
+ "validation_secret_key": "12…34", # optional – validator
43
+ "hot_storage_limit": 1073741824, # cap hot cache at 1 GiB
44
+ "cold_storage_limit": 10737418240, # cap cold storage at 10 GiB
45
+ "cold_storage_path": "./data/node1",
46
+ "incoming_port": 7373,
47
+ "use_ipv6": False,
48
+ "bootstrap": [
49
+ "bootstrap.astreum.org:7373",
50
+ "127.0.0.1:7374"
51
+ ]
52
+ }
53
+
54
+ node = Node(config)
55
+ # … your code …
56
+ ```
57
+
58
+
59
+ ## Astreum Machine Quickstart
60
+
61
+ The Astreum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Astreum script, and the node tokenizes, parses, and evaluates.
62
+
63
+ ```python
64
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
65
+
66
+ import uuid
67
+ from astreum import Node, Env, Expr
68
+
69
+ # 1) Spin‑up a stand‑alone VM
70
+ node = Node()
71
+
72
+ # 2) Create an environment (simple manual setup)
73
+ env_id = uuid.uuid4()
74
+ node.environments[env_id] = Env()
75
+
76
+ # 3) Build a function value using a low‑level stack body via `sk`.
77
+ # Body does: $0 $1 add (i.e., a + b)
78
+ low_body = Expr.ListExpr([
79
+ Expr.Symbol("$0"), # a (first arg)
80
+ Expr.Symbol("$1"), # b (second arg)
81
+ Expr.Symbol("add"),
82
+ ])
83
+
84
+ fn_body = Expr.ListExpr([
85
+ Expr.Symbol("a"),
86
+ Expr.Symbol("b"),
87
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
88
+ ])
89
+
90
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
91
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
92
+
93
+ # 4) Store under the name "int.add"
94
+ node.env_set(env_id, "int.add", int_add_fn)
95
+
96
+ # 5) Retrieve the function and call it with bytes 1 and 2
97
+ bound = node.env_get(env_id, "int.add")
98
+ call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
99
+ res = node.high_eval(env_id, call)
100
+
101
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
102
+ print([b.value for b in res.elements]) # [3]
103
+ ```
104
+
105
+ ### Handling errors
106
+
107
+ Both helpers raise `ParseError` (from `astreum.machine.error`) when something goes wrong:
108
+
109
+ * Unterminated string literals are caught by `tokenize`.
110
+ * Unexpected or missing parentheses are caught by `parse`.
111
+
112
+ Catch the exception to provide developer‑friendly diagnostics:
113
+
114
+ ```python
115
+ try:
116
+ tokens = tokenize(bad_source)
117
+ expr, _ = parse(tokens)
118
+ except ParseError as e:
119
+ print("Parse failed:", e)
120
+ ```
121
+
122
+ ---
123
+
124
+
125
+ ## Logging
126
+
127
+ Every `Node` instance wires up structured logging automatically:
128
+
129
+ - Logs land in per-instance files named `node.log` under `%LOCALAPPDATA%\Astreum\lib-py\logs/<instance_id>` on Windows and `$XDG_STATE_HOME` (or `~/.local/state`)/`Astreum/lib-py/logs/<instance_id>` on other platforms. The `<instance_id>` is the first 16 hex characters of a BLAKE3 hash of the caller's file path, so running the node from different entry points keeps their logs isolated.
130
+ - Files rotate at midnight UTC with gzip compression (`node-YYYY-MM-DD.log.gz`) and retain 90 days by default. Override via `config["logging_retention"]`.
131
+ - Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
132
+ - Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
133
+ - The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
134
+
135
+ ## Testing
136
+
137
+ ```bash
138
+ python3 -m venv venv
139
+ source venv/bin/activate
140
+ pip install -e .
141
+ python3 -m unittest discover -s tests
142
+ ```
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.2.42"
3
+ version = "0.3.5"
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 = [
@@ -19,5 +19,5 @@ dependencies = [
19
19
  ]
20
20
 
21
21
  [project.urls]
22
- Homepage = "https://github.com/astreum/lib"
23
- 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,18 @@
1
+
2
+ from astreum.consensus import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
3
+ from astreum.machine import Env, Expr
4
+ from astreum.node import Node
5
+
6
+
7
+ __all__: list[str] = [
8
+ "Node",
9
+ "Env",
10
+ "Expr",
11
+ "Block",
12
+ "Chain",
13
+ "Fork",
14
+ "Receipt",
15
+ "Transaction",
16
+ "Account",
17
+ "Accounts",
18
+ ]
@@ -1,6 +1,6 @@
1
- from .message import Message
2
- from .peer import Peer
3
- from .route import Route
1
+ from .models.message import Message
2
+ from .models.peer import Peer
3
+ from .models.route import Route
4
4
  from .setup import communication_setup
5
5
 
6
6
  __all__ = [
@@ -0,0 +1,81 @@
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
10
+
11
+ if TYPE_CHECKING:
12
+ from .... import Node
13
+
14
+
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
+ """
20
+ logger = node.logger
21
+
22
+ sender_public_key_bytes = message.sender_bytes
23
+ try:
24
+ sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
25
+ except Exception as exc:
26
+ logger.warning("Error extracting sender key bytes: %s", exc)
27
+ return True
28
+
29
+ try:
30
+ host, port = addr[0], int(addr[1])
31
+ except Exception:
32
+ return True
33
+ address_key = (host, port)
34
+
35
+ old_key_bytes = node.addresses.get(address_key)
36
+ node.addresses[address_key] = sender_public_key_bytes
37
+
38
+ if old_key_bytes is None:
39
+ try:
40
+ peer = Peer(node.relay_secret_key, sender_key)
41
+ except Exception:
42
+ return True
43
+ peer.address = address_key
44
+
45
+ node.peers[sender_public_key_bytes] = peer
46
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
47
+
48
+ logger.info(
49
+ "Handshake accepted from %s:%s; peer added",
50
+ address_key[0],
51
+ address_key[1],
52
+ )
53
+ response = Message(handshake=True, sender=node.relay_public_key)
54
+ node.outgoing_queue.put((response.to_bytes(), address_key))
55
+ return True
56
+
57
+ if old_key_bytes == sender_public_key_bytes:
58
+ peer = node.peers.get(sender_public_key_bytes)
59
+ if peer is not None:
60
+ peer.address = address_key
61
+ return False
62
+
63
+ node.peers.pop(old_key_bytes, None)
64
+ try:
65
+ node.peer_route.remove_peer(old_key_bytes)
66
+ except Exception:
67
+ pass
68
+ try:
69
+ peer = Peer(node.relay_secret_key, sender_key)
70
+ except Exception:
71
+ return True
72
+ peer.address = address_key
73
+
74
+ node.peers[sender_public_key_bytes] = peer
75
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
76
+ logger.info(
77
+ "Peer at %s:%s replaced due to key change",
78
+ address_key[0],
79
+ address_key[1],
80
+ )
81
+ return False
@@ -0,0 +1,153 @@
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
+
9
+ if TYPE_CHECKING:
10
+ from .. import Node
11
+ from ..models.peer import Peer
12
+
13
+
14
+ class ObjectRequestType(IntEnum):
15
+ OBJECT_GET = 0
16
+ OBJECT_PUT = 1
17
+
18
+
19
+ class ObjectRequest:
20
+ type: ObjectRequestType
21
+ data: bytes
22
+ atom_id: bytes
23
+
24
+ def __init__(self, type: ObjectRequestType, data: bytes, atom_id: bytes = None):
25
+ self.type = type
26
+ self.data = data
27
+ self.atom_id = atom_id
28
+
29
+ def to_bytes(self):
30
+ return [self.type.value] + self.atom_id + self.data
31
+
32
+ @classmethod
33
+ def from_bytes(cls, data: bytes) -> "ObjectRequest":
34
+ # need at least 1 byte for type + 32 bytes for hash
35
+ if len(data) < 1 + 32:
36
+ raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
37
+
38
+ type_val = data[0]
39
+ try:
40
+ req_type = ObjectRequestType(type_val)
41
+ except ValueError:
42
+ raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
43
+
44
+ atom_id_bytes = data[1:33]
45
+ payload = data[33:]
46
+ return cls(req_type, payload, atom_id_bytes)
47
+
48
+
49
+ def encode_peer_contact_bytes(peer: "Peer") -> bytes:
50
+ """Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
51
+ if not peer.address:
52
+ raise ValueError("peer address is required for encoding peer info")
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", addr: Tuple[str, int], message: Message) -> None:
66
+ node_logger = getattr(node, "logger", logging.getLogger(__name__))
67
+ try:
68
+ object_request = ObjectRequest.from_bytes(message.body)
69
+ except Exception as exc:
70
+ node_logger.warning("Error decoding OBJECT_REQUEST from %s: %s", addr, exc)
71
+ return
72
+
73
+ match object_request.type:
74
+ case ObjectRequestType.OBJECT_GET:
75
+ atom_id = object_request.atom_id
76
+ node_logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), addr)
77
+
78
+ local_atom = node.local_get(atom_id)
79
+ if local_atom is not None:
80
+ node_logger.debug("Object %s found locally; returning to %s", atom_id.hex(), addr)
81
+ resp = ObjectResponse(
82
+ type=ObjectResponseType.OBJECT_FOUND,
83
+ data=local_atom.to_bytes(),
84
+ atom_id=atom_id
85
+ )
86
+ obj_res_msg = Message(
87
+ topic=MessageTopic.OBJECT_RESPONSE,
88
+ body=resp.to_bytes(),
89
+ sender=node.relay_public_key,
90
+ )
91
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
92
+ return
93
+
94
+ storage_index = getattr(node, "storage_index", None) or {}
95
+ if atom_id in storage_index:
96
+ node_logger.debug("Known provider for %s; informing %s", atom_id.hex(), addr)
97
+ provider_bytes = storage_index[atom_id]
98
+ resp = ObjectResponse(
99
+ type=ObjectResponseType.OBJECT_PROVIDER,
100
+ data=provider_bytes,
101
+ atom_id=atom_id
102
+ )
103
+ obj_res_msg = Message(
104
+ topic=MessageTopic.OBJECT_RESPONSE,
105
+ body=resp.to_bytes(),
106
+ sender=node.relay_public_key,
107
+ )
108
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
109
+ return
110
+
111
+ nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
112
+ if nearest_peer:
113
+ node_logger.debug("Forwarding requester %s to nearest peer for %s", addr, atom_id.hex())
114
+ peer_info = encode_peer_contact_bytes(nearest_peer)
115
+ resp = ObjectResponse(
116
+ type=ObjectResponseType.OBJECT_PROVIDER,
117
+ # type=ObjectResponseType.OBJECT_NEAREST_PEER,
118
+ data=peer_info,
119
+ atom_id=atom_id
120
+ )
121
+ obj_res_msg = Message(
122
+ topic=MessageTopic.OBJECT_RESPONSE,
123
+ body=resp.to_bytes(),
124
+ sender=node.relay_public_key,
125
+ )
126
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
127
+
128
+ case ObjectRequestType.OBJECT_PUT:
129
+ atom_hash = object_request.data[:32]
130
+ node_logger.debug("Handling OBJECT_PUT for %s from %s", atom_hash.hex(), addr)
131
+
132
+ nearest_peer = node.peer_route.closest_peer_for_hash(atom_hash)
133
+ nearest = (nearest_peer.public_key, nearest_peer) if nearest_peer else None
134
+ if nearest:
135
+ node_logger.debug("Forwarding OBJECT_PUT for %s to nearer peer %s", atom_hash.hex(), nearest[1].address)
136
+ fwd_req = ObjectRequest(
137
+ type=ObjectRequestType.OBJECT_PUT,
138
+ data=object_request.data,
139
+ )
140
+ obj_req_msg = Message(
141
+ topic=MessageTopic.OBJECT_REQUEST,
142
+ body=fwd_req.to_bytes(),
143
+ sender=node.relay_public_key,
144
+ )
145
+ node.outgoing_queue.put((obj_req_msg.to_bytes(), nearest[1].address))
146
+ else:
147
+ node_logger.debug("Storing provider info for %s locally", atom_hash.hex())
148
+ if not hasattr(node, "storage_index") or not isinstance(node.storage_index, dict):
149
+ node.storage_index = {}
150
+ node.storage_index[atom_hash] = object_request.data[32:]
151
+
152
+ case _:
153
+ node_logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, addr)
@@ -0,0 +1,37 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class ObjectResponseType(IntEnum):
5
+ OBJECT_FOUND = 0
6
+ OBJECT_PROVIDER = 1
7
+ OBJECT_NEAREST_PEER = 2
8
+
9
+
10
+ class ObjectResponse:
11
+ type: ObjectResponseType
12
+ data: bytes
13
+ atom_id: bytes
14
+
15
+ def __init__(self, type: ObjectResponseType, data: bytes, atom_id: bytes = None):
16
+ self.type = type
17
+ self.data = data
18
+ self.atom_id = atom_id
19
+
20
+ def to_bytes(self):
21
+ return [self.type.value] + self.atom_id + self.data
22
+
23
+ @classmethod
24
+ def from_bytes(cls, data: bytes) -> "ObjectResponse":
25
+ # need at least 1 byte for type + 32 bytes for atom id
26
+ if len(data) < 1 + 32:
27
+ raise ValueError(f"Too short to be a valid ObjectResponse ({len(data)} bytes)")
28
+
29
+ type_val = data[0]
30
+ try:
31
+ resp_type = ObjectResponseType(type_val)
32
+ except ValueError:
33
+ raise ValueError(f"Unknown ObjectResponseType: {type_val}")
34
+
35
+ atom_id = data[1:33]
36
+ payload = data[33:]
37
+ return cls(resp_type, payload, atom_id)
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING, Sequence
5
+
6
+ from ..models.ping import Ping
7
+
8
+ if TYPE_CHECKING:
9
+ from .... import Node
10
+
11
+
12
+ def handle_ping(node: "Node", addr: Sequence[object], payload: bytes) -> None:
13
+ """Update peer and validation state based on an incoming ping message."""
14
+ logger = node.logger
15
+ try:
16
+ host, port = addr[0], int(addr[1])
17
+ except Exception:
18
+ return
19
+
20
+ address_key = (host, port)
21
+ sender_public_key_bytes = node.addresses.get(address_key)
22
+ if sender_public_key_bytes is None:
23
+ return
24
+
25
+ peer = node.peers.get(sender_public_key_bytes)
26
+ if peer is None:
27
+ return
28
+
29
+ try:
30
+ ping = Ping.from_bytes(payload)
31
+ except Exception as exc:
32
+ logger.warning("Error decoding ping: %s", exc)
33
+ return
34
+
35
+ peer.timestamp = datetime.now(timezone.utc)
36
+ peer.latest_block = ping.latest_block
37
+
38
+ validation_route = node.validation_route
39
+ if validation_route is None:
40
+ return
41
+
42
+ try:
43
+ if ping.is_validator:
44
+ validation_route.add_peer(sender_public_key_bytes)
45
+ else:
46
+ validation_route.remove_peer(sender_public_key_bytes)
47
+ except Exception:
48
+ pass