astreum 0.3.5__tar.gz → 0.3.10__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 (82) hide show
  1. {astreum-0.3.5/src/astreum.egg-info → astreum-0.3.10}/PKG-INFO +3 -3
  2. {astreum-0.3.5 → astreum-0.3.10}/README.md +7 -7
  3. {astreum-0.3.5 → astreum-0.3.10}/pyproject.toml +1 -1
  4. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/__init__.py +4 -2
  5. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/handlers/handshake.py +89 -81
  6. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/handlers/object_request.py +53 -30
  7. astreum-0.3.10/src/astreum/communication/handlers/object_response.py +115 -0
  8. astreum-0.3.10/src/astreum/communication/handlers/ping.py +34 -0
  9. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/handlers/route_request.py +19 -21
  10. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/handlers/route_response.py +12 -11
  11. astreum-0.3.10/src/astreum/communication/models/message.py +124 -0
  12. astreum-0.3.10/src/astreum/communication/models/peer.py +51 -0
  13. astreum-0.3.10/src/astreum/communication/processors/incoming.py +98 -0
  14. astreum-0.3.10/src/astreum/communication/processors/outgoing.py +20 -0
  15. astreum-0.3.10/src/astreum/communication/setup.py +166 -0
  16. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/start.py +9 -10
  17. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/start.py +7 -8
  18. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/workers/discovery.py +6 -7
  19. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/workers/validation.py +307 -292
  20. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/workers/verify.py +8 -10
  21. astreum-0.3.10/src/astreum/crypto/chacha20poly1305.py +74 -0
  22. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/evaluations/high_evaluation.py +237 -237
  23. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/evaluations/low_evaluation.py +18 -18
  24. astreum-0.3.10/src/astreum/machine/models/__init__.py +0 -0
  25. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/node.py +20 -1
  26. astreum-0.3.10/src/astreum/storage/actions/get.py +183 -0
  27. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/storage/actions/set.py +55 -15
  28. astreum-0.3.10/src/astreum/storage/requests.py +28 -0
  29. {astreum-0.3.5 → astreum-0.3.10/src/astreum.egg-info}/PKG-INFO +3 -3
  30. {astreum-0.3.5 → astreum-0.3.10}/src/astreum.egg-info/SOURCES.txt +5 -0
  31. astreum-0.3.5/src/astreum/communication/handlers/object_response.py +0 -37
  32. astreum-0.3.5/src/astreum/communication/handlers/ping.py +0 -48
  33. astreum-0.3.5/src/astreum/communication/models/message.py +0 -105
  34. astreum-0.3.5/src/astreum/communication/models/peer.py +0 -23
  35. astreum-0.3.5/src/astreum/communication/setup.py +0 -262
  36. astreum-0.3.5/src/astreum/storage/actions/get.py +0 -85
  37. {astreum-0.3.5 → astreum-0.3.10}/LICENSE +0 -0
  38. {astreum-0.3.5 → astreum-0.3.10}/setup.cfg +0 -0
  39. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/__init__.py +0 -0
  40. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/handlers/__init__.py +0 -0
  41. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/models/__init__.py +0 -0
  42. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/models/ping.py +0 -0
  43. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/models/route.py +0 -0
  44. {astreum-0.3.5/src/astreum/consensus/models → astreum-0.3.10/src/astreum/communication/processors}/__init__.py +0 -0
  45. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/communication/util.py +0 -0
  46. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/__init__.py +0 -0
  47. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/genesis.py +0 -0
  48. {astreum-0.3.5/src/astreum/crypto → astreum-0.3.10/src/astreum/consensus/models}/__init__.py +0 -0
  49. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/models/account.py +0 -0
  50. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/models/accounts.py +0 -0
  51. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/models/block.py +0 -0
  52. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/models/chain.py +0 -0
  53. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/models/fork.py +0 -0
  54. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/models/receipt.py +0 -0
  55. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/models/transaction.py +0 -0
  56. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/setup.py +0 -0
  57. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/validator.py +0 -0
  58. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/consensus/workers/__init__.py +0 -0
  59. {astreum-0.3.5/src/astreum/machine/evaluations → astreum-0.3.10/src/astreum/crypto}/__init__.py +0 -0
  60. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/crypto/ed25519.py +0 -0
  61. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/crypto/quadratic_form.py +0 -0
  62. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/crypto/wesolowski.py +0 -0
  63. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/crypto/x25519.py +0 -0
  64. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/__init__.py +0 -0
  65. {astreum-0.3.5/src/astreum/machine/models → astreum-0.3.10/src/astreum/machine/evaluations}/__init__.py +0 -0
  66. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/evaluations/script_evaluation.py +0 -0
  67. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/models/environment.py +0 -0
  68. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/models/expression.py +0 -0
  69. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/models/meter.py +0 -0
  70. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/parser.py +0 -0
  71. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/machine/tokenizer.py +0 -0
  72. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/storage/__init__.py +0 -0
  73. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/storage/models/atom.py +0 -0
  74. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/storage/models/trie.py +0 -0
  75. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/storage/setup.py +0 -0
  76. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/utils/bytes.py +0 -0
  77. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/utils/config.py +0 -0
  78. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/utils/integer.py +0 -0
  79. {astreum-0.3.5 → astreum-0.3.10}/src/astreum/utils/logging.py +0 -0
  80. {astreum-0.3.5 → astreum-0.3.10}/src/astreum.egg-info/dependency_links.txt +0 -0
  81. {astreum-0.3.5 → astreum-0.3.10}/src/astreum.egg-info/requires.txt +0 -0
  82. {astreum-0.3.5 → astreum-0.3.10}/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.10
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.10"
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
  ]
@@ -1,81 +1,89 @@
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
- logger = node.logger
21
-
22
- sender_public_key_bytes = message.sender_bytes
23
- try:
24
- sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
25
- except Exception as exc:
26
- logger.warning("Error extracting sender key bytes: %s", exc)
27
- return True
28
-
29
- try:
30
- host, port = addr[0], int(addr[1])
31
- except Exception:
32
- return True
33
- address_key = (host, port)
34
-
35
- old_key_bytes = node.addresses.get(address_key)
36
- node.addresses[address_key] = sender_public_key_bytes
37
-
38
- if old_key_bytes is None:
39
- try:
40
- peer = Peer(node.relay_secret_key, sender_key)
41
- except Exception:
42
- return True
43
- peer.address = address_key
44
-
45
- node.peers[sender_public_key_bytes] = peer
46
- node.peer_route.add_peer(sender_public_key_bytes, peer)
47
-
48
- logger.info(
49
- "Handshake accepted from %s:%s; peer added",
50
- address_key[0],
51
- address_key[1],
52
- )
53
- response = Message(handshake=True, sender=node.relay_public_key)
54
- node.outgoing_queue.put((response.to_bytes(), address_key))
55
- return True
56
-
57
- if old_key_bytes == sender_public_key_bytes:
58
- peer = node.peers.get(sender_public_key_bytes)
59
- if peer is not None:
60
- peer.address = address_key
61
- return False
62
-
63
- node.peers.pop(old_key_bytes, None)
64
- try:
65
- node.peer_route.remove_peer(old_key_bytes)
66
- except Exception:
67
- pass
68
- try:
69
- peer = Peer(node.relay_secret_key, sender_key)
70
- except Exception:
71
- return True
72
- peer.address = address_key
73
-
74
- node.peers[sender_public_key_bytes] = peer
75
- node.peer_route.add_peer(sender_public_key_bytes, peer)
76
- logger.info(
77
- "Peer at %s:%s replaced due to key change",
78
- address_key[0],
79
- address_key[1],
80
- )
81
- return False
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
+ old_key_bytes = node.addresses.get(peer_address)
35
+ node.addresses[peer_address] = sender_public_key_bytes
36
+
37
+ if old_key_bytes is None:
38
+ try:
39
+ peer = Peer(
40
+ node_secret_key=node.relay_secret_key,
41
+ peer_public_key=sender_key,
42
+ address=peer_address,
43
+ )
44
+ except Exception:
45
+ return True
46
+
47
+ node.add_peer(sender_public_key_bytes, peer)
48
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
49
+
50
+ node.logger.info(
51
+ "Handshake accepted from %s:%s; peer added",
52
+ peer_address[0],
53
+ peer_address[1],
54
+ )
55
+ response = Message(
56
+ handshake=True,
57
+ sender=node.relay_public_key,
58
+ content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
59
+ )
60
+ node.outgoing_queue.put((response.to_bytes(), peer_address))
61
+ return True
62
+
63
+ if old_key_bytes == sender_public_key_bytes:
64
+ peer = node.get_peer(sender_public_key_bytes)
65
+ if peer is not None:
66
+ peer.address = peer_address
67
+ return False
68
+
69
+ try:
70
+ node.peer_route.remove_peer(old_key_bytes)
71
+ except Exception:
72
+ pass
73
+ try:
74
+ peer = Peer(
75
+ node_secret_key=node.relay_secret_key,
76
+ peer_public_key=sender_key,
77
+ address=peer_address,
78
+ )
79
+ except Exception:
80
+ return True
81
+
82
+ node.replace_peer(old_key_bytes, sender_public_key_bytes, peer)
83
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
84
+ node.logger.info(
85
+ "Peer at %s:%s replaced due to key change",
86
+ peer_address[0],
87
+ peer_address[1],
88
+ )
89
+ return False
@@ -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))