astreum 0.2.61__py3-none-any.whl → 0.3.9__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.
- astreum/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +89 -0
- astreum/communication/handlers/object_request.py +176 -0
- astreum/communication/handlers/object_response.py +115 -0
- astreum/communication/handlers/ping.py +34 -0
- astreum/communication/handlers/route_request.py +76 -0
- astreum/communication/handlers/route_response.py +53 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/communication/models/message.py +124 -0
- astreum/communication/models/peer.py +51 -0
- astreum/{_communication → communication/models}/route.py +7 -12
- astreum/communication/processors/__init__.py +0 -0
- astreum/communication/processors/incoming.py +98 -0
- astreum/communication/processors/outgoing.py +20 -0
- astreum/communication/setup.py +166 -0
- astreum/communication/start.py +37 -0
- astreum/{_communication → communication}/util.py +7 -0
- astreum/consensus/__init__.py +20 -0
- astreum/consensus/genesis.py +66 -0
- astreum/consensus/models/__init__.py +0 -0
- astreum/consensus/models/account.py +84 -0
- astreum/consensus/models/accounts.py +72 -0
- astreum/consensus/models/block.py +364 -0
- astreum/{_consensus → consensus/models}/chain.py +7 -7
- astreum/{_consensus → consensus/models}/fork.py +8 -8
- astreum/consensus/models/receipt.py +98 -0
- astreum/{_consensus → consensus/models}/transaction.py +76 -78
- astreum/{_consensus → consensus}/setup.py +18 -50
- astreum/consensus/start.py +67 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +19 -1
- astreum/consensus/workers/validation.py +307 -0
- astreum/{_consensus → consensus}/workers/verify.py +29 -2
- astreum/crypto/chacha20poly1305.py +74 -0
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
- astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum/machine/models/__init__.py +0 -0
- astreum/machine/models/environment.py +31 -0
- astreum/{_lispeum → machine/models}/expression.py +36 -8
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +78 -767
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +183 -0
- astreum/storage/actions/set.py +178 -0
- astreum/{_storage → storage/models}/atom.py +55 -57
- astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
- astreum/storage/requests.py +28 -0
- astreum/storage/setup.py +22 -15
- astreum/utils/config.py +48 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
- astreum-0.3.9.dist-info/RECORD +71 -0
- astreum/_communication/message.py +0 -101
- astreum/_communication/peer.py +0 -23
- astreum/_communication/setup.py +0 -322
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -95
- astreum/_consensus/accounts.py +0 -38
- astreum/_consensus/block.py +0 -311
- astreum/_consensus/genesis.py +0 -72
- astreum/_consensus/receipt.py +0 -136
- astreum/_consensus/workers/validation.py +0 -125
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -198
- astreum/_storage/__init__.py +0 -7
- astreum/_storage/setup.py +0 -35
- astreum/format.py +0 -75
- astreum/models/block.py +0 -441
- astreum/models/merkle.py +0 -205
- astreum/models/patricia.py +0 -393
- astreum/storage/object.py +0 -68
- astreum-0.2.61.dist-info/RECORD +0 -57
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- /astreum/{_lispeum → machine}/parser.py +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
astreum/storage/__init__.py
CHANGED
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
atom = self.hot_storage.get(key)
|
|
12
|
+
if atom is not None:
|
|
13
|
+
self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
|
|
14
|
+
self.logger.debug("Hot storage hit for %s", key.hex())
|
|
15
|
+
else:
|
|
16
|
+
self.logger.debug("Hot storage miss for %s", key.hex())
|
|
17
|
+
return atom
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _network_get(self, key: bytes) -> Optional[Atom]:
|
|
21
|
+
"""Attempt to fetch an atom from network peers when local storage misses."""
|
|
22
|
+
if not getattr(self, "is_connected", False):
|
|
23
|
+
self.logger.debug("Network fetch skipped for %s; node not connected", key.hex())
|
|
24
|
+
return None
|
|
25
|
+
self.logger.debug("Attempting network fetch for %s", key.hex())
|
|
26
|
+
try:
|
|
27
|
+
from ...communication.handlers.object_request import (
|
|
28
|
+
ObjectRequest,
|
|
29
|
+
ObjectRequestType,
|
|
30
|
+
)
|
|
31
|
+
from ...communication.models.message import Message, MessageTopic
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
self.logger.warning(
|
|
34
|
+
"Communication module unavailable; cannot fetch %s: %s",
|
|
35
|
+
key.hex(),
|
|
36
|
+
exc,
|
|
37
|
+
)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
closest_peer = self.peer_route.closest_peer_for_hash(key)
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
self.logger.warning("Peer lookup failed for %s: %s", key.hex(), exc)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if closest_peer is None or closest_peer.address is None:
|
|
47
|
+
self.logger.debug("No peer available to fetch %s", key.hex())
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
obj_req = ObjectRequest(
|
|
51
|
+
type=ObjectRequestType.OBJECT_GET,
|
|
52
|
+
data=b"",
|
|
53
|
+
atom_id=key,
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
message = Message(
|
|
57
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
58
|
+
content=obj_req.to_bytes(),
|
|
59
|
+
sender=self.relay_public_key,
|
|
60
|
+
)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
self.logger.warning("Failed to build object request for %s: %s", key.hex(), exc)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# encrypt the outbound request for the target peer
|
|
66
|
+
message.encrypt(closest_peer.shared_key_bytes)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
self.add_atom_req(key)
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
self.logger.warning("Failed to track object request for %s: %s", key.hex(), exc)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
self.outgoing_queue.put((message.to_bytes(), closest_peer.address))
|
|
75
|
+
self.logger.debug(
|
|
76
|
+
"Queued OBJECT_GET for %s to peer %s",
|
|
77
|
+
key.hex(),
|
|
78
|
+
closest_peer.address,
|
|
79
|
+
)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
self.logger.warning(
|
|
82
|
+
"Failed to queue OBJECT_GET for %s to %s: %s",
|
|
83
|
+
key.hex(),
|
|
84
|
+
closest_peer.address,
|
|
85
|
+
exc,
|
|
86
|
+
)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def storage_get(self, key: bytes) -> Optional[Atom]:
|
|
91
|
+
"""Retrieve an Atom by checking local storage first, then the network."""
|
|
92
|
+
self.logger.debug("Fetching atom %s", key.hex())
|
|
93
|
+
atom = self._hot_storage_get(key)
|
|
94
|
+
if atom is not None:
|
|
95
|
+
self.logger.debug("Returning atom %s from hot storage", key.hex())
|
|
96
|
+
return atom
|
|
97
|
+
atom = self._cold_storage_get(key)
|
|
98
|
+
if atom is not None:
|
|
99
|
+
self.logger.debug("Returning atom %s from cold storage", key.hex())
|
|
100
|
+
return atom
|
|
101
|
+
|
|
102
|
+
if not self.is_connected:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
provider_payload = self.storage_index.get(key)
|
|
106
|
+
if provider_payload is not None:
|
|
107
|
+
try:
|
|
108
|
+
from ...communication.handlers.object_response import decode_object_provider
|
|
109
|
+
from ...communication.handlers.object_request import (
|
|
110
|
+
ObjectRequest,
|
|
111
|
+
ObjectRequestType,
|
|
112
|
+
)
|
|
113
|
+
from ...communication.models.message import Message, MessageTopic
|
|
114
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
115
|
+
|
|
116
|
+
provider_key, provider_address, provider_port = decode_object_provider(provider_payload)
|
|
117
|
+
provider_public_key = X25519PublicKey.from_public_bytes(provider_key)
|
|
118
|
+
shared_key_bytes = self.relay_secret_key.exchange(provider_public_key)
|
|
119
|
+
|
|
120
|
+
obj_req = ObjectRequest(
|
|
121
|
+
type=ObjectRequestType.OBJECT_GET,
|
|
122
|
+
data=b"",
|
|
123
|
+
atom_id=key,
|
|
124
|
+
)
|
|
125
|
+
message = Message(
|
|
126
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
127
|
+
content=obj_req.to_bytes(),
|
|
128
|
+
sender=self.relay_public_key,
|
|
129
|
+
)
|
|
130
|
+
message.encrypt(shared_key_bytes)
|
|
131
|
+
self.add_atom_req(key)
|
|
132
|
+
self.outgoing_queue.put((message.to_bytes(), (provider_address, provider_port)))
|
|
133
|
+
self.logger.debug(
|
|
134
|
+
"Requested atom %s from indexed provider %s:%s",
|
|
135
|
+
key.hex(),
|
|
136
|
+
provider_address,
|
|
137
|
+
provider_port,
|
|
138
|
+
)
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
self.logger.warning("Failed indexed fetch for %s: %s", key.hex(), exc)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
self.logger.debug("Falling back to network fetch for %s", key.hex())
|
|
144
|
+
return self._network_get(key)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def local_get(self, key: bytes) -> Optional[Atom]:
|
|
148
|
+
"""Retrieve an Atom by checking only local hot and cold storage."""
|
|
149
|
+
self.logger.debug("Fetching atom %s (local only)", key.hex())
|
|
150
|
+
atom = self._hot_storage_get(key)
|
|
151
|
+
if atom is not None:
|
|
152
|
+
self.logger.debug("Returning atom %s from hot storage", key.hex())
|
|
153
|
+
return atom
|
|
154
|
+
atom = self._cold_storage_get(key)
|
|
155
|
+
if atom is not None:
|
|
156
|
+
self.logger.debug("Returning atom %s from cold storage", key.hex())
|
|
157
|
+
return atom
|
|
158
|
+
self.logger.debug("Local storage miss for %s", key.hex())
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
|
|
163
|
+
"""Read an atom from the cold storage directory if configured."""
|
|
164
|
+
if not self.config["cold_storage_path"]:
|
|
165
|
+
self.logger.debug("Cold storage disabled; cannot fetch %s", key.hex())
|
|
166
|
+
return None
|
|
167
|
+
filename = f"{key.hex().upper()}.bin"
|
|
168
|
+
file_path = Path(self.config["cold_storage_path"]) / filename
|
|
169
|
+
try:
|
|
170
|
+
data = file_path.read_bytes()
|
|
171
|
+
except FileNotFoundError:
|
|
172
|
+
self.logger.debug("Cold storage miss for %s", key.hex())
|
|
173
|
+
return None
|
|
174
|
+
except OSError as exc:
|
|
175
|
+
self.logger.warning("Error reading cold storage file %s: %s", file_path, exc)
|
|
176
|
+
return None
|
|
177
|
+
try:
|
|
178
|
+
atom = Atom.from_bytes(data)
|
|
179
|
+
self.logger.debug("Loaded atom %s from cold storage", key.hex())
|
|
180
|
+
return atom
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
self.logger.warning("Cold storage data corrupted for %s: %s", file_path, exc)
|
|
183
|
+
return None
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives import serialization
|
|
7
|
+
|
|
8
|
+
from ..models.atom import Atom
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _hot_storage_set(self, key: bytes, value: Atom) -> bool:
|
|
12
|
+
"""Store atom in hot storage without exceeding the configured limit."""
|
|
13
|
+
node_logger = self.logger
|
|
14
|
+
projected = self.hot_storage_size + value.size
|
|
15
|
+
hot_limit = self.config["hot_storage_default_limit"]
|
|
16
|
+
if projected > hot_limit:
|
|
17
|
+
node_logger.warning(
|
|
18
|
+
"Hot storage limit reached (%s > %s); skipping atom %s",
|
|
19
|
+
projected,
|
|
20
|
+
hot_limit,
|
|
21
|
+
key.hex(),
|
|
22
|
+
)
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
self.hot_storage[key] = value
|
|
26
|
+
self.hot_storage_size = projected
|
|
27
|
+
node_logger.debug(
|
|
28
|
+
"Stored atom %s in hot storage (bytes=%s, total=%s)",
|
|
29
|
+
key.hex(),
|
|
30
|
+
value.size,
|
|
31
|
+
projected,
|
|
32
|
+
)
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cold_storage_set(self, atom: Atom) -> None:
|
|
37
|
+
"""Persist an atom into the cold storage directory if it already exists."""
|
|
38
|
+
node_logger = self.logger
|
|
39
|
+
atom_id = atom.object_id()
|
|
40
|
+
atom_hex = atom_id.hex()
|
|
41
|
+
if not self.config["cold_storage_path"]:
|
|
42
|
+
node_logger.debug("Cold storage disabled; skipping atom %s", atom_hex)
|
|
43
|
+
return
|
|
44
|
+
atom_bytes = atom.to_bytes()
|
|
45
|
+
projected = self.cold_storage_size + len(atom_bytes)
|
|
46
|
+
cold_limit = self.config["cold_storage_limit"]
|
|
47
|
+
if cold_limit and projected > cold_limit:
|
|
48
|
+
node_logger.warning(
|
|
49
|
+
"Cold storage limit reached (%s > %s); skipping atom %s",
|
|
50
|
+
projected,
|
|
51
|
+
cold_limit,
|
|
52
|
+
atom_hex,
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
directory = Path(self.config["cold_storage_path"])
|
|
56
|
+
if not directory.exists():
|
|
57
|
+
node_logger.warning(
|
|
58
|
+
"Cold storage path %s missing; skipping atom %s",
|
|
59
|
+
directory,
|
|
60
|
+
atom_hex,
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
filename = f"{atom_hex.upper()}.bin"
|
|
64
|
+
file_path = directory / filename
|
|
65
|
+
try:
|
|
66
|
+
file_path.write_bytes(atom_bytes)
|
|
67
|
+
self.cold_storage_size = projected
|
|
68
|
+
node_logger.debug("Persisted atom %s to cold storage", atom_hex)
|
|
69
|
+
except OSError as exc:
|
|
70
|
+
node_logger.error(
|
|
71
|
+
"Failed writing atom %s to cold storage %s: %s",
|
|
72
|
+
atom_hex,
|
|
73
|
+
file_path,
|
|
74
|
+
exc,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _network_set(self, atom: Atom) -> None:
|
|
79
|
+
"""Advertise an atom to the closest known peer so they can fetch it from us."""
|
|
80
|
+
node_logger = self.logger
|
|
81
|
+
atom_id = atom.object_id()
|
|
82
|
+
atom_hex = atom_id.hex()
|
|
83
|
+
try:
|
|
84
|
+
from ...communication.handlers.object_request import (
|
|
85
|
+
ObjectRequest,
|
|
86
|
+
ObjectRequestType,
|
|
87
|
+
)
|
|
88
|
+
from ...communication.models.message import Message, MessageTopic
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
node_logger.warning(
|
|
91
|
+
"Communication module unavailable; cannot advertise atom %s: %s",
|
|
92
|
+
atom_hex,
|
|
93
|
+
exc,
|
|
94
|
+
)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
provider_ip, provider_port = self.incoming_socket.getsockname()[:2]
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
node_logger.warning(
|
|
101
|
+
"Unable to determine provider address for atom %s: %s",
|
|
102
|
+
atom_hex,
|
|
103
|
+
exc,
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
provider_ip_bytes = socket.inet_aton(provider_ip)
|
|
109
|
+
provider_port_bytes = int(provider_port).to_bytes(2, "big", signed=False)
|
|
110
|
+
provider_key_bytes = self.relay_public_key_bytes
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
node_logger.warning("Unable to encode provider info for %s: %s", atom_hex, exc)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
provider_payload = provider_key_bytes + provider_ip_bytes + provider_port_bytes
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
node_logger.warning("Peer lookup failed for atom %s: %s", atom_hex, exc)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
is_self_closest = False
|
|
124
|
+
if closest_peer is None or closest_peer.address is None:
|
|
125
|
+
is_self_closest = True
|
|
126
|
+
else:
|
|
127
|
+
try:
|
|
128
|
+
from ...communication.util import xor_distance
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
node_logger.warning("Failed to import xor_distance for atom %s: %s", atom_hex, exc)
|
|
131
|
+
is_self_closest = True
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
self_distance = xor_distance(atom_id, self.relay_public_key_bytes)
|
|
135
|
+
peer_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
node_logger.warning("Failed computing distance for atom %s: %s", atom_hex, exc)
|
|
138
|
+
is_self_closest = True
|
|
139
|
+
else:
|
|
140
|
+
is_self_closest = self_distance <= peer_distance
|
|
141
|
+
|
|
142
|
+
if is_self_closest:
|
|
143
|
+
node_logger.debug("Self is closest; indexing provider for atom %s", atom_hex)
|
|
144
|
+
self.storage_index[atom_id] = provider_payload
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
target_addr = closest_peer.address
|
|
148
|
+
|
|
149
|
+
obj_req = ObjectRequest(
|
|
150
|
+
type=ObjectRequestType.OBJECT_PUT,
|
|
151
|
+
data=provider_payload,
|
|
152
|
+
atom_id=atom_id,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
message_body = obj_req.to_bytes()
|
|
156
|
+
|
|
157
|
+
message = Message(
|
|
158
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
159
|
+
content=message_body,
|
|
160
|
+
sender=self.relay_public_key,
|
|
161
|
+
)
|
|
162
|
+
message.encrypt(closest_peer.shared_key_bytes)
|
|
163
|
+
try:
|
|
164
|
+
self.outgoing_queue.put((message.to_bytes(), target_addr))
|
|
165
|
+
node_logger.debug(
|
|
166
|
+
"Advertised atom %s to peer at %s:%s",
|
|
167
|
+
atom_hex,
|
|
168
|
+
target_addr[0],
|
|
169
|
+
target_addr[1],
|
|
170
|
+
)
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
node_logger.error(
|
|
173
|
+
"Failed to queue advertisement for atom %s to %s:%s: %s",
|
|
174
|
+
atom_hex,
|
|
175
|
+
target_addr[0],
|
|
176
|
+
target_addr[1],
|
|
177
|
+
exc,
|
|
178
|
+
)
|
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|