astreum 0.3.5__tar.gz → 0.3.14__tar.gz

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.
Files changed (84) hide show
  1. {astreum-0.3.5/src/astreum.egg-info → astreum-0.3.14}/PKG-INFO +3 -3
  2. {astreum-0.3.5 → astreum-0.3.14}/README.md +7 -7
  3. {astreum-0.3.5 → astreum-0.3.14}/pyproject.toml +1 -1
  4. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/__init__.py +4 -2
  5. astreum-0.3.14/src/astreum/communication/handlers/handshake.py +62 -0
  6. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/handlers/object_request.py +53 -30
  7. astreum-0.3.14/src/astreum/communication/handlers/object_response.py +115 -0
  8. astreum-0.3.14/src/astreum/communication/handlers/ping.py +34 -0
  9. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/handlers/route_request.py +19 -21
  10. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/handlers/route_response.py +12 -11
  11. astreum-0.3.14/src/astreum/communication/models/message.py +124 -0
  12. astreum-0.3.14/src/astreum/communication/models/peer.py +51 -0
  13. astreum-0.3.14/src/astreum/communication/processors/incoming.py +98 -0
  14. astreum-0.3.14/src/astreum/communication/processors/outgoing.py +20 -0
  15. astreum-0.3.14/src/astreum/communication/processors/peer.py +59 -0
  16. astreum-0.3.14/src/astreum/communication/setup.py +168 -0
  17. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/start.py +9 -10
  18. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/start.py +7 -8
  19. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/validator.py +15 -8
  20. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/workers/discovery.py +6 -7
  21. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/workers/validation.py +334 -292
  22. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/workers/verify.py +8 -10
  23. astreum-0.3.14/src/astreum/crypto/chacha20poly1305.py +74 -0
  24. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/evaluations/high_evaluation.py +237 -237
  25. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/evaluations/low_evaluation.py +18 -18
  26. astreum-0.3.14/src/astreum/machine/models/__init__.py +0 -0
  27. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/node.py +21 -1
  28. astreum-0.3.14/src/astreum/storage/actions/get.py +183 -0
  29. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/storage/actions/set.py +55 -15
  30. astreum-0.3.14/src/astreum/storage/requests.py +28 -0
  31. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/utils/config.py +29 -1
  32. {astreum-0.3.5 → astreum-0.3.14/src/astreum.egg-info}/PKG-INFO +3 -3
  33. {astreum-0.3.5 → astreum-0.3.14}/src/astreum.egg-info/SOURCES.txt +6 -0
  34. astreum-0.3.5/src/astreum/communication/handlers/handshake.py +0 -81
  35. astreum-0.3.5/src/astreum/communication/handlers/object_response.py +0 -37
  36. astreum-0.3.5/src/astreum/communication/handlers/ping.py +0 -48
  37. astreum-0.3.5/src/astreum/communication/models/message.py +0 -105
  38. astreum-0.3.5/src/astreum/communication/models/peer.py +0 -23
  39. astreum-0.3.5/src/astreum/communication/setup.py +0 -262
  40. astreum-0.3.5/src/astreum/storage/actions/get.py +0 -85
  41. {astreum-0.3.5 → astreum-0.3.14}/LICENSE +0 -0
  42. {astreum-0.3.5 → astreum-0.3.14}/setup.cfg +0 -0
  43. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/__init__.py +0 -0
  44. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/handlers/__init__.py +0 -0
  45. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/models/__init__.py +0 -0
  46. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/models/ping.py +0 -0
  47. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/models/route.py +0 -0
  48. {astreum-0.3.5/src/astreum/consensus/models → astreum-0.3.14/src/astreum/communication/processors}/__init__.py +0 -0
  49. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/communication/util.py +0 -0
  50. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/__init__.py +0 -0
  51. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/genesis.py +0 -0
  52. {astreum-0.3.5/src/astreum/crypto → astreum-0.3.14/src/astreum/consensus/models}/__init__.py +0 -0
  53. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/models/account.py +0 -0
  54. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/models/accounts.py +0 -0
  55. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/models/block.py +0 -0
  56. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/models/chain.py +0 -0
  57. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/models/fork.py +0 -0
  58. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/models/receipt.py +0 -0
  59. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/models/transaction.py +0 -0
  60. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/setup.py +0 -0
  61. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/consensus/workers/__init__.py +0 -0
  62. {astreum-0.3.5/src/astreum/machine/evaluations → astreum-0.3.14/src/astreum/crypto}/__init__.py +0 -0
  63. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/crypto/ed25519.py +0 -0
  64. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/crypto/quadratic_form.py +0 -0
  65. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/crypto/wesolowski.py +0 -0
  66. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/crypto/x25519.py +0 -0
  67. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/__init__.py +0 -0
  68. {astreum-0.3.5/src/astreum/machine/models → astreum-0.3.14/src/astreum/machine/evaluations}/__init__.py +0 -0
  69. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/evaluations/script_evaluation.py +0 -0
  70. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/models/environment.py +0 -0
  71. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/models/expression.py +0 -0
  72. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/models/meter.py +0 -0
  73. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/parser.py +0 -0
  74. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/machine/tokenizer.py +0 -0
  75. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/storage/__init__.py +0 -0
  76. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/storage/models/atom.py +0 -0
  77. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/storage/models/trie.py +0 -0
  78. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/storage/setup.py +0 -0
  79. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/utils/bytes.py +0 -0
  80. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/utils/integer.py +0 -0
  81. {astreum-0.3.5 → astreum-0.3.14}/src/astreum/utils/logging.py +0 -0
  82. {astreum-0.3.5 → astreum-0.3.14}/src/astreum.egg-info/dependency_links.txt +0 -0
  83. {astreum-0.3.5 → astreum-0.3.14}/src/astreum.egg-info/requires.txt +0 -0
  84. {astreum-0.3.5 → astreum-0.3.14}/src/astreum.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.3.5
3
+ Version: 0.3.14
4
4
  Summary: Python library to interact with the Astreum blockchain and its virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib-py
@@ -113,11 +113,11 @@ node.env_set(env_id, "int.add", int_add_fn)
113
113
 
114
114
  # 5) Retrieve the function and call it with bytes 1 and 2
115
115
  bound = node.env_get(env_id, "int.add")
116
- call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
116
+ call = Expr.ListExpr([Expr.Bytes(b"\x01"), Expr.Bytes(b"\x02"), bound])
117
117
  res = node.high_eval(env_id, call)
118
118
 
119
119
  # sk returns a list of bytes; for 1+2 expect a single byte with value 3
120
- print([b.value for b in res.elements]) # [3]
120
+ print([int.from_bytes(b.value, 'big', signed=True) for b in res.elements]) # [3]
121
121
  ```
122
122
 
123
123
  ### Handling errors
@@ -90,16 +90,16 @@ fn_body = Expr.ListExpr([
90
90
  params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
91
91
  int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
92
92
 
93
- # 4) Store under the name "int.add"
94
- node.env_set(env_id, "int.add", int_add_fn)
95
-
96
- # 5) Retrieve the function and call it with bytes 1 and 2
97
- bound = node.env_get(env_id, "int.add")
98
- call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
93
+ # 4) Store under the name "int.add"
94
+ node.env_set(env_id, "int.add", int_add_fn)
95
+
96
+ # 5) Retrieve the function and call it with bytes 1 and 2
97
+ bound = node.env_get(env_id, "int.add")
98
+ call = Expr.ListExpr([Expr.Bytes(b"\x01"), Expr.Bytes(b"\x02"), bound])
99
99
  res = node.high_eval(env_id, call)
100
100
 
101
101
  # sk returns a list of bytes; for 1+2 expect a single byte with value 3
102
- print([b.value for b in res.elements]) # [3]
102
+ print([int.from_bytes(b.value, 'big', signed=True) for b in res.elements]) # [3]
103
103
  ```
104
104
 
105
105
  ### Handling errors
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.3.5"
3
+ version = "0.3.14"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -1,6 +1,6 @@
1
-
1
+
2
2
  from astreum.consensus import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
3
- from astreum.machine import Env, Expr
3
+ from astreum.machine import Env, Expr, parse, tokenize
4
4
  from astreum.node import Node
5
5
 
6
6
 
@@ -15,4 +15,6 @@ __all__: list[str] = [
15
15
  "Transaction",
16
16
  "Account",
17
17
  "Accounts",
18
+ "parse",
19
+ "tokenize",
18
20
  ]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from cryptography.hazmat.primitives import serialization
6
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
7
+
8
+ from ..models.peer import Peer
9
+ from ..models.message import Message
10
+
11
+ if TYPE_CHECKING:
12
+ from .... import Node
13
+
14
+
15
+ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) -> bool:
16
+ """Handle incoming handshake messages.
17
+
18
+ Returns True if the outer loop should `continue`, False otherwise.
19
+ """
20
+ sender_public_key_bytes = message.sender_bytes
21
+ try:
22
+ sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
23
+ except Exception as exc:
24
+ node.logger.warning("Error extracting sender key bytes: %s", exc)
25
+ return True
26
+
27
+ try:
28
+ host = addr[0]
29
+ port = int.from_bytes(message.content[:2], "big", signed=False)
30
+ except Exception:
31
+ return True
32
+ peer_address = (host, port)
33
+
34
+ existing_peer = node.get_peer(sender_public_key_bytes)
35
+ if existing_peer is not None:
36
+ existing_peer.address = peer_address
37
+ return False
38
+
39
+ try:
40
+ peer = Peer(
41
+ node_secret_key=node.relay_secret_key,
42
+ peer_public_key=sender_key,
43
+ address=peer_address,
44
+ )
45
+ except Exception:
46
+ return True
47
+
48
+ node.add_peer(sender_public_key_bytes, peer)
49
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
50
+
51
+ node.logger.info(
52
+ "Handshake accepted from %s:%s; peer added",
53
+ peer_address[0],
54
+ peer_address[1],
55
+ )
56
+ response = Message(
57
+ handshake=True,
58
+ sender=node.relay_public_key,
59
+ content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
60
+ )
61
+ node.outgoing_queue.put((response.to_bytes(), peer_address))
62
+ return True
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Tuple
5
5
 
6
6
  from .object_response import ObjectResponse, ObjectResponseType
7
7
  from ..models.message import Message, MessageTopic
8
+ from ..util import xor_distance
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from .. import Node
@@ -27,7 +28,7 @@ class ObjectRequest:
27
28
  self.atom_id = atom_id
28
29
 
29
30
  def to_bytes(self):
30
- return [self.type.value] + self.atom_id + self.data
31
+ return bytes([self.type.value]) + self.atom_id + self.data
31
32
 
32
33
  @classmethod
33
34
  def from_bytes(cls, data: bytes) -> "ObjectRequest":
@@ -48,8 +49,6 @@ class ObjectRequest:
48
49
 
49
50
  def encode_peer_contact_bytes(peer: "Peer") -> bytes:
50
51
  """Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
51
- if not peer.address:
52
- raise ValueError("peer address is required for encoding peer info")
53
52
  host, port = peer.address
54
53
  key_bytes = peer.public_key_bytes
55
54
  try:
@@ -62,22 +61,25 @@ def encode_peer_contact_bytes(peer: "Peer") -> bytes:
62
61
  return key_bytes + ip_bytes + port_bytes
63
62
 
64
63
 
65
- def handle_object_request(node: "Node", addr: Tuple[str, int], message: Message) -> None:
66
- node_logger = getattr(node, "logger", logging.getLogger(__name__))
64
+ def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
65
+ if message.content is None:
66
+ node.logger.warning("OBJECT_REQUEST from %s missing content", peer.address)
67
+ return
68
+
67
69
  try:
68
- object_request = ObjectRequest.from_bytes(message.body)
70
+ object_request = ObjectRequest.from_bytes(message.content)
69
71
  except Exception as exc:
70
- node_logger.warning("Error decoding OBJECT_REQUEST from %s: %s", addr, exc)
72
+ node.logger.warning("Error decoding OBJECT_REQUEST from %s: %s", peer.address, exc)
71
73
  return
72
74
 
73
75
  match object_request.type:
74
76
  case ObjectRequestType.OBJECT_GET:
75
77
  atom_id = object_request.atom_id
76
- node_logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), addr)
78
+ node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
77
79
 
78
80
  local_atom = node.local_get(atom_id)
79
81
  if local_atom is not None:
80
- node_logger.debug("Object %s found locally; returning to %s", atom_id.hex(), addr)
82
+ node.logger.debug("Object %s found locally; returning to %s", atom_id.hex(), peer.address)
81
83
  resp = ObjectResponse(
82
84
  type=ObjectResponseType.OBJECT_FOUND,
83
85
  data=local_atom.to_bytes(),
@@ -88,13 +90,13 @@ def handle_object_request(node: "Node", addr: Tuple[str, int], message: Message)
88
90
  body=resp.to_bytes(),
89
91
  sender=node.relay_public_key,
90
92
  )
91
- node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
93
+ obj_res_msg.encrypt(peer.shared_key_bytes)
94
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
92
95
  return
93
96
 
94
- storage_index = getattr(node, "storage_index", None) or {}
95
- if atom_id in storage_index:
96
- node_logger.debug("Known provider for %s; informing %s", atom_id.hex(), addr)
97
- provider_bytes = storage_index[atom_id]
97
+ if atom_id in node.storage_index:
98
+ node.logger.debug("Known provider for %s; informing %s", atom_id.hex(), peer.address)
99
+ provider_bytes = node.storage_index[atom_id]
98
100
  resp = ObjectResponse(
99
101
  type=ObjectResponseType.OBJECT_PROVIDER,
100
102
  data=provider_bytes,
@@ -105,12 +107,13 @@ def handle_object_request(node: "Node", addr: Tuple[str, int], message: Message)
105
107
  body=resp.to_bytes(),
106
108
  sender=node.relay_public_key,
107
109
  )
108
- node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
110
+ obj_res_msg.encrypt(peer.shared_key_bytes)
111
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
109
112
  return
110
113
 
111
114
  nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
112
115
  if nearest_peer:
113
- node_logger.debug("Forwarding requester %s to nearest peer for %s", addr, atom_id.hex())
116
+ node.logger.debug("Forwarding requester %s to nearest peer for %s", peer.address, atom_id.hex())
114
117
  peer_info = encode_peer_contact_bytes(nearest_peer)
115
118
  resp = ObjectResponse(
116
119
  type=ObjectResponseType.OBJECT_PROVIDER,
@@ -123,31 +126,51 @@ def handle_object_request(node: "Node", addr: Tuple[str, int], message: Message)
123
126
  body=resp.to_bytes(),
124
127
  sender=node.relay_public_key,
125
128
  )
126
- node.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
129
+ obj_res_msg.encrypt(nearest_peer.shared_key_bytes)
130
+ node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
127
131
 
128
132
  case ObjectRequestType.OBJECT_PUT:
129
- atom_hash = object_request.data[:32]
130
- node_logger.debug("Handling OBJECT_PUT for %s from %s", atom_hash.hex(), addr)
133
+ node.logger.debug("Handling OBJECT_PUT for %s from %s", object_request.atom_id.hex(), peer.address)
131
134
 
132
- nearest_peer = node.peer_route.closest_peer_for_hash(atom_hash)
133
- nearest = (nearest_peer.public_key, nearest_peer) if nearest_peer else None
134
- if nearest:
135
- node_logger.debug("Forwarding OBJECT_PUT for %s to nearer peer %s", atom_hash.hex(), nearest[1].address)
135
+ nearest_peer = node.peer_route.closest_peer_for_hash(object_request.atom_id)
136
+ is_self_closest = False
137
+ if nearest_peer is None or nearest_peer.address is None:
138
+ is_self_closest = True
139
+ else:
140
+ try:
141
+ self_distance = xor_distance(object_request.atom_id, node.relay_public_key_bytes)
142
+ peer_distance = xor_distance(object_request.atom_id, nearest_peer.public_key_bytes)
143
+ except Exception as exc:
144
+ node.logger.warning(
145
+ "Failed distance comparison for OBJECT_PUT %s: %s",
146
+ object_request.atom_id.hex(),
147
+ exc,
148
+ )
149
+ is_self_closest = True
150
+ else:
151
+ is_self_closest = self_distance <= peer_distance
152
+
153
+ if is_self_closest:
154
+ node.logger.debug("Storing provider info for %s locally", object_request.atom_id.hex())
155
+ node.storage_index[object_request.atom_id] = object_request.data
156
+ else:
157
+ node.logger.debug(
158
+ "Forwarding OBJECT_PUT for %s to nearer peer %s",
159
+ object_request.atom_id.hex(),
160
+ nearest_peer.address,
161
+ )
136
162
  fwd_req = ObjectRequest(
137
163
  type=ObjectRequestType.OBJECT_PUT,
138
164
  data=object_request.data,
165
+ atom_id=object_request.atom_id,
139
166
  )
140
167
  obj_req_msg = Message(
141
168
  topic=MessageTopic.OBJECT_REQUEST,
142
169
  body=fwd_req.to_bytes(),
143
170
  sender=node.relay_public_key,
144
171
  )
145
- node.outgoing_queue.put((obj_req_msg.to_bytes(), nearest[1].address))
146
- else:
147
- node_logger.debug("Storing provider info for %s locally", atom_hash.hex())
148
- if not hasattr(node, "storage_index") or not isinstance(node.storage_index, dict):
149
- node.storage_index = {}
150
- node.storage_index[atom_hash] = object_request.data[32:]
172
+ obj_req_msg.encrypt(nearest_peer.shared_key_bytes)
173
+ node.outgoing_queue.put((obj_req_msg.to_bytes(), nearest_peer.address))
151
174
 
152
175
  case _:
153
- node_logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, addr)
176
+ node.logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, peer.address)
@@ -0,0 +1,115 @@
1
+ import socket
2
+ from enum import IntEnum
3
+ from typing import Tuple, TYPE_CHECKING
4
+
5
+ from ..models.message import Message, MessageTopic
6
+ from ...storage.models.atom import Atom
7
+
8
+ if TYPE_CHECKING:
9
+ from .. import Node
10
+ from ..models.peer import Peer
11
+
12
+
13
+ class ObjectResponseType(IntEnum):
14
+ OBJECT_FOUND = 0
15
+ OBJECT_PROVIDER = 1
16
+ OBJECT_NEAREST_PEER = 2
17
+
18
+
19
+ class ObjectResponse:
20
+ type: ObjectResponseType
21
+ data: bytes
22
+ atom_id: bytes
23
+
24
+ def __init__(self, type: ObjectResponseType, data: bytes, atom_id: bytes = None):
25
+ self.type = type
26
+ self.data = data
27
+ self.atom_id = atom_id
28
+
29
+ def to_bytes(self):
30
+ return bytes([self.type.value]) + self.atom_id + self.data
31
+
32
+ @classmethod
33
+ def from_bytes(cls, data: bytes) -> "ObjectResponse":
34
+ # need at least 1 byte for type + 32 bytes for atom id
35
+ if len(data) < 1 + 32:
36
+ raise ValueError(f"Too short to be a valid ObjectResponse ({len(data)} bytes)")
37
+
38
+ type_val = data[0]
39
+ try:
40
+ resp_type = ObjectResponseType(type_val)
41
+ except ValueError:
42
+ raise ValueError(f"Unknown ObjectResponseType: {type_val}")
43
+
44
+ atom_id = data[1:33]
45
+ payload = data[33:]
46
+ return cls(resp_type, payload, atom_id)
47
+
48
+
49
+ def decode_object_provider(payload: bytes) -> Tuple[bytes, str, int]:
50
+ expected_len = 32 + 4 + 2
51
+ if len(payload) < expected_len:
52
+ raise ValueError("provider payload too short")
53
+
54
+ provider_public_key = payload[:32]
55
+ provider_ip_bytes = payload[32:36]
56
+ provider_port_bytes = payload[36:38]
57
+
58
+ provider_address = socket.inet_ntoa(provider_ip_bytes)
59
+ provider_port = int.from_bytes(provider_port_bytes, byteorder="big", signed=False)
60
+ return provider_public_key, provider_address, provider_port
61
+
62
+
63
+ def handle_object_response(node: "Node", peer: "Peer", message: Message) -> None:
64
+ if message.content is None:
65
+ node.logger.warning("OBJECT_RESPONSE from %s missing content", peer.address)
66
+ return
67
+
68
+ try:
69
+ object_response = ObjectResponse.from_bytes(message.content)
70
+ except Exception as exc:
71
+ node.logger.warning("Error decoding OBJECT_RESPONSE from %s: %s", peer.address, exc)
72
+ return
73
+
74
+ if not node.has_atom_req(object_response.atom_id):
75
+ return
76
+
77
+ match object_response.type:
78
+ case ObjectResponseType.OBJECT_FOUND:
79
+ atom = Atom.from_bytes(object_response.data)
80
+ atom_id = atom.object_id()
81
+ if object_response.atom_id == atom_id:
82
+ node.pop_atom_req(atom_id)
83
+ node._hot_storage_set(atom_id, atom)
84
+ else:
85
+ node.logger.warning(
86
+ "OBJECT_FOUND atom ID mismatch (expected=%s got=%s)",
87
+ object_response.atom_id.hex(),
88
+ atom_id.hex(),
89
+ )
90
+
91
+ case ObjectResponseType.OBJECT_PROVIDER:
92
+ try:
93
+ _, provider_address, provider_port = decode_object_provider(object_response.data)
94
+ except Exception as exc:
95
+ node.logger.warning("Invalid OBJECT_PROVIDER payload from %s: %s", peer.address, exc)
96
+ return
97
+
98
+ from .object_request import ObjectRequest, ObjectRequestType
99
+
100
+ obj_req = ObjectRequest(
101
+ type=ObjectRequestType.OBJECT_GET,
102
+ data=b"",
103
+ atom_id=object_response.atom_id,
104
+ )
105
+ obj_req_bytes = obj_req.to_bytes()
106
+ obj_req_msg = Message(
107
+ topic=MessageTopic.OBJECT_REQUEST,
108
+ body=obj_req_bytes,
109
+ sender=node.relay_public_key,
110
+ )
111
+ obj_req_msg.encrypt(peer.shared_key_bytes)
112
+ node.outgoing_queue.put((obj_req_msg.to_bytes(), (provider_address, provider_port)))
113
+
114
+ case ObjectResponseType.OBJECT_NEAREST_PEER:
115
+ node.logger.debug("Ignoring OBJECT_NEAREST_PEER response from %s", peer.address)
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ..models.ping import Ping
7
+ from ..models.peer import Peer
8
+
9
+ if TYPE_CHECKING:
10
+ from .... import Node
11
+
12
+
13
+ def handle_ping(node: "Node", peer: Peer, payload: bytes) -> None:
14
+ """Update peer and validation state based on an incoming ping message."""
15
+ try:
16
+ ping = Ping.from_bytes(payload)
17
+ except Exception as exc:
18
+ node.logger.warning("Error decoding ping: %s", exc)
19
+ return
20
+
21
+ peer.timestamp = datetime.now(timezone.utc)
22
+ peer.latest_block = ping.latest_block
23
+
24
+ validation_route = node.validation_route
25
+ if validation_route is None:
26
+ return
27
+
28
+ try:
29
+ if ping.is_validator:
30
+ validation_route.add_peer(peer.public_key_bytes)
31
+ else:
32
+ validation_route.remove_peer(peer.public_key_bytes)
33
+ except Exception:
34
+ pass
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Sequence
4
-
5
3
  import socket
6
4
 
7
5
  from ..models.message import Message, MessageTopic
@@ -10,19 +8,17 @@ from ..util import xor_distance
10
8
  from typing import TYPE_CHECKING
11
9
  if TYPE_CHECKING:
12
10
  from .... import Node
11
+ from ..models.peer import Peer
13
12
 
14
13
 
15
- def handle_route_request(node: "Node", addr: Sequence[object], message: Message) -> None:
16
- logger = node.logger
17
-
18
- request_addr = (addr[0], int(addr[1])) if len(addr) >= 2 else addr
19
- sender_public_key = node.addresses.get(request_addr)
14
+ def handle_route_request(node: "Node", peer: "Peer", message: Message) -> None:
15
+ sender_public_key = getattr(peer, "public_key_bytes", None)
20
16
  if not sender_public_key:
21
- logger.warning("Unknown sender for ROUTE_REQUEST from %s", addr)
17
+ node.logger.warning("Unknown sender for ROUTE_REQUEST from %s", peer.address)
22
18
  return
23
19
 
24
20
  if not message.content:
25
- logger.warning("ROUTE_REQUEST missing route id from %s", addr)
21
+ node.logger.warning("ROUTE_REQUEST missing route id from %s", peer.address)
26
22
  return
27
23
  route_id = message.content[0]
28
24
  if route_id == 0:
@@ -30,38 +26,44 @@ def handle_route_request(node: "Node", addr: Sequence[object], message: Message)
30
26
  elif route_id == 1:
31
27
  route = node.validation_route
32
28
  if route is None:
33
- logger.warning("Validation route not initialized for %s", addr)
29
+ node.logger.warning("Validation route not initialized for %s", peer.address)
34
30
  return
35
31
  else:
36
- logger.warning("Unknown route id %s in ROUTE_REQUEST from %s", route_id, addr)
32
+ node.logger.warning("Unknown route id %s in ROUTE_REQUEST from %s", route_id, peer.address)
37
33
  return
38
34
 
39
35
  payload_parts = []
40
36
  for bucket in route.buckets.values():
41
37
  closest_key = None
42
38
  closest_distance = None
39
+
43
40
  for peer_key in bucket:
44
41
  try:
45
42
  distance = xor_distance(sender_public_key, peer_key)
46
43
  except ValueError:
47
44
  continue
45
+
48
46
  if closest_distance is None or distance < closest_distance:
49
47
  closest_distance = distance
50
48
  closest_key = peer_key
49
+
51
50
  if closest_key is None:
52
51
  continue
53
- peer = node.peers.get(closest_key)
54
- if not peer or not peer.address:
52
+
53
+ bucket_peer = node.get_peer(closest_key)
54
+ if bucket_peer is None or bucket_peer.address is None:
55
55
  continue
56
- host, port = peer.address
56
+
57
+ host, port = bucket_peer.address
57
58
  try:
58
59
  address_bytes = socket.inet_pton(socket.AF_INET, host)
59
60
  except OSError:
60
61
  try:
61
62
  address_bytes = socket.inet_pton(socket.AF_INET6, host)
62
63
  except OSError as exc:
63
- logger.warning("Invalid peer address %s: %s", peer.address, exc)
64
+ node.logger.warning("Invalid peer address %s: %s", bucket_peer.address, exc)
64
65
  continue
66
+
65
67
  port_bytes = int(port).to_bytes(2, "big", signed=False)
66
68
  payload_parts.append(address_bytes + port_bytes)
67
69
 
@@ -70,9 +72,5 @@ def handle_route_request(node: "Node", addr: Sequence[object], message: Message)
70
72
  content=b"".join(payload_parts),
71
73
  sender=node.relay_public_key,
72
74
  )
73
- try:
74
- request_host, request_port = addr[0], int(addr[1])
75
- except Exception:
76
- logger.warning("Invalid requester address %s", addr)
77
- return
78
- node.outgoing_queue.put((response.to_bytes(), (request_host, request_port)))
75
+ response.encrypt(peer.shared_key_bytes)
76
+ node.outgoing_queue.put((response.to_bytes(), peer.address))
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Sequence
4
-
5
3
  import socket
6
4
 
7
5
  from ..models.message import Message
@@ -9,21 +7,20 @@ from ..models.message import Message
9
7
  from typing import TYPE_CHECKING
10
8
  if TYPE_CHECKING:
11
9
  from .... import Node
10
+ from ..models.peer import Peer
12
11
 
13
12
 
14
- def handle_route_response(node: "Node", addr: Sequence[object], message: Message) -> None:
15
- logger = node.logger
16
-
13
+ def handle_route_response(node: "Node", peer: "Peer", message: Message) -> None:
17
14
  payload = message.content
18
15
  if not payload:
19
16
  return
20
17
  host_len = 16 if node.use_ipv6 else 4
21
18
  chunk_size = host_len + 2
22
19
  if len(payload) % chunk_size != 0:
23
- logger.warning(
20
+ node.logger.warning(
24
21
  "ROUTE_RESPONSE payload size mismatch (%s bytes) from %s",
25
22
  len(payload),
26
- addr,
23
+ peer.address,
27
24
  )
28
25
  return
29
26
 
@@ -35,9 +32,9 @@ def handle_route_response(node: "Node", addr: Sequence[object], message: Message
35
32
  try:
36
33
  host = socket.inet_ntop(family, host_bytes)
37
34
  except OSError as exc:
38
- logger.warning(
35
+ node.logger.warning(
39
36
  "Invalid host bytes in ROUTE_RESPONSE from %s: %s",
40
- addr,
37
+ peer.address,
41
38
  exc,
42
39
  )
43
40
  continue
@@ -45,8 +42,12 @@ def handle_route_response(node: "Node", addr: Sequence[object], message: Message
45
42
  decoded_addresses.append((host, port))
46
43
  if not decoded_addresses:
47
44
  return
48
- logger.debug("Decoded %s addresses from ROUTE_RESPONSE", len(decoded_addresses))
45
+ node.logger.debug("Decoded %s addresses from ROUTE_RESPONSE", len(decoded_addresses))
49
46
 
50
- handshake_message = Message(handshake=True, sender=node.relay_public_key)
47
+ handshake_message = Message(
48
+ handshake=True,
49
+ sender=node.relay_public_key,
50
+ content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
51
+ )
51
52
  for host, port in decoded_addresses:
52
53
  node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))