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 CHANGED
@@ -1,7 +1,9 @@
1
1
  import os
2
2
  import hashlib
3
3
  import time
4
- from typing import Tuple, Optional
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
- # Check if we're in the validation route
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
- # Update our latest block
412
- self.latest_block = block
413
- # Check if this block is ahead of our current chain
414
- elif our_latest and block.height > our_latest.height:
415
- # Block is ahead but doesn't link directly to our latest
416
- # Add to candidate chains for potential future adoption
417
- self._add_candidate_chain(block)
418
-
419
- # No automatic broadcasting - nodes will request latest blocks when needed
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 for potential future adoption.
590
+ Add a block to the candidate chains.
510
591
 
511
592
  Args:
512
- block: The block to add as a candidate
593
+ block: Block to add
513
594
  """
514
595
  chain_id = block.chain_id
515
596
 
516
- # If we already have this chain as a candidate, only update if this block is newer
597
+ # Check if we already have this chain as a candidate
517
598
  if chain_id in self.candidate_chains:
518
- current_candidate = self.candidate_chains[chain_id]['latest_block']
519
- if block.height > current_candidate.height:
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
- # Prune old candidates (older than 1 hour)
532
- self._prune_candidate_chains()
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()
@@ -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 (0 = peer route, 1 = validation route)
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
- # Peer route is always enabled
54
+ # Always join peer route
50
55
  self.routes.append(0) # Peer route
51
56
 
52
- # Check if the node should join validation route
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 add_peer_to_route(self, peer: Peer, route_types: List[int]):
114
+ def get_random_peers_from_route(self, route_type: int, count: int = 3) -> List[Peer]:
107
115
  """
108
- Add a peer to specified routes.
116
+ Get a list of random peers from different buckets in the specified route.
109
117
 
110
118
  Args:
111
- peer (Peer): The peer to add
112
- route_types (List[int]): List of route types to add the peer to (0 = peer, 1 = validation)
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
- for route_type in route_types:
115
- if route_type == 0: # Peer route
116
- # Add to top of bucket, eject last if at capacity
117
- self.peer_route_bucket.add(peer, to_front=True)
118
- elif route_type == 1: # Validation route
119
- # Add to top of bucket, eject last if at capacity
120
- self.validation_route_bucket.add(peer, to_front=True)
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
- def get_route_peers(self, route_type: int) -> List[Peer]:
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
- # Check if this is a transaction or block message that requires validation route
201
- if envelope.message.topic in (Topic.TRANSACTION, Topic.BLOCK):
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
- elif envelope.message.topic == Topic.LATEST_BLOCK:
206
- # For latest_block, we only process if we're in the validation route
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
- elif envelope.message.topic in (Topic.LATEST_BLOCK_REQUEST, Topic.GET_BLOCKS):
210
- # Allow all nodes to request blocks for syncing
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.13
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=VlyhTLOjqy5VJlcLiTfTqgcsWuN3mudv_WtM5ixrojc,24121
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=k-_lEenUvLnBriSqjsqpnbwua1j6ZUghqkGD_Jc9ZYM,13493
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.13.dist-info/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
34
- astreum-0.1.13.dist-info/METADATA,sha256=0wxUc4qtd3ipFd3-IwFjDcsWTBqAK8I-u3hrgBmgVX8,3261
35
- astreum-0.1.13.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
36
- astreum-0.1.13.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
37
- astreum-0.1.13.dist-info/RECORD,,
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,,