astreum 0.2.27__tar.gz → 0.2.29__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.
- {astreum-0.2.27/src/astreum.egg-info → astreum-0.2.29}/PKG-INFO +1 -1
- {astreum-0.2.27 → astreum-0.2.29}/pyproject.toml +1 -1
- astreum-0.2.29/src/astreum/lispeum/environment.py +40 -0
- astreum-0.2.29/src/astreum/lispeum/expression.py +86 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/node.py +63 -404
- astreum-0.2.29/src/astreum/relay/__init__.py +0 -0
- astreum-0.2.29/src/astreum/relay/peer.py +9 -0
- astreum-0.2.29/src/astreum/relay/route.py +25 -0
- astreum-0.2.29/src/astreum/relay/setup.py +58 -0
- astreum-0.2.29/src/astreum/storage/__init__.py +0 -0
- astreum-0.2.29/src/astreum/storage/object.py +68 -0
- astreum-0.2.29/src/astreum/storage/setup.py +15 -0
- {astreum-0.2.27 → astreum-0.2.29/src/astreum.egg-info}/PKG-INFO +1 -1
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/SOURCES.txt +9 -0
- {astreum-0.2.27 → astreum-0.2.29}/LICENSE +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/README.md +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/setup.cfg +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/__init__.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/__init__.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/wesolowski.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/format.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/lispeum/__init__.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/lispeum/parser.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/lispeum/tokenizer.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/__init__.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/account.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/accounts.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/block.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/merkle.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/message.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/patricia.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/transaction.py +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/top_level.txt +0 -0
- {astreum-0.2.27 → astreum-0.2.29}/tests/test_node_machine.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.29
|
|
4
4
|
Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
|
|
5
5
|
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
6
|
Project-URL: Homepage, https://github.com/astreum/lib
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from astreum.lispeum.expression import Expr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Env:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
data: Optional[Dict[str, Expr]] = None,
|
|
11
|
+
parent_id: Optional[uuid.UUID] = None,
|
|
12
|
+
max_exprs: Optional[int] = 8,
|
|
13
|
+
):
|
|
14
|
+
self.data: Dict[str, Expr] = data if data is not None else {}
|
|
15
|
+
self.parent_id: Optional[uuid.UUID] = parent_id
|
|
16
|
+
self.max_exprs: Optional[int] = max_exprs
|
|
17
|
+
|
|
18
|
+
def put(self, name: str, value: Expr) -> None:
|
|
19
|
+
if (
|
|
20
|
+
self.max_exprs is not None
|
|
21
|
+
and name not in self.data
|
|
22
|
+
and len(self.data) >= self.max_exprs
|
|
23
|
+
):
|
|
24
|
+
raise RuntimeError(
|
|
25
|
+
f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
|
|
26
|
+
)
|
|
27
|
+
self.data[name] = value
|
|
28
|
+
|
|
29
|
+
def get(self, name: str) -> Optional[Expr]:
|
|
30
|
+
return self.data.get(name)
|
|
31
|
+
|
|
32
|
+
def pop(self, name: str) -> Optional[Expr]:
|
|
33
|
+
return self.data.pop(name, None)
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return (
|
|
37
|
+
f"Env(size={len(self.data)}, "
|
|
38
|
+
f"max_exprs={self.max_exprs}, "
|
|
39
|
+
f"parent_id={self.parent_id})"
|
|
40
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Expr:
|
|
6
|
+
class ListExpr:
|
|
7
|
+
def __init__(self, elements: List['Expr']):
|
|
8
|
+
self.elements = elements
|
|
9
|
+
|
|
10
|
+
def __eq__(self, other):
|
|
11
|
+
if not isinstance(other, Expr.ListExpr):
|
|
12
|
+
return NotImplemented
|
|
13
|
+
return self.elements == other.elements
|
|
14
|
+
|
|
15
|
+
def __ne__(self, other):
|
|
16
|
+
return not self.__eq__(other)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def value(self):
|
|
20
|
+
inner = " ".join(str(e) for e in self.elements)
|
|
21
|
+
return f"({inner})"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
if not self.elements:
|
|
26
|
+
return "()"
|
|
27
|
+
|
|
28
|
+
inner = " ".join(str(e) for e in self.elements)
|
|
29
|
+
return f"({inner})"
|
|
30
|
+
|
|
31
|
+
def __iter__(self):
|
|
32
|
+
return iter(self.elements)
|
|
33
|
+
|
|
34
|
+
def __getitem__(self, index: Union[int, slice]):
|
|
35
|
+
return self.elements[index]
|
|
36
|
+
|
|
37
|
+
def __len__(self):
|
|
38
|
+
return len(self.elements)
|
|
39
|
+
|
|
40
|
+
class Symbol:
|
|
41
|
+
def __init__(self, value: str):
|
|
42
|
+
self.value = value
|
|
43
|
+
|
|
44
|
+
def __repr__(self):
|
|
45
|
+
return self.value
|
|
46
|
+
|
|
47
|
+
class Integer:
|
|
48
|
+
def __init__(self, value: int):
|
|
49
|
+
self.value = value
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return str(self.value)
|
|
53
|
+
|
|
54
|
+
class String:
|
|
55
|
+
def __init__(self, value: str):
|
|
56
|
+
self.value = value
|
|
57
|
+
|
|
58
|
+
def __repr__(self):
|
|
59
|
+
return f'"{self.value}"'
|
|
60
|
+
|
|
61
|
+
class Boolean:
|
|
62
|
+
def __init__(self, value: bool):
|
|
63
|
+
self.value = value
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return "true" if self.value else "false"
|
|
67
|
+
|
|
68
|
+
class Function:
|
|
69
|
+
def __init__(self, params: List[str], body: 'Expr'):
|
|
70
|
+
self.params = params
|
|
71
|
+
self.body = body
|
|
72
|
+
|
|
73
|
+
def __repr__(self):
|
|
74
|
+
params_str = " ".join(self.params)
|
|
75
|
+
body_str = str(self.body)
|
|
76
|
+
return f"(fn ({params_str}) {body_str})"
|
|
77
|
+
|
|
78
|
+
class Error:
|
|
79
|
+
def __init__(self, message: str, origin: Optional['Expr'] = None):
|
|
80
|
+
self.message = message
|
|
81
|
+
self.origin = origin
|
|
82
|
+
|
|
83
|
+
def __repr__(self):
|
|
84
|
+
if self.origin is None:
|
|
85
|
+
return f'(error "{self.message}")'
|
|
86
|
+
return f'(error "{self.message}" in {self.origin})'
|
|
@@ -7,92 +7,23 @@ from typing import Tuple, Dict, Union, Optional, List
|
|
|
7
7
|
from datetime import datetime, timedelta, timezone
|
|
8
8
|
import uuid
|
|
9
9
|
|
|
10
|
+
from astreum.lispeum.environment import Env
|
|
11
|
+
from astreum.lispeum.expression import Expr
|
|
12
|
+
from astreum.relay.peer import Peer
|
|
13
|
+
from astreum.relay.route import Route
|
|
14
|
+
from astreum.relay.setup import load_ed25519, load_x25519, make_routes, setup_outgoing, setup_udp
|
|
15
|
+
from astreum.storage.object import ObjectRequest, ObjectRequestType, ObjectResponse, ObjectResponseType
|
|
16
|
+
from astreum.storage.setup import storage_setup
|
|
17
|
+
|
|
10
18
|
from .models.transaction import Transaction
|
|
11
19
|
from .format import encode, decode
|
|
12
20
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
13
21
|
from cryptography.hazmat.primitives import serialization
|
|
14
22
|
from .crypto import ed25519, x25519
|
|
15
|
-
from enum import IntEnum
|
|
16
23
|
import blake3
|
|
17
24
|
import struct
|
|
18
25
|
from .models.message import Message, MessageTopic
|
|
19
26
|
|
|
20
|
-
class ObjectRequestType(IntEnum):
|
|
21
|
-
OBJECT_GET = 0
|
|
22
|
-
OBJECT_PUT = 1
|
|
23
|
-
|
|
24
|
-
class ObjectRequest:
|
|
25
|
-
type: ObjectRequestType
|
|
26
|
-
data: bytes
|
|
27
|
-
hash: bytes
|
|
28
|
-
|
|
29
|
-
def __init__(self, type: ObjectRequestType, data: bytes, hash: bytes = None):
|
|
30
|
-
self.type = type
|
|
31
|
-
self.data = data
|
|
32
|
-
self.hash = hash
|
|
33
|
-
|
|
34
|
-
def to_bytes(self):
|
|
35
|
-
return encode([self.type.value, self.data, self.hash])
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def from_bytes(cls, data: bytes):
|
|
39
|
-
type_val, data_val, hash_val = decode(data)
|
|
40
|
-
return cls(type=ObjectRequestType(type_val[0]), data=data_val, hash=hash_val)
|
|
41
|
-
|
|
42
|
-
class ObjectResponseType(IntEnum):
|
|
43
|
-
OBJECT_FOUND = 0
|
|
44
|
-
OBJECT_PROVIDER = 1
|
|
45
|
-
OBJECT_NEAREST_PEER = 2
|
|
46
|
-
|
|
47
|
-
class ObjectResponse:
|
|
48
|
-
type: ObjectResponseType
|
|
49
|
-
data: bytes
|
|
50
|
-
hash: bytes
|
|
51
|
-
|
|
52
|
-
def __init__(self, type: ObjectResponseType, data: bytes, hash: bytes = None):
|
|
53
|
-
self.type = type
|
|
54
|
-
self.data = data
|
|
55
|
-
self.hash = hash
|
|
56
|
-
|
|
57
|
-
def to_bytes(self):
|
|
58
|
-
return encode([self.type.value, self.data, self.hash])
|
|
59
|
-
|
|
60
|
-
@classmethod
|
|
61
|
-
def from_bytes(cls, data: bytes):
|
|
62
|
-
type_val, data_val, hash_val = decode(data)
|
|
63
|
-
return cls(type=ObjectResponseType(type_val[0]), data=data_val, hash=hash_val)
|
|
64
|
-
|
|
65
|
-
class Peer:
|
|
66
|
-
shared_key: bytes
|
|
67
|
-
timestamp: datetime
|
|
68
|
-
def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
|
|
69
|
-
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
70
|
-
self.timestamp = datetime.now(timezone.utc)
|
|
71
|
-
|
|
72
|
-
class Route:
|
|
73
|
-
def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
|
|
74
|
-
self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
75
|
-
self.bucket_size = bucket_size
|
|
76
|
-
self.buckets: Dict[int, List[X25519PublicKey]] = {
|
|
77
|
-
i: [] for i in range(len(self.relay_public_key_bytes) * 8)
|
|
78
|
-
}
|
|
79
|
-
self.peers = {}
|
|
80
|
-
|
|
81
|
-
@staticmethod
|
|
82
|
-
def _matching_leading_bits(a: bytes, b: bytes) -> int:
|
|
83
|
-
for byte_index, (ba, bb) in enumerate(zip(a, b)):
|
|
84
|
-
diff = ba ^ bb
|
|
85
|
-
if diff:
|
|
86
|
-
return byte_index * 8 + (8 - diff.bit_length())
|
|
87
|
-
return len(a) * 8
|
|
88
|
-
|
|
89
|
-
def add_peer(self, peer_public_key: X25519PublicKey):
|
|
90
|
-
peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
91
|
-
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
92
|
-
if len(self.buckets[bucket_idx]) < self.bucket_size:
|
|
93
|
-
self.buckets[bucket_idx].append(peer_public_key)
|
|
94
|
-
|
|
95
|
-
|
|
96
27
|
def encode_ip_address(host: str, port: int) -> bytes:
|
|
97
28
|
ip_bytes = socket.inet_pton(socket.AF_INET6 if ':' in host else socket.AF_INET, host)
|
|
98
29
|
port_bytes = struct.pack("!H", port)
|
|
@@ -109,135 +40,18 @@ def decode_ip_address(data: bytes) -> tuple[str, int]:
|
|
|
109
40
|
raise ValueError("Invalid address byte format")
|
|
110
41
|
return ip, port
|
|
111
42
|
|
|
112
|
-
# =========
|
|
113
|
-
# MACHINE
|
|
114
|
-
# =========
|
|
115
|
-
|
|
116
|
-
class Expr:
|
|
117
|
-
class ListExpr:
|
|
118
|
-
def __init__(self, elements: List['Expr']):
|
|
119
|
-
self.elements = elements
|
|
120
|
-
|
|
121
|
-
def __eq__(self, other):
|
|
122
|
-
if not isinstance(other, Expr.ListExpr):
|
|
123
|
-
return NotImplemented
|
|
124
|
-
return self.elements == other.elements
|
|
125
|
-
|
|
126
|
-
def __ne__(self, other):
|
|
127
|
-
return not self.__eq__(other)
|
|
128
|
-
|
|
129
|
-
@property
|
|
130
|
-
def value(self):
|
|
131
|
-
inner = " ".join(str(e) for e in self.elements)
|
|
132
|
-
return f"({inner})"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def __repr__(self):
|
|
136
|
-
if not self.elements:
|
|
137
|
-
return "()"
|
|
138
|
-
|
|
139
|
-
inner = " ".join(str(e) for e in self.elements)
|
|
140
|
-
return f"({inner})"
|
|
141
|
-
|
|
142
|
-
def __iter__(self):
|
|
143
|
-
return iter(self.elements)
|
|
144
|
-
|
|
145
|
-
def __getitem__(self, index: Union[int, slice]):
|
|
146
|
-
return self.elements[index]
|
|
147
|
-
|
|
148
|
-
def __len__(self):
|
|
149
|
-
return len(self.elements)
|
|
150
|
-
|
|
151
|
-
class Symbol:
|
|
152
|
-
def __init__(self, value: str):
|
|
153
|
-
self.value = value
|
|
154
|
-
|
|
155
|
-
def __repr__(self):
|
|
156
|
-
return self.value
|
|
157
|
-
|
|
158
|
-
class Integer:
|
|
159
|
-
def __init__(self, value: int):
|
|
160
|
-
self.value = value
|
|
161
|
-
|
|
162
|
-
def __repr__(self):
|
|
163
|
-
return str(self.value)
|
|
164
|
-
|
|
165
|
-
class String:
|
|
166
|
-
def __init__(self, value: str):
|
|
167
|
-
self.value = value
|
|
168
|
-
|
|
169
|
-
def __repr__(self):
|
|
170
|
-
return f'"{self.value}"'
|
|
171
|
-
|
|
172
|
-
class Boolean:
|
|
173
|
-
def __init__(self, value: bool):
|
|
174
|
-
self.value = value
|
|
175
|
-
|
|
176
|
-
def __repr__(self):
|
|
177
|
-
return "true" if self.value else "false"
|
|
178
|
-
|
|
179
|
-
class Function:
|
|
180
|
-
def __init__(self, params: List[str], body: 'Expr'):
|
|
181
|
-
self.params = params
|
|
182
|
-
self.body = body
|
|
183
|
-
|
|
184
|
-
def __repr__(self):
|
|
185
|
-
params_str = " ".join(self.params)
|
|
186
|
-
body_str = str(self.body)
|
|
187
|
-
return f"(fn ({params_str}) {body_str})"
|
|
188
|
-
|
|
189
|
-
class Error:
|
|
190
|
-
def __init__(self, message: str, origin: Optional['Expr'] = None):
|
|
191
|
-
self.message = message
|
|
192
|
-
self.origin = origin
|
|
193
|
-
|
|
194
|
-
def __repr__(self):
|
|
195
|
-
if self.origin is None:
|
|
196
|
-
return f'(error "{self.message}")'
|
|
197
|
-
return f'(error "{self.message}" in {self.origin})'
|
|
198
|
-
|
|
199
|
-
class Env:
|
|
200
|
-
def __init__(
|
|
201
|
-
self,
|
|
202
|
-
data: Optional[Dict[str, Expr]] = None,
|
|
203
|
-
parent_id: Optional[uuid.UUID] = None,
|
|
204
|
-
max_exprs: Optional[int] = 8,
|
|
205
|
-
):
|
|
206
|
-
self.data: Dict[str, Expr] = data if data is not None else {}
|
|
207
|
-
self.parent_id: Optional[uuid.UUID] = parent_id
|
|
208
|
-
self.max_exprs: Optional[int] = max_exprs
|
|
209
|
-
|
|
210
|
-
def put(self, name: str, value: Expr) -> None:
|
|
211
|
-
if (
|
|
212
|
-
self.max_exprs is not None
|
|
213
|
-
and name not in self.data
|
|
214
|
-
and len(self.data) >= self.max_exprs
|
|
215
|
-
):
|
|
216
|
-
raise RuntimeError(
|
|
217
|
-
f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
|
|
218
|
-
)
|
|
219
|
-
self.data[name] = value
|
|
220
|
-
|
|
221
|
-
def get(self, name: str) -> Optional[Expr]:
|
|
222
|
-
return self.data.get(name)
|
|
223
|
-
|
|
224
|
-
def pop(self, name: str) -> Optional[Expr]:
|
|
225
|
-
return self.data.pop(name, None)
|
|
226
|
-
|
|
227
|
-
def __repr__(self) -> str:
|
|
228
|
-
return (
|
|
229
|
-
f"Env(size={len(self.data)}, "
|
|
230
|
-
f"max_exprs={self.max_exprs}, "
|
|
231
|
-
f"parent_id={self.parent_id})"
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
43
|
class Node:
|
|
236
44
|
def __init__(self, config: dict = {}):
|
|
237
45
|
self._machine_setup()
|
|
238
46
|
machine_only = bool(config.get('machine-only', True))
|
|
239
47
|
if not machine_only:
|
|
240
|
-
|
|
48
|
+
(
|
|
49
|
+
self.storage_path,
|
|
50
|
+
self.memory_storage,
|
|
51
|
+
self.storage_get_relay_timeout,
|
|
52
|
+
self.storage_index
|
|
53
|
+
) = storage_setup(config)
|
|
54
|
+
|
|
241
55
|
self._relay_setup(config=config)
|
|
242
56
|
self._validation_setup(config=config)
|
|
243
57
|
|
|
@@ -250,87 +64,46 @@ class Node:
|
|
|
250
64
|
def _create_block(self):
|
|
251
65
|
pass
|
|
252
66
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
storage_path_str = config.get('storage_path')
|
|
256
|
-
if storage_path_str is None:
|
|
257
|
-
self.storage_path = None
|
|
258
|
-
self.memory_storage = {}
|
|
259
|
-
else:
|
|
260
|
-
self.storage_path = Path(storage_path_str)
|
|
261
|
-
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
262
|
-
self.memory_storage = None
|
|
67
|
+
def _relay_setup(self, config: dict):
|
|
68
|
+
self.use_ipv6 = config.get('use_ipv6', False)
|
|
263
69
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
self.
|
|
70
|
+
# key loading
|
|
71
|
+
self.relay_secret_key = load_x25519(config.get('relay_secret_key'))
|
|
72
|
+
self.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
|
|
267
73
|
|
|
268
|
-
|
|
269
|
-
self.
|
|
270
|
-
|
|
74
|
+
# derive pubs + routes
|
|
75
|
+
self.relay_public_key = self.relay_secret_key.public_key()
|
|
76
|
+
self.peer_route, self.validation_route = make_routes(
|
|
77
|
+
self.relay_public_key,
|
|
78
|
+
self.validation_secret_key
|
|
79
|
+
)
|
|
271
80
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
self.relay_secret_key = ed25519.Ed25519PrivateKey.generate()
|
|
280
|
-
|
|
281
|
-
self.relay_public_key = self.relay_secret_key.public_key()
|
|
81
|
+
# sockets + queues + threads
|
|
82
|
+
(self.incoming_socket,
|
|
83
|
+
self.incoming_port,
|
|
84
|
+
self.incoming_queue,
|
|
85
|
+
self.incoming_populate_thread,
|
|
86
|
+
self.incoming_process_thread
|
|
87
|
+
) = setup_udp(config.get('incoming_port', 7373), self.use_ipv6)
|
|
282
88
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
except Exception as e:
|
|
288
|
-
raise Exception(f"Error loading validation secret key provided: {e}")
|
|
289
|
-
|
|
290
|
-
# setup peer route and validation route
|
|
291
|
-
self.peer_route = Route(self.relay_public_key)
|
|
292
|
-
if self.validation_secret_key:
|
|
293
|
-
self.validation_route = Route(self.relay_public_key)
|
|
294
|
-
|
|
295
|
-
# Choose address family based on IPv4 or IPv6
|
|
296
|
-
family = socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
|
|
297
|
-
|
|
298
|
-
self.incoming_socket = socket.socket(family, socket.SOCK_DGRAM)
|
|
299
|
-
if self.use_ipv6:
|
|
300
|
-
self.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
301
|
-
bind_address = "::" if self.use_ipv6 else "0.0.0.0"
|
|
302
|
-
self.incoming_socket.bind((bind_address, incoming_port or 0))
|
|
303
|
-
self.incoming_port = self.incoming_socket.getsockname()[1]
|
|
304
|
-
self.incoming_queue = Queue()
|
|
305
|
-
|
|
306
|
-
self.incoming_populate_thread = threading.Thread(target=self._relay_incoming_queue_populating)
|
|
307
|
-
self.incoming_populate_thread.daemon = True
|
|
308
|
-
self.incoming_populate_thread.start()
|
|
309
|
-
|
|
310
|
-
self.incoming_process_thread = threading.Thread(target=self._relay_incoming_queue_processing)
|
|
311
|
-
self.incoming_process_thread.daemon = True
|
|
312
|
-
self.incoming_process_thread.start()
|
|
313
|
-
|
|
314
|
-
# outgoing thread
|
|
315
|
-
self.outgoing_socket = socket.socket(family, socket.SOCK_DGRAM)
|
|
316
|
-
self.outgoing_queue = Queue()
|
|
317
|
-
self.outgoing_thread = threading.Thread(target=self._relay_outgoing_queue_processor)
|
|
318
|
-
self.outgoing_thread.daemon = True
|
|
319
|
-
self.outgoing_thread.start()
|
|
89
|
+
(self.outgoing_socket,
|
|
90
|
+
self.outgoing_queue,
|
|
91
|
+
self.outgoing_thread
|
|
92
|
+
) = setup_outgoing(self.use_ipv6)
|
|
320
93
|
|
|
94
|
+
# other workers & maps
|
|
321
95
|
self.object_request_queue = Queue()
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
96
|
+
self.peer_manager_thread = threading.Thread(
|
|
97
|
+
target=self._relay_peer_manager,
|
|
98
|
+
daemon=True
|
|
99
|
+
)
|
|
325
100
|
self.peer_manager_thread.start()
|
|
326
101
|
|
|
327
|
-
self.peers = Dict[X25519PublicKey,
|
|
328
|
-
self.addresses = Dict[Tuple[str, int], X25519PublicKey]
|
|
329
|
-
|
|
330
|
-
if 'bootstrap' in config:
|
|
331
|
-
for addr in config['bootstrap']:
|
|
332
|
-
self._send_ping(addr)
|
|
102
|
+
self.peers, self.addresses = {}, {} # peers: Dict[X25519PublicKey,Peer], addresses: Dict[(str,int),X25519PublicKey]
|
|
333
103
|
|
|
104
|
+
# bootstrap pings
|
|
105
|
+
for addr in config.get('bootstrap', []):
|
|
106
|
+
self._send_ping(addr)
|
|
334
107
|
|
|
335
108
|
def _local_object_get(self, data_hash: bytes) -> Optional[bytes]:
|
|
336
109
|
if self.memory_storage is not None:
|
|
@@ -701,137 +474,10 @@ class Node:
|
|
|
701
474
|
return result
|
|
702
475
|
|
|
703
476
|
# # List
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
# args = expr.elements[1:]
|
|
709
|
-
# if len(args) != 2:
|
|
710
|
-
# return Expr.Error(
|
|
711
|
-
# category="SyntaxError",
|
|
712
|
-
# message="list.get expects exactly two arguments: a list and an index"
|
|
713
|
-
# )
|
|
714
|
-
# list_obj = self.evaluate_expression(args[0], env)
|
|
715
|
-
# index = self.evaluate_expression(args[1], env)
|
|
716
|
-
# return handle_list_get(self, list_obj, index, env)
|
|
717
|
-
|
|
718
|
-
# elif first.value == "list.insert":
|
|
719
|
-
# args = expr.elements[1:]
|
|
720
|
-
# if len(args) != 3:
|
|
721
|
-
# return Expr.ListExpr([
|
|
722
|
-
# Expr.ListExpr([]),
|
|
723
|
-
# Expr.String("list.insert expects exactly three arguments: a list, an index, and a value")
|
|
724
|
-
# ])
|
|
725
|
-
|
|
726
|
-
# return handle_list_insert(
|
|
727
|
-
# list=self.evaluate_expression(args[0], env),
|
|
728
|
-
# index=self.evaluate_expression(args[1], env),
|
|
729
|
-
# value=self.evaluate_expression(args[2], env),
|
|
730
|
-
# )
|
|
731
|
-
|
|
732
|
-
# elif first.value == "list.remove":
|
|
733
|
-
# args = expr.elements[1:]
|
|
734
|
-
# if len(args) != 2:
|
|
735
|
-
# return Expr.ListExpr([
|
|
736
|
-
# Expr.ListExpr([]),
|
|
737
|
-
# Expr.String("list.remove expects exactly two arguments: a list and an index")
|
|
738
|
-
# ])
|
|
739
|
-
|
|
740
|
-
# return handle_list_remove(
|
|
741
|
-
# list=self.evaluate_expression(args[0], env),
|
|
742
|
-
# index=self.evaluate_expression(args[1], env),
|
|
743
|
-
# )
|
|
744
|
-
|
|
745
|
-
# elif first.value == "list.length":
|
|
746
|
-
# args = expr.elements[1:]
|
|
747
|
-
# if len(args) != 1:
|
|
748
|
-
# return Expr.ListExpr([
|
|
749
|
-
# Expr.ListExpr([]),
|
|
750
|
-
# Expr.String("list.length expects exactly one argument: a list")
|
|
751
|
-
# ])
|
|
752
|
-
|
|
753
|
-
# list_obj = self.evaluate_expression(args[0], env)
|
|
754
|
-
# if not isinstance(list_obj, Expr.ListExpr):
|
|
755
|
-
# return Expr.ListExpr([
|
|
756
|
-
# Expr.ListExpr([]),
|
|
757
|
-
# Expr.String("Argument must be a list")
|
|
758
|
-
# ])
|
|
759
|
-
|
|
760
|
-
# return Expr.ListExpr([
|
|
761
|
-
# Expr.Integer(len(list_obj.elements)),
|
|
762
|
-
# Expr.ListExpr([])
|
|
763
|
-
# ])
|
|
764
|
-
|
|
765
|
-
# elif first.value == "list.fold":
|
|
766
|
-
# if len(args) != 3:
|
|
767
|
-
# return Expr.ListExpr([
|
|
768
|
-
# Expr.ListExpr([]),
|
|
769
|
-
# Expr.String("list.fold expects exactly three arguments: a list, an initial value, and a function")
|
|
770
|
-
# ])
|
|
771
|
-
|
|
772
|
-
# return handle_list_fold(
|
|
773
|
-
# machine=self,
|
|
774
|
-
# list=self.evaluate_expression(args[0], env),
|
|
775
|
-
# initial=self.evaluate_expression(args[1], env),
|
|
776
|
-
# func=self.evaluate_expression(args[2], env),
|
|
777
|
-
# env=env,
|
|
778
|
-
# )
|
|
779
|
-
|
|
780
|
-
# elif first.value == "list.map":
|
|
781
|
-
# if len(args) != 2:
|
|
782
|
-
# return Expr.ListExpr([
|
|
783
|
-
# Expr.ListExpr([]),
|
|
784
|
-
# Expr.String("list.map expects exactly two arguments: a list and a function")
|
|
785
|
-
# ])
|
|
786
|
-
|
|
787
|
-
# return handle_list_map(
|
|
788
|
-
# machine=self,
|
|
789
|
-
# list=self.evaluate_expression(args[0], env),
|
|
790
|
-
# func=self.evaluate_expression(args[1], env),
|
|
791
|
-
# env=env,
|
|
792
|
-
# )
|
|
793
|
-
|
|
794
|
-
# elif first.value == "list.position":
|
|
795
|
-
# if len(args) != 2:
|
|
796
|
-
# return Expr.ListExpr([
|
|
797
|
-
# Expr.ListExpr([]),
|
|
798
|
-
# Expr.String("list.position expects exactly two arguments: a list and a function")
|
|
799
|
-
# ])
|
|
800
|
-
|
|
801
|
-
# return handle_list_position(
|
|
802
|
-
# machine=self,
|
|
803
|
-
# list=self.evaluate_expression(args[0], env),
|
|
804
|
-
# predicate=self.evaluate_expression(args[1], env),
|
|
805
|
-
# env=env,
|
|
806
|
-
# )
|
|
807
|
-
|
|
808
|
-
# elif first.value == "list.any":
|
|
809
|
-
# if len(args) != 2:
|
|
810
|
-
# return Expr.ListExpr([
|
|
811
|
-
# Expr.ListExpr([]),
|
|
812
|
-
# Expr.String("list.any expects exactly two arguments: a list and a function")
|
|
813
|
-
# ])
|
|
814
|
-
|
|
815
|
-
# return handle_list_any(
|
|
816
|
-
# machine=self,
|
|
817
|
-
# list=self.evaluate_expression(args[0], env),
|
|
818
|
-
# predicate=self.evaluate_expression(args[1], env),
|
|
819
|
-
# env=env,
|
|
820
|
-
# )
|
|
821
|
-
|
|
822
|
-
# elif first.value == "list.all":
|
|
823
|
-
# if len(args) != 2:
|
|
824
|
-
# return Expr.ListExpr([
|
|
825
|
-
# Expr.ListExpr([]),
|
|
826
|
-
# Expr.String("list.all expects exactly two arguments: a list and a function")
|
|
827
|
-
# ])
|
|
828
|
-
|
|
829
|
-
# return handle_list_all(
|
|
830
|
-
# machine=self,
|
|
831
|
-
# list=self.evaluate_expression(args[0], env),
|
|
832
|
-
# predicate=self.evaluate_expression(args[1], env),
|
|
833
|
-
# env=env,
|
|
834
|
-
# )
|
|
477
|
+
elif first.value == "list.each":
|
|
478
|
+
internal_function = expr.elements[1]
|
|
479
|
+
|
|
480
|
+
|
|
835
481
|
|
|
836
482
|
# Integer arithmetic primitives
|
|
837
483
|
elif first.value == "+":
|
|
@@ -918,6 +564,19 @@ class Node:
|
|
|
918
564
|
|
|
919
565
|
return Expr.Boolean(res)
|
|
920
566
|
|
|
567
|
+
if isinstance(first, Expr.Function):
|
|
568
|
+
arg_exprs = expr.elements[1:]
|
|
569
|
+
if len(arg_exprs) != len(first.params):
|
|
570
|
+
return Expr.Error(f"arity mismatch: expected {len(first.params)}, got {len(arg_exprs)}", origin=expr)
|
|
571
|
+
|
|
572
|
+
call_env = self.machine_create_environment(parent_id=env_id)
|
|
573
|
+
for name, aexpr in zip(first.params, arg_exprs):
|
|
574
|
+
val = self.machine_expr_eval(env_id, aexpr)
|
|
575
|
+
if isinstance(val, Expr.Error): return val
|
|
576
|
+
self.machine_expr_put(call_env, name, val)
|
|
577
|
+
|
|
578
|
+
return self.machine_expr_eval(env_id=call_env, expr=first.body)
|
|
579
|
+
|
|
921
580
|
else:
|
|
922
581
|
evaluated_elements = [self.machine_expr_eval(env_id=env_id, expr=e) for e in expr.elements]
|
|
923
582
|
return Expr.ListExpr(evaluated_elements)
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
class Peer:
|
|
5
|
+
shared_key: bytes
|
|
6
|
+
timestamp: datetime
|
|
7
|
+
def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
|
|
8
|
+
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
9
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
3
|
+
|
|
4
|
+
class Route:
|
|
5
|
+
def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
|
|
6
|
+
self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
7
|
+
self.bucket_size = bucket_size
|
|
8
|
+
self.buckets: Dict[int, List[X25519PublicKey]] = {
|
|
9
|
+
i: [] for i in range(len(self.relay_public_key_bytes) * 8)
|
|
10
|
+
}
|
|
11
|
+
self.peers = {}
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _matching_leading_bits(a: bytes, b: bytes) -> int:
|
|
15
|
+
for byte_index, (ba, bb) in enumerate(zip(a, b)):
|
|
16
|
+
diff = ba ^ bb
|
|
17
|
+
if diff:
|
|
18
|
+
return byte_index * 8 + (8 - diff.bit_length())
|
|
19
|
+
return len(a) * 8
|
|
20
|
+
|
|
21
|
+
def add_peer(self, peer_public_key: X25519PublicKey):
|
|
22
|
+
peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
23
|
+
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
24
|
+
if len(self.buckets[bucket_idx]) < self.bucket_size:
|
|
25
|
+
self.buckets[bucket_idx].append(peer_public_key)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import socket, threading
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import (
|
|
6
|
+
X25519PrivateKey,
|
|
7
|
+
X25519PublicKey,
|
|
8
|
+
)
|
|
9
|
+
from yourproject.routes import Route
|
|
10
|
+
|
|
11
|
+
def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
|
|
12
|
+
"""DH key for relaying (always X25519)."""
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
|
|
16
|
+
"""Signing key for validation (Ed25519), or None if absent."""
|
|
17
|
+
return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
|
|
18
|
+
if hex_key else None
|
|
19
|
+
|
|
20
|
+
def make_routes(
|
|
21
|
+
relay_pk: X25519PublicKey,
|
|
22
|
+
val_sk: Optional[ed25519.Ed25519PrivateKey]
|
|
23
|
+
) -> Tuple[Route, Optional[Route]]:
|
|
24
|
+
"""Peer route (DH pubkey) + optional validation route (ed pubkey)."""
|
|
25
|
+
peer_rt = Route(relay_pk)
|
|
26
|
+
val_rt = Route(val_sk.public_key()) if val_sk else None
|
|
27
|
+
return peer_rt, val_rt
|
|
28
|
+
|
|
29
|
+
def setup_udp(
|
|
30
|
+
bind_port: int,
|
|
31
|
+
use_ipv6: bool
|
|
32
|
+
) -> Tuple[socket.socket, int, Queue, threading.Thread, threading.Thread]:
|
|
33
|
+
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
34
|
+
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
35
|
+
if use_ipv6:
|
|
36
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
37
|
+
sock.bind(("::" if use_ipv6 else "0.0.0.0", bind_port or 0))
|
|
38
|
+
port = sock.getsockname()[1]
|
|
39
|
+
|
|
40
|
+
q = Queue()
|
|
41
|
+
pop = threading.Thread(target=lambda: None, daemon=True)
|
|
42
|
+
proc = threading.Thread(target=lambda: None, daemon=True)
|
|
43
|
+
pop.start(); proc.start()
|
|
44
|
+
return sock, port, q, pop, proc
|
|
45
|
+
|
|
46
|
+
def setup_outgoing(
|
|
47
|
+
use_ipv6: bool
|
|
48
|
+
) -> Tuple[socket.socket, Queue, threading.Thread]:
|
|
49
|
+
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
50
|
+
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
51
|
+
q = Queue()
|
|
52
|
+
thr = threading.Thread(target=lambda: None, daemon=True)
|
|
53
|
+
thr.start()
|
|
54
|
+
return sock, q, thr
|
|
55
|
+
|
|
56
|
+
def make_maps():
|
|
57
|
+
"""Empty lookup maps: peers and addresses."""
|
|
58
|
+
return
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
class ObjectRequestType(IntEnum):
|
|
4
|
+
OBJECT_GET = 0
|
|
5
|
+
OBJECT_PUT = 1
|
|
6
|
+
|
|
7
|
+
class ObjectRequest:
|
|
8
|
+
type: ObjectRequestType
|
|
9
|
+
data: bytes
|
|
10
|
+
hash: bytes
|
|
11
|
+
|
|
12
|
+
def __init__(self, type: ObjectRequestType, data: bytes, hash: bytes = None):
|
|
13
|
+
self.type = type
|
|
14
|
+
self.data = data
|
|
15
|
+
self.hash = hash
|
|
16
|
+
|
|
17
|
+
def to_bytes(self):
|
|
18
|
+
return [self.type.value] + self.hash + self.data
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_bytes(cls, data: bytes) -> "ObjectRequest":
|
|
22
|
+
# need at least 1 byte for type + 32 bytes for hash
|
|
23
|
+
if len(data) < 1 + 32:
|
|
24
|
+
raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
|
|
25
|
+
|
|
26
|
+
type_val = data[0]
|
|
27
|
+
try:
|
|
28
|
+
req_type = ObjectRequestType(type_val)
|
|
29
|
+
except ValueError:
|
|
30
|
+
raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
|
|
31
|
+
|
|
32
|
+
hash_bytes = data[1:33]
|
|
33
|
+
payload = data[33:]
|
|
34
|
+
return cls(req_type, payload, hash_bytes)
|
|
35
|
+
|
|
36
|
+
class ObjectResponseType(IntEnum):
|
|
37
|
+
OBJECT_FOUND = 0
|
|
38
|
+
OBJECT_PROVIDER = 1
|
|
39
|
+
OBJECT_NEAREST_PEER = 2
|
|
40
|
+
|
|
41
|
+
class ObjectResponse:
|
|
42
|
+
type: ObjectResponseType
|
|
43
|
+
data: bytes
|
|
44
|
+
hash: bytes
|
|
45
|
+
|
|
46
|
+
def __init__(self, type: ObjectResponseType, data: bytes, hash: bytes = None):
|
|
47
|
+
self.type = type
|
|
48
|
+
self.data = data
|
|
49
|
+
self.hash = hash
|
|
50
|
+
|
|
51
|
+
def to_bytes(self):
|
|
52
|
+
return [self.type.value] + self.hash + self.data
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_bytes(cls, data: bytes) -> "ObjectResponse":
|
|
56
|
+
# need at least 1 byte for type + 32 bytes for hash
|
|
57
|
+
if len(data) < 1 + 32:
|
|
58
|
+
raise ValueError(f"Too short to be a valid ObjectResponse ({len(data)} bytes)")
|
|
59
|
+
|
|
60
|
+
type_val = data[0]
|
|
61
|
+
try:
|
|
62
|
+
resp_type = ObjectResponseType(type_val)
|
|
63
|
+
except ValueError:
|
|
64
|
+
raise ValueError(f"Unknown ObjectResponseType: {type_val}")
|
|
65
|
+
|
|
66
|
+
hash_bytes = data[1:33]
|
|
67
|
+
payload = data[33:]
|
|
68
|
+
return cls(resp_type, payload, hash_bytes)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Dict, Tuple, Any
|
|
3
|
+
|
|
4
|
+
def storage_setup(config: dict) -> Tuple[Optional[Path], Dict[bytes, Any], int, Dict[bytes, bytes]]:
|
|
5
|
+
storage_path_str = config.get('storage_path')
|
|
6
|
+
if storage_path_str is None:
|
|
7
|
+
storage_path, memory_storage = None, {}
|
|
8
|
+
else:
|
|
9
|
+
storage_path = Path(storage_path_str)
|
|
10
|
+
storage_path.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
memory_storage = None
|
|
12
|
+
|
|
13
|
+
timeout = config.get('storage_get_relay_timeout', 5)
|
|
14
|
+
storage_index: Dict[bytes, bytes] = {}
|
|
15
|
+
return storage_path, memory_storage, timeout, storage_index
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.29
|
|
4
4
|
Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
|
|
5
5
|
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
6
|
Project-URL: Homepage, https://github.com/astreum/lib
|
|
@@ -15,6 +15,8 @@ src/astreum/crypto/quadratic_form.py
|
|
|
15
15
|
src/astreum/crypto/wesolowski.py
|
|
16
16
|
src/astreum/crypto/x25519.py
|
|
17
17
|
src/astreum/lispeum/__init__.py
|
|
18
|
+
src/astreum/lispeum/environment.py
|
|
19
|
+
src/astreum/lispeum/expression.py
|
|
18
20
|
src/astreum/lispeum/parser.py
|
|
19
21
|
src/astreum/lispeum/tokenizer.py
|
|
20
22
|
src/astreum/models/__init__.py
|
|
@@ -25,4 +27,11 @@ src/astreum/models/merkle.py
|
|
|
25
27
|
src/astreum/models/message.py
|
|
26
28
|
src/astreum/models/patricia.py
|
|
27
29
|
src/astreum/models/transaction.py
|
|
30
|
+
src/astreum/relay/__init__.py
|
|
31
|
+
src/astreum/relay/peer.py
|
|
32
|
+
src/astreum/relay/route.py
|
|
33
|
+
src/astreum/relay/setup.py
|
|
34
|
+
src/astreum/storage/__init__.py
|
|
35
|
+
src/astreum/storage/object.py
|
|
36
|
+
src/astreum/storage/setup.py
|
|
28
37
|
tests/test_node_machine.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|