astreum 0.1.5__py3-none-any.whl → 0.1.7__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.

@@ -0,0 +1,461 @@
1
+ import os
2
+ import hashlib
3
+ import time
4
+ from typing import Tuple
5
+
6
+ from .relay import Relay, Topic
7
+ from .relay.peer import Peer
8
+ from .storage import Storage
9
+ from .route_table import RouteTable
10
+ from .machine import AstreumMachine
11
+ from .utils import encode, decode
12
+ from .models import Block, Transaction
13
+ from astreum.lispeum.storage import store_expr, get_expr_from_storage
14
+
15
+ class Node:
16
+ def __init__(self, config: dict):
17
+ self.config = config
18
+ self.node_id = config.get('node_id', os.urandom(32)) # Default to random ID if not provided
19
+ self.relay = Relay(config)
20
+ self.storage = Storage(config)
21
+
22
+ # Latest block of the chain this node is following
23
+ self.latest_block = None
24
+ self.followed_chain_id = config.get('followed_chain_id', None)
25
+
26
+ # Candidate chains that might be adopted
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
34
+ self.machine = AstreumMachine(node=self)
35
+
36
+ # Register message handlers
37
+ self._register_message_handlers()
38
+
39
+ # Initialize latest block from storage if available
40
+ self._initialize_latest_block()
41
+
42
+ def _register_message_handlers(self):
43
+ """Register handlers for different message topics."""
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)
53
+
54
+ def _handle_ping(self, body: bytes, addr: Tuple[str, int], envelope):
55
+ """
56
+ Handle ping messages by storing peer info and responding with a pong.
57
+
58
+ The ping message contains:
59
+ - public_key: The sender's public key
60
+ - difficulty: The sender's preferred proof-of-work difficulty
61
+ - routes: The sender's available routes
62
+ """
63
+ try:
64
+ # Parse peer information from the ping message
65
+ parts = decode(body)
66
+ if len(parts) != 3:
67
+ return
68
+
69
+ public_key, difficulty_bytes, routes_data = parts
70
+ difficulty = int.from_bytes(difficulty_bytes, byteorder='big')
71
+
72
+ # Store peer information in routing table
73
+ peer = self.route_table.update_peer(addr, public_key, difficulty)
74
+
75
+ # Process the routes the sender is participating in
76
+ if routes_data:
77
+ # routes_data is a simple list like [0, 1] meaning peer route and validation route
78
+ # Add peer to each route they participate in
79
+ self.relay.add_peer_to_route(peer, list(routes_data))
80
+
81
+ # Create response with our public key, difficulty and routes we participate in
82
+ pong_data = encode([
83
+ self.node_id, # Our public key
84
+ self.config.get('difficulty', 1).to_bytes(4, byteorder='big'), # Our difficulty
85
+ self.relay.get_routes() # Our routes as bytes([0, 1]) for peer and validation
86
+ ])
87
+
88
+ self.relay.send_message(pong_data, Topic.PONG, addr)
89
+ except Exception as e:
90
+ print(f"Error handling ping message: {e}")
91
+
92
+ def _handle_pong(self, body: bytes, addr: Tuple[str, int], envelope):
93
+ """
94
+ Handle pong messages by updating peer information.
95
+ No response is sent to a pong message.
96
+ """
97
+ try:
98
+ # Parse peer information from the pong message
99
+ parts = decode(body)
100
+ if len(parts) != 3:
101
+ return
102
+
103
+ public_key, difficulty_bytes, routes_data = parts
104
+ difficulty = int.from_bytes(difficulty_bytes, byteorder='big')
105
+
106
+ # Update peer information in routing table
107
+ peer = self.route_table.update_peer(addr, public_key, difficulty)
108
+
109
+ # Process the routes the sender is participating in
110
+ if routes_data:
111
+ # routes_data is a simple list like [0, 1] meaning peer route and validation route
112
+ # Add peer to each route they participate in
113
+ self.relay.add_peer_to_route(peer, list(routes_data))
114
+ except Exception as e:
115
+ print(f"Error handling pong message: {e}")
116
+
117
+ def _handle_object_request(self, body: bytes, addr: Tuple[str, int], envelope):
118
+ """
119
+ Handle request for an object by its hash.
120
+ Check storage and return if available, otherwise ignore.
121
+ """
122
+ try:
123
+ # The body is the hash of the requested object
124
+ object_hash = body
125
+ object_data = self.storage.get(object_hash)
126
+
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
+ except Exception as e:
132
+ print(f"Error handling object request: {e}")
133
+
134
+ def _handle_object(self, body: bytes, addr: Tuple[str, int], envelope):
135
+ """
136
+ Handle receipt of an object.
137
+ If not in storage, verify the hash and put in storage.
138
+ """
139
+ try:
140
+ # Verify hash matches the object
141
+ object_hash = hashlib.sha256(body).digest()
142
+
143
+ # Check if we already have this object
144
+ if not self.storage.exists(object_hash):
145
+ # Store the object
146
+ self.storage.put(object_hash, body)
147
+ except Exception as e:
148
+ print(f"Error handling object: {e}")
149
+
150
+ def _handle_route_request(self, body: bytes, addr: Tuple[str, int], envelope):
151
+ """
152
+ Handle request for routing information.
153
+ Seed route to peer with one peer per bucket in the route table.
154
+ """
155
+ try:
156
+ # Create a list to store one peer from each bucket
157
+ route_peers = []
158
+
159
+ # Get one peer from each bucket
160
+ for bucket_index in range(self.route_table.num_buckets):
161
+ peers = self.route_table.get_bucket_peers(bucket_index)
162
+ if peers and len(peers) > 0:
163
+ # Add one peer from this bucket
164
+ route_peers.append(peers[0])
165
+
166
+ # Serialize the peer list
167
+ # Format: List of [peer_addr, peer_port, peer_key]
168
+ peer_data = []
169
+ for peer in route_peers:
170
+ peer_addr, peer_port = peer.address
171
+ peer_data.append(encode([
172
+ peer_addr.encode('utf-8'),
173
+ peer_port.to_bytes(2, byteorder='big'),
174
+ peer.node_id
175
+ ]))
176
+
177
+ # Encode the complete route data
178
+ route_data = encode(peer_data)
179
+
180
+ # Send routing information back
181
+ self.relay.send_message(route_data, Topic.ROUTE, addr)
182
+ except Exception as e:
183
+ print(f"Error handling route request: {e}")
184
+
185
+ def _handle_route(self, body: bytes, addr: Tuple[str, int], envelope):
186
+ """
187
+ Handle receipt of a route message containing a list of IP addresses to ping.
188
+ """
189
+ try:
190
+ # Decode the list of peers
191
+ peer_entries = decode(body)
192
+
193
+ # Process each peer
194
+ for peer_data in peer_entries:
195
+ try:
196
+ peer_parts = decode(peer_data)
197
+ if len(peer_parts) != 3:
198
+ continue
199
+
200
+ peer_addr_bytes, peer_port_bytes, peer_id = peer_parts
201
+ peer_addr = peer_addr_bytes.decode('utf-8')
202
+ peer_port = int.from_bytes(peer_port_bytes, byteorder='big')
203
+
204
+ # Create peer address tuple
205
+ peer_address = (peer_addr, peer_port)
206
+
207
+ # Ping this peer if it's not already in our routing table
208
+ # and it's not our own address
209
+ if (not self.route_table.has_peer(peer_address) and
210
+ peer_address != self.relay.get_address()):
211
+ # Create ping message with our info and routes
212
+ # Encode our peer and validation routes
213
+ peer_routes_list = self.relay.get_routes()
214
+
215
+ # Combine into a single list of routes with type flags
216
+ # For each route: [is_validation_route, route_id]
217
+ routes = []
218
+
219
+ # Add peer routes (type flag = 0)
220
+ for route in peer_routes_list:
221
+ routes.append(encode([bytes([0]), route]))
222
+
223
+ # Encode the complete routes list
224
+ all_routes = encode(routes)
225
+
226
+ ping_data = encode([
227
+ self.node_id, # Our public key
228
+ self.config.get('difficulty', 1).to_bytes(4, byteorder='big'), # Our difficulty
229
+ all_routes # All routes we participate in
230
+ ])
231
+
232
+ # Send ping to the peer
233
+ self.relay.send_message(ping_data, Topic.PING, peer_address)
234
+ except Exception as e:
235
+ print(f"Error processing peer in route: {e}")
236
+ continue
237
+ except Exception as e:
238
+ print(f"Error handling route message: {e}")
239
+
240
+ def _handle_latest_block_request(self, body: bytes, addr: Tuple[str, int], envelope):
241
+ """
242
+ Handle request for the latest block from the chain currently following.
243
+ Any node can request the latest block for syncing purposes.
244
+ """
245
+ try:
246
+ # Return our latest block from the followed chain
247
+ if self.latest_block:
248
+ # Send latest block to the requester
249
+ self.relay.send_message(self.latest_block.to_bytes(), Topic.LATEST_BLOCK, addr)
250
+ except Exception as e:
251
+ print(f"Error handling latest block request: {e}")
252
+
253
+ def _handle_latest_block(self, body: bytes, addr: Tuple[str, int], envelope):
254
+ """
255
+ Handle receipt of a latest block message.
256
+ Identify chain, validate if following chain, only accept if latest block
257
+ in chain is in the previous field.
258
+ """
259
+ try:
260
+ # Check if we're in the validation route
261
+ # This is now already checked by the relay's _handle_message method
262
+ if not self.relay.is_in_validation_route():
263
+ return
264
+
265
+ # Deserialize the block
266
+ block = Block.from_bytes(body)
267
+ if not block:
268
+ return
269
+
270
+ # Check if we're following this chain
271
+ if not self.machine.is_following_chain(block.chain_id):
272
+ # Store as a potential candidate chain if it has a higher height
273
+ if not self.followed_chain_id or block.chain_id != self.followed_chain_id:
274
+ self._add_candidate_chain(block)
275
+ return
276
+
277
+ # Get our current latest block
278
+ our_latest = self.latest_block
279
+
280
+ # Verify block hash links to our latest block
281
+ if our_latest and block.previous_hash == our_latest.hash:
282
+ # Process the valid block
283
+ self.machine.process_block(block)
284
+
285
+ # Update our latest block
286
+ self.latest_block = block
287
+ # Check if this block is ahead of our current chain
288
+ elif our_latest and block.height > our_latest.height:
289
+ # Block is ahead but doesn't link directly to our latest
290
+ # Add to candidate chains for potential future adoption
291
+ self._add_candidate_chain(block)
292
+
293
+ # No automatic broadcasting - nodes will request latest blocks when needed
294
+ except Exception as e:
295
+ print(f"Error handling latest block: {e}")
296
+
297
+ def _handle_transaction(self, body: bytes, addr: Tuple[str, int], envelope):
298
+ """
299
+ Handle receipt of a transaction.
300
+ Accept if validation route is present and counter is valid relative to the latest block in our chain.
301
+ """
302
+ try:
303
+ # Check if we're in the validation route
304
+ # This is now already checked by the relay's _handle_message method
305
+ if not self.relay.is_in_validation_route():
306
+ return
307
+
308
+ # Deserialize the transaction
309
+ transaction = Transaction.from_bytes(body)
310
+ if not transaction:
311
+ return
312
+
313
+ # Check if we're following this chain
314
+ if not self.machine.is_following_chain(transaction.chain_id):
315
+ return
316
+
317
+ # Verify transaction has a valid validation route
318
+ if not transaction.has_valid_route():
319
+ return
320
+
321
+ # Get latest block from this chain
322
+ latest_block = self.machine.get_latest_block(transaction.chain_id)
323
+ if not latest_block:
324
+ return
325
+
326
+ # Verify transaction counter is valid relative to the latest block
327
+ if not transaction.is_counter_valid(latest_block):
328
+ return
329
+
330
+ # Process the valid transaction
331
+ self.machine.process_transaction(transaction)
332
+
333
+ # Relay to other peers in the validation route
334
+ validation_peers = self.relay.get_route_peers(1) # 1 = validation route
335
+ for peer in validation_peers:
336
+ if peer.address != addr: # Don't send back to originator
337
+ self.relay.send_message(body, Topic.TRANSACTION, peer.address)
338
+ except Exception as e:
339
+ print(f"Error handling transaction: {e}")
340
+
341
+ def _initialize_latest_block(self):
342
+ """Initialize the latest block from storage if available."""
343
+ try:
344
+ if self.followed_chain_id:
345
+ # Get the latest block for the chain we're following
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
+
357
+ def set_followed_chain(self, chain_id):
358
+ """
359
+ Set the chain that this node follows.
360
+
361
+ Args:
362
+ chain_id: The ID of the chain to follow
363
+ """
364
+ self.followed_chain_id = chain_id
365
+ self.latest_block = self.machine.get_latest_block(chain_id)
366
+
367
+ def get_latest_block(self):
368
+ """
369
+ Get the latest block of the chain this node is following.
370
+
371
+ Returns:
372
+ The latest block, or None if not available
373
+ """
374
+ return self.latest_block
375
+
376
+ def _add_candidate_chain(self, block):
377
+ """
378
+ Add a block to candidate chains for potential future adoption.
379
+
380
+ Args:
381
+ block: The block to add as a candidate
382
+ """
383
+ chain_id = block.chain_id
384
+
385
+ # If we already have this chain as a candidate, only update if this block is newer
386
+ if chain_id in self.candidate_chains:
387
+ current_candidate = self.candidate_chains[chain_id]['latest_block']
388
+ if block.height > current_candidate.height:
389
+ self.candidate_chains[chain_id] = {
390
+ 'latest_block': block,
391
+ 'timestamp': time.time()
392
+ }
393
+ else:
394
+ # Add as a new candidate chain
395
+ self.candidate_chains[chain_id] = {
396
+ 'latest_block': block,
397
+ 'timestamp': time.time()
398
+ }
399
+
400
+ # Prune old candidates (older than 1 hour)
401
+ self._prune_candidate_chains()
402
+
403
+ def _prune_candidate_chains(self):
404
+ """Remove candidate chains that are older than 1 hour."""
405
+ current_time = time.time()
406
+ chains_to_remove = []
407
+
408
+ for chain_id, data in self.candidate_chains.items():
409
+ if current_time - data['timestamp'] > 3600: # 1 hour in seconds
410
+ chains_to_remove.append(chain_id)
411
+
412
+ for chain_id in chains_to_remove:
413
+ del self.candidate_chains[chain_id]
414
+
415
+ def evaluate_candidate_chains(self):
416
+ """
417
+ Evaluate all candidate chains to see if we should switch to one.
418
+ This is a placeholder for now - in a real implementation, you would
419
+ verify the chain and potentially switch to it if it's valid and better.
420
+ """
421
+ # TODO: Implement chain evaluation logic
422
+ pass
423
+
424
+ def post_global_storage(self, name: str, value):
425
+ """
426
+ Store a global variable in node storage.
427
+
428
+ Args:
429
+ name: Name of the variable
430
+ value: Value to store
431
+ """
432
+ # Store the expression directly in node storage using DAG representation
433
+ root_hash = store_expr(value, self.storage)
434
+
435
+ # Create a key for this variable name (without special prefixes)
436
+ key = hashlib.sha256(name.encode()).digest()
437
+
438
+ # Store the root hash reference
439
+ self.storage.put(key, root_hash)
440
+
441
+ def query_global_storage(self, name: str):
442
+ """
443
+ Retrieve a global variable from node storage.
444
+
445
+ Args:
446
+ name: Name of the variable to retrieve
447
+
448
+ Returns:
449
+ The stored expression, or None if not found
450
+ """
451
+ # Create the key for this variable name
452
+ key = hashlib.sha256(name.encode()).digest()
453
+
454
+ # Try to retrieve the root hash
455
+ root_hash = self.storage.get(key)
456
+
457
+ if root_hash:
458
+ # Load the expression using its root hash
459
+ return get_expr_from_storage(root_hash, self.storage)
460
+
461
+ return None
astreum/node/models.py ADDED
@@ -0,0 +1,96 @@
1
+ import socket
2
+ from pathlib import Path
3
+ from typing import Optional, Tuple
4
+ from astreum.machine import AstreumMachine
5
+ from .relay import Relay
6
+ from .relay.message import Topic
7
+ from .relay.route import RouteTable
8
+ from .relay.peer import Peer
9
+ import os
10
+
11
+ class Storage:
12
+ def __init__(self, config: dict):
13
+ self.max_space = config.get('max_storage_space', 1024 * 1024 * 1024) # Default 1GB
14
+ self.current_space = 0
15
+ self.storage_path = Path(config.get('storage_path', 'storage'))
16
+ self.storage_path.mkdir(parents=True, exist_ok=True)
17
+
18
+ # Calculate current space usage
19
+ self.current_space = sum(f.stat().st_size for f in self.storage_path.glob('*') if f.is_file())
20
+
21
+ def put(self, data_hash: bytes, data: bytes) -> bool:
22
+ """Store data with its hash. Returns True if successful, False if space limit exceeded."""
23
+ data_size = len(data)
24
+ if self.current_space + data_size > self.max_space:
25
+ return False
26
+
27
+ file_path = self.storage_path / data_hash.hex()
28
+
29
+ # Don't store if already exists
30
+ if file_path.exists():
31
+ return True
32
+
33
+ # Store the data
34
+ file_path.write_bytes(data)
35
+ self.current_space += data_size
36
+ return True
37
+
38
+ def get(self, data_hash: bytes) -> Optional[bytes]:
39
+ """Retrieve data by its hash. Returns None if not found."""
40
+ file_path = self.storage_path / data_hash.hex()
41
+ if not file_path.exists():
42
+ return None
43
+ return file_path.read_bytes()
44
+
45
+ def contains(self, data_hash: bytes) -> bool:
46
+ """Check if data exists in storage."""
47
+ return (self.storage_path / data_hash.hex()).exists()
48
+
49
+ class Account:
50
+ def __init__(self, public_key: bytes, balance: int, counter: int):
51
+ self.public_key = public_key
52
+ self.balance = balance
53
+ self.counter = counter
54
+
55
+ class Block:
56
+ def __init__(
57
+ self,
58
+ accounts: bytes,
59
+ chain: Chain,
60
+ difficulty: int,
61
+ delay: int,
62
+ number: int,
63
+ previous: Block,
64
+ receipts: bytes,
65
+ aster: int,
66
+ time: int,
67
+ transactions: bytes,
68
+ validator: Account,
69
+ signature: bytes
70
+ ):
71
+ self.accounts = accounts
72
+ self.chain = chain
73
+ self.difficulty = difficulty
74
+ self.delay = delay
75
+ self.number = number
76
+ self.previous = previous
77
+ self.receipts = receipts
78
+ self.aster = aster
79
+ self.time = time
80
+ self.transactions = transactions
81
+ self.validator = validator
82
+ self.signature = signature
83
+
84
+ class Chain:
85
+ def __init__(self, latest_block: Block):
86
+ self.latest_block = latest_block
87
+
88
+ class Transaction:
89
+ def __init__(self, chain: Chain, receipient: Account, sender: Account, counter: int, amount: int, signature: bytes, data: bytes):
90
+ self.chain = chain
91
+ self.receipient = receipient
92
+ self.sender = sender
93
+ self.counter = counter
94
+ self.amount = amount
95
+ self.signature = signature
96
+ self.data = data