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

Files changed (37) hide show
  1. astreum/__init__.py +1 -0
  2. astreum/lispeum/__init__.py +0 -0
  3. astreum/lispeum/expression.py +95 -0
  4. astreum/{machine → lispeum}/parser.py +1 -1
  5. astreum/lispeum/special/__init__.py +0 -0
  6. astreum/lispeum/special/definition.py +27 -0
  7. astreum/lispeum/special/list/__init__.py +0 -0
  8. astreum/lispeum/special/list/all.py +32 -0
  9. astreum/lispeum/special/list/any.py +32 -0
  10. astreum/lispeum/special/list/fold.py +29 -0
  11. astreum/lispeum/special/list/get.py +20 -0
  12. astreum/lispeum/special/list/insert.py +23 -0
  13. astreum/lispeum/special/list/map.py +30 -0
  14. astreum/lispeum/special/list/position.py +33 -0
  15. astreum/lispeum/special/list/remove.py +22 -0
  16. astreum/lispeum/special/number/__init__.py +0 -0
  17. astreum/lispeum/special/number/addition.py +0 -0
  18. astreum/machine/__init__.py +186 -53
  19. astreum/machine/environment.py +10 -3
  20. astreum/node/__init__.py +416 -0
  21. astreum/node/models.py +96 -0
  22. astreum/node/relay/__init__.py +248 -0
  23. astreum/node/relay/bucket.py +80 -0
  24. astreum/node/relay/envelope.py +280 -0
  25. astreum/node/relay/message.py +105 -0
  26. astreum/node/relay/peer.py +171 -0
  27. astreum/node/relay/route.py +125 -0
  28. astreum/utils/__init__.py +0 -0
  29. astreum/utils/bytes_format.py +75 -0
  30. {astreum-0.1.4.dist-info → astreum-0.1.6.dist-info}/METADATA +3 -3
  31. astreum-0.1.6.dist-info/RECORD +36 -0
  32. {astreum-0.1.4.dist-info → astreum-0.1.6.dist-info}/WHEEL +1 -1
  33. astreum/machine/expression.py +0 -50
  34. astreum-0.1.4.dist-info/RECORD +0 -12
  35. /astreum/{machine → lispeum}/tokenizer.py +0 -0
  36. {astreum-0.1.4.dist-info → astreum-0.1.6.dist-info}/LICENSE +0 -0
  37. {astreum-0.1.4.dist-info → astreum-0.1.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,248 @@
1
+ """
2
+ Relay module for handling network communication in the Astreum node.
3
+ """
4
+
5
+ import socket
6
+ import threading
7
+ from queue import Queue
8
+ from typing import Tuple, Callable, Dict, Set, Optional, List
9
+ from .message import Message, Topic
10
+ from .envelope import Envelope
11
+ from .bucket import KBucket
12
+ from .peer import Peer, PeerManager
13
+ from .route import RouteTable
14
+
15
+ class Relay:
16
+ def __init__(self, config: dict):
17
+ self.use_ipv6 = config.get('use_ipv6', False)
18
+ incoming_port = config.get('incoming_port', 7373)
19
+ self.max_message_size = config.get('max_message_size', 65536) # Max UDP datagram size
20
+ self.num_workers = config.get('num_workers', 4)
21
+
22
+ # Routes that this node participates in (0 = peer route, 1 = validation route)
23
+ self.routes: List[int] = []
24
+
25
+ # Initialize routes from config if provided
26
+ if config.get('peer_route', False):
27
+ self.routes.append(0) # Peer route
28
+
29
+ if config.get('validation_route', False):
30
+ self.routes.append(1) # Validation route
31
+
32
+ # Choose address family based on IPv4 or IPv6
33
+ family = socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
34
+
35
+ # Create a UDP socket
36
+ self.incoming_socket = socket.socket(family, socket.SOCK_DGRAM)
37
+
38
+ # Allow dual-stack support (IPv4-mapped addresses on IPv6)
39
+ if self.use_ipv6:
40
+ self.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
41
+
42
+ # Bind to an address (IPv6 "::" or IPv4 "0.0.0.0") and port
43
+ bind_address = "::" if self.use_ipv6 else "0.0.0.0"
44
+ self.incoming_socket.bind((bind_address, incoming_port or 0))
45
+
46
+ # Get the actual port assigned
47
+ self.incoming_port = self.incoming_socket.getsockname()[1]
48
+
49
+ # Create a UDP socket for sending messages
50
+ self.outgoing_socket = socket.socket(family, socket.SOCK_DGRAM)
51
+
52
+ # Message queues
53
+ self.incoming_queue = Queue()
54
+ self.outgoing_queue = Queue()
55
+
56
+ # Message handling
57
+ self.message_handlers: Dict[Topic, Callable] = {}
58
+
59
+ # Route buckets (peers for each route)
60
+ self.peer_route_bucket = KBucket(k=20) # Bucket for peer route
61
+ self.validation_route_bucket = KBucket(k=20) # Bucket for validation route
62
+
63
+ # Start worker threads
64
+ self._start_workers()
65
+
66
+ def is_in_peer_route(self) -> bool:
67
+ """Check if this node is part of the peer route."""
68
+ return 0 in self.routes
69
+
70
+ def is_in_validation_route(self) -> bool:
71
+ """Check if this node is part of the validation route."""
72
+ return 1 in self.routes
73
+
74
+ def add_to_peer_route(self):
75
+ """Add this node to the peer route."""
76
+ if 0 not in self.routes:
77
+ self.routes.append(0)
78
+
79
+ def add_to_validation_route(self):
80
+ """Add this node to the validation route."""
81
+ if 1 not in self.routes:
82
+ self.routes.append(1)
83
+
84
+ def remove_from_peer_route(self):
85
+ """Remove this node from the peer route."""
86
+ if 0 in self.routes:
87
+ self.routes.remove(0)
88
+
89
+ def remove_from_validation_route(self):
90
+ """Remove this node from the validation route."""
91
+ if 1 in self.routes:
92
+ self.routes.remove(1)
93
+
94
+ def add_peer_to_route(self, peer: Peer, route_types: List[int]):
95
+ """
96
+ Add a peer to specified routes.
97
+
98
+ Args:
99
+ peer (Peer): The peer to add
100
+ route_types (List[int]): List of route types to add the peer to (0 = peer, 1 = validation)
101
+ """
102
+ for route_type in route_types:
103
+ if route_type == 0: # Peer route
104
+ # Add to top of bucket, eject last if at capacity
105
+ self.peer_route_bucket.add(peer, to_front=True)
106
+ elif route_type == 1: # Validation route
107
+ # Add to top of bucket, eject last if at capacity
108
+ self.validation_route_bucket.add(peer, to_front=True)
109
+
110
+ def get_route_peers(self, route_type: int) -> List[Peer]:
111
+ """
112
+ Get all peers in a specific route.
113
+
114
+ Args:
115
+ route_type (int): Route type (0 for peer, 1 for validation)
116
+
117
+ Returns:
118
+ List[Peer]: List of peers in the route
119
+ """
120
+ if route_type == 0: # Peer route
121
+ return self.peer_route_bucket.get_peers()
122
+ elif route_type == 1: # Validation route
123
+ return self.validation_route_bucket.get_peers()
124
+ return []
125
+
126
+ def register_message_handler(self, topic: Topic, handler_func):
127
+ """Register a handler function for a specific message topic."""
128
+ self.message_handlers[topic] = handler_func
129
+
130
+ def _start_workers(self):
131
+ """Start worker threads for processing incoming and outgoing messages."""
132
+ self.running = True
133
+
134
+ # Start receiver thread
135
+ self.receiver_thread = threading.Thread(target=self._receive_messages)
136
+ self.receiver_thread.daemon = True
137
+ self.receiver_thread.start()
138
+
139
+ # Start sender thread
140
+ self.sender_thread = threading.Thread(target=self._send_messages)
141
+ self.sender_thread.daemon = True
142
+ self.sender_thread.start()
143
+
144
+ # Start worker threads for processing incoming messages
145
+ self.worker_threads = []
146
+ for _ in range(self.num_workers):
147
+ thread = threading.Thread(target=self._process_messages)
148
+ thread.daemon = True
149
+ thread.start()
150
+ self.worker_threads.append(thread)
151
+
152
+ def _receive_messages(self):
153
+ """Continuously receive messages and add them to the incoming queue."""
154
+ while self.running:
155
+ try:
156
+ data, addr = self.incoming_socket.recvfrom(self.max_message_size)
157
+ self.incoming_queue.put((data, addr))
158
+ except Exception as e:
159
+ # Log error but continue running
160
+ print(f"Error receiving message: {e}")
161
+
162
+ def _send_messages(self):
163
+ """Continuously send messages from the outgoing queue."""
164
+ while self.running:
165
+ try:
166
+ data, addr = self.outgoing_queue.get()
167
+ self.outgoing_socket.sendto(data, addr)
168
+ self.outgoing_queue.task_done()
169
+ except Exception as e:
170
+ # Log error but continue running
171
+ print(f"Error sending message: {e}")
172
+
173
+ def _process_messages(self):
174
+ """Process messages from the incoming queue."""
175
+ while self.running:
176
+ try:
177
+ data, addr = self.incoming_queue.get()
178
+ self._handle_message(data, addr)
179
+ self.incoming_queue.task_done()
180
+ except Exception as e:
181
+ # Log error but continue running
182
+ print(f"Error processing message: {e}")
183
+
184
+ def _handle_message(self, data: bytes, addr: Tuple[str, int]):
185
+ """Handle an incoming message."""
186
+ envelope = Envelope.from_bytes(data)
187
+ if envelope and envelope.message.topic in self.message_handlers:
188
+ # Check if this is a transaction or block message that requires validation route
189
+ if envelope.message.topic in (Topic.TRANSACTION, Topic.BLOCK):
190
+ # Only process if we're part of the validation route
191
+ if self.is_in_validation_route():
192
+ self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
193
+ elif envelope.message.topic == Topic.LATEST_BLOCK:
194
+ # For latest_block, we only process if we're in the validation route
195
+ if self.is_in_validation_route():
196
+ self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
197
+ elif envelope.message.topic in (Topic.LATEST_BLOCK_REQUEST, Topic.GET_BLOCKS):
198
+ # Allow all nodes to request blocks for syncing
199
+ self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
200
+ else:
201
+ # For other message types, always process
202
+ self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
203
+
204
+ def send(self, data: bytes, addr: Tuple[str, int]):
205
+ """Send raw data to a specific address."""
206
+ self.outgoing_queue.put((data, addr))
207
+
208
+ def get_address(self) -> Tuple[str, int]:
209
+ """
210
+ Get the local address of this relay node.
211
+
212
+ Returns:
213
+ Tuple[str, int]: The local address (host, port)
214
+ """
215
+ # This is a simplification - in a real implementation this would determine the
216
+ # actual public-facing IP address, which may be different from the binding address
217
+ return ("localhost", self.incoming_port)
218
+
219
+ def get_routes(self) -> bytes:
220
+ """
221
+ Get the routes this node is part of as a bytes object.
222
+
223
+ Returns:
224
+ bytes: List of route types (0 for peer, 1 for validation)
225
+ """
226
+ return bytes(self.routes)
227
+
228
+ def send_message(self, body: bytes, topic: Topic, addr: Tuple[str, int], encrypted: bool = False, difficulty: int = 1):
229
+ """
230
+ Create and send a message to a specific address.
231
+
232
+ Args:
233
+ body (bytes): The message body
234
+ topic (Topic): The message topic
235
+ addr (Tuple[str, int]): The recipient's address (host, port)
236
+ encrypted (bool): Whether the message is encrypted
237
+ difficulty (int): Number of leading zero bits required in the nonce hash
238
+ """
239
+ envelope = Envelope.create(body, topic, encrypted, difficulty)
240
+ encoded_data = envelope.to_bytes()
241
+ self.send(encoded_data, addr)
242
+
243
+ def stop(self):
244
+ """Stop all worker threads."""
245
+ self.running = False
246
+ # Wait for queues to be processed
247
+ self.incoming_queue.join()
248
+ self.outgoing_queue.join()
@@ -0,0 +1,80 @@
1
+ """
2
+ K-bucket implementation for Kademlia-style routing in Astreum node.
3
+ """
4
+
5
+ from typing import List, Tuple
6
+
7
+ class KBucket:
8
+ """
9
+ A Kademlia k-bucket that stores peers.
10
+
11
+ K-buckets are used to store contact information for nodes in the DHT.
12
+ When a new node is added, it's placed at the tail of the list.
13
+ If a node is already in the list, it is moved to the tail.
14
+ This creates a least-recently seen eviction policy.
15
+ """
16
+
17
+ def __init__(self, size: int):
18
+ """
19
+ Initialize a k-bucket with a fixed size.
20
+
21
+ Args:
22
+ size (int): Maximum number of peers in the bucket
23
+ """
24
+ self.size = size
25
+ self.peers: List[Tuple[str, int]] = []
26
+
27
+ def add(self, peer: Tuple[str, int]) -> bool:
28
+ """
29
+ Add peer to bucket if not full or if peer exists.
30
+
31
+ Args:
32
+ peer (Tuple[str, int]): Peer address (host, port)
33
+
34
+ Returns:
35
+ bool: True if added/exists, False if bucket full and peer not in bucket
36
+ """
37
+ if peer in self.peers:
38
+ # Move to end (most recently seen)
39
+ self.peers.remove(peer)
40
+ self.peers.append(peer)
41
+ return True
42
+
43
+ if len(self.peers) < self.size:
44
+ self.peers.append(peer)
45
+ return True
46
+
47
+ return False
48
+
49
+ def remove(self, peer: Tuple[str, int]) -> bool:
50
+ """
51
+ Remove peer from bucket.
52
+
53
+ Args:
54
+ peer (Tuple[str, int]): Peer address to remove
55
+
56
+ Returns:
57
+ bool: True if peer was removed, False if peer not in bucket
58
+ """
59
+ if peer in self.peers:
60
+ self.peers.remove(peer)
61
+ return True
62
+ return False
63
+
64
+ def get_peers(self) -> List[Tuple[str, int]]:
65
+ """
66
+ Get all peers in the bucket.
67
+
68
+ Returns:
69
+ List[Tuple[str, int]]: List of peer addresses
70
+ """
71
+ return self.peers.copy()
72
+
73
+ def __len__(self) -> int:
74
+ """
75
+ Get the number of peers in the bucket.
76
+
77
+ Returns:
78
+ int: Number of peers
79
+ """
80
+ return len(self.peers)
@@ -0,0 +1,280 @@
1
+ """
2
+ Envelope related classes and utilities for Astreum node network.
3
+
4
+ Message Structure:
5
+ + - - - - - - - +
6
+ | Envelope |
7
+ + - - - - - - - +
8
+ ^
9
+ . - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .
10
+ ^ ^ ^ ^
11
+ + - - - - - - - + + - - - - - - - + + - - - - - - - + + - - - - - - - +
12
+ | Time | | Encrypted | | Nonce | | Message |
13
+ + - - - - - - - + + - - - - - - - + + - - - - - - - + + - - - - - - - +
14
+ ^
15
+ . - - - - - - - - - - - .
16
+ ^ ^
17
+ + - - - - - - - + + - - - - - - - +
18
+ | Topic | | Body |
19
+ + - - - - - - - + + - - - - - - - +
20
+
21
+ The Envelope uses a Merkle tree structure with the following leaves:
22
+ - Timestamp
23
+ - Encrypted flag
24
+ - Nonce
25
+ - Message bytes
26
+
27
+ The root hash of this Merkle tree must have a specified number of leading zero bits,
28
+ determined by the difficulty parameter. The nonce is adjusted until this requirement is met.
29
+ """
30
+
31
+ import struct
32
+ import time
33
+ import os
34
+ import hashlib
35
+ from dataclasses import dataclass
36
+ from typing import Optional, Tuple, List
37
+ from .message import Message, Topic
38
+ from astreum.utils.bytes_format import encode, decode
39
+
40
+ @dataclass
41
+ class Envelope:
42
+ """
43
+ Represents an envelope that wraps a message with additional metadata.
44
+
45
+ Attributes:
46
+ encrypted (bool): True if the message is encrypted, False otherwise
47
+ message (Message): The message being sent
48
+ nonce (bytes): Nonce for encryption and proof of work
49
+ timestamp (int): Time when the envelope was created
50
+ """
51
+ encrypted: bool
52
+ message: Message
53
+ nonce: bytes
54
+ timestamp: int
55
+
56
+ @classmethod
57
+ def create(cls, body: bytes, topic: Topic, encrypted: bool = False, difficulty: int = 1) -> 'Envelope':
58
+ """
59
+ Create a new envelope with the current timestamp and a nonce that satisfies
60
+ the given difficulty level using a Merkle tree structure.
61
+
62
+ Args:
63
+ body (bytes): The message body
64
+ topic (Topic): The message topic
65
+ encrypted (bool): Whether the message is encrypted
66
+ difficulty (int): Number of leading zero bits required in the Merkle root hash
67
+
68
+ Returns:
69
+ Envelope: A new envelope with a valid nonce
70
+ """
71
+ timestamp = int(time.time())
72
+ message = Message(body=body, topic=topic)
73
+
74
+ # Generate a valid nonce for the Merkle tree
75
+ nonce = cls._generate_nonce(message, timestamp, encrypted, difficulty)
76
+
77
+ return cls(
78
+ encrypted=encrypted,
79
+ message=message,
80
+ nonce=nonce,
81
+ timestamp=timestamp
82
+ )
83
+
84
+ @staticmethod
85
+ def _generate_nonce(message: Message, timestamp: int, encrypted: bool, difficulty: int) -> bytes:
86
+ """
87
+ Generate a nonce that results in a Merkle tree root hash with the specified
88
+ number of leading zero bits.
89
+
90
+ Args:
91
+ message (Message): The message to include in the Merkle tree
92
+ timestamp (int): The timestamp to include in the Merkle tree
93
+ encrypted (bool): Whether the message is encrypted
94
+ difficulty (int): Number of leading zero bits required
95
+
96
+ Returns:
97
+ bytes: A valid nonce
98
+ """
99
+ # Prepare the message data
100
+ message_data = message.to_bytes()
101
+ timestamp_data = struct.pack('!Q', timestamp)
102
+ encrypted_flag = b'\x01' if encrypted else b'\x00'
103
+
104
+ # Calculate how many bytes need to be zero
105
+ zero_bytes = difficulty // 8
106
+ # Calculate how many bits in the last byte need to be zero
107
+ remaining_bits = difficulty % 8
108
+
109
+ # Create a mask for the remaining bits
110
+ mask = 0
111
+ if remaining_bits > 0:
112
+ mask = 0xFF >> remaining_bits
113
+
114
+ while True:
115
+ # Generate a random nonce
116
+ nonce = os.urandom(32)
117
+
118
+ # Calculate the Merkle root using the leaves
119
+ merkle_root = Envelope._calculate_merkle_root([
120
+ timestamp_data,
121
+ encrypted_flag,
122
+ nonce,
123
+ message_data
124
+ ])
125
+
126
+ # Check if it meets the difficulty requirement
127
+ valid = True
128
+
129
+ # Check full zero bytes
130
+ for i in range(zero_bytes):
131
+ if merkle_root[i] != 0:
132
+ valid = False
133
+ break
134
+
135
+ # If we need to check partial bits in a byte
136
+ if valid and remaining_bits > 0:
137
+ # The next byte should have required number of leading zeros
138
+ if (merkle_root[zero_bytes] & (0xFF ^ mask)) != 0:
139
+ valid = False
140
+
141
+ if valid:
142
+ return nonce
143
+
144
+ @staticmethod
145
+ def _calculate_merkle_root(leaves: List[bytes]) -> bytes:
146
+ """
147
+ Calculate the Merkle root hash from a list of leaf node data.
148
+
149
+ Args:
150
+ leaves (List[bytes]): List of leaf node data
151
+
152
+ Returns:
153
+ bytes: The Merkle root hash
154
+ """
155
+ if not leaves:
156
+ return hashlib.sha256(b'').digest()
157
+
158
+ if len(leaves) == 1:
159
+ return hashlib.sha256(leaves[0]).digest()
160
+
161
+ # Hash all leaf nodes
162
+ hashed_leaves = [hashlib.sha256(leaf).digest() for leaf in leaves]
163
+
164
+ # Build the Merkle tree
165
+ while len(hashed_leaves) > 1:
166
+ if len(hashed_leaves) % 2 != 0:
167
+ # Duplicate the last element if there's an odd number
168
+ hashed_leaves.append(hashed_leaves[-1])
169
+
170
+ # Combine adjacent pairs and hash them
171
+ next_level = []
172
+ for i in range(0, len(hashed_leaves), 2):
173
+ combined = hashed_leaves[i] + hashed_leaves[i+1]
174
+ next_level.append(hashlib.sha256(combined).digest())
175
+
176
+ hashed_leaves = next_level
177
+
178
+ # Return the root hash
179
+ return hashed_leaves[0]
180
+
181
+ def verify_nonce(self, difficulty: int = 1) -> bool:
182
+ """
183
+ Verify that the nonce produces a valid Merkle tree root hash
184
+ with the specified number of leading zero bits.
185
+
186
+ Args:
187
+ difficulty (int): Number of leading zero bits required in the root hash
188
+
189
+ Returns:
190
+ bool: True if the nonce is valid, False otherwise
191
+ """
192
+ # Prepare the message data
193
+ message_data = self.message.to_bytes()
194
+ timestamp_data = struct.pack('!Q', self.timestamp)
195
+ encrypted_flag = b'\x01' if self.encrypted else b'\x00'
196
+
197
+ # Calculate the Merkle root
198
+ merkle_root = self._calculate_merkle_root([
199
+ timestamp_data,
200
+ encrypted_flag,
201
+ self.nonce,
202
+ message_data
203
+ ])
204
+
205
+ # Calculate how many bytes need to be zero
206
+ zero_bytes = difficulty // 8
207
+ # Calculate how many bits in the last byte need to be zero
208
+ remaining_bits = difficulty % 8
209
+
210
+ # Create a mask for the remaining bits
211
+ mask = 0
212
+ if remaining_bits > 0:
213
+ mask = 0xFF >> remaining_bits
214
+
215
+ # Check if it meets the difficulty requirement
216
+ valid = True
217
+
218
+ # Check full zero bytes
219
+ for i in range(zero_bytes):
220
+ if merkle_root[i] != 0:
221
+ valid = False
222
+ break
223
+
224
+ # If we need to check partial bits in a byte
225
+ if valid and remaining_bits > 0:
226
+ # The next byte should have required number of leading zeros
227
+ if (merkle_root[zero_bytes] & (0xFF ^ mask)) != 0:
228
+ valid = False
229
+
230
+ return valid
231
+
232
+ def to_bytes(self) -> bytes:
233
+ """
234
+ Convert this Envelope to bytes.
235
+
236
+ Returns:
237
+ bytes: Serialized envelope
238
+ """
239
+ return encode([
240
+ struct.pack('!Q', self.timestamp),
241
+ b'\x01' if self.encrypted else b'\x00',
242
+ self.nonce,
243
+ self.message.to_bytes()
244
+ ])
245
+
246
+ @classmethod
247
+ def from_bytes(cls, data: bytes) -> Optional['Envelope']:
248
+ """
249
+ Create an Envelope from its serialized form.
250
+
251
+ Args:
252
+ data (bytes): Serialized envelope
253
+
254
+ Returns:
255
+ Optional[Envelope]: The deserialized envelope, or None if the data is invalid
256
+ """
257
+ try:
258
+ parts = decode(data)
259
+ if len(parts) != 4:
260
+ return None
261
+
262
+ timestamp_data, encrypted_flag, nonce, message_data = parts
263
+
264
+ timestamp = struct.unpack('!Q', timestamp_data)[0]
265
+ encrypted = encrypted_flag == b'\x01'
266
+ nonce = nonce
267
+ message = Message.from_bytes(message_data)
268
+
269
+ if not message:
270
+ return None
271
+
272
+ return cls(
273
+ encrypted=encrypted,
274
+ message=message,
275
+ nonce=nonce,
276
+ timestamp=timestamp
277
+ )
278
+ except (ValueError, struct.error) as e:
279
+ print(f"Error deserializing envelope: {e}")
280
+ return None