astreum 0.2.26__py3-none-any.whl → 0.2.28__py3-none-any.whl
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/lispeum/environment.py +40 -0
- astreum/lispeum/expression.py +86 -0
- astreum/models/message.py +64 -0
- astreum/node.py +52 -334
- astreum/storage/__init__.py +0 -0
- astreum/storage/object.py +68 -0
- astreum/storage/setup.py +16 -0
- {astreum-0.2.26.dist-info → astreum-0.2.28.dist-info}/METADATA +1 -1
- {astreum-0.2.26.dist-info → astreum-0.2.28.dist-info}/RECORD +12 -6
- {astreum-0.2.26.dist-info → astreum-0.2.28.dist-info}/WHEEL +0 -0
- {astreum-0.2.26.dist-info → astreum-0.2.28.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.26.dist-info → astreum-0.2.28.dist-info}/top_level.txt +0 -0
|
@@ -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})'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
3
|
+
|
|
4
|
+
class MessageTopic(IntEnum):
|
|
5
|
+
PING = 0
|
|
6
|
+
OBJECT_REQUEST = 1
|
|
7
|
+
OBJECT_RESPONSE = 2
|
|
8
|
+
ROUTE_REQUEST = 3
|
|
9
|
+
ROUTE_RESPONSE = 4
|
|
10
|
+
|
|
11
|
+
class Message:
|
|
12
|
+
handshake: bool
|
|
13
|
+
sender: X25519PublicKey
|
|
14
|
+
|
|
15
|
+
topic: MessageTopic
|
|
16
|
+
content: bytes
|
|
17
|
+
|
|
18
|
+
def to_bytes(self):
|
|
19
|
+
if self.handshake:
|
|
20
|
+
# handshake byte (1) + raw public key bytes
|
|
21
|
+
return bytes([1]) + self.sender.public_bytes(
|
|
22
|
+
encoding=serialization.Encoding.Raw,
|
|
23
|
+
format=serialization.PublicFormat.Raw
|
|
24
|
+
)
|
|
25
|
+
else:
|
|
26
|
+
# normal message: 0 + topic + content
|
|
27
|
+
return bytes([0, self.topic.value]) + self.content
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_bytes(cls, data: bytes) -> "Message":
|
|
31
|
+
if len(data) < 1:
|
|
32
|
+
raise ValueError("Cannot parse Message: no data")
|
|
33
|
+
flag = data[0]
|
|
34
|
+
# create empty instance
|
|
35
|
+
msg = cls.__new__(cls)
|
|
36
|
+
|
|
37
|
+
if flag == 1:
|
|
38
|
+
# handshake message: the rest is the peer’s public key
|
|
39
|
+
key_bytes = data[1:]
|
|
40
|
+
try:
|
|
41
|
+
sender = X25519PublicKey.from_public_bytes(key_bytes)
|
|
42
|
+
except ValueError:
|
|
43
|
+
raise ValueError("Invalid public key bytes")
|
|
44
|
+
msg.handshake = True
|
|
45
|
+
msg.sender = sender
|
|
46
|
+
msg.topic = None
|
|
47
|
+
msg.content = b''
|
|
48
|
+
elif flag == 0:
|
|
49
|
+
# normal message: next byte is topic, rest is content
|
|
50
|
+
if len(data) < 2:
|
|
51
|
+
raise ValueError("Cannot parse Message: missing topic byte")
|
|
52
|
+
topic_val = data[1]
|
|
53
|
+
try:
|
|
54
|
+
topic = MessageTopic(topic_val)
|
|
55
|
+
except ValueError:
|
|
56
|
+
raise ValueError(f"Unknown MessageTopic: {topic_val}")
|
|
57
|
+
msg.handshake = False
|
|
58
|
+
msg.sender = None
|
|
59
|
+
msg.topic = topic
|
|
60
|
+
msg.content = data[2:]
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Invalid handshake flag: {flag}")
|
|
63
|
+
|
|
64
|
+
return msg
|
astreum/node.py
CHANGED
|
@@ -7,160 +7,27 @@ 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.storage.object import ObjectRequest, ObjectRequestType, ObjectResponse, ObjectResponseType
|
|
13
|
+
from astreum.storage.setup import storage_setup
|
|
14
|
+
|
|
10
15
|
from .models.transaction import Transaction
|
|
11
16
|
from .format import encode, decode
|
|
12
17
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
13
18
|
from cryptography.hazmat.primitives import serialization
|
|
14
19
|
from .crypto import ed25519, x25519
|
|
15
|
-
from enum import IntEnum
|
|
16
20
|
import blake3
|
|
17
21
|
import struct
|
|
22
|
+
from .models.message import Message, MessageTopic
|
|
18
23
|
|
|
19
|
-
class ObjectRequestType(IntEnum):
|
|
20
|
-
OBJECT_GET = 0
|
|
21
|
-
OBJECT_PUT = 1
|
|
22
|
-
|
|
23
|
-
class ObjectRequest:
|
|
24
|
-
type: ObjectRequestType
|
|
25
|
-
data: bytes
|
|
26
|
-
hash: bytes
|
|
27
|
-
|
|
28
|
-
def __init__(self, type: ObjectRequestType, data: bytes, hash: bytes = None):
|
|
29
|
-
self.type = type
|
|
30
|
-
self.data = data
|
|
31
|
-
self.hash = hash
|
|
32
|
-
|
|
33
|
-
def to_bytes(self):
|
|
34
|
-
return encode([self.type.value, self.data, self.hash])
|
|
35
|
-
|
|
36
|
-
@classmethod
|
|
37
|
-
def from_bytes(cls, data: bytes):
|
|
38
|
-
type_val, data_val, hash_val = decode(data)
|
|
39
|
-
return cls(type=ObjectRequestType(type_val[0]), data=data_val, hash=hash_val)
|
|
40
|
-
|
|
41
|
-
class ObjectResponseType(IntEnum):
|
|
42
|
-
OBJECT_FOUND = 0
|
|
43
|
-
OBJECT_PROVIDER = 1
|
|
44
|
-
OBJECT_NEAREST_PEER = 2
|
|
45
|
-
|
|
46
|
-
class ObjectResponse:
|
|
47
|
-
type: ObjectResponseType
|
|
48
|
-
data: bytes
|
|
49
|
-
hash: bytes
|
|
50
|
-
|
|
51
|
-
def __init__(self, type: ObjectResponseType, data: bytes, hash: bytes = None):
|
|
52
|
-
self.type = type
|
|
53
|
-
self.data = data
|
|
54
|
-
self.hash = hash
|
|
55
|
-
|
|
56
|
-
def to_bytes(self):
|
|
57
|
-
return encode([self.type.value, self.data, self.hash])
|
|
58
|
-
|
|
59
|
-
@classmethod
|
|
60
|
-
def from_bytes(cls, data: bytes):
|
|
61
|
-
type_val, data_val, hash_val = decode(data)
|
|
62
|
-
return cls(type=ObjectResponseType(type_val[0]), data=data_val, hash=hash_val)
|
|
63
|
-
|
|
64
|
-
class MessageTopic(IntEnum):
|
|
65
|
-
PING = 0
|
|
66
|
-
OBJECT_REQUEST = 1
|
|
67
|
-
OBJECT_RESPONSE = 2
|
|
68
|
-
ROUTE_REQUEST = 3
|
|
69
|
-
ROUTE_RESPONSE = 4
|
|
70
|
-
|
|
71
|
-
class Message:
|
|
72
|
-
body: bytes
|
|
73
|
-
topic: MessageTopic
|
|
74
|
-
|
|
75
|
-
def to_bytes(self):
|
|
76
|
-
return encode([self.body, [self.topic.value]])
|
|
77
|
-
|
|
78
|
-
@classmethod
|
|
79
|
-
def from_bytes(cls, data: bytes):
|
|
80
|
-
body, topic = decode(data)
|
|
81
|
-
return cls(body=body, topic=MessageTopic(topic[0]))
|
|
82
|
-
|
|
83
|
-
class Envelope:
|
|
84
|
-
encrypted: bool
|
|
85
|
-
message: Message
|
|
86
|
-
nonce: int
|
|
87
|
-
sender: X25519PublicKey
|
|
88
|
-
timestamp: datetime
|
|
89
|
-
|
|
90
|
-
def __init__(self, message: Message, sender: X25519PublicKey, encrypted: bool = False, nonce: int = 0, timestamp: Union[int, datetime, None] = None, difficulty: int = 1):
|
|
91
|
-
self.encrypted = encrypted
|
|
92
|
-
encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
|
|
93
|
-
|
|
94
|
-
self.message = message
|
|
95
|
-
message_bytes = message.to_bytes()
|
|
96
|
-
|
|
97
|
-
self.sender = sender
|
|
98
|
-
self.sender_bytes = sender.public_bytes()
|
|
99
|
-
|
|
100
|
-
self.nonce = nonce
|
|
101
|
-
|
|
102
|
-
if timestamp is None:
|
|
103
|
-
self.timestamp = datetime.now(timezone.utc)
|
|
104
|
-
timestamp_int = int(self.timestamp.timestamp())
|
|
105
|
-
elif isinstance(timestamp, int):
|
|
106
|
-
self.timestamp = datetime.fromtimestamp(timestamp, timezone.utc)
|
|
107
|
-
timestamp_int = timestamp
|
|
108
|
-
elif isinstance(timestamp, datetime):
|
|
109
|
-
self.timestamp = timestamp
|
|
110
|
-
timestamp_int = int(timestamp.timestamp())
|
|
111
|
-
else:
|
|
112
|
-
raise TypeError("Timestamp must be an int (Unix timestamp), datetime object, or None")
|
|
113
|
-
|
|
114
|
-
def count_leading_zero_bits(data: bytes) -> int:
|
|
115
|
-
count = 0
|
|
116
|
-
for b in data:
|
|
117
|
-
if b == 0:
|
|
118
|
-
count += 8
|
|
119
|
-
else:
|
|
120
|
-
count += 8 - b.bit_length()
|
|
121
|
-
break
|
|
122
|
-
return count
|
|
123
24
|
|
|
124
|
-
while True:
|
|
125
|
-
envelope_bytes = encode([
|
|
126
|
-
encrypted_bytes,
|
|
127
|
-
message_bytes,
|
|
128
|
-
self.nonce,
|
|
129
|
-
self.sender_bytes,
|
|
130
|
-
timestamp_int
|
|
131
|
-
])
|
|
132
|
-
envelope_hash = blake3.blake3(envelope_bytes).digest()
|
|
133
|
-
if count_leading_zero_bits(envelope_hash) >= difficulty:
|
|
134
|
-
self.hash = envelope_hash
|
|
135
|
-
break
|
|
136
|
-
self.nonce += 1
|
|
137
|
-
|
|
138
|
-
def to_bytes(self):
|
|
139
|
-
encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
|
|
140
|
-
|
|
141
|
-
return encode([
|
|
142
|
-
encrypted_bytes,
|
|
143
|
-
self.message.to_bytes(),
|
|
144
|
-
self.nonce,
|
|
145
|
-
self.sender.public_bytes(),
|
|
146
|
-
int(self.timestamp.timestamp())
|
|
147
|
-
])
|
|
148
|
-
|
|
149
|
-
@classmethod
|
|
150
|
-
def from_bytes(cls, data: bytes):
|
|
151
|
-
encrypted_bytes, message_bytes, nonce, sender_bytes, timestamp_int = decode(data)
|
|
152
|
-
return cls(
|
|
153
|
-
encrypted=(encrypted_bytes == b'\x01'),
|
|
154
|
-
message=Message.from_bytes(message_bytes),
|
|
155
|
-
nonce=nonce,
|
|
156
|
-
sender=X25519PublicKey.from_public_bytes(sender_bytes),
|
|
157
|
-
timestamp=datetime.fromtimestamp(timestamp_int, timezone.utc)
|
|
158
|
-
)
|
|
159
25
|
|
|
160
26
|
class Peer:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
27
|
+
shared_key: bytes
|
|
28
|
+
timestamp: datetime
|
|
29
|
+
def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
|
|
30
|
+
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
164
31
|
self.timestamp = datetime.now(timezone.utc)
|
|
165
32
|
|
|
166
33
|
class Route:
|
|
@@ -203,135 +70,18 @@ def decode_ip_address(data: bytes) -> tuple[str, int]:
|
|
|
203
70
|
raise ValueError("Invalid address byte format")
|
|
204
71
|
return ip, port
|
|
205
72
|
|
|
206
|
-
# =========
|
|
207
|
-
# MACHINE
|
|
208
|
-
# =========
|
|
209
|
-
|
|
210
|
-
class Expr:
|
|
211
|
-
class ListExpr:
|
|
212
|
-
def __init__(self, elements: List['Expr']):
|
|
213
|
-
self.elements = elements
|
|
214
|
-
|
|
215
|
-
def __eq__(self, other):
|
|
216
|
-
if not isinstance(other, Expr.ListExpr):
|
|
217
|
-
return NotImplemented
|
|
218
|
-
return self.elements == other.elements
|
|
219
|
-
|
|
220
|
-
def __ne__(self, other):
|
|
221
|
-
return not self.__eq__(other)
|
|
222
|
-
|
|
223
|
-
@property
|
|
224
|
-
def value(self):
|
|
225
|
-
inner = " ".join(str(e) for e in self.elements)
|
|
226
|
-
return f"({inner})"
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def __repr__(self):
|
|
230
|
-
if not self.elements:
|
|
231
|
-
return "()"
|
|
232
|
-
|
|
233
|
-
inner = " ".join(str(e) for e in self.elements)
|
|
234
|
-
return f"({inner})"
|
|
235
|
-
|
|
236
|
-
def __iter__(self):
|
|
237
|
-
return iter(self.elements)
|
|
238
|
-
|
|
239
|
-
def __getitem__(self, index: Union[int, slice]):
|
|
240
|
-
return self.elements[index]
|
|
241
|
-
|
|
242
|
-
def __len__(self):
|
|
243
|
-
return len(self.elements)
|
|
244
|
-
|
|
245
|
-
class Symbol:
|
|
246
|
-
def __init__(self, value: str):
|
|
247
|
-
self.value = value
|
|
248
|
-
|
|
249
|
-
def __repr__(self):
|
|
250
|
-
return self.value
|
|
251
|
-
|
|
252
|
-
class Integer:
|
|
253
|
-
def __init__(self, value: int):
|
|
254
|
-
self.value = value
|
|
255
|
-
|
|
256
|
-
def __repr__(self):
|
|
257
|
-
return str(self.value)
|
|
258
|
-
|
|
259
|
-
class String:
|
|
260
|
-
def __init__(self, value: str):
|
|
261
|
-
self.value = value
|
|
262
|
-
|
|
263
|
-
def __repr__(self):
|
|
264
|
-
return f'"{self.value}"'
|
|
265
|
-
|
|
266
|
-
class Boolean:
|
|
267
|
-
def __init__(self, value: bool):
|
|
268
|
-
self.value = value
|
|
269
|
-
|
|
270
|
-
def __repr__(self):
|
|
271
|
-
return "true" if self.value else "false"
|
|
272
|
-
|
|
273
|
-
class Function:
|
|
274
|
-
def __init__(self, params: List[str], body: 'Expr'):
|
|
275
|
-
self.params = params
|
|
276
|
-
self.body = body
|
|
277
|
-
|
|
278
|
-
def __repr__(self):
|
|
279
|
-
params_str = " ".join(self.params)
|
|
280
|
-
body_str = str(self.body)
|
|
281
|
-
return f"(fn ({params_str}) {body_str})"
|
|
282
|
-
|
|
283
|
-
class Error:
|
|
284
|
-
def __init__(self, message: str, origin: Optional['Expr'] = None):
|
|
285
|
-
self.message = message
|
|
286
|
-
self.origin = origin
|
|
287
|
-
|
|
288
|
-
def __repr__(self):
|
|
289
|
-
if self.origin is None:
|
|
290
|
-
return f'(error "{self.message}")'
|
|
291
|
-
return f'(error "{self.message}" in {self.origin})'
|
|
292
|
-
|
|
293
|
-
class Env:
|
|
294
|
-
def __init__(
|
|
295
|
-
self,
|
|
296
|
-
data: Optional[Dict[str, Expr]] = None,
|
|
297
|
-
parent_id: Optional[uuid.UUID] = None,
|
|
298
|
-
max_exprs: Optional[int] = 8,
|
|
299
|
-
):
|
|
300
|
-
self.data: Dict[str, Expr] = data if data is not None else {}
|
|
301
|
-
self.parent_id: Optional[uuid.UUID] = parent_id
|
|
302
|
-
self.max_exprs: Optional[int] = max_exprs
|
|
303
|
-
|
|
304
|
-
def put(self, name: str, value: Expr) -> None:
|
|
305
|
-
if (
|
|
306
|
-
self.max_exprs is not None
|
|
307
|
-
and name not in self.data
|
|
308
|
-
and len(self.data) >= self.max_exprs
|
|
309
|
-
):
|
|
310
|
-
raise RuntimeError(
|
|
311
|
-
f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
|
|
312
|
-
)
|
|
313
|
-
self.data[name] = value
|
|
314
|
-
|
|
315
|
-
def get(self, name: str) -> Optional[Expr]:
|
|
316
|
-
return self.data.get(name)
|
|
317
|
-
|
|
318
|
-
def pop(self, name: str) -> Optional[Expr]:
|
|
319
|
-
return self.data.pop(name, None)
|
|
320
|
-
|
|
321
|
-
def __repr__(self) -> str:
|
|
322
|
-
return (
|
|
323
|
-
f"Env(size={len(self.data)}, "
|
|
324
|
-
f"max_exprs={self.max_exprs}, "
|
|
325
|
-
f"parent_id={self.parent_id})"
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
|
|
329
73
|
class Node:
|
|
330
74
|
def __init__(self, config: dict = {}):
|
|
331
75
|
self._machine_setup()
|
|
332
76
|
machine_only = bool(config.get('machine-only', True))
|
|
333
77
|
if not machine_only:
|
|
334
|
-
|
|
78
|
+
(
|
|
79
|
+
self.storage_path,
|
|
80
|
+
self.memory_storage,
|
|
81
|
+
self.storage_get_relay_timeout,
|
|
82
|
+
self.storage_index
|
|
83
|
+
) = storage_setup(config)
|
|
84
|
+
|
|
335
85
|
self._relay_setup(config=config)
|
|
336
86
|
self._validation_setup(config=config)
|
|
337
87
|
|
|
@@ -344,21 +94,6 @@ class Node:
|
|
|
344
94
|
def _create_block(self):
|
|
345
95
|
pass
|
|
346
96
|
|
|
347
|
-
# STORAGE METHODS
|
|
348
|
-
def _storage_setup(self, config: dict):
|
|
349
|
-
storage_path_str = config.get('storage_path')
|
|
350
|
-
if storage_path_str is None:
|
|
351
|
-
self.storage_path = None
|
|
352
|
-
self.memory_storage = {}
|
|
353
|
-
else:
|
|
354
|
-
self.storage_path = Path(storage_path_str)
|
|
355
|
-
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
356
|
-
self.memory_storage = None
|
|
357
|
-
|
|
358
|
-
self.storage_get_relay_timeout = config.get('storage_get_relay_timeout', 5)
|
|
359
|
-
# STORAGE INDEX: (object_hash, encoded (provider_public_key, provider_address))
|
|
360
|
-
self.storage_index = Dict[bytes, bytes]
|
|
361
|
-
|
|
362
97
|
def _relay_setup(self, config: dict):
|
|
363
98
|
self.use_ipv6 = config.get('use_ipv6', False)
|
|
364
99
|
incoming_port = config.get('incoming_port', 7373)
|
|
@@ -419,6 +154,7 @@ class Node:
|
|
|
419
154
|
self.peer_manager_thread.start()
|
|
420
155
|
|
|
421
156
|
self.peers = Dict[X25519PublicKey, Peer]
|
|
157
|
+
self.addresses = Dict[Tuple[str, int], X25519PublicKey]
|
|
422
158
|
|
|
423
159
|
if 'bootstrap' in config:
|
|
424
160
|
for addr in config['bootstrap']:
|
|
@@ -451,9 +187,8 @@ class Node:
|
|
|
451
187
|
# find the nearest peer route node to the hash and send an object request
|
|
452
188
|
closest_peer = self._get_closest_local_peer(hash)
|
|
453
189
|
if closest_peer:
|
|
454
|
-
object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST,
|
|
455
|
-
|
|
456
|
-
self.outgoing_queue.put((object_request_envelope.to_bytes(), self.peers[closest_peer].address))
|
|
190
|
+
object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST, content=hash)
|
|
191
|
+
self.outgoing_queue.put((object_request_message.to_bytes(), self.peers[closest_peer].address))
|
|
457
192
|
|
|
458
193
|
# wait for upto self.storage_get_relay_timeout seconds for the object to be stored/until local_object_get returns something
|
|
459
194
|
start_time = time.time()
|
|
@@ -481,30 +216,31 @@ class Node:
|
|
|
481
216
|
while True:
|
|
482
217
|
try:
|
|
483
218
|
data, addr = self.incoming_queue.get()
|
|
484
|
-
|
|
485
|
-
match
|
|
219
|
+
message = Message.from_bytes(data)
|
|
220
|
+
match message.topic:
|
|
486
221
|
case MessageTopic.PING:
|
|
487
|
-
|
|
488
|
-
|
|
222
|
+
peer_pub_key = self.addresses.get(addr)
|
|
223
|
+
if peer_pub_key in self.peers:
|
|
224
|
+
self.peers[peer_pub_key].timestamp = datetime.now(timezone.utc)
|
|
489
225
|
continue
|
|
490
226
|
|
|
491
|
-
is_validator_flag = decode(
|
|
227
|
+
is_validator_flag = decode(message.body)
|
|
492
228
|
|
|
493
|
-
if
|
|
229
|
+
if peer_pub_key not in self.peers:
|
|
494
230
|
self._send_ping(addr)
|
|
495
231
|
|
|
496
|
-
peer = Peer(self.relay_secret_key,
|
|
232
|
+
peer = Peer(my_sec_key=self.relay_secret_key, peer_pub_key=peer_pub_key)
|
|
497
233
|
self.peers[peer.sender] = peer
|
|
498
|
-
self.peer_route.add_peer(
|
|
234
|
+
self.peer_route.add_peer(peer_pub_key)
|
|
499
235
|
if is_validator_flag == [1]:
|
|
500
|
-
self.validation_route.add_peer(
|
|
236
|
+
self.validation_route.add_peer(peer_pub_key)
|
|
501
237
|
|
|
502
238
|
if peer.timestamp < datetime.now(timezone.utc) - timedelta(minutes=5.0):
|
|
503
239
|
self._send_ping(addr)
|
|
504
240
|
|
|
505
241
|
case MessageTopic.OBJECT_REQUEST:
|
|
506
242
|
try:
|
|
507
|
-
object_request = ObjectRequest.from_bytes(
|
|
243
|
+
object_request = ObjectRequest.from_bytes(message.body)
|
|
508
244
|
|
|
509
245
|
match object_request.type:
|
|
510
246
|
# -------------- OBJECT_GET --------------
|
|
@@ -519,9 +255,8 @@ class Node:
|
|
|
519
255
|
data=local_data,
|
|
520
256
|
hash=object_hash
|
|
521
257
|
)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
258
|
+
obj_res_msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
259
|
+
self.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
525
260
|
return # done
|
|
526
261
|
|
|
527
262
|
# 2. If we know a provider, tell the requester.
|
|
@@ -534,9 +269,8 @@ class Node:
|
|
|
534
269
|
data=provider_bytes,
|
|
535
270
|
hash=object_hash
|
|
536
271
|
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
272
|
+
obj_res_msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
273
|
+
self.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
540
274
|
return # done
|
|
541
275
|
|
|
542
276
|
# 3. Otherwise, direct the requester to a peer nearer to the hash.
|
|
@@ -555,14 +289,13 @@ class Node:
|
|
|
555
289
|
data=peer_info,
|
|
556
290
|
hash=object_hash
|
|
557
291
|
)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
292
|
+
obj_res_msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
293
|
+
self.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
561
294
|
|
|
562
295
|
# -------------- OBJECT_PUT --------------
|
|
563
296
|
case ObjectRequestType.OBJECT_PUT:
|
|
564
297
|
# Ensure the hash is present / correct.
|
|
565
|
-
obj_hash = object_request.hash or
|
|
298
|
+
obj_hash = object_request.hash or blake3.blake3(object_request.data).digest()
|
|
566
299
|
|
|
567
300
|
nearest = self._get_closest_local_peer(obj_hash)
|
|
568
301
|
# If a strictly nearer peer exists, forward the PUT.
|
|
@@ -572,13 +305,13 @@ class Node:
|
|
|
572
305
|
data=object_request.data,
|
|
573
306
|
hash=obj_hash
|
|
574
307
|
)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
self.outgoing_queue.put((fwd_env.to_bytes(), nearest[1].address))
|
|
308
|
+
obj_req_msg = Message(topic=MessageTopic.OBJECT_REQUEST, body=fwd_req.to_bytes())
|
|
309
|
+
self.outgoing_queue.put((obj_req_msg.to_bytes(), nearest[1].address))
|
|
578
310
|
else:
|
|
579
311
|
# We are closest → remember who can provide the object.
|
|
312
|
+
peer_pub_key = self.addresses.get(addr)
|
|
580
313
|
provider_record = encode([
|
|
581
|
-
|
|
314
|
+
peer_pub_key.public_bytes(),
|
|
582
315
|
encode_ip_address(*addr)
|
|
583
316
|
])
|
|
584
317
|
if not hasattr(self, "storage_index") or not isinstance(self.storage_index, dict):
|
|
@@ -590,13 +323,13 @@ class Node:
|
|
|
590
323
|
|
|
591
324
|
case MessageTopic.OBJECT_RESPONSE:
|
|
592
325
|
try:
|
|
593
|
-
object_response = ObjectResponse.from_bytes(
|
|
326
|
+
object_response = ObjectResponse.from_bytes(message.body)
|
|
594
327
|
if object_response.hash not in self.object_request_queue:
|
|
595
328
|
continue
|
|
596
329
|
|
|
597
330
|
match object_response.type:
|
|
598
331
|
case ObjectResponseType.OBJECT_FOUND:
|
|
599
|
-
if object_response.hash !=
|
|
332
|
+
if object_response.hash != blake3.blake3(object_response.data).digest():
|
|
600
333
|
continue
|
|
601
334
|
self.object_request_queue.remove(object_response.hash)
|
|
602
335
|
self._local_object_put(object_response.hash, object_response.data)
|
|
@@ -604,9 +337,8 @@ class Node:
|
|
|
604
337
|
case ObjectResponseType.OBJECT_PROVIDER:
|
|
605
338
|
_provider_public_key, provider_address = decode(object_response.data)
|
|
606
339
|
provider_ip, provider_port = decode_ip_address(provider_address)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
self.outgoing_queue.put((object_request_envelope.to_bytes(), (provider_ip, provider_port)))
|
|
340
|
+
obj_req_msg = Message(topic=MessageTopic.OBJECT_REQUEST, body=object_hash)
|
|
341
|
+
self.outgoing_queue.put((obj_req_msg.to_bytes(), (provider_ip, provider_port)))
|
|
610
342
|
|
|
611
343
|
case ObjectResponseType.OBJECT_NEAREST_PEER:
|
|
612
344
|
# -- decode the peer info sent back
|
|
@@ -630,23 +362,10 @@ class Node:
|
|
|
630
362
|
if self._is_closer_than_local_peers(
|
|
631
363
|
object_response.hash, nearest_peer_public_key
|
|
632
364
|
):
|
|
633
|
-
nearest_peer_ip, nearest_peer_port = decode_ip_address(
|
|
634
|
-
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
topic=MessageTopic.OBJECT_REQUEST,
|
|
638
|
-
body=object_response.hash,
|
|
639
|
-
)
|
|
640
|
-
object_request_envelope = Envelope(
|
|
641
|
-
message=object_request_message,
|
|
642
|
-
sender=self.relay_public_key,
|
|
643
|
-
)
|
|
644
|
-
self.outgoing_queue.put(
|
|
645
|
-
(
|
|
646
|
-
object_request_envelope.to_bytes(),
|
|
647
|
-
(nearest_peer_ip, nearest_peer_port),
|
|
648
|
-
)
|
|
649
|
-
)
|
|
365
|
+
nearest_peer_ip, nearest_peer_port = decode_ip_address(nearest_peer_address)
|
|
366
|
+
obj_req_msg = Message(topic=MessageTopic.OBJECT_REQUEST, content=object_response.hash)
|
|
367
|
+
self.outgoing_queue.put((obj_req_msg.to_bytes(), (nearest_peer_ip, nearest_peer_port),)
|
|
368
|
+
)
|
|
650
369
|
|
|
651
370
|
|
|
652
371
|
except Exception as e:
|
|
@@ -678,9 +397,8 @@ class Node:
|
|
|
678
397
|
|
|
679
398
|
def _send_ping(self, addr: Tuple[str, int]):
|
|
680
399
|
is_validator_flag = encode([1] if self.validation_secret_key else [0])
|
|
681
|
-
ping_message = Message(topic=MessageTopic.PING,
|
|
682
|
-
|
|
683
|
-
self.outgoing_queue.put((ping_envelope.to_bytes(), addr))
|
|
400
|
+
ping_message = Message(topic=MessageTopic.PING, content=is_validator_flag)
|
|
401
|
+
self.outgoing_queue.put((ping_message.to_bytes(), addr))
|
|
684
402
|
|
|
685
403
|
def _get_closest_local_peer(self, hash: bytes) -> Optional[Tuple[X25519PublicKey, Peer]]:
|
|
686
404
|
# Find the globally closest peer using XOR distance
|
|
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)
|
astreum/storage/setup.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Dict, Tuple, Any
|
|
3
|
+
|
|
4
|
+
def storage_setup(config: dict
|
|
5
|
+
) -> Tuple[Optional[Path], Dict[bytes, Any], int, Dict[bytes, bytes]]:
|
|
6
|
+
storage_path_str = config.get('storage_path')
|
|
7
|
+
if storage_path_str is None:
|
|
8
|
+
storage_path, memory_storage = None, {}
|
|
9
|
+
else:
|
|
10
|
+
storage_path = Path(storage_path_str)
|
|
11
|
+
storage_path.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
memory_storage = None
|
|
13
|
+
|
|
14
|
+
timeout = config.get('storage_get_relay_timeout', 5)
|
|
15
|
+
storage_index: Dict[bytes, bytes] = {}
|
|
16
|
+
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.28
|
|
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
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
astreum/__init__.py,sha256=y2Ok3EY_FstcmlVASr80lGR_0w-dH-SXDCCQFmL6uwA,28
|
|
2
2
|
astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
|
|
3
|
-
astreum/node.py,sha256=
|
|
3
|
+
astreum/node.py,sha256=L2YpIxyhf0f-y85_vBGBaGiItgFGzLgXYhyabDetaKM,37116
|
|
4
4
|
astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
|
|
6
6
|
astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
|
|
7
7
|
astreum/crypto/wesolowski.py,sha256=SUgGXW3Id07dJtWzDcs4dluIhjqbRWQ8YWjn_mK78AQ,4092
|
|
8
8
|
astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
|
|
9
9
|
astreum/lispeum/__init__.py,sha256=K-NDzIjtIsXzC9X7lnYvlvIaVxjFcY7WNsgLIE3DH3U,58
|
|
10
|
+
astreum/lispeum/environment.py,sha256=wolwt9psDl62scgjaVG0G59xlBs1AM4NPgryUbxzG_4,1220
|
|
11
|
+
astreum/lispeum/expression.py,sha256=K8gFifDaHu394bs9qnpvP8tjeiymFGQpnDC_iW9nU4E,2379
|
|
10
12
|
astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
|
|
11
13
|
astreum/lispeum/tokenizer.py,sha256=J-I7MEd0r2ZoVqxvRPlu-Afe2ZdM0tKXXhf1R4SxYTo,1429
|
|
12
14
|
astreum/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -14,10 +16,14 @@ astreum/models/account.py,sha256=sHujGSwtV13rvOGJ5LZXuMrJ4F9XUdvyuWKz-zJ9lkE,298
|
|
|
14
16
|
astreum/models/accounts.py,sha256=aFSEWlq6zRf65-KGAdNGqEJyNVY3fpKhx8y1vU6sgSc,1164
|
|
15
17
|
astreum/models/block.py,sha256=-5j7uO0woVtNi0h52__e7AxpDQSVhzKUhr6Qc-2xZsE,17870
|
|
16
18
|
astreum/models/merkle.py,sha256=lvWJa9nmrBL0n_2h_uNqpB_9a5s5Hn1FceRLx0IZIVQ,6778
|
|
19
|
+
astreum/models/message.py,sha256=vv8yx-ndVYjCmPM4gXRVMToCTlKY_mflPu0uKsb9iiE,2117
|
|
17
20
|
astreum/models/patricia.py,sha256=ohmXrcaz7Ae561tyC4u4iPOkQPkKr8N0IWJek4upFIg,13392
|
|
18
21
|
astreum/models/transaction.py,sha256=MkLL5YX18kIf9-O4LBaZ4eWjkXDAaYIrDcDehbDZoqg,3038
|
|
19
|
-
astreum
|
|
20
|
-
astreum
|
|
21
|
-
astreum
|
|
22
|
-
astreum-0.2.
|
|
23
|
-
astreum-0.2.
|
|
22
|
+
astreum/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
astreum/storage/object.py,sha256=knFlvw_tpcC4twSu1DGNpHX31wlANN8E5dgEqIfU--Q,2041
|
|
24
|
+
astreum/storage/setup.py,sha256=EhMKr4wnBaESbzF8AlE2R-8YPIzxWx3afSv5BL8X6Lo,632
|
|
25
|
+
astreum-0.2.28.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
26
|
+
astreum-0.2.28.dist-info/METADATA,sha256=dB1O5if24FmM7erOlVfX-WAa923EO8KgzCl4GpaHS7o,5478
|
|
27
|
+
astreum-0.2.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
28
|
+
astreum-0.2.28.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
29
|
+
astreum-0.2.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|