astreum 0.2.61__py3-none-any.whl → 0.3.1__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.
Files changed (75) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +83 -0
  4. astreum/communication/handlers/ping.py +48 -0
  5. astreum/communication/handlers/storage_request.py +81 -0
  6. astreum/communication/models/__init__.py +0 -0
  7. astreum/{_communication → communication/models}/route.py +5 -5
  8. astreum/communication/setup.py +205 -0
  9. astreum/communication/start.py +38 -0
  10. astreum/consensus/__init__.py +20 -0
  11. astreum/consensus/genesis.py +66 -0
  12. astreum/consensus/models/__init__.py +0 -0
  13. astreum/consensus/models/account.py +84 -0
  14. astreum/consensus/models/accounts.py +72 -0
  15. astreum/consensus/models/block.py +364 -0
  16. astreum/{_consensus → consensus/models}/chain.py +7 -7
  17. astreum/{_consensus → consensus/models}/fork.py +8 -8
  18. astreum/consensus/models/receipt.py +98 -0
  19. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  20. astreum/{_consensus → consensus}/setup.py +18 -50
  21. astreum/consensus/start.py +68 -0
  22. astreum/consensus/validator.py +95 -0
  23. astreum/{_consensus → consensus}/workers/discovery.py +20 -1
  24. astreum/consensus/workers/validation.py +291 -0
  25. astreum/{_consensus → consensus}/workers/verify.py +31 -2
  26. astreum/machine/__init__.py +20 -0
  27. astreum/machine/evaluations/__init__.py +0 -0
  28. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +16 -15
  29. astreum/machine/evaluations/low_evaluation.py +281 -0
  30. astreum/machine/evaluations/script_evaluation.py +27 -0
  31. astreum/machine/models/__init__.py +0 -0
  32. astreum/machine/models/environment.py +31 -0
  33. astreum/{_lispeum → machine/models}/expression.py +36 -8
  34. astreum/machine/tokenizer.py +90 -0
  35. astreum/node.py +73 -781
  36. astreum/storage/__init__.py +7 -0
  37. astreum/storage/actions/get.py +69 -0
  38. astreum/storage/actions/set.py +132 -0
  39. astreum/{_storage → storage/models}/atom.py +55 -57
  40. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  41. astreum/storage/setup.py +44 -15
  42. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/METADATA +25 -24
  43. astreum-0.3.1.dist-info/RECORD +62 -0
  44. astreum/_communication/setup.py +0 -322
  45. astreum/_consensus/__init__.py +0 -20
  46. astreum/_consensus/account.py +0 -95
  47. astreum/_consensus/accounts.py +0 -38
  48. astreum/_consensus/block.py +0 -311
  49. astreum/_consensus/genesis.py +0 -72
  50. astreum/_consensus/receipt.py +0 -136
  51. astreum/_consensus/workers/validation.py +0 -125
  52. astreum/_lispeum/__init__.py +0 -16
  53. astreum/_lispeum/environment.py +0 -13
  54. astreum/_lispeum/low_evaluation.py +0 -123
  55. astreum/_lispeum/tokenizer.py +0 -22
  56. astreum/_node.py +0 -198
  57. astreum/_storage/__init__.py +0 -7
  58. astreum/_storage/setup.py +0 -35
  59. astreum/format.py +0 -75
  60. astreum/models/block.py +0 -441
  61. astreum/models/merkle.py +0 -205
  62. astreum/models/patricia.py +0 -393
  63. astreum/storage/object.py +0 -68
  64. astreum-0.2.61.dist-info/RECORD +0 -57
  65. /astreum/{models → communication/handlers}/__init__.py +0 -0
  66. /astreum/{_communication → communication/models}/message.py +0 -0
  67. /astreum/{_communication → communication/models}/peer.py +0 -0
  68. /astreum/{_communication → communication/models}/ping.py +0 -0
  69. /astreum/{_communication → communication}/util.py +0 -0
  70. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  71. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  72. /astreum/{_lispeum → machine}/parser.py +0 -0
  73. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
  74. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
  75. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,7 @@
1
+ from .models.atom import Atom
2
+ from .setup import storage_setup
3
+
4
+ __all__ = [
5
+ "Atom",
6
+ "storage_setup",
7
+ ]
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from ..models.atom import Atom
7
+
8
+
9
+ def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
10
+ """Retrieve an atom from in-memory cache while tracking hit statistics."""
11
+ node_logger = self.logger
12
+ atom = self.hot_storage.get(key)
13
+ if atom is not None:
14
+ self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
15
+ node_logger.debug("Hot storage hit for %s", key.hex())
16
+ else:
17
+ node_logger.debug("Hot storage miss for %s", key.hex())
18
+ return atom
19
+
20
+
21
+ def _network_get(self, key: bytes) -> Optional[Atom]:
22
+ """Attempt to fetch an atom from network peers when local storage misses."""
23
+ node_logger = self.logger
24
+ node_logger.debug("Attempting network fetch for %s", key.hex())
25
+ # locate storage provider
26
+ # query storage provider
27
+ node_logger.warning("Network fetch for %s is not implemented", key.hex())
28
+ return None
29
+
30
+
31
+ def storage_get(self, key: bytes) -> Optional[Atom]:
32
+ """Retrieve an Atom by checking local storage first, then the network."""
33
+ node_logger = self.logger
34
+ node_logger.debug("Fetching atom %s", key.hex())
35
+ atom = self._hot_storage_get(key)
36
+ if atom is not None:
37
+ node_logger.debug("Returning atom %s from hot storage", key.hex())
38
+ return atom
39
+ atom = self._cold_storage_get(key)
40
+ if atom is not None:
41
+ node_logger.debug("Returning atom %s from cold storage", key.hex())
42
+ return atom
43
+ node_logger.debug("Falling back to network fetch for %s", key.hex())
44
+ return self._network_get(key)
45
+
46
+
47
+ def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
48
+ """Read an atom from the cold storage directory if configured."""
49
+ node_logger = self.logger
50
+ if not self.cold_storage_path:
51
+ node_logger.debug("Cold storage disabled; cannot fetch %s", key.hex())
52
+ return None
53
+ filename = f"{key.hex().upper()}.bin"
54
+ file_path = Path(self.cold_storage_path) / filename
55
+ try:
56
+ data = file_path.read_bytes()
57
+ except FileNotFoundError:
58
+ node_logger.debug("Cold storage miss for %s", key.hex())
59
+ return None
60
+ except OSError as exc:
61
+ node_logger.warning("Error reading cold storage file %s: %s", file_path, exc)
62
+ return None
63
+ try:
64
+ atom = Atom.from_bytes(data)
65
+ node_logger.debug("Loaded atom %s from cold storage", key.hex())
66
+ return atom
67
+ except ValueError as exc:
68
+ node_logger.warning("Cold storage data corrupted for %s: %s", file_path, exc)
69
+ return None
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from ..models.atom import Atom
6
+
7
+
8
+ def _hot_storage_set(self, key: bytes, value: Atom) -> bool:
9
+ """Store atom in hot storage without exceeding the configured limit."""
10
+ node_logger = self.logger
11
+ projected = self.hot_storage_size + value.size
12
+ if projected > self.hot_storage_limit:
13
+ node_logger.warning(
14
+ "Hot storage limit reached (%s > %s); skipping atom %s",
15
+ projected,
16
+ self.hot_storage_limit,
17
+ key.hex(),
18
+ )
19
+ return False
20
+
21
+ self.hot_storage[key] = value
22
+ self.hot_storage_size = projected
23
+ node_logger.debug(
24
+ "Stored atom %s in hot storage (bytes=%s, total=%s)",
25
+ key.hex(),
26
+ value.size,
27
+ projected,
28
+ )
29
+ return True
30
+
31
+
32
+ def _cold_storage_set(self, atom: Atom) -> None:
33
+ """Persist an atom into the cold storage directory if it already exists."""
34
+ node_logger = self.logger
35
+ atom_id = atom.object_id()
36
+ atom_hex = atom_id.hex()
37
+ if not self.cold_storage_path:
38
+ node_logger.debug("Cold storage disabled; skipping atom %s", atom_hex)
39
+ return
40
+ atom_bytes = atom.to_bytes()
41
+ projected = self.cold_storage_size + len(atom_bytes)
42
+ if self.cold_storage_limit and projected > self.cold_storage_limit:
43
+ node_logger.warning(
44
+ "Cold storage limit reached (%s > %s); skipping atom %s",
45
+ projected,
46
+ self.cold_storage_limit,
47
+ atom_hex,
48
+ )
49
+ return
50
+ directory = Path(self.cold_storage_path)
51
+ if not directory.exists():
52
+ node_logger.warning(
53
+ "Cold storage path %s missing; skipping atom %s",
54
+ directory,
55
+ atom_hex,
56
+ )
57
+ return
58
+ filename = f"{atom_hex.upper()}.bin"
59
+ file_path = directory / filename
60
+ try:
61
+ file_path.write_bytes(atom_bytes)
62
+ self.cold_storage_size = projected
63
+ node_logger.debug("Persisted atom %s to cold storage", atom_hex)
64
+ except OSError as exc:
65
+ node_logger.error(
66
+ "Failed writing atom %s to cold storage %s: %s",
67
+ atom_hex,
68
+ file_path,
69
+ exc,
70
+ )
71
+
72
+
73
+ def _network_set(self, atom: Atom) -> None:
74
+ """Advertise an atom to the closest known peer so they can fetch it from us."""
75
+ node_logger = self.logger
76
+ atom_id = atom.object_id()
77
+ atom_hex = atom_id.hex()
78
+ try:
79
+ from ...communication.models.message import Message, MessageTopic
80
+ except Exception as exc:
81
+ node_logger.warning(
82
+ "Communication module unavailable; cannot advertise atom %s: %s",
83
+ atom_hex,
84
+ exc,
85
+ )
86
+ return
87
+
88
+ try:
89
+ closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
90
+ except Exception as exc:
91
+ node_logger.warning("Peer lookup failed for atom %s: %s", atom_hex, exc)
92
+ return
93
+ if closest_peer is None or closest_peer.address is None:
94
+ node_logger.debug("No peer available to advertise atom %s", atom_hex)
95
+ return
96
+ target_addr = closest_peer.address
97
+
98
+ try:
99
+ provider_ip, provider_port = self.incoming_socket.getsockname()[:2]
100
+ except Exception as exc:
101
+ node_logger.warning(
102
+ "Unable to determine provider address for atom %s: %s",
103
+ atom_hex,
104
+ exc,
105
+ )
106
+ return
107
+
108
+ provider_str = f"{provider_ip}:{int(provider_port)}"
109
+ try:
110
+ provider_bytes = provider_str.encode("utf-8")
111
+ except Exception as exc:
112
+ node_logger.warning("Unable to encode provider info for %s: %s", atom_hex, exc)
113
+ return
114
+
115
+ payload = atom_id + provider_bytes
116
+ message = Message(topic=MessageTopic.STORAGE_REQUEST, content=payload)
117
+ try:
118
+ self.outgoing_queue.put((message.to_bytes(), target_addr))
119
+ node_logger.debug(
120
+ "Advertised atom %s to peer at %s:%s",
121
+ atom_hex,
122
+ target_addr[0],
123
+ target_addr[1],
124
+ )
125
+ except Exception as exc:
126
+ node_logger.error(
127
+ "Failed to queue advertisement for atom %s to %s:%s: %s",
128
+ atom_hex,
129
+ target_addr[0],
130
+ target_addr[1],
131
+ exc,
132
+ )
@@ -1,18 +1,18 @@
1
-
2
-
1
+
2
+
3
3
  from enum import IntEnum
4
4
  from typing import List, Optional, Tuple
5
-
5
+
6
6
  from blake3 import blake3
7
-
8
- ZERO32 = b"\x00"*32
9
-
10
- def u64_le(n: int) -> bytes:
11
- return int(n).to_bytes(8, "little", signed=False)
12
-
13
- def hash_bytes(b: bytes) -> bytes:
14
- return blake3(b).digest()
15
-
7
+
8
+ ZERO32 = b"\x00"*32
9
+
10
+ def u64_le(n: int) -> bytes:
11
+ return int(n).to_bytes(8, "little", signed=False)
12
+
13
+ def hash_bytes(b: bytes) -> bytes:
14
+ return blake3(b).digest()
15
+
16
16
  class AtomKind(IntEnum):
17
17
  SYMBOL = 0
18
18
  BYTES = 1
@@ -20,35 +20,23 @@ class AtomKind(IntEnum):
20
20
 
21
21
 
22
22
  class Atom:
23
- data: bytes
24
- next: bytes
25
- size: int
26
-
27
- def __init__(
28
- self,
29
- data: bytes,
30
- next: bytes = ZERO32,
31
- size: Optional[int] = None,
32
- kind: AtomKind = AtomKind.BYTES,
33
- ):
23
+ data: bytes
24
+ kind: AtomKind
25
+ next_id: bytes
26
+ size: int
27
+
28
+ def __init__(self, data: bytes, kind: AtomKind, next_id: bytes = ZERO32):
34
29
  self.data = data
35
- self.next = next
36
- self.size = len(data) if size is None else size
37
30
  self.kind = kind
31
+ self.next_id = next_id
32
+ self.size = len(data)
33
+
38
34
 
39
- @staticmethod
40
- def from_data(
41
- data: bytes,
42
- next_hash: bytes = ZERO32,
43
- kind: AtomKind = AtomKind.BYTES,
44
- ) -> "Atom":
45
- return Atom(data=data, next=next_hash, size=len(data), kind=kind)
46
-
47
35
  def generate_id(self) -> bytes:
48
36
  """Compute the object id using this atom's metadata."""
49
37
  kind_bytes = int(self.kind).to_bytes(1, "little", signed=False)
50
38
  return blake3(
51
- kind_bytes + self.data_hash() + self.next + u64_le(self.size)
39
+ kind_bytes + self.data_hash() + self.next_id + u64_le(self.size)
52
40
  ).digest()
53
41
 
54
42
  def data_hash(self) -> bytes:
@@ -68,11 +56,11 @@ class Atom:
68
56
  kind_bytes = int(kind).to_bytes(1, "little", signed=False)
69
57
  expected = blake3(kind_bytes + data_hash + next_hash + u64_le(size)).digest()
70
58
  return object_id == expected
71
-
59
+
72
60
  def to_bytes(self) -> bytes:
73
61
  """Serialize as next-hash + kind byte + payload."""
74
62
  kind_byte = int(self.kind).to_bytes(1, "little", signed=False)
75
- return self.next + kind_byte + self.data
63
+ return self.next_id + kind_byte + self.data
76
64
 
77
65
  @staticmethod
78
66
  def from_bytes(buf: bytes) -> "Atom":
@@ -86,24 +74,34 @@ class Atom:
86
74
  kind = AtomKind(kind_value)
87
75
  except ValueError as exc:
88
76
  raise ValueError(f"unknown atom kind: {kind_value}") from exc
89
- return Atom(data=data, next=next_hash, size=len(data), kind=kind)
90
-
91
- def bytes_list_to_atoms(values: List[bytes]) -> Tuple[bytes, List[Atom]]:
92
- """Build a forward-ordered linked list of atoms from byte payloads.
93
-
94
- Returns the head object's hash (ZERO32 if no values) and the atoms created.
95
- """
96
- next_hash = ZERO32
97
- atoms: List[Atom] = []
98
-
99
- for value in reversed(values):
100
- atom = Atom.from_data(
101
- data=bytes(value),
102
- next_hash=next_hash,
103
- kind=AtomKind.BYTES,
104
- )
105
- atoms.append(atom)
106
- next_hash = atom.object_id()
107
-
108
- atoms.reverse()
109
- return (next_hash if values else ZERO32), atoms
77
+ return Atom(data=data, next_id=next_hash, kind=kind)
78
+
79
+ def bytes_list_to_atoms(values: List[bytes]) -> Tuple[bytes, List[Atom]]:
80
+ """Build a forward-ordered linked list of atoms from byte payloads.
81
+
82
+ Returns the head object's hash (ZERO32 if no values) and the atoms created.
83
+ """
84
+ next_hash = ZERO32
85
+ atoms: List[Atom] = []
86
+
87
+ for value in reversed(values):
88
+ atom = Atom(data=bytes(value), next_id=next_hash, kind=AtomKind.BYTES)
89
+ atoms.append(atom)
90
+ next_hash = atom.object_id()
91
+
92
+ atoms.reverse()
93
+ return (next_hash if values else ZERO32), atoms
94
+
95
+
96
+ def get_atom_list_from_storage(self, root_hash: bytes) -> Optional[List["Atom"]]:
97
+ """Follow the list chain starting at root_hash, returning atoms or None on gaps."""
98
+ next_id: bytes = root_hash
99
+ atom_list: List["Atom"] = []
100
+ while next_id != ZERO32:
101
+ elem = self.storage_get(key=next_id)
102
+ if elem:
103
+ atom_list.append(elem)
104
+ next_id = elem.next_id
105
+ else:
106
+ return None
107
+ return atom_list