astreum 0.2.41__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 (82) 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}/message.py +1 -0
  8. astreum/communication/models/peer.py +23 -0
  9. astreum/{_communication → communication/models}/route.py +45 -8
  10. astreum/{_communication → communication}/setup.py +46 -95
  11. astreum/communication/start.py +38 -0
  12. astreum/consensus/__init__.py +20 -0
  13. astreum/consensus/genesis.py +66 -0
  14. astreum/consensus/models/__init__.py +0 -0
  15. astreum/consensus/models/account.py +84 -0
  16. astreum/consensus/models/accounts.py +72 -0
  17. astreum/consensus/models/block.py +364 -0
  18. astreum/{_consensus → consensus/models}/chain.py +7 -7
  19. astreum/{_consensus → consensus/models}/fork.py +8 -8
  20. astreum/consensus/models/receipt.py +98 -0
  21. astreum/consensus/models/transaction.py +213 -0
  22. astreum/{_consensus → consensus}/setup.py +26 -11
  23. astreum/consensus/start.py +68 -0
  24. astreum/consensus/validator.py +95 -0
  25. astreum/{_consensus → consensus}/workers/discovery.py +20 -1
  26. astreum/consensus/workers/validation.py +291 -0
  27. astreum/{_consensus → consensus}/workers/verify.py +32 -3
  28. astreum/machine/__init__.py +20 -0
  29. astreum/machine/evaluations/__init__.py +0 -0
  30. astreum/machine/evaluations/high_evaluation.py +237 -0
  31. astreum/machine/evaluations/low_evaluation.py +281 -0
  32. astreum/machine/evaluations/script_evaluation.py +27 -0
  33. astreum/machine/models/__init__.py +0 -0
  34. astreum/machine/models/environment.py +31 -0
  35. astreum/machine/models/expression.py +218 -0
  36. astreum/{_lispeum → machine}/parser.py +26 -31
  37. astreum/machine/tokenizer.py +90 -0
  38. astreum/node.py +73 -781
  39. astreum/storage/__init__.py +7 -0
  40. astreum/storage/actions/get.py +69 -0
  41. astreum/storage/actions/set.py +132 -0
  42. astreum/storage/models/atom.py +107 -0
  43. astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
  44. astreum/storage/setup.py +44 -15
  45. astreum/utils/bytes.py +24 -0
  46. astreum/utils/integer.py +25 -0
  47. astreum/utils/logging.py +219 -0
  48. astreum-0.3.1.dist-info/METADATA +160 -0
  49. astreum-0.3.1.dist-info/RECORD +62 -0
  50. astreum/_communication/peer.py +0 -11
  51. astreum/_consensus/__init__.py +0 -20
  52. astreum/_consensus/account.py +0 -170
  53. astreum/_consensus/accounts.py +0 -67
  54. astreum/_consensus/block.py +0 -328
  55. astreum/_consensus/genesis.py +0 -141
  56. astreum/_consensus/receipt.py +0 -177
  57. astreum/_consensus/transaction.py +0 -192
  58. astreum/_consensus/workers/validation.py +0 -122
  59. astreum/_lispeum/__init__.py +0 -16
  60. astreum/_lispeum/environment.py +0 -13
  61. astreum/_lispeum/expression.py +0 -37
  62. astreum/_lispeum/high_evaluation.py +0 -177
  63. astreum/_lispeum/low_evaluation.py +0 -123
  64. astreum/_lispeum/tokenizer.py +0 -22
  65. astreum/_node.py +0 -58
  66. astreum/_storage/__init__.py +0 -5
  67. astreum/_storage/atom.py +0 -117
  68. astreum/format.py +0 -75
  69. astreum/models/block.py +0 -441
  70. astreum/models/merkle.py +0 -205
  71. astreum/models/patricia.py +0 -393
  72. astreum/storage/object.py +0 -68
  73. astreum-0.2.41.dist-info/METADATA +0 -146
  74. astreum-0.2.41.dist-info/RECORD +0 -53
  75. /astreum/{models → communication/handlers}/__init__.py +0 -0
  76. /astreum/{_communication → communication/models}/ping.py +0 -0
  77. /astreum/{_communication → communication}/util.py +0 -0
  78. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  79. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  80. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
  81. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
  82. {astreum-0.2.41.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
+ )
@@ -0,0 +1,107 @@
1
+
2
+
3
+ from enum import IntEnum
4
+ from typing import List, Optional, Tuple
5
+
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
+
16
+ class AtomKind(IntEnum):
17
+ SYMBOL = 0
18
+ BYTES = 1
19
+ LIST = 2
20
+
21
+
22
+ class Atom:
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):
29
+ self.data = data
30
+ self.kind = kind
31
+ self.next_id = next_id
32
+ self.size = len(data)
33
+
34
+
35
+ def generate_id(self) -> bytes:
36
+ """Compute the object id using this atom's metadata."""
37
+ kind_bytes = int(self.kind).to_bytes(1, "little", signed=False)
38
+ return blake3(
39
+ kind_bytes + self.data_hash() + self.next_id + u64_le(self.size)
40
+ ).digest()
41
+
42
+ def data_hash(self) -> bytes:
43
+ return hash_bytes(self.data)
44
+
45
+ def object_id(self) -> bytes:
46
+ return self.generate_id()
47
+
48
+ @staticmethod
49
+ def verify_metadata(
50
+ object_id: bytes,
51
+ size: int,
52
+ next_hash: bytes,
53
+ data_hash: bytes,
54
+ kind: AtomKind,
55
+ ) -> bool:
56
+ kind_bytes = int(kind).to_bytes(1, "little", signed=False)
57
+ expected = blake3(kind_bytes + data_hash + next_hash + u64_le(size)).digest()
58
+ return object_id == expected
59
+
60
+ def to_bytes(self) -> bytes:
61
+ """Serialize as next-hash + kind byte + payload."""
62
+ kind_byte = int(self.kind).to_bytes(1, "little", signed=False)
63
+ return self.next_id + kind_byte + self.data
64
+
65
+ @staticmethod
66
+ def from_bytes(buf: bytes) -> "Atom":
67
+ header_len = len(ZERO32)
68
+ if len(buf) < header_len + 1:
69
+ raise ValueError("buffer too short for Atom header")
70
+ next_hash = buf[:header_len]
71
+ kind_value = buf[header_len]
72
+ data = buf[header_len + 1 :]
73
+ try:
74
+ kind = AtomKind(kind_value)
75
+ except ValueError as exc:
76
+ raise ValueError(f"unknown atom kind: {kind_value}") from exc
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