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.
- astreum/__init__.py +1 -0
- astreum/lispeum/storage.py +457 -0
- astreum/machine/__init__.py +3 -4
- astreum/machine/environment.py +9 -2
- astreum/node/__init__.py +461 -0
- astreum/node/models.py +96 -0
- astreum/node/relay/__init__.py +248 -0
- astreum/node/relay/bucket.py +80 -0
- astreum/node/relay/envelope.py +280 -0
- astreum/node/relay/message.py +105 -0
- astreum/node/relay/peer.py +171 -0
- astreum/node/relay/route.py +125 -0
- astreum/utils/__init__.py +0 -0
- astreum/utils/bytes_format.py +75 -0
- {astreum-0.1.5.dist-info → astreum-0.1.7.dist-info}/METADATA +2 -2
- {astreum-0.1.5.dist-info → astreum-0.1.7.dist-info}/RECORD +19 -8
- {astreum-0.1.5.dist-info → astreum-0.1.7.dist-info}/WHEEL +1 -1
- {astreum-0.1.5.dist-info → astreum-0.1.7.dist-info}/LICENSE +0 -0
- {astreum-0.1.5.dist-info → astreum-0.1.7.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
|