astreum 0.1.8__py3-none-any.whl → 0.1.10__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/storage.py +229 -276
- astreum/machine/__init__.py +352 -254
- astreum/node/__init__.py +143 -54
- astreum/node/models.py +193 -5
- astreum/node/relay/__init__.py +100 -4
- astreum/node/relay/bucket.py +41 -31
- astreum/node/relay/message.py +17 -12
- astreum/node/relay/peer.py +3 -0
- astreum/node/relay/route.py +100 -64
- {astreum-0.1.8.dist-info → astreum-0.1.10.dist-info}/METADATA +11 -7
- {astreum-0.1.8.dist-info → astreum-0.1.10.dist-info}/RECORD +14 -14
- {astreum-0.1.8.dist-info → astreum-0.1.10.dist-info}/WHEEL +1 -1
- {astreum-0.1.8.dist-info → astreum-0.1.10.dist-info}/LICENSE +0 -0
- {astreum-0.1.8.dist-info → astreum-0.1.10.dist-info}/top_level.txt +0 -0
astreum/node/__init__.py
CHANGED
|
@@ -1,55 +1,49 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import hashlib
|
|
3
3
|
import time
|
|
4
|
-
from typing import Tuple
|
|
4
|
+
from typing import Tuple, Optional
|
|
5
|
+
import json
|
|
5
6
|
|
|
6
7
|
from .relay import Relay, Topic
|
|
7
8
|
from .relay.peer import Peer
|
|
8
|
-
from .
|
|
9
|
-
from .route_table import RouteTable
|
|
9
|
+
from .models import Storage, Block, Transaction
|
|
10
10
|
from .machine import AstreumMachine
|
|
11
11
|
from .utils import encode, decode
|
|
12
|
-
from .models import Block, Transaction
|
|
13
12
|
from astreum.lispeum.storage import store_expr, get_expr_from_storage
|
|
14
13
|
|
|
15
14
|
class Node:
|
|
16
15
|
def __init__(self, config: dict):
|
|
17
16
|
self.config = config
|
|
18
|
-
self.node_id = config.get('node_id', os.urandom(32)) # Default to random ID if not provided
|
|
19
17
|
self.relay = Relay(config)
|
|
18
|
+
# Get the node_id from relay instead of generating our own
|
|
19
|
+
self.node_id = self.relay.node_id
|
|
20
20
|
self.storage = Storage(config)
|
|
21
|
+
self.storage.node = self # Set the storage node reference to self
|
|
21
22
|
|
|
22
23
|
# Latest block of the chain this node is following
|
|
23
24
|
self.latest_block = None
|
|
24
25
|
self.followed_chain_id = config.get('followed_chain_id', None)
|
|
25
26
|
|
|
26
|
-
#
|
|
27
|
-
self.candidate_chains = {} # chain_id -> {'latest_block': block, 'timestamp': time.time()}
|
|
28
|
-
|
|
29
|
-
# Initialize route table with our node ID
|
|
30
|
-
self.route_table = RouteTable(config, self.node_id)
|
|
31
|
-
|
|
32
|
-
# Initialize machine after storage so it can use it
|
|
33
|
-
# Pass self to machine so it can access the storage
|
|
27
|
+
# Initialize machine
|
|
34
28
|
self.machine = AstreumMachine(node=self)
|
|
35
29
|
|
|
36
30
|
# Register message handlers
|
|
37
|
-
self.
|
|
31
|
+
self.relay.message_handlers[Topic.PEER_ROUTE] = self._handle_peer_route
|
|
32
|
+
self.relay.message_handlers[Topic.PING] = self._handle_ping
|
|
33
|
+
self.relay.message_handlers[Topic.PONG] = self._handle_pong
|
|
34
|
+
self.relay.message_handlers[Topic.OBJECT_REQUEST] = self._handle_object_request
|
|
35
|
+
self.relay.message_handlers[Topic.OBJECT_RESPONSE] = self._handle_object_response
|
|
36
|
+
self.relay.message_handlers[Topic.ROUTE_REQUEST] = self._handle_route_request
|
|
37
|
+
self.relay.message_handlers[Topic.ROUTE] = self._handle_route
|
|
38
|
+
self.relay.message_handlers[Topic.LATEST_BLOCK_REQUEST] = self._handle_latest_block_request
|
|
39
|
+
self.relay.message_handlers[Topic.LATEST_BLOCK] = self._handle_latest_block
|
|
40
|
+
self.relay.message_handlers[Topic.TRANSACTION] = self._handle_transaction
|
|
38
41
|
|
|
39
42
|
# Initialize latest block from storage if available
|
|
40
43
|
self._initialize_latest_block()
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.relay.register_message_handler(Topic.PING, self._handle_ping)
|
|
45
|
-
self.relay.register_message_handler(Topic.PONG, self._handle_pong)
|
|
46
|
-
self.relay.register_message_handler(Topic.OBJECT_REQUEST, self._handle_object_request)
|
|
47
|
-
self.relay.register_message_handler(Topic.OBJECT, self._handle_object)
|
|
48
|
-
self.relay.register_message_handler(Topic.ROUTE_REQUEST, self._handle_route_request)
|
|
49
|
-
self.relay.register_message_handler(Topic.ROUTE, self._handle_route)
|
|
50
|
-
self.relay.register_message_handler(Topic.LATEST_BLOCK_REQUEST, self._handle_latest_block_request)
|
|
51
|
-
self.relay.register_message_handler(Topic.LATEST_BLOCK, self._handle_latest_block)
|
|
52
|
-
self.relay.register_message_handler(Topic.TRANSACTION, self._handle_transaction)
|
|
45
|
+
# Candidate chains that might be adopted
|
|
46
|
+
self.candidate_chains = {} # chain_id -> {'latest_block': block, 'timestamp': time.time()}
|
|
53
47
|
|
|
54
48
|
def _handle_ping(self, body: bytes, addr: Tuple[str, int], envelope):
|
|
55
49
|
"""
|
|
@@ -70,7 +64,7 @@ class Node:
|
|
|
70
64
|
difficulty = int.from_bytes(difficulty_bytes, byteorder='big')
|
|
71
65
|
|
|
72
66
|
# Store peer information in routing table
|
|
73
|
-
peer = self.
|
|
67
|
+
peer = self.relay.add_peer(addr, public_key, difficulty)
|
|
74
68
|
|
|
75
69
|
# Process the routes the sender is participating in
|
|
76
70
|
if routes_data:
|
|
@@ -104,7 +98,7 @@ class Node:
|
|
|
104
98
|
difficulty = int.from_bytes(difficulty_bytes, byteorder='big')
|
|
105
99
|
|
|
106
100
|
# Update peer information in routing table
|
|
107
|
-
peer = self.
|
|
101
|
+
peer = self.relay.add_peer(addr, public_key, difficulty)
|
|
108
102
|
|
|
109
103
|
# Process the routes the sender is participating in
|
|
110
104
|
if routes_data:
|
|
@@ -116,21 +110,65 @@ class Node:
|
|
|
116
110
|
|
|
117
111
|
def _handle_object_request(self, body: bytes, addr: Tuple[str, int], envelope):
|
|
118
112
|
"""
|
|
119
|
-
Handle
|
|
120
|
-
|
|
113
|
+
Handle an object request from a peer.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
body: Message body containing the object hash
|
|
117
|
+
addr: Address of the requesting peer
|
|
118
|
+
envelope: Full message envelope
|
|
121
119
|
"""
|
|
122
120
|
try:
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
# Decode the request
|
|
122
|
+
request = json.loads(body.decode('utf-8'))
|
|
123
|
+
object_hash = bytes.fromhex(request.get('hash'))
|
|
124
|
+
|
|
125
|
+
# Check if we have the requested object
|
|
126
|
+
if not self.storage.contains(object_hash):
|
|
127
|
+
# We don't have the object, ignore the request
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Get the object data
|
|
131
|
+
object_data = self.storage._local_get(object_hash)
|
|
132
|
+
if not object_data:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Create a response message
|
|
136
|
+
response = {
|
|
137
|
+
'hash': object_hash.hex(),
|
|
138
|
+
'data': object_data.hex()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Send the response
|
|
142
|
+
self.relay.send_message_to_addr(
|
|
143
|
+
addr,
|
|
144
|
+
Topic.OBJECT_RESPONSE,
|
|
145
|
+
json.dumps(response).encode('utf-8')
|
|
146
|
+
)
|
|
126
147
|
|
|
127
|
-
if object_data:
|
|
128
|
-
# Object found, send it back
|
|
129
|
-
self.relay.send_message(object_data, Topic.OBJECT, addr)
|
|
130
|
-
# If object not found, simply ignore the request
|
|
131
148
|
except Exception as e:
|
|
132
149
|
print(f"Error handling object request: {e}")
|
|
133
150
|
|
|
151
|
+
def _handle_object_response(self, body: bytes, addr: Tuple[str, int], envelope):
|
|
152
|
+
"""
|
|
153
|
+
Handle an object response from a peer.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
body: Message body containing the object hash and data
|
|
157
|
+
addr: Address of the responding peer
|
|
158
|
+
envelope: Full message envelope
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
# Decode the response
|
|
162
|
+
response = json.loads(body.decode('utf-8'))
|
|
163
|
+
object_hash = bytes.fromhex(response.get('hash'))
|
|
164
|
+
object_data = bytes.fromhex(response.get('data'))
|
|
165
|
+
|
|
166
|
+
# Store the object
|
|
167
|
+
self.storage.put(object_hash, object_data)
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"Error handling object response: {e}")
|
|
171
|
+
|
|
134
172
|
def _handle_object(self, body: bytes, addr: Tuple[str, int], envelope):
|
|
135
173
|
"""
|
|
136
174
|
Handle receipt of an object.
|
|
@@ -147,6 +185,68 @@ class Node:
|
|
|
147
185
|
except Exception as e:
|
|
148
186
|
print(f"Error handling object: {e}")
|
|
149
187
|
|
|
188
|
+
def request_object(self, object_hash: bytes, max_attempts: int = 3) -> Optional[bytes]:
|
|
189
|
+
"""
|
|
190
|
+
Request an object from the network by its hash.
|
|
191
|
+
|
|
192
|
+
This method sends an object request to peers closest to the object hash
|
|
193
|
+
and waits for a response until timeout.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
object_hash: The hash of the object to request
|
|
197
|
+
max_attempts: Maximum number of request attempts
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The object data if found, None otherwise
|
|
201
|
+
"""
|
|
202
|
+
# First check if we already have the object
|
|
203
|
+
if self.storage.contains(object_hash):
|
|
204
|
+
return self.storage._local_get(object_hash)
|
|
205
|
+
|
|
206
|
+
# Find the bucket containing the peers closest to the object's hash
|
|
207
|
+
closest_peers = self.relay.get_closest_peers(object_hash, count=3)
|
|
208
|
+
if not closest_peers:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
# Create a message to request the object
|
|
212
|
+
topic = Topic.OBJECT_REQUEST
|
|
213
|
+
object_request_msg = {
|
|
214
|
+
'hash': object_hash.hex()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Track which peers we've already tried
|
|
218
|
+
attempted_peers = set()
|
|
219
|
+
|
|
220
|
+
# We'll try up to max_attempts times
|
|
221
|
+
for _ in range(max_attempts):
|
|
222
|
+
# Find peers we haven't tried yet
|
|
223
|
+
untried_peers = [p for p in closest_peers if p.id not in attempted_peers]
|
|
224
|
+
if not untried_peers:
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
# Send the request to all untried peers
|
|
228
|
+
request_sent = False
|
|
229
|
+
for peer in untried_peers:
|
|
230
|
+
try:
|
|
231
|
+
self.relay.send_message_to_peer(peer, topic, object_request_msg)
|
|
232
|
+
attempted_peers.add(peer.id)
|
|
233
|
+
request_sent = True
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"Failed to send object request to peer {peer.id.hex()}: {e}")
|
|
236
|
+
|
|
237
|
+
if not request_sent:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
# Short wait to allow for response
|
|
241
|
+
time.sleep(0.5)
|
|
242
|
+
|
|
243
|
+
# Check if any of the requests succeeded
|
|
244
|
+
if self.storage.contains(object_hash):
|
|
245
|
+
return self.storage._local_get(object_hash)
|
|
246
|
+
|
|
247
|
+
# If we get here, we couldn't get the object
|
|
248
|
+
return None
|
|
249
|
+
|
|
150
250
|
def _handle_route_request(self, body: bytes, addr: Tuple[str, int], envelope):
|
|
151
251
|
"""
|
|
152
252
|
Handle request for routing information.
|
|
@@ -157,8 +257,8 @@ class Node:
|
|
|
157
257
|
route_peers = []
|
|
158
258
|
|
|
159
259
|
# Get one peer from each bucket
|
|
160
|
-
for bucket_index in range(self.
|
|
161
|
-
peers = self.
|
|
260
|
+
for bucket_index in range(self.relay.num_buckets):
|
|
261
|
+
peers = self.relay.get_bucket_peers(bucket_index)
|
|
162
262
|
if peers and len(peers) > 0:
|
|
163
263
|
# Add one peer from this bucket
|
|
164
264
|
route_peers.append(peers[0])
|
|
@@ -206,7 +306,7 @@ class Node:
|
|
|
206
306
|
|
|
207
307
|
# Ping this peer if it's not already in our routing table
|
|
208
308
|
# and it's not our own address
|
|
209
|
-
if (not self.
|
|
309
|
+
if (not self.relay.has_peer(peer_address) and
|
|
210
310
|
peer_address != self.relay.get_address()):
|
|
211
311
|
# Create ping message with our info and routes
|
|
212
312
|
# Encode our peer and validation routes
|
|
@@ -339,21 +439,10 @@ class Node:
|
|
|
339
439
|
print(f"Error handling transaction: {e}")
|
|
340
440
|
|
|
341
441
|
def _initialize_latest_block(self):
|
|
342
|
-
"""Initialize
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
self.latest_block = self.machine.get_latest_block(self.followed_chain_id)
|
|
347
|
-
else:
|
|
348
|
-
# If no specific chain is set to follow, get the latest block from the default chain
|
|
349
|
-
self.latest_block = self.machine.get_latest_block()
|
|
350
|
-
|
|
351
|
-
# If we have a latest block, set the followed chain ID
|
|
352
|
-
if self.latest_block:
|
|
353
|
-
self.followed_chain_id = self.latest_block.chain_id
|
|
354
|
-
except Exception as e:
|
|
355
|
-
print(f"Error initializing latest block: {e}")
|
|
356
|
-
|
|
442
|
+
"""Initialize latest block from storage if available."""
|
|
443
|
+
# Implementation would load the latest block from storage
|
|
444
|
+
pass
|
|
445
|
+
|
|
357
446
|
def set_followed_chain(self, chain_id):
|
|
358
447
|
"""
|
|
359
448
|
Set the chain that this node follows.
|
astreum/node/models.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import socket
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Optional, Tuple
|
|
3
|
+
from typing import Optional, Tuple, Dict
|
|
4
4
|
from astreum.machine import AstreumMachine
|
|
5
5
|
from .relay import Relay
|
|
6
6
|
from .relay.message import Topic
|
|
7
7
|
from .relay.route import RouteTable
|
|
8
8
|
from .relay.peer import Peer
|
|
9
9
|
import os
|
|
10
|
+
import struct
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
10
13
|
|
|
11
14
|
class Storage:
|
|
12
15
|
def __init__(self, config: dict):
|
|
@@ -14,6 +17,13 @@ class Storage:
|
|
|
14
17
|
self.current_space = 0
|
|
15
18
|
self.storage_path = Path(config.get('storage_path', 'storage'))
|
|
16
19
|
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
self.max_object_recursion = config.get('max_object_recursion', 50)
|
|
21
|
+
self.network_request_timeout = config.get('network_request_timeout', 5.0) # Default 5 second timeout
|
|
22
|
+
self.node = None # Will be set by the Node after initialization
|
|
23
|
+
|
|
24
|
+
# In-progress requests tracking
|
|
25
|
+
self.pending_requests = {} # hash -> (start_time, event)
|
|
26
|
+
self.request_lock = threading.Lock()
|
|
17
27
|
|
|
18
28
|
# Calculate current space usage
|
|
19
29
|
self.current_space = sum(f.stat().st_size for f in self.storage_path.glob('*') if f.is_file())
|
|
@@ -33,18 +43,196 @@ class Storage:
|
|
|
33
43
|
# Store the data
|
|
34
44
|
file_path.write_bytes(data)
|
|
35
45
|
self.current_space += data_size
|
|
46
|
+
|
|
47
|
+
# If this was a pending request, mark it as complete
|
|
48
|
+
with self.request_lock:
|
|
49
|
+
if data_hash in self.pending_requests:
|
|
50
|
+
_, event = self.pending_requests[data_hash]
|
|
51
|
+
event.set() # Signal that the data is now available
|
|
52
|
+
|
|
36
53
|
return True
|
|
37
54
|
|
|
38
|
-
def
|
|
39
|
-
"""
|
|
55
|
+
def _local_get(self, data_hash: bytes) -> Optional[bytes]:
|
|
56
|
+
"""Get data from local storage only, no network requests."""
|
|
40
57
|
file_path = self.storage_path / data_hash.hex()
|
|
41
|
-
if
|
|
58
|
+
if file_path.exists():
|
|
59
|
+
return file_path.read_bytes()
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def get(self, data_hash: bytes, timeout: Optional[float] = None) -> Optional[bytes]:
|
|
63
|
+
"""
|
|
64
|
+
Retrieve data by its hash, with network fallback.
|
|
65
|
+
|
|
66
|
+
This function will first check local storage. If not found and a node is attached,
|
|
67
|
+
it will initiate a network request asynchronously.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data_hash: The hash of the data to retrieve
|
|
71
|
+
timeout: Timeout in seconds to wait for network request, None for default
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The data bytes if found, None otherwise
|
|
75
|
+
"""
|
|
76
|
+
if timeout is None:
|
|
77
|
+
timeout = self.network_request_timeout
|
|
78
|
+
|
|
79
|
+
# First check local storage
|
|
80
|
+
local_data = self._local_get(data_hash)
|
|
81
|
+
if local_data:
|
|
82
|
+
return local_data
|
|
83
|
+
|
|
84
|
+
# If no node is attached, we can't make network requests
|
|
85
|
+
if self.node is None:
|
|
42
86
|
return None
|
|
43
|
-
|
|
87
|
+
|
|
88
|
+
# Check if there's already a pending request for this hash
|
|
89
|
+
with self.request_lock:
|
|
90
|
+
if data_hash in self.pending_requests:
|
|
91
|
+
start_time, event = self.pending_requests[data_hash]
|
|
92
|
+
# If this request has been going on too long, cancel it and start a new one
|
|
93
|
+
elapsed = time.time() - start_time
|
|
94
|
+
if elapsed > timeout:
|
|
95
|
+
# Cancel the old request
|
|
96
|
+
self.pending_requests.pop(data_hash)
|
|
97
|
+
else:
|
|
98
|
+
# Wait for the existing request to complete
|
|
99
|
+
wait_time = timeout - elapsed
|
|
100
|
+
else:
|
|
101
|
+
# No existing request, create a new one
|
|
102
|
+
event = threading.Event()
|
|
103
|
+
self.pending_requests[data_hash] = (time.time(), event)
|
|
104
|
+
# Start the actual network request in a separate thread
|
|
105
|
+
threading.Thread(
|
|
106
|
+
target=self._request_from_network,
|
|
107
|
+
args=(data_hash,),
|
|
108
|
+
daemon=True
|
|
109
|
+
).start()
|
|
110
|
+
wait_time = timeout
|
|
111
|
+
|
|
112
|
+
# Wait for the request to complete or timeout
|
|
113
|
+
if event.wait(wait_time):
|
|
114
|
+
# Event was set, data should be available now
|
|
115
|
+
with self.request_lock:
|
|
116
|
+
if data_hash in self.pending_requests:
|
|
117
|
+
self.pending_requests.pop(data_hash)
|
|
118
|
+
|
|
119
|
+
# Check if data is now in local storage
|
|
120
|
+
return self._local_get(data_hash)
|
|
121
|
+
else:
|
|
122
|
+
# Timed out waiting for data
|
|
123
|
+
with self.request_lock:
|
|
124
|
+
if data_hash in self.pending_requests:
|
|
125
|
+
self.pending_requests.pop(data_hash)
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def _request_from_network(self, data_hash: bytes):
|
|
129
|
+
"""
|
|
130
|
+
Request object from the network.
|
|
131
|
+
This is meant to be run in a separate thread.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
data_hash: The hash of the object to request
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
if hasattr(self.node, 'request_object'):
|
|
138
|
+
# Use the node's request_object method
|
|
139
|
+
self.node.request_object(data_hash)
|
|
140
|
+
# Note: We don't need to return anything or signal completion here
|
|
141
|
+
# The put() method will signal completion when the object is received
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"Error requesting object {data_hash.hex()} from network: {e}")
|
|
44
144
|
|
|
45
145
|
def contains(self, data_hash: bytes) -> bool:
|
|
46
146
|
"""Check if data exists in storage."""
|
|
47
147
|
return (self.storage_path / data_hash.hex()).exists()
|
|
148
|
+
|
|
149
|
+
def get_recursive(self, root_hash: bytes, max_depth: Optional[int] = None,
|
|
150
|
+
timeout: Optional[float] = None) -> Dict[bytes, bytes]:
|
|
151
|
+
"""
|
|
152
|
+
Recursively retrieve all objects starting from a root hash.
|
|
153
|
+
|
|
154
|
+
Objects not found locally will be requested from the network.
|
|
155
|
+
This method will continue processing objects that are available
|
|
156
|
+
while waiting for network responses.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
root_hash: The hash of the root object
|
|
160
|
+
max_depth: Maximum recursion depth, defaults to self.max_object_recursion
|
|
161
|
+
timeout: Time to wait for each object request, None for default
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dictionary mapping object hashes to their data
|
|
165
|
+
"""
|
|
166
|
+
if max_depth is None:
|
|
167
|
+
max_depth = self.max_object_recursion
|
|
168
|
+
|
|
169
|
+
if timeout is None:
|
|
170
|
+
timeout = self.network_request_timeout
|
|
171
|
+
|
|
172
|
+
# Start with the root object
|
|
173
|
+
objects = {}
|
|
174
|
+
pending_queue = [(root_hash, 0)] # (hash, depth)
|
|
175
|
+
processed = set()
|
|
176
|
+
|
|
177
|
+
# Process objects in the queue
|
|
178
|
+
while pending_queue:
|
|
179
|
+
current_hash, current_depth = pending_queue.pop(0)
|
|
180
|
+
|
|
181
|
+
# Skip if already processed or too deep
|
|
182
|
+
if current_hash in processed or current_depth > max_depth:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
processed.add(current_hash)
|
|
186
|
+
|
|
187
|
+
# Try to get the object (which may start a network request)
|
|
188
|
+
obj_data = self.get(current_hash, timeout)
|
|
189
|
+
if obj_data is None:
|
|
190
|
+
# Failed to get this object, but we continue with the rest
|
|
191
|
+
print(f"Warning: Failed to get object {current_hash.hex()}")
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
# Store the object in our result
|
|
195
|
+
objects[current_hash] = obj_data
|
|
196
|
+
|
|
197
|
+
# Only process non-leaf nodes for recursion
|
|
198
|
+
try:
|
|
199
|
+
# Extract leaf flag and type
|
|
200
|
+
is_leaf = struct.unpack("?", obj_data[0:1])[0]
|
|
201
|
+
if is_leaf:
|
|
202
|
+
# Leaf node, no need to recurse
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
type_indicator = obj_data[1:2]
|
|
206
|
+
next_depth = current_depth + 1
|
|
207
|
+
|
|
208
|
+
if type_indicator == b'L': # List
|
|
209
|
+
# Non-leaf list has child element hashes
|
|
210
|
+
elements_bytes = obj_data[2:]
|
|
211
|
+
element_hashes = [elements_bytes[i:i+32] for i in range(0, len(elements_bytes), 32)]
|
|
212
|
+
|
|
213
|
+
# Add each element hash to the queue
|
|
214
|
+
for elem_hash in element_hashes:
|
|
215
|
+
pending_queue.append((elem_hash, next_depth))
|
|
216
|
+
|
|
217
|
+
elif type_indicator == b'F': # Function
|
|
218
|
+
# Non-leaf function has body hash
|
|
219
|
+
remaining_bytes = obj_data[2:]
|
|
220
|
+
|
|
221
|
+
# Find the separator between params and body hash
|
|
222
|
+
params_end = remaining_bytes.find(b',', remaining_bytes.rfind(b','))
|
|
223
|
+
if params_end == -1:
|
|
224
|
+
params_end = 0 # No params
|
|
225
|
+
|
|
226
|
+
body_hash = remaining_bytes[params_end+1:]
|
|
227
|
+
|
|
228
|
+
# Add body hash to the queue
|
|
229
|
+
pending_queue.append((body_hash, next_depth))
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"Error processing object {current_hash.hex()}: {e}")
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
return objects
|
|
48
236
|
|
|
49
237
|
class Account:
|
|
50
238
|
def __init__(self, public_key: bytes, balance: int, counter: int):
|
astreum/node/relay/__init__.py
CHANGED
|
@@ -11,20 +11,43 @@ from .envelope import Envelope
|
|
|
11
11
|
from .bucket import KBucket
|
|
12
12
|
from .peer import Peer, PeerManager
|
|
13
13
|
from .route import RouteTable
|
|
14
|
+
import json
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
14
16
|
|
|
15
17
|
class Relay:
|
|
16
18
|
def __init__(self, config: dict):
|
|
19
|
+
"""Initialize relay with configuration."""
|
|
20
|
+
self.config = config
|
|
17
21
|
self.use_ipv6 = config.get('use_ipv6', False)
|
|
18
22
|
incoming_port = config.get('incoming_port', 7373)
|
|
19
23
|
self.max_message_size = config.get('max_message_size', 65536) # Max UDP datagram size
|
|
20
24
|
self.num_workers = config.get('num_workers', 4)
|
|
21
25
|
|
|
26
|
+
# Generate Ed25519 keypair for this node
|
|
27
|
+
if 'private_key' in config:
|
|
28
|
+
# Load existing private key if provided
|
|
29
|
+
try:
|
|
30
|
+
private_key_bytes = bytes.fromhex(config['private_key'])
|
|
31
|
+
self.private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(f"Error loading private key: {e}, generating new one")
|
|
34
|
+
self.private_key = ed25519.Ed25519PrivateKey.generate()
|
|
35
|
+
else:
|
|
36
|
+
# Generate new keypair
|
|
37
|
+
self.private_key = ed25519.Ed25519PrivateKey.generate()
|
|
38
|
+
|
|
39
|
+
# Use public key as node ID
|
|
40
|
+
self.public_key = self.private_key.public_key()
|
|
41
|
+
self.node_id = self.public_key.public_bytes_raw()
|
|
42
|
+
|
|
43
|
+
# Save private key bytes for config persistence
|
|
44
|
+
self.private_key_bytes = self.private_key.private_bytes_raw()
|
|
45
|
+
|
|
22
46
|
# Routes that this node participates in (0 = peer route, 1 = validation route)
|
|
23
47
|
self.routes: List[int] = []
|
|
24
48
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
self.routes.append(0) # Peer route
|
|
49
|
+
# Peer route is always enabled
|
|
50
|
+
self.routes.append(0) # Peer route
|
|
28
51
|
|
|
29
52
|
if config.get('validation_route', False):
|
|
30
53
|
self.routes.append(1) # Validation route
|
|
@@ -54,11 +77,18 @@ class Relay:
|
|
|
54
77
|
self.outgoing_queue = Queue()
|
|
55
78
|
|
|
56
79
|
# Message handling
|
|
57
|
-
self.message_handlers: Dict[Topic, Callable] = {
|
|
80
|
+
self.message_handlers: Dict[Topic, Callable] = {
|
|
81
|
+
Topic.PEER_ROUTE: None, # set by Node later
|
|
82
|
+
Topic.OBJECT_REQUEST: None, # set by Node later
|
|
83
|
+
Topic.OBJECT_RESPONSE: None, # set by Node later
|
|
84
|
+
}
|
|
58
85
|
|
|
59
86
|
# Route buckets (peers for each route)
|
|
60
87
|
self.peer_route_bucket = KBucket(k=20) # Bucket for peer route
|
|
61
88
|
self.validation_route_bucket = KBucket(k=20) # Bucket for validation route
|
|
89
|
+
|
|
90
|
+
# Initialize route table with our node ID
|
|
91
|
+
self.route_table = RouteTable(self)
|
|
62
92
|
|
|
63
93
|
# Start worker threads
|
|
64
94
|
self._start_workers()
|
|
@@ -197,6 +227,12 @@ class Relay:
|
|
|
197
227
|
elif envelope.message.topic in (Topic.LATEST_BLOCK_REQUEST, Topic.GET_BLOCKS):
|
|
198
228
|
# Allow all nodes to request blocks for syncing
|
|
199
229
|
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
230
|
+
elif envelope.message.topic == Topic.OBJECT_REQUEST:
|
|
231
|
+
# Handle object request
|
|
232
|
+
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
233
|
+
elif envelope.message.topic == Topic.OBJECT_RESPONSE:
|
|
234
|
+
# Handle object response
|
|
235
|
+
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
200
236
|
else:
|
|
201
237
|
# For other message types, always process
|
|
202
238
|
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
@@ -240,9 +276,69 @@ class Relay:
|
|
|
240
276
|
encoded_data = envelope.to_bytes()
|
|
241
277
|
self.send(encoded_data, addr)
|
|
242
278
|
|
|
279
|
+
def send_message_to_addr(self, addr: tuple, topic: Topic, body: bytes):
|
|
280
|
+
"""
|
|
281
|
+
Send a message to a specific address.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
addr: Tuple of (ip, port) to send to
|
|
285
|
+
topic: Message topic
|
|
286
|
+
body: Message body
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
# Create an envelope with our node id and the message
|
|
290
|
+
message = Message(self.node_id, topic, body)
|
|
291
|
+
envelope = Envelope(message)
|
|
292
|
+
|
|
293
|
+
# Serialize and send
|
|
294
|
+
self.outgoing_socket.sendto(envelope.to_bytes(), addr)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
print(f"Error sending message to {addr}: {e}")
|
|
297
|
+
|
|
298
|
+
def send_message_to_peer(self, peer: Peer, topic: Topic, body):
|
|
299
|
+
"""
|
|
300
|
+
Send a message to a specific peer.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
peer: Peer to send to
|
|
304
|
+
topic: Message topic
|
|
305
|
+
body: Message body (bytes or JSON serializable)
|
|
306
|
+
"""
|
|
307
|
+
# Convert body to bytes if it's not already
|
|
308
|
+
if not isinstance(body, bytes):
|
|
309
|
+
if isinstance(body, dict) or isinstance(body, list):
|
|
310
|
+
body = json.dumps(body).encode('utf-8')
|
|
311
|
+
else:
|
|
312
|
+
body = str(body).encode('utf-8')
|
|
313
|
+
|
|
314
|
+
# Send to the peer's address
|
|
315
|
+
self.send_message_to_addr(peer.address, topic, body)
|
|
316
|
+
|
|
243
317
|
def stop(self):
|
|
244
318
|
"""Stop all worker threads."""
|
|
245
319
|
self.running = False
|
|
246
320
|
# Wait for queues to be processed
|
|
247
321
|
self.incoming_queue.join()
|
|
248
322
|
self.outgoing_queue.join()
|
|
323
|
+
|
|
324
|
+
# RouteTable wrapper methods
|
|
325
|
+
def add_peer(self, addr, public_key, difficulty):
|
|
326
|
+
"""Add a peer to the routing table."""
|
|
327
|
+
return self.route_table.update_peer(addr, public_key, difficulty)
|
|
328
|
+
|
|
329
|
+
def get_closest_peers(self, target_id, count=3):
|
|
330
|
+
"""Get the closest peers to the target ID."""
|
|
331
|
+
return self.route_table.get_closest_peers(target_id, count=count)
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def num_buckets(self):
|
|
335
|
+
"""Get the number of buckets in the routing table."""
|
|
336
|
+
return self.route_table.num_buckets
|
|
337
|
+
|
|
338
|
+
def get_bucket_peers(self, bucket_index):
|
|
339
|
+
"""Get peers from a specific bucket."""
|
|
340
|
+
return self.route_table.get_bucket_peers(bucket_index)
|
|
341
|
+
|
|
342
|
+
def has_peer(self, addr):
|
|
343
|
+
"""Check if a peer with the given address exists in the routing table."""
|
|
344
|
+
return self.route_table.has_peer(addr)
|