astreum 0.1.13__py3-none-any.whl → 0.1.14__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/node/__init__.py +137 -30
- astreum/node/relay/__init__.py +72 -30
- {astreum-0.1.13.dist-info → astreum-0.1.14.dist-info}/METADATA +1 -1
- {astreum-0.1.13.dist-info → astreum-0.1.14.dist-info}/RECORD +7 -7
- {astreum-0.1.13.dist-info → astreum-0.1.14.dist-info}/LICENSE +0 -0
- {astreum-0.1.13.dist-info → astreum-0.1.14.dist-info}/WHEEL +0 -0
- {astreum-0.1.13.dist-info → astreum-0.1.14.dist-info}/top_level.txt +0 -0
astreum/node/__init__.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import hashlib
|
|
3
3
|
import time
|
|
4
|
-
|
|
4
|
+
import threading
|
|
5
|
+
import random
|
|
6
|
+
from typing import Tuple, Optional, List
|
|
5
7
|
import json
|
|
6
8
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
7
9
|
|
|
@@ -71,6 +73,84 @@ class Node:
|
|
|
71
73
|
# Candidate chains that might be adopted
|
|
72
74
|
self.candidate_chains = {} # chain_id -> {'latest_block': block, 'timestamp': time.time()}
|
|
73
75
|
|
|
76
|
+
# Block query timers for different routes
|
|
77
|
+
self.running = False
|
|
78
|
+
self.block_query_threads = []
|
|
79
|
+
|
|
80
|
+
def start(self):
|
|
81
|
+
"""Start the node and all its services."""
|
|
82
|
+
self.running = True
|
|
83
|
+
|
|
84
|
+
# Start periodic block query thread for validation route
|
|
85
|
+
validation_query_thread = threading.Thread(
|
|
86
|
+
target=self._periodic_validation_route_query,
|
|
87
|
+
daemon=True
|
|
88
|
+
)
|
|
89
|
+
validation_query_thread.start()
|
|
90
|
+
self.block_query_threads.append(validation_query_thread)
|
|
91
|
+
|
|
92
|
+
print(f"Node started with ID {self.node_id.hex()}")
|
|
93
|
+
print(f"Listening on port {self.relay.incoming_port}")
|
|
94
|
+
|
|
95
|
+
def stop(self):
|
|
96
|
+
"""Stop the node and all its services."""
|
|
97
|
+
self.running = False
|
|
98
|
+
|
|
99
|
+
# Stop all threads
|
|
100
|
+
for thread in self.block_query_threads:
|
|
101
|
+
if thread.is_alive():
|
|
102
|
+
thread.join(timeout=1.0) # Give threads 1 second to shut down
|
|
103
|
+
|
|
104
|
+
print("Node stopped")
|
|
105
|
+
|
|
106
|
+
def _periodic_validation_route_query(self):
|
|
107
|
+
"""Periodically query random peers in the validation route for latest blocks."""
|
|
108
|
+
while self.running:
|
|
109
|
+
try:
|
|
110
|
+
# Query 3 random peers from validation route for latest block
|
|
111
|
+
self._query_random_peers_for_latest_block(route_type=1, count=3)
|
|
112
|
+
|
|
113
|
+
# Prune old candidate chains periodically
|
|
114
|
+
self._prune_candidate_chains()
|
|
115
|
+
|
|
116
|
+
# Sleep according to validator status
|
|
117
|
+
if self.is_validator:
|
|
118
|
+
# Validators check more frequently (every second)
|
|
119
|
+
time.sleep(1)
|
|
120
|
+
else:
|
|
121
|
+
# Regular nodes check less frequently (every 3 seconds)
|
|
122
|
+
time.sleep(3)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f"Error in validation route query: {e}")
|
|
125
|
+
time.sleep(1) # Sleep briefly before retrying
|
|
126
|
+
|
|
127
|
+
def _query_random_peers_for_latest_block(self, route_type: int, count: int = 3):
|
|
128
|
+
"""
|
|
129
|
+
Query random peers from specified route for latest block.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
route_type (int): Route type (0 for peer, 1 for validation)
|
|
133
|
+
count (int): Number of random peers to query
|
|
134
|
+
"""
|
|
135
|
+
# Only continue if we're tracking this route
|
|
136
|
+
if not self.relay.is_tracking_route(route_type):
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Get random peers from the route
|
|
140
|
+
random_peers = self.relay.get_random_peers_from_route(route_type, count)
|
|
141
|
+
|
|
142
|
+
# Query each peer for latest block
|
|
143
|
+
for peer in random_peers:
|
|
144
|
+
try:
|
|
145
|
+
# Create empty request message
|
|
146
|
+
request_data = b''
|
|
147
|
+
|
|
148
|
+
# Send request to peer
|
|
149
|
+
addr = (peer.ip, peer.port)
|
|
150
|
+
self.relay.send_message(request_data, Topic.LATEST_BLOCK_REQUEST, addr)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f"Error querying peer {peer.node_id.hex()}: {e}")
|
|
153
|
+
|
|
74
154
|
def _handle_ping(self, body: bytes, addr: Tuple[str, int], envelope):
|
|
75
155
|
"""
|
|
76
156
|
Handle ping messages by storing peer info and responding with a pong.
|
|
@@ -383,10 +463,7 @@ class Node:
|
|
|
383
463
|
in chain is in the previous field.
|
|
384
464
|
"""
|
|
385
465
|
try:
|
|
386
|
-
#
|
|
387
|
-
# This is now already checked by the relay's _handle_message method
|
|
388
|
-
if not self.relay.is_in_validation_route():
|
|
389
|
-
return
|
|
466
|
+
# All nodes can process latest blocks now, regardless of route membership
|
|
390
467
|
|
|
391
468
|
# Deserialize the block
|
|
392
469
|
block = Block.from_bytes(body)
|
|
@@ -399,24 +476,28 @@ class Node:
|
|
|
399
476
|
if not self.followed_chain_id or block.chain_id != self.followed_chain_id:
|
|
400
477
|
self._add_candidate_chain(block)
|
|
401
478
|
return
|
|
402
|
-
|
|
403
|
-
# Get our current latest block
|
|
404
|
-
our_latest = self.latest_block
|
|
405
|
-
|
|
406
|
-
# Verify block hash links to our latest block
|
|
407
|
-
if our_latest and block.previous_hash == our_latest.hash:
|
|
408
|
-
# Process the valid block
|
|
409
|
-
self.machine.process_block(block)
|
|
410
479
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
480
|
+
# Only proceed if block chain_id matches what we're following
|
|
481
|
+
if self.followed_chain_id and block.chain_id != self.followed_chain_id:
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
if not self.latest_block:
|
|
485
|
+
# We don't have a latest block, so this might be the first one we've seen
|
|
486
|
+
# Store it as our latest
|
|
487
|
+
self._update_latest_block(block)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# If this block is newer than our latest, update our latest block
|
|
491
|
+
if block.height > self.latest_block.height:
|
|
492
|
+
# Verify chain continuity
|
|
493
|
+
if self.latest_block.hash_bytes in block.previous_blocks:
|
|
494
|
+
# This is a valid continuation of our chain
|
|
495
|
+
self._update_latest_block(block)
|
|
496
|
+
else:
|
|
497
|
+
# This block doesn't build on our latest, check for forking
|
|
498
|
+
# but continue tracking it as a candidate
|
|
499
|
+
self._add_candidate_chain(block)
|
|
500
|
+
|
|
420
501
|
except Exception as e:
|
|
421
502
|
print(f"Error handling latest block: {e}")
|
|
422
503
|
|
|
@@ -506,17 +587,19 @@ class Node:
|
|
|
506
587
|
|
|
507
588
|
def _add_candidate_chain(self, block):
|
|
508
589
|
"""
|
|
509
|
-
Add a block to candidate chains
|
|
590
|
+
Add a block to the candidate chains.
|
|
510
591
|
|
|
511
592
|
Args:
|
|
512
|
-
block:
|
|
593
|
+
block: Block to add
|
|
513
594
|
"""
|
|
514
595
|
chain_id = block.chain_id
|
|
515
596
|
|
|
516
|
-
#
|
|
597
|
+
# Check if we already have this chain as a candidate
|
|
517
598
|
if chain_id in self.candidate_chains:
|
|
518
|
-
|
|
519
|
-
|
|
599
|
+
existing_block = self.candidate_chains[chain_id]['latest_block']
|
|
600
|
+
|
|
601
|
+
# Only update if this block is newer
|
|
602
|
+
if block.height > existing_block.height:
|
|
520
603
|
self.candidate_chains[chain_id] = {
|
|
521
604
|
'latest_block': block,
|
|
522
605
|
'timestamp': time.time()
|
|
@@ -527,10 +610,34 @@ class Node:
|
|
|
527
610
|
'latest_block': block,
|
|
528
611
|
'timestamp': time.time()
|
|
529
612
|
}
|
|
613
|
+
|
|
614
|
+
print(f"Added candidate chain {chain_id.hex()} with height {block.height}")
|
|
615
|
+
|
|
616
|
+
def _update_latest_block(self, block):
|
|
617
|
+
"""
|
|
618
|
+
Update our latest block and process it.
|
|
530
619
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
620
|
+
Args:
|
|
621
|
+
block: New latest block to set
|
|
622
|
+
"""
|
|
623
|
+
# Process the block if it's new
|
|
624
|
+
if not self.latest_block or block.hash_bytes != self.latest_block.hash_bytes:
|
|
625
|
+
# Process block logic in the machine
|
|
626
|
+
self.machine.process_block(block)
|
|
627
|
+
|
|
628
|
+
# Update our latest block reference
|
|
629
|
+
self.latest_block = block
|
|
630
|
+
|
|
631
|
+
# Update followed chain ID if needed
|
|
632
|
+
if not self.followed_chain_id:
|
|
633
|
+
self.followed_chain_id = block.chain_id
|
|
634
|
+
|
|
635
|
+
print(f"Updated latest block to height {block.height}, hash {block.hash}")
|
|
636
|
+
|
|
637
|
+
# Save latest block to storage for persistence
|
|
638
|
+
if self.storage:
|
|
639
|
+
self.storage.put_latest_block(block)
|
|
640
|
+
|
|
534
641
|
def _prune_candidate_chains(self):
|
|
535
642
|
"""Remove candidate chains that are older than 1 hour."""
|
|
536
643
|
current_time = time.time()
|
astreum/node/relay/__init__.py
CHANGED
|
@@ -4,6 +4,8 @@ Relay module for handling network communication in the Astreum node.
|
|
|
4
4
|
|
|
5
5
|
import socket
|
|
6
6
|
import threading
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
7
9
|
from queue import Queue
|
|
8
10
|
from typing import Tuple, Callable, Dict, Set, Optional, List
|
|
9
11
|
from .message import Message, Topic
|
|
@@ -43,14 +45,16 @@ class Relay:
|
|
|
43
45
|
# Save private key bytes for config persistence
|
|
44
46
|
self.private_key_bytes = self.private_key.private_bytes_raw()
|
|
45
47
|
|
|
46
|
-
# Routes that this node participates in
|
|
48
|
+
# Routes that this node participates in
|
|
49
|
+
# 0 = peer route, 1 = validation route
|
|
50
|
+
# All routes are tracked by default, but we only join some
|
|
47
51
|
self.routes: List[int] = []
|
|
52
|
+
self.tracked_routes: List[int] = [0, 1] # Track all routes
|
|
48
53
|
|
|
49
|
-
#
|
|
54
|
+
# Always join peer route
|
|
50
55
|
self.routes.append(0) # Peer route
|
|
51
56
|
|
|
52
|
-
# Check if
|
|
53
|
-
# This is now controlled by the parent Node class based on validation_private_key
|
|
57
|
+
# Check if this node should join validation route
|
|
54
58
|
if config.get('validation_route', False):
|
|
55
59
|
self.routes.append(1) # Validation route
|
|
56
60
|
|
|
@@ -102,24 +106,53 @@ class Relay:
|
|
|
102
106
|
def is_in_validation_route(self) -> bool:
|
|
103
107
|
"""Check if this node is part of the validation route."""
|
|
104
108
|
return 1 in self.routes
|
|
109
|
+
|
|
110
|
+
def is_tracking_route(self, route_type: int) -> bool:
|
|
111
|
+
"""Check if this node is tracking a specific route."""
|
|
112
|
+
return route_type in self.tracked_routes
|
|
105
113
|
|
|
106
|
-
def
|
|
114
|
+
def get_random_peers_from_route(self, route_type: int, count: int = 3) -> List[Peer]:
|
|
107
115
|
"""
|
|
108
|
-
|
|
116
|
+
Get a list of random peers from different buckets in the specified route.
|
|
109
117
|
|
|
110
118
|
Args:
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
route_type (int): Route type (0 for peer, 1 for validation)
|
|
120
|
+
count (int): Number of random peers to select (one from each bucket)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List[Peer]: List of randomly selected peers from different buckets
|
|
113
124
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
result = []
|
|
126
|
+
route_id = self._get_route_id(route_type)
|
|
127
|
+
|
|
128
|
+
# Get all buckets that have peers for this route
|
|
129
|
+
buckets_with_peers = []
|
|
130
|
+
for i, bucket in enumerate(self.routing_table):
|
|
131
|
+
# For each bucket, collect peers that are in this route
|
|
132
|
+
route_peers_in_bucket = [peer for peer in bucket.values()
|
|
133
|
+
if peer.routes and route_id in peer.routes]
|
|
134
|
+
if route_peers_in_bucket:
|
|
135
|
+
buckets_with_peers.append((i, route_peers_in_bucket))
|
|
136
|
+
|
|
137
|
+
# If we don't have any buckets with peers, return empty list
|
|
138
|
+
if not buckets_with_peers:
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
# If we have fewer buckets than requested count, adjust count
|
|
142
|
+
sample_count = min(count, len(buckets_with_peers))
|
|
143
|
+
|
|
144
|
+
# Sample random buckets
|
|
145
|
+
selected_buckets = random.sample(buckets_with_peers, sample_count)
|
|
146
|
+
|
|
147
|
+
# For each selected bucket, pick one random peer
|
|
148
|
+
for bucket_idx, peers in selected_buckets:
|
|
149
|
+
# Select one random peer from this bucket
|
|
150
|
+
selected_peer = random.choice(peers)
|
|
151
|
+
result.append(selected_peer)
|
|
121
152
|
|
|
122
|
-
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
def get_peers_in_route(self, route_type: int) -> List[Peer]:
|
|
123
156
|
"""
|
|
124
157
|
Get all peers in a specific route.
|
|
125
158
|
|
|
@@ -135,6 +168,22 @@ class Relay:
|
|
|
135
168
|
return self.validation_route_bucket.get_peers()
|
|
136
169
|
return []
|
|
137
170
|
|
|
171
|
+
def add_peer_to_route(self, peer: Peer, route_types: List[int]):
|
|
172
|
+
"""
|
|
173
|
+
Add a peer to specified routes.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
peer (Peer): The peer to add
|
|
177
|
+
route_types (List[int]): List of route types to add the peer to (0 = peer, 1 = validation)
|
|
178
|
+
"""
|
|
179
|
+
for route_type in route_types:
|
|
180
|
+
if route_type == 0: # Peer route
|
|
181
|
+
# Add to top of bucket, eject last if at capacity
|
|
182
|
+
self.peer_route_bucket.add(peer, to_front=True)
|
|
183
|
+
elif route_type == 1: # Validation route
|
|
184
|
+
# Add to top of bucket, eject last if at capacity
|
|
185
|
+
self.validation_route_bucket.add(peer, to_front=True)
|
|
186
|
+
|
|
138
187
|
def register_message_handler(self, topic: Topic, handler_func):
|
|
139
188
|
"""Register a handler function for a specific message topic."""
|
|
140
189
|
self.message_handlers[topic] = handler_func
|
|
@@ -197,26 +246,19 @@ class Relay:
|
|
|
197
246
|
"""Handle an incoming message."""
|
|
198
247
|
envelope = Envelope.from_bytes(data)
|
|
199
248
|
if envelope and envelope.message.topic in self.message_handlers:
|
|
200
|
-
#
|
|
201
|
-
if envelope.message.topic
|
|
202
|
-
# Only process if we're part of the validation route
|
|
249
|
+
# For transaction messages, only process if we're in validation route
|
|
250
|
+
if envelope.message.topic == Topic.TRANSACTION:
|
|
203
251
|
if self.is_in_validation_route():
|
|
204
252
|
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
205
|
-
|
|
206
|
-
|
|
253
|
+
# For block messages, only process if we're in validation route
|
|
254
|
+
elif envelope.message.topic == Topic.BLOCK:
|
|
207
255
|
if self.is_in_validation_route():
|
|
208
256
|
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
212
|
-
elif envelope.message.topic == Topic.OBJECT_REQUEST:
|
|
213
|
-
# Handle object request
|
|
214
|
-
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
215
|
-
elif envelope.message.topic == Topic.OBJECT_RESPONSE:
|
|
216
|
-
# Handle object response
|
|
257
|
+
# Latest block and latest block requests can be handled by any node tracking the routes
|
|
258
|
+
elif envelope.message.topic in (Topic.LATEST_BLOCK, Topic.LATEST_BLOCK_REQUEST):
|
|
217
259
|
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
260
|
+
# For other message types, always process
|
|
218
261
|
else:
|
|
219
|
-
# For other message types, always process
|
|
220
262
|
self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
|
|
221
263
|
|
|
222
264
|
def send(self, data: bytes, addr: Tuple[str, int]):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
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
|
|
@@ -20,9 +20,9 @@ astreum/lispeum/special/number/addition.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
|
20
20
|
astreum/machine/__init__.py,sha256=GOdZl1tS9uIJHbq5WVcplifMDPDLQroX7CVew-K2YbA,15262
|
|
21
21
|
astreum/machine/environment.py,sha256=K0084U6B7wwjrDZ9b2_7cEcbBzsB7UOy_Zpbrr7B3GY,834
|
|
22
22
|
astreum/machine/error.py,sha256=MvqBaZZt33rNELNhUJ2lER3TE3aS8WVqsWF2hz2AwoA,38
|
|
23
|
-
astreum/node/__init__.py,sha256=
|
|
23
|
+
astreum/node/__init__.py,sha256=Zvu7_oadjB8V3TQpDR9pTdURXZpcHKI5Qhxz4Ykep7U,28271
|
|
24
24
|
astreum/node/models.py,sha256=9Uf2_u55uxWG0ujjySvFJUO5Ub-EzlMnnMJWcgJHjHk,11980
|
|
25
|
-
astreum/node/relay/__init__.py,sha256=
|
|
25
|
+
astreum/node/relay/__init__.py,sha256=0zvbchIbLUPqGA7QXJbXokKupcIq6Iu3X3VUpxejIQc,14981
|
|
26
26
|
astreum/node/relay/bucket.py,sha256=pcmollbbM-xeHlmDxLZnzvf0Ut-9v9RoN6SijYiQuu8,2893
|
|
27
27
|
astreum/node/relay/envelope.py,sha256=TfkynttoPX7smvMV7xEAdtIlfz-Z-EZjuhZ826csZxA,10078
|
|
28
28
|
astreum/node/relay/message.py,sha256=uezmGjNaQK4fZmYQLCHd2YpiosaaFb8DOa3H58HS1jA,2887
|
|
@@ -30,8 +30,8 @@ astreum/node/relay/peer.py,sha256=DlvTR9j0BZQ1dW-p_9UGgfLvQqwNdpNLMSCYEW4FhyI,58
|
|
|
30
30
|
astreum/node/relay/route.py,sha256=fyOSsAe1mfsCVeN6LtQ_OEUEb1FiC5dobZBEJKNGU9U,5814
|
|
31
31
|
astreum/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
astreum/utils/bytes_format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
|
|
33
|
-
astreum-0.1.
|
|
34
|
-
astreum-0.1.
|
|
35
|
-
astreum-0.1.
|
|
36
|
-
astreum-0.1.
|
|
37
|
-
astreum-0.1.
|
|
33
|
+
astreum-0.1.14.dist-info/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
34
|
+
astreum-0.1.14.dist-info/METADATA,sha256=W3SxJCJeN-OxSs_rGNuG1FrqQunyOFYBOlaCZkjlEyk,3261
|
|
35
|
+
astreum-0.1.14.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
36
|
+
astreum-0.1.14.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
37
|
+
astreum-0.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|