astreum 0.1.5__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.

@@ -0,0 +1,105 @@
1
+ """
2
+ Message related classes and utilities for Astreum node network.
3
+ """
4
+
5
+ import struct
6
+ from enum import Enum, auto
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+ from astreum.utils.bytes_format import encode, decode
10
+
11
+ class Topic(Enum):
12
+ """
13
+ Enum for different message topics in the Astreum network.
14
+ """
15
+ OBJECT_REQUEST = auto()
16
+ OBJECT = auto()
17
+ PING = auto()
18
+ PONG = auto()
19
+ ROUTE = auto()
20
+ ROUTE_REQUEST = auto()
21
+ LATEST_BLOCK = auto()
22
+ BLOCK = auto()
23
+ LATEST_BLOCK_REQUEST = auto()
24
+ TRANSACTION = auto()
25
+
26
+ def to_bytes(self) -> bytes:
27
+ """
28
+ Convert this Topic enum value to bytes.
29
+
30
+ Returns:
31
+ bytes: Single byte representing the topic
32
+ """
33
+ return struct.pack('!B', self.value)
34
+
35
+ @classmethod
36
+ def from_bytes(cls, data: bytes) -> Optional['Topic']:
37
+ """
38
+ Create a Topic from its serialized form.
39
+
40
+ Args:
41
+ data (bytes): Serialized topic (single byte)
42
+
43
+ Returns:
44
+ Optional[Topic]: The deserialized topic, or None if the data is invalid
45
+ """
46
+ if not data or len(data) != 1:
47
+ return None
48
+
49
+ try:
50
+ topic_value = struct.unpack('!B', data)[0]
51
+ return cls(topic_value)
52
+ except (struct.error, ValueError) as e:
53
+ print(f"Error deserializing topic: {e}")
54
+ return None
55
+
56
+ @dataclass
57
+ class Message:
58
+ """
59
+ Represents a message in the Astreum network.
60
+
61
+ Attributes:
62
+ body (bytes): The actual content of the message
63
+ topic (Topic): The topic/type of the message
64
+ """
65
+ body: bytes
66
+ topic: Topic
67
+
68
+ def to_bytes(self) -> bytes:
69
+ """
70
+ Convert this Message to bytes using bytes_format.
71
+
72
+ Returns:
73
+ bytes: Serialized message
74
+ """
75
+ return encode([
76
+ self.topic.to_bytes(),
77
+ self.body
78
+ ])
79
+
80
+ @classmethod
81
+ def from_bytes(cls, data: bytes) -> Optional['Message']:
82
+ """
83
+ Create a Message from its serialized form using bytes_format.
84
+
85
+ Args:
86
+ data (bytes): Serialized message
87
+
88
+ Returns:
89
+ Optional[Message]: The deserialized message, or None if the data is invalid
90
+ """
91
+ try:
92
+ parts = decode(data)
93
+ if len(parts) != 2:
94
+ return None
95
+
96
+ topic_data, body = parts
97
+ topic = Topic.from_bytes(topic_data)
98
+
99
+ if not topic:
100
+ return None
101
+
102
+ return cls(body=body, topic=topic)
103
+ except (ValueError, struct.error) as e:
104
+ print(f"Error deserializing message: {e}")
105
+ return None
@@ -0,0 +1,171 @@
1
+ """
2
+ Peer management for Astreum node's Kademlia-style network.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Tuple, Optional
7
+ import time
8
+
9
+ @dataclass
10
+ class Peer:
11
+ """
12
+ Represents a peer in the Astreum network.
13
+
14
+ Attributes:
15
+ address (Tuple[str, int]): The network address (host, port)
16
+ public_key (bytes): The public key of the peer
17
+ difficulty (int): The proof-of-work difficulty required for this peer
18
+ last_seen (int): Timestamp when the peer was last seen
19
+ failed_attempts (int): Number of consecutive failed communication attempts
20
+ """
21
+ address: Tuple[str, int]
22
+ public_key: bytes
23
+ difficulty: int = 1
24
+ last_seen: int = 0
25
+ failed_attempts: int = 0
26
+
27
+ def __post_init__(self):
28
+ if self.last_seen == 0:
29
+ self.last_seen = int(time.time())
30
+
31
+ def update_last_seen(self):
32
+ """Update the last seen timestamp to current time."""
33
+ self.last_seen = int(time.time())
34
+ self.failed_attempts = 0
35
+
36
+ def register_failed_attempt(self):
37
+ """Register a failed communication attempt with this peer."""
38
+ self.failed_attempts += 1
39
+
40
+ def is_active(self, max_age: int = 3600, max_failures: int = 3) -> bool:
41
+ """
42
+ Check if the peer is considered active.
43
+
44
+ Args:
45
+ max_age (int): Maximum age in seconds before peer is considered inactive
46
+ max_failures (int): Maximum consecutive failures before peer is considered inactive
47
+
48
+ Returns:
49
+ bool: True if the peer is active, False otherwise
50
+ """
51
+ current_time = int(time.time())
52
+ return (
53
+ (current_time - self.last_seen) <= max_age and
54
+ self.failed_attempts < max_failures
55
+ )
56
+
57
+ class PeerManager:
58
+ """
59
+ Manages a collection of peers and provides utilities for peer operations.
60
+ """
61
+
62
+ def __init__(self, our_node_id: bytes):
63
+ """
64
+ Initialize a peer manager.
65
+
66
+ Args:
67
+ our_node_id (bytes): Our node's unique identifier
68
+ """
69
+ self.our_node_id = our_node_id
70
+ self.peers_by_id = {} # node_id: Peer
71
+ self.peers_by_address = {} # address: Peer
72
+
73
+ def add_or_update_peer(self, address: Tuple[str, int], public_key: bytes) -> Peer:
74
+ """
75
+ Add a new peer or update an existing one.
76
+
77
+ Args:
78
+ address (Tuple[str, int]): Network address (host, port)
79
+ public_key (bytes): The public key of the peer
80
+
81
+ Returns:
82
+ Peer: The added or updated peer
83
+ """
84
+ # Check if we already know this peer by ID
85
+ if public_key in self.peers_by_id:
86
+ peer = self.peers_by_id[public_key]
87
+ # Update address if changed
88
+ if peer.address != address:
89
+ if peer.address in self.peers_by_address:
90
+ del self.peers_by_address[peer.address]
91
+ peer.address = address
92
+ self.peers_by_address[address] = peer
93
+ peer.update_last_seen()
94
+ return peer
95
+
96
+ # Check if address exists with different ID
97
+ if address in self.peers_by_address:
98
+ old_peer = self.peers_by_address[address]
99
+ if old_peer.public_key in self.peers_by_id:
100
+ del self.peers_by_id[old_peer.public_key]
101
+
102
+ # Create and add new peer
103
+ peer = Peer(address=address, public_key=public_key)
104
+ self.peers_by_id[public_key] = peer
105
+ self.peers_by_address[address] = peer
106
+ return peer
107
+
108
+ def get_peer_by_id(self, public_key: bytes) -> Optional[Peer]:
109
+ """
110
+ Get a peer by its public key.
111
+
112
+ Args:
113
+ public_key (bytes): The peer's public key
114
+
115
+ Returns:
116
+ Optional[Peer]: The peer if found, None otherwise
117
+ """
118
+ return self.peers_by_id.get(public_key)
119
+
120
+ def get_peer_by_address(self, address: Tuple[str, int]) -> Optional[Peer]:
121
+ """
122
+ Get a peer by its network address.
123
+
124
+ Args:
125
+ address (Tuple[str, int]): The peer's network address
126
+
127
+ Returns:
128
+ Optional[Peer]: The peer if found, None otherwise
129
+ """
130
+ return self.peers_by_address.get(address)
131
+
132
+ def remove_peer(self, public_key: bytes) -> bool:
133
+ """
134
+ Remove a peer by its public key.
135
+
136
+ Args:
137
+ public_key (bytes): The peer's public key
138
+
139
+ Returns:
140
+ bool: True if the peer was removed, False otherwise
141
+ """
142
+ if public_key in self.peers_by_id:
143
+ peer = self.peers_by_id[public_key]
144
+ del self.peers_by_id[public_key]
145
+ if peer.address in self.peers_by_address:
146
+ del self.peers_by_address[peer.address]
147
+ return True
148
+ return False
149
+
150
+ def calculate_distance(self, public_key: bytes) -> int:
151
+ """
152
+ Calculate the XOR distance between our node ID and the given public key.
153
+
154
+ Args:
155
+ public_key (bytes): The remote node's public key
156
+
157
+ Returns:
158
+ int: XOR distance (0-255)
159
+ """
160
+ # Assuming IDs are 256-bit (32 bytes)
161
+ for i in range(min(len(self.our_node_id), len(public_key))):
162
+ xor_byte = self.our_node_id[i] ^ public_key[i]
163
+ if xor_byte == 0:
164
+ continue
165
+
166
+ # Find the most significant bit
167
+ for bit in range(7, -1, -1):
168
+ if (xor_byte >> bit) & 1:
169
+ return (i * 8) + (7 - bit)
170
+
171
+ return 255 # Default maximum distance
@@ -0,0 +1,125 @@
1
+ """
2
+ Kademlia-style routing table implementation for Astreum node.
3
+ """
4
+
5
+ from typing import List, Dict, Set, Tuple, Optional
6
+ from .bucket import KBucket
7
+ from .peer import Peer, PeerManager
8
+
9
+ class RouteTable:
10
+ """
11
+ Kademlia-style routing table using k-buckets.
12
+
13
+ The routing table consists of k-buckets, each covering a specific range of distances.
14
+ Each k-bucket is a list of nodes with specific IDs in a certain distance range from ourselves.
15
+ """
16
+
17
+ def __init__(self, config: dict, our_node_id: bytes):
18
+ """
19
+ Initialize the routing table.
20
+
21
+ Args:
22
+ config (dict): Configuration dictionary
23
+ our_node_id (bytes): Our node's unique identifier
24
+ """
25
+ self.our_node_id = our_node_id
26
+ self.bucket_size = config.get('max_peers_per_bucket', 20)
27
+ self.buckets: Dict[int, KBucket] = {}
28
+ self.peer_manager = PeerManager(our_node_id)
29
+
30
+ def add_peer(self, peer: Peer) -> bool:
31
+ """
32
+ Add a peer to the appropriate k-bucket based on distance.
33
+
34
+ Args:
35
+ peer (Peer): The peer to add
36
+
37
+ Returns:
38
+ bool: True if the peer was added, False otherwise
39
+ """
40
+ distance = self.peer_manager.calculate_distance(peer.public_key)
41
+
42
+ # Create bucket if it doesn't exist
43
+ if distance not in self.buckets:
44
+ self.buckets[distance] = KBucket(self.bucket_size)
45
+
46
+ # Add to bucket
47
+ return self.buckets[distance].add(peer.address)
48
+
49
+ def remove_peer(self, peer: Peer) -> bool:
50
+ """
51
+ Remove a peer from its k-bucket.
52
+
53
+ Args:
54
+ peer (Peer): The peer to remove
55
+
56
+ Returns:
57
+ bool: True if the peer was removed, False otherwise
58
+ """
59
+ distance = self.peer_manager.calculate_distance(peer.public_key)
60
+
61
+ if distance in self.buckets:
62
+ return self.buckets[distance].remove(peer.address)
63
+ return False
64
+
65
+ def get_closest_peers(self, target_id: bytes, limit: int = 20) -> List[Peer]:
66
+ """
67
+ Get the closest peers to a target ID.
68
+
69
+ Args:
70
+ target_id (bytes): The target ID to find closest peers to
71
+ limit (int): Maximum number of peers to return
72
+
73
+ Returns:
74
+ List[Peer]: The closest peers
75
+ """
76
+ # Calculate distances from all known peers to the target
77
+ peers_with_distance = []
78
+
79
+ for bucket in self.buckets.values():
80
+ for address in bucket.addresses:
81
+ peer = self.peer_manager.get_peer_by_address(address)
82
+ if peer:
83
+ # Calculate XOR distance between target and this peer
84
+ xor_distance = 0
85
+ for i in range(min(len(target_id), len(peer.public_key))):
86
+ xor_bit = target_id[i] ^ peer.public_key[i]
87
+ xor_distance = (xor_distance << 8) | xor_bit
88
+
89
+ peers_with_distance.append((peer, xor_distance))
90
+
91
+ # Sort by distance (closest first)
92
+ peers_with_distance.sort(key=lambda x: x[1])
93
+
94
+ # Return only the peers (without distances), up to the limit
95
+ return [p[0] for p in peers_with_distance[:limit]]
96
+
97
+ def get_bucket_stats(self) -> Dict[int, int]:
98
+ """
99
+ Get statistics about the buckets in the routing table.
100
+
101
+ Returns:
102
+ Dict[int, int]: Mapping of distance to number of peers in that bucket
103
+ """
104
+ return {distance: len(bucket) for distance, bucket in self.buckets.items()}
105
+
106
+ def get_peers_in_bucket(self, distance: int) -> List[Peer]:
107
+ """
108
+ Get all peers in a specific bucket.
109
+
110
+ Args:
111
+ distance (int): Bucket distance
112
+
113
+ Returns:
114
+ List[Peer]: Peers in the bucket
115
+ """
116
+ if distance not in self.buckets:
117
+ return []
118
+
119
+ peers = []
120
+ for address in self.buckets[distance].addresses:
121
+ peer = self.peer_manager.get_peer_by_address(address)
122
+ if peer:
123
+ peers.append(peer)
124
+
125
+ return peers
File without changes
@@ -0,0 +1,75 @@
1
+ def encode(iterable, encoder=lambda x: x):
2
+ """
3
+ Encode an iterable of items into a single bytes object, applying the
4
+ provided encoder function to each item to convert it into bytes.
5
+
6
+ For each item (after applying encoder), we:
7
+ - Determine its length (n)
8
+ - Write a single byte to indicate how many bytes were used for the length:
9
+ 0: n == 0
10
+ 1: n <= 255
11
+ 2: 256 <= n <= 65535
12
+ 4: 65536 <= n <= 4294967295
13
+ 8: n > 4294967295
14
+ - Write the little-endian encoding of n in the specified number of bytes (if n > 0)
15
+ - Append the item’s data
16
+ """
17
+ result = bytearray()
18
+ for item in iterable:
19
+ item_bytes = encoder(item)
20
+ n = len(item_bytes)
21
+ if n > 4294967295:
22
+ result.append(8)
23
+ result.extend(n.to_bytes(8, byteorder='little'))
24
+ elif n > 65535:
25
+ result.append(4)
26
+ result.extend(n.to_bytes(4, byteorder='little'))
27
+ elif n > 255:
28
+ result.append(2)
29
+ result.extend(n.to_bytes(2, byteorder='little'))
30
+ elif n > 0:
31
+ result.append(1)
32
+ result.extend(n.to_bytes(1, byteorder='little'))
33
+ else:
34
+ result.append(0)
35
+ result.extend(item_bytes)
36
+ return bytes(result)
37
+
38
+
39
+ def decode(buffer, decoder=lambda x: x):
40
+ """
41
+ Decode a bytes buffer into a list of items, applying the provided decoder
42
+ function to convert the bytes into the desired type.
43
+
44
+ The buffer is read sequentially:
45
+ - Read one byte that indicates how many bytes were used for the length.
46
+ - If this value is 0, then the item is empty.
47
+ - Otherwise, read that many bytes to get the item's length (as a little-endian integer).
48
+ - Then, slice the next 'length' bytes from the buffer to get the item data.
49
+ - Apply the decoder function to the item data before appending it.
50
+
51
+ By default, the decoder is the identity function, so items are returned as bytes.
52
+ """
53
+ decoded_data = []
54
+ offset = 0
55
+ buf_len = len(buffer)
56
+
57
+ while offset < buf_len:
58
+ length_type = buffer[offset]
59
+ offset += 1
60
+
61
+ if length_type == 0:
62
+ n = 0
63
+ else:
64
+ if offset + length_type > buf_len:
65
+ raise ValueError("Buffer too short for length field")
66
+ n = int.from_bytes(buffer[offset: offset+length_type], byteorder='little')
67
+ offset += length_type
68
+
69
+ if offset + n > buf_len:
70
+ raise ValueError("Buffer is too short for item data")
71
+ item_data = buffer[offset:offset+n]
72
+ offset += n
73
+ decoded_data.append(decoder(item_data))
74
+
75
+ return decoded_data
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: astreum
3
- Version: 0.1.5
3
+ Version: 0.1.6
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
@@ -1,4 +1,4 @@
1
- astreum/__init__.py,sha256=-hmy95qFWlCbmHEcj5sGniM-UtpIn-iwwhVWFrFkd_w,37
1
+ astreum/__init__.py,sha256=di8SwGUW1lNKUwvNWCjH9eLmz__sk1SbtuNZVjjpRe4,59
2
2
  astreum/lispeum/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  astreum/lispeum/expression.py,sha256=3zOEoXFHpzEFUIi1clONY55WYAh5K0YkYhaLmtvQj0I,2939
4
4
  astreum/lispeum/parser.py,sha256=SU8mjPj1ub4xQbU4CeX15HmKZAj4vI6TeefX2B72VCo,1191
@@ -16,11 +16,21 @@ astreum/lispeum/special/list/position.py,sha256=1EcD5fzfjUCUhHCk8iotFoWdeE9Z2Rjj
16
16
  astreum/lispeum/special/list/remove.py,sha256=EPhkWFwyKG8HyWPheVvtxCvMaEGvODFUbx7h1fli_hg,690
17
17
  astreum/lispeum/special/number/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  astreum/lispeum/special/number/addition.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- astreum/machine/__init__.py,sha256=DJjbYnR--oeUta5xYZtxHBf1-zFzJ33vkRufTVWFkNM,11027
20
- astreum/machine/environment.py,sha256=sW1ws7M6cfa2xWL9783ZRRte5AZ0qslYDVZozcTdiQk,607
19
+ astreum/machine/__init__.py,sha256=5Sx-_MEYFG-089q2G8JtLp9_ELiPYnYIXhagjy6C4w0,11048
20
+ astreum/machine/environment.py,sha256=K0084U6B7wwjrDZ9b2_7cEcbBzsB7UOy_Zpbrr7B3GY,834
21
21
  astreum/machine/error.py,sha256=MvqBaZZt33rNELNhUJ2lER3TE3aS8WVqsWF2hz2AwoA,38
22
- astreum-0.1.5.dist-info/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
23
- astreum-0.1.5.dist-info/METADATA,sha256=H8-xQAHYH0aBx4S9nnC4sb-YQDjazdyyB_tdtJoCqnA,741
24
- astreum-0.1.5.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
25
- astreum-0.1.5.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
26
- astreum-0.1.5.dist-info/RECORD,,
22
+ astreum/node/__init__.py,sha256=OUU7zcTp-eFv-4jB9JETfrei0wqx8CzozOaNfPHRm-0,18003
23
+ astreum/node/models.py,sha256=CBYuMJVD0uI5i6fWSFE2egkZXtFQHvR79kKTU4KP1WE,3086
24
+ astreum/node/relay/__init__.py,sha256=i_uD8ZMuA8Xa4vdzVAdiyd5uhIv-7-n9SBUG7WiL8mk,10024
25
+ astreum/node/relay/bucket.py,sha256=w0jzOrSUVT5WmRW_ggmtBcdf8sgZW-_MTeT4YQuhAvg,2211
26
+ astreum/node/relay/envelope.py,sha256=TfkynttoPX7smvMV7xEAdtIlfz-Z-EZjuhZ826csZxA,10078
27
+ astreum/node/relay/message.py,sha256=9j3Q5T5VVj47g0xqxUCWxtf2EFXutIPbj0mt1KbVw3M,2812
28
+ astreum/node/relay/peer.py,sha256=eOml9G4xMoQeR_BuONVgJvpvCwhQo-QXCpRhShAxz4Q,5721
29
+ astreum/node/relay/route.py,sha256=ZyWoTC2EXX-SU_80fFS0TP-lITH6e9mDeS-irsfcMp4,4303
30
+ astreum/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ astreum/utils/bytes_format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
32
+ astreum-0.1.6.dist-info/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
33
+ astreum-0.1.6.dist-info/METADATA,sha256=b_LZeCHI1_ST9e53ACed8__f9NwZkEUTA7qwSu9oep0,741
34
+ astreum-0.1.6.dist-info/WHEEL,sha256=EaM1zKIUYa7rQnxGiOCGhzJABRwy4WO57rWMR3_tj4I,91
35
+ astreum-0.1.6.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
36
+ astreum-0.1.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.9.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5