astreum 0.2.41__py3-none-any.whl → 0.2.61__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/_communication/message.py +1 -0
- astreum/_communication/peer.py +23 -11
- astreum/_communication/route.py +40 -3
- astreum/_communication/setup.py +72 -4
- astreum/_consensus/account.py +62 -137
- astreum/_consensus/accounts.py +7 -36
- astreum/_consensus/block.py +311 -328
- astreum/_consensus/genesis.py +15 -84
- astreum/_consensus/receipt.py +67 -108
- astreum/_consensus/setup.py +50 -3
- astreum/_consensus/transaction.py +141 -118
- astreum/_consensus/workers/validation.py +5 -2
- astreum/_consensus/workers/verify.py +1 -1
- astreum/_lispeum/expression.py +190 -37
- astreum/_lispeum/high_evaluation.py +232 -173
- astreum/_lispeum/low_evaluation.py +21 -21
- astreum/_lispeum/parser.py +26 -31
- astreum/_node.py +154 -14
- astreum/_storage/__init__.py +7 -5
- astreum/_storage/atom.py +88 -96
- astreum/_storage/patricia.py +51 -16
- astreum/_storage/setup.py +35 -0
- astreum/models/block.py +20 -20
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/METADATA +14 -1
- astreum-0.2.61.dist-info/RECORD +57 -0
- astreum-0.2.41.dist-info/RECORD +0 -53
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
astreum/_node.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, List, Optional
|
|
3
4
|
import uuid
|
|
4
5
|
import threading
|
|
5
6
|
|
|
6
|
-
from
|
|
7
|
-
|
|
7
|
+
from astreum._storage.atom import AtomKind
|
|
8
|
+
|
|
9
|
+
from ._storage import Atom, storage_setup
|
|
10
|
+
from ._lispeum import Env, Expr, Meter, low_eval, parse, tokenize, ParseError
|
|
11
|
+
from .utils.logging import logging_setup
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Node",
|
|
15
|
+
"Env",
|
|
16
|
+
"Expr",
|
|
17
|
+
"Meter",
|
|
18
|
+
"parse",
|
|
19
|
+
"tokenize",
|
|
20
|
+
]
|
|
8
21
|
|
|
9
22
|
def bytes_touched(*vals: bytes) -> int:
|
|
10
23
|
"""For metering: how many bytes were manipulated (max of operands)."""
|
|
@@ -12,9 +25,10 @@ def bytes_touched(*vals: bytes) -> int:
|
|
|
12
25
|
|
|
13
26
|
class Node:
|
|
14
27
|
def __init__(self, config: dict):
|
|
28
|
+
self.logger = logging_setup(config)
|
|
29
|
+
self.logger.info("Starting Astreum Node")
|
|
15
30
|
# Storage Setup
|
|
16
|
-
self
|
|
17
|
-
self.in_memory_storage_lock = threading.RLock()
|
|
31
|
+
storage_setup(self, config=config)
|
|
18
32
|
# Lispeum Setup
|
|
19
33
|
self.environments: Dict[uuid.UUID, Env] = {}
|
|
20
34
|
self.machine_environments_lock = threading.RLock()
|
|
@@ -25,11 +39,13 @@ class Node:
|
|
|
25
39
|
communication_setup(node=self, config=config)
|
|
26
40
|
except Exception:
|
|
27
41
|
pass
|
|
28
|
-
try:
|
|
29
|
-
from astreum._consensus import consensus_setup # type: ignore
|
|
30
|
-
consensus_setup(node=self)
|
|
42
|
+
try:
|
|
43
|
+
from astreum._consensus import consensus_setup # type: ignore
|
|
44
|
+
consensus_setup(node=self, config=config)
|
|
31
45
|
except Exception:
|
|
32
46
|
pass
|
|
47
|
+
|
|
48
|
+
|
|
33
49
|
|
|
34
50
|
# ---- Env helpers ----
|
|
35
51
|
def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
|
|
@@ -49,10 +65,134 @@ class Node:
|
|
|
49
65
|
return True
|
|
50
66
|
|
|
51
67
|
# Storage
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
|
|
69
|
+
atom = self.hot_storage.get(key)
|
|
70
|
+
if atom is not None:
|
|
71
|
+
self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
|
|
72
|
+
return atom
|
|
73
|
+
|
|
74
|
+
def _hot_storage_set(self, key: bytes, value: Atom) -> bool:
|
|
75
|
+
"""Store atom in hot storage without exceeding the configured limit."""
|
|
76
|
+
projected = self.hot_storage_size + value.size
|
|
77
|
+
if projected > self.hot_storage_limit:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
self.hot_storage[key] = value
|
|
81
|
+
self.hot_storage_size = projected
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def _network_get(self, key: bytes) -> Optional[Atom]:
|
|
85
|
+
# locate storage provider
|
|
86
|
+
# query storage provider
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def storage_get(self, key: bytes) -> Optional[Atom]:
|
|
90
|
+
"""Retrieve an Atom by checking local storage first, then the network."""
|
|
91
|
+
atom = self._hot_storage_get(key)
|
|
92
|
+
if atom is not None:
|
|
93
|
+
return atom
|
|
94
|
+
atom = self._cold_storage_get(key)
|
|
95
|
+
if atom is not None:
|
|
96
|
+
return atom
|
|
97
|
+
return self._network_get(key)
|
|
98
|
+
|
|
99
|
+
def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
|
|
100
|
+
"""Read an atom from the cold storage directory if configured."""
|
|
101
|
+
if not self.cold_storage_path:
|
|
102
|
+
return None
|
|
103
|
+
filename = f"{key.hex().upper()}.bin"
|
|
104
|
+
file_path = Path(self.cold_storage_path) / filename
|
|
105
|
+
try:
|
|
106
|
+
data = file_path.read_bytes()
|
|
107
|
+
except FileNotFoundError:
|
|
108
|
+
return None
|
|
109
|
+
except OSError:
|
|
110
|
+
return None
|
|
111
|
+
try:
|
|
112
|
+
return Atom.from_bytes(data)
|
|
113
|
+
except ValueError:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def _cold_storage_set(self, atom: Atom) -> None:
|
|
117
|
+
"""Persist an atom into the cold storage directory if it already exists."""
|
|
118
|
+
if not self.cold_storage_path:
|
|
119
|
+
return
|
|
120
|
+
atom_bytes = atom.to_bytes()
|
|
121
|
+
projected = self.cold_storage_size + len(atom_bytes)
|
|
122
|
+
if self.cold_storage_limit and projected > self.cold_storage_limit:
|
|
123
|
+
return
|
|
124
|
+
directory = Path(self.cold_storage_path)
|
|
125
|
+
if not directory.exists():
|
|
126
|
+
return
|
|
127
|
+
atom_id = atom.object_id()
|
|
128
|
+
filename = f"{atom_id.hex().upper()}.bin"
|
|
129
|
+
file_path = directory / filename
|
|
130
|
+
try:
|
|
131
|
+
file_path.write_bytes(atom_bytes)
|
|
132
|
+
self.cold_storage_size = projected
|
|
133
|
+
except OSError:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
def _network_set(self, atom: Atom) -> None:
|
|
137
|
+
"""Advertise an atom to the closest known peer so they can fetch it from us."""
|
|
138
|
+
try:
|
|
139
|
+
from ._communication.message import Message, MessageTopic
|
|
140
|
+
except Exception:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
atom_id = atom.object_id()
|
|
144
|
+
try:
|
|
145
|
+
closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
|
|
146
|
+
except Exception:
|
|
147
|
+
return
|
|
148
|
+
if closest_peer is None or closest_peer.address is None:
|
|
149
|
+
return
|
|
150
|
+
target_addr = closest_peer.address
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
provider_ip, provider_port = self.incoming_socket.getsockname()[:2]
|
|
154
|
+
except Exception:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
provider_str = f"{provider_ip}:{int(provider_port)}"
|
|
158
|
+
try:
|
|
159
|
+
provider_bytes = provider_str.encode("utf-8")
|
|
160
|
+
except Exception:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
payload = atom_id + provider_bytes
|
|
164
|
+
message = Message(topic=MessageTopic.STORAGE_REQUEST, content=payload)
|
|
165
|
+
self.outgoing_queue.put((message.to_bytes(), target_addr))
|
|
166
|
+
|
|
167
|
+
def get_expr_list_from_storage(self, key: bytes) -> Optional["ListExpr"]:
|
|
168
|
+
atoms = self.get_atom_list_from_storage(root_hash=key)
|
|
169
|
+
if atoms is None:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
expr_list = []
|
|
173
|
+
for atom in atoms:
|
|
174
|
+
match atom.kind:
|
|
175
|
+
case AtomKind.SYMBOL:
|
|
176
|
+
expr_list.append(Expr.Symbol(atom.data))
|
|
177
|
+
case AtomKind.BYTES:
|
|
178
|
+
expr_list.append(Expr.Bytes(atom.data))
|
|
179
|
+
case AtomKind.LIST:
|
|
180
|
+
expr_list.append(Expr.ListExpr([
|
|
181
|
+
Expr.Bytes(atom.data),
|
|
182
|
+
Expr.Symbol("ref")
|
|
183
|
+
]))
|
|
55
184
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
185
|
+
expr_list.reverse()
|
|
186
|
+
return Expr.ListExpr(expr_list)
|
|
187
|
+
|
|
188
|
+
def get_atom_list_from_storage(self, root_hash: bytes) -> Optional[List["Atom"]]:
|
|
189
|
+
next_id: Optional[bytes] = root_hash
|
|
190
|
+
atom_list: List["Atom"] = []
|
|
191
|
+
while next_id:
|
|
192
|
+
elem = self.storage_get(key=next_id)
|
|
193
|
+
if elem:
|
|
194
|
+
atom_list.append(elem)
|
|
195
|
+
next_id = elem.next
|
|
196
|
+
else:
|
|
197
|
+
return None
|
|
198
|
+
return atom_list
|
astreum/_storage/__init__.py
CHANGED
astreum/_storage/atom.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
4
5
|
|
|
5
|
-
from
|
|
6
|
-
from blake3 import blake3
|
|
6
|
+
from blake3 import blake3
|
|
7
7
|
|
|
8
8
|
ZERO32 = b"\x00"*32
|
|
9
9
|
|
|
@@ -13,105 +13,97 @@ def u64_le(n: int) -> bytes:
|
|
|
13
13
|
def hash_bytes(b: bytes) -> bytes:
|
|
14
14
|
return blake3(b).digest()
|
|
15
15
|
|
|
16
|
-
class
|
|
16
|
+
class AtomKind(IntEnum):
|
|
17
|
+
SYMBOL = 0
|
|
18
|
+
BYTES = 1
|
|
19
|
+
LIST = 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Atom:
|
|
17
23
|
data: bytes
|
|
18
24
|
next: bytes
|
|
19
25
|
size: int
|
|
20
26
|
|
|
21
|
-
def __init__(
|
|
22
|
-
self
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return blake3(data_hash + next_hash + u64_le(size)).digest()
|
|
33
|
-
|
|
34
|
-
def data_hash(self) -> bytes:
|
|
35
|
-
return hash_bytes(self.data)
|
|
36
|
-
|
|
37
|
-
def object_id(self) -> bytes:
|
|
38
|
-
return self.object_id_from_parts(self.data_hash(), self.next, self.size)
|
|
39
|
-
|
|
40
|
-
@staticmethod
|
|
41
|
-
def verify_metadata(object_id: bytes, size: int, next_hash: bytes, data_hash: bytes) -> bool:
|
|
42
|
-
return object_id == blake3(data_hash + next_hash + u64_le(size)).digest()
|
|
43
|
-
|
|
44
|
-
def to_bytes(self) -> bytes:
|
|
45
|
-
return self.next + self.data
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def from_bytes(buf: bytes) -> "Atom":
|
|
49
|
-
if len(buf) < len(ZERO32):
|
|
50
|
-
raise ValueError("buffer too short for Atom header")
|
|
51
|
-
next_hash = buf[:len(ZERO32)]
|
|
52
|
-
data = buf[len(ZERO32):]
|
|
53
|
-
return Atom(data=data, next=next_hash, size=len(data))
|
|
54
|
-
|
|
55
|
-
def expr_to_atoms(e: Expr) -> Tuple[bytes, List[Atom]]:
|
|
56
|
-
def symbol(value: str) -> Tuple[bytes, List[Atom]]:
|
|
57
|
-
val = value.encode("utf-8")
|
|
58
|
-
val_atom = Atom.from_data(data=val)
|
|
59
|
-
typ_atom = Atom.from_data(b"symbol", val_atom.object_id())
|
|
60
|
-
return typ_atom.object_id(), [val_atom, typ_atom]
|
|
61
|
-
|
|
62
|
-
def bytes(data: bytes) -> Tuple[bytes, List[Atom]]:
|
|
63
|
-
val_atom = Atom.from_data(data=data)
|
|
64
|
-
typ_atom = Atom.from_data(b"byte", val_atom.object_id())
|
|
65
|
-
return typ_atom.object_id(), [val_atom, typ_atom]
|
|
66
|
-
|
|
67
|
-
def err(topic: str, origin: Optional[Expr]) -> Tuple[bytes, List[Atom]]:
|
|
68
|
-
topic_bytes = topic.encode("utf-8")
|
|
69
|
-
topic_atom = Atom.from_data(data=topic_bytes)
|
|
70
|
-
typ_atom = Atom.from_data(data=b"error", next_hash=topic_atom.object_id())
|
|
71
|
-
return typ_atom.object_id(), [topic_atom, typ_atom]
|
|
72
|
-
|
|
73
|
-
def lst(items: List[Expr]) -> Tuple[bytes, List[Atom]]:
|
|
74
|
-
acc: List[Atom] = []
|
|
75
|
-
child_hashes: List[bytes] = []
|
|
76
|
-
for it in items:
|
|
77
|
-
h, atoms = expr_to_atoms(it)
|
|
78
|
-
acc.extend(atoms)
|
|
79
|
-
child_hashes.append(h)
|
|
80
|
-
next_hash = ZERO32
|
|
81
|
-
elem_atoms: List[Atom] = []
|
|
82
|
-
for h in reversed(child_hashes):
|
|
83
|
-
a = Atom.from_data(h, next_hash)
|
|
84
|
-
next_hash = a.object_id()
|
|
85
|
-
elem_atoms.append(a)
|
|
86
|
-
elem_atoms.reverse()
|
|
87
|
-
head = next_hash
|
|
88
|
-
val_atom = Atom.from_data(data=u64_le(len(items)), next_hash=head)
|
|
89
|
-
typ_atom = Atom.from_data(data=b"list", next_hash=val_atom.object_id())
|
|
90
|
-
return typ_atom.object_id(), acc + elem_atoms + [val_atom, typ_atom]
|
|
91
|
-
|
|
92
|
-
if isinstance(e, Expr.Symbol):
|
|
93
|
-
return symbol(e.value)
|
|
94
|
-
if isinstance(e, Expr.Bytes):
|
|
95
|
-
return bytes(e.value)
|
|
96
|
-
if isinstance(e, Expr.Error):
|
|
97
|
-
return err(e.topic, e.origin)
|
|
98
|
-
if isinstance(e, Expr.ListExpr):
|
|
99
|
-
return lst(e.elements)
|
|
100
|
-
raise TypeError("unknown Expr variant")
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
data: bytes,
|
|
30
|
+
next: bytes = ZERO32,
|
|
31
|
+
size: Optional[int] = None,
|
|
32
|
+
kind: AtomKind = AtomKind.BYTES,
|
|
33
|
+
):
|
|
34
|
+
self.data = data
|
|
35
|
+
self.next = next
|
|
36
|
+
self.size = len(data) if size is None else size
|
|
37
|
+
self.kind = kind
|
|
101
38
|
|
|
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
|
+
def generate_id(self) -> bytes:
|
|
48
|
+
"""Compute the object id using this atom's metadata."""
|
|
49
|
+
kind_bytes = int(self.kind).to_bytes(1, "little", signed=False)
|
|
50
|
+
return blake3(
|
|
51
|
+
kind_bytes + self.data_hash() + self.next + u64_le(self.size)
|
|
52
|
+
).digest()
|
|
102
53
|
|
|
103
|
-
def
|
|
104
|
-
|
|
54
|
+
def data_hash(self) -> bytes:
|
|
55
|
+
return hash_bytes(self.data)
|
|
105
56
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
next_hash = ZERO32
|
|
109
|
-
atoms: List[Atom] = []
|
|
57
|
+
def object_id(self) -> bytes:
|
|
58
|
+
return self.generate_id()
|
|
110
59
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
60
|
+
@staticmethod
|
|
61
|
+
def verify_metadata(
|
|
62
|
+
object_id: bytes,
|
|
63
|
+
size: int,
|
|
64
|
+
next_hash: bytes,
|
|
65
|
+
data_hash: bytes,
|
|
66
|
+
kind: AtomKind,
|
|
67
|
+
) -> bool:
|
|
68
|
+
kind_bytes = int(kind).to_bytes(1, "little", signed=False)
|
|
69
|
+
expected = blake3(kind_bytes + data_hash + next_hash + u64_le(size)).digest()
|
|
70
|
+
return object_id == expected
|
|
71
|
+
|
|
72
|
+
def to_bytes(self) -> bytes:
|
|
73
|
+
"""Serialize as next-hash + kind byte + payload."""
|
|
74
|
+
kind_byte = int(self.kind).to_bytes(1, "little", signed=False)
|
|
75
|
+
return self.next + kind_byte + self.data
|
|
115
76
|
|
|
116
|
-
|
|
117
|
-
|
|
77
|
+
@staticmethod
|
|
78
|
+
def from_bytes(buf: bytes) -> "Atom":
|
|
79
|
+
header_len = len(ZERO32)
|
|
80
|
+
if len(buf) < header_len + 1:
|
|
81
|
+
raise ValueError("buffer too short for Atom header")
|
|
82
|
+
next_hash = buf[:header_len]
|
|
83
|
+
kind_value = buf[header_len]
|
|
84
|
+
data = buf[header_len + 1 :]
|
|
85
|
+
try:
|
|
86
|
+
kind = AtomKind(kind_value)
|
|
87
|
+
except ValueError as exc:
|
|
88
|
+
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
|
astreum/_storage/patricia.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import blake3
|
|
2
2
|
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
|
|
3
3
|
|
|
4
|
-
from .atom import Atom, ZERO32
|
|
4
|
+
from .atom import Atom, AtomKind, ZERO32
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
7
|
from .._node import Node
|
|
@@ -43,10 +43,11 @@ class PatriciaNode:
|
|
|
43
43
|
|
|
44
44
|
def to_atoms(self) -> Tuple[bytes, List[Atom]]:
|
|
45
45
|
"""
|
|
46
|
-
Materialise this node
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
Materialise this node with the canonical atom layout used by the
|
|
47
|
+
storage layer: a leading SYMBOL atom with payload ``b"radix"`` whose
|
|
48
|
+
``next`` pointer links to four BYTES atoms containing, in order:
|
|
49
|
+
key (len byte + key payload), child_0 hash, child_1 hash, value bytes.
|
|
50
|
+
Returns the top atom hash and the emitted atoms.
|
|
50
51
|
"""
|
|
51
52
|
if self.key_len > 255:
|
|
52
53
|
raise ValueError("Patricia key length > 255 bits cannot be encoded in a single atom field")
|
|
@@ -58,16 +59,23 @@ class PatriciaNode:
|
|
|
58
59
|
self.value or b"",
|
|
59
60
|
]
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
data_atoms: List[Atom] = []
|
|
62
63
|
next_hash = ZERO32
|
|
63
64
|
for payload in reversed(entries):
|
|
64
|
-
atom = Atom.from_data(data=payload, next_hash=next_hash)
|
|
65
|
-
|
|
65
|
+
atom = Atom.from_data(data=payload, next_hash=next_hash, kind=AtomKind.BYTES)
|
|
66
|
+
data_atoms.append(atom)
|
|
66
67
|
next_hash = atom.object_id()
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
data_atoms.reverse()
|
|
70
|
+
|
|
71
|
+
type_atom = Atom.from_data(
|
|
72
|
+
data=b"radix",
|
|
73
|
+
next_hash=next_hash,
|
|
74
|
+
kind=AtomKind.SYMBOL,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
atoms = data_atoms + [type_atom]
|
|
78
|
+
return type_atom.object_id(), atoms
|
|
71
79
|
|
|
72
80
|
@classmethod
|
|
73
81
|
def from_atoms(
|
|
@@ -82,19 +90,46 @@ class PatriciaNode:
|
|
|
82
90
|
if head_hash == ZERO32:
|
|
83
91
|
raise ValueError("empty atom chain for Patricia node")
|
|
84
92
|
|
|
93
|
+
def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
|
|
94
|
+
kind_value = getattr(atom, "kind", None)
|
|
95
|
+
if isinstance(kind_value, AtomKind):
|
|
96
|
+
return kind_value
|
|
97
|
+
if isinstance(kind_value, int):
|
|
98
|
+
try:
|
|
99
|
+
return AtomKind(kind_value)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return None
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def _require_atom(atom_hash: Optional[bytes], context: str) -> Atom:
|
|
105
|
+
if not atom_hash or atom_hash == ZERO32:
|
|
106
|
+
raise ValueError(f"missing {context}")
|
|
107
|
+
atom = node.storage_get(atom_hash)
|
|
108
|
+
if atom is None:
|
|
109
|
+
raise ValueError(f"missing {context}")
|
|
110
|
+
return atom
|
|
111
|
+
|
|
112
|
+
type_atom = _require_atom(head_hash, "Patricia type atom")
|
|
113
|
+
if _atom_kind(type_atom) is not AtomKind.SYMBOL:
|
|
114
|
+
raise ValueError("malformed Patricia node (type atom kind)")
|
|
115
|
+
if type_atom.data != b"radix":
|
|
116
|
+
raise ValueError("not a Patricia node (type mismatch)")
|
|
117
|
+
|
|
85
118
|
entries: List[bytes] = []
|
|
86
|
-
current =
|
|
119
|
+
current = type_atom.next
|
|
87
120
|
hops = 0
|
|
88
121
|
|
|
89
|
-
while current != ZERO32 and hops < 4:
|
|
90
|
-
atom = node.
|
|
122
|
+
while current and current != ZERO32 and hops < 4:
|
|
123
|
+
atom = node.storage_get(current)
|
|
91
124
|
if atom is None:
|
|
92
125
|
raise ValueError("missing atom while decoding Patricia node")
|
|
126
|
+
if _atom_kind(atom) is not AtomKind.BYTES:
|
|
127
|
+
raise ValueError("Patricia node detail atoms must be bytes")
|
|
93
128
|
entries.append(atom.data)
|
|
94
129
|
current = atom.next
|
|
95
130
|
hops += 1
|
|
96
131
|
|
|
97
|
-
if current != ZERO32:
|
|
132
|
+
if current and current != ZERO32:
|
|
98
133
|
raise ValueError("too many fields while decoding Patricia node")
|
|
99
134
|
|
|
100
135
|
if len(entries) != 4:
|
|
@@ -163,7 +198,7 @@ class PatriciaTrie:
|
|
|
163
198
|
if cached is not None:
|
|
164
199
|
return cached
|
|
165
200
|
|
|
166
|
-
if storage_node.
|
|
201
|
+
if storage_node.storage_get(h) is None:
|
|
167
202
|
return None
|
|
168
203
|
|
|
169
204
|
pat_node = PatriciaNode.from_atoms(storage_node, h)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def storage_setup(node: Any, config: dict) -> None:
|
|
8
|
+
"""Initialize hot/cold storage helpers on the node."""
|
|
9
|
+
|
|
10
|
+
node.hot_storage = {}
|
|
11
|
+
node.hot_storage_hits = {}
|
|
12
|
+
node.storage_index = {}
|
|
13
|
+
node.hot_storage_size = 0
|
|
14
|
+
hot_storage_default_limit = 1 << 30 # 1 GiB
|
|
15
|
+
hot_storage_limit_value = config.get("hot_storage_limit", hot_storage_default_limit)
|
|
16
|
+
try:
|
|
17
|
+
node.hot_storage_limit = int(hot_storage_limit_value)
|
|
18
|
+
except (TypeError, ValueError):
|
|
19
|
+
node.hot_storage_limit = hot_storage_default_limit
|
|
20
|
+
|
|
21
|
+
node.cold_storage_size = 0
|
|
22
|
+
cold_storage_default_limit = 10 << 30 # 10 GiB
|
|
23
|
+
cold_storage_limit_value = config.get("cold_storage_limit", cold_storage_default_limit)
|
|
24
|
+
try:
|
|
25
|
+
node.cold_storage_limit = int(cold_storage_limit_value)
|
|
26
|
+
except (TypeError, ValueError):
|
|
27
|
+
node.cold_storage_limit = cold_storage_default_limit
|
|
28
|
+
|
|
29
|
+
cold_storage_path = config.get("cold_storage_path")
|
|
30
|
+
if cold_storage_path:
|
|
31
|
+
try:
|
|
32
|
+
Path(cold_storage_path).mkdir(parents=True, exist_ok=True)
|
|
33
|
+
except OSError:
|
|
34
|
+
cold_storage_path = None
|
|
35
|
+
node.cold_storage_path = cold_storage_path
|
astreum/models/block.py
CHANGED
|
@@ -103,9 +103,9 @@ class Block:
|
|
|
103
103
|
stake_root = stake_trie.root_hash
|
|
104
104
|
|
|
105
105
|
# 2. three Account bodies
|
|
106
|
-
validator_acct = Account.create(balance=0, data=b"",
|
|
107
|
-
treasury_acct = Account.create(balance=1, data=stake_root,
|
|
108
|
-
burn_acct = Account.create(balance=0, data=b"",
|
|
106
|
+
validator_acct = Account.create(balance=0, data=b"", counter=0)
|
|
107
|
+
treasury_acct = Account.create(balance=1, data=stake_root, counter=0)
|
|
108
|
+
burn_acct = Account.create(balance=0, data=b"", counter=0)
|
|
109
109
|
|
|
110
110
|
# 3. global Accounts structure
|
|
111
111
|
accts = Accounts()
|
|
@@ -190,7 +190,7 @@ class Block:
|
|
|
190
190
|
|
|
191
191
|
def _credit(addr: bytes, amt: int):
|
|
192
192
|
acc = blk.accounts.get_account(addr) or Account.create(0, b"", 0)
|
|
193
|
-
blk.accounts.set_account(addr, Account.create(acc.balance
|
|
193
|
+
blk.accounts.set_account(addr, Account.create(acc.balance + amt, acc.data, acc.counter))
|
|
194
194
|
|
|
195
195
|
if burn_amt:
|
|
196
196
|
_credit(BURN, burn_amt)
|
|
@@ -280,17 +280,17 @@ class Block:
|
|
|
280
280
|
|
|
281
281
|
sender_acct = self.accounts.get_account(sender_pk)
|
|
282
282
|
if (sender_acct is None
|
|
283
|
-
or sender_acct.
|
|
284
|
-
or sender_acct.balance
|
|
283
|
+
or sender_acct.counter != nonce
|
|
284
|
+
or sender_acct.balance < amount + fee):
|
|
285
285
|
raise ValueError("invalid or unaffordable transaction")
|
|
286
286
|
|
|
287
287
|
# --- debit sender --------------------------------------------------
|
|
288
288
|
self.accounts.set_account(
|
|
289
289
|
sender_pk,
|
|
290
290
|
Account.create(
|
|
291
|
-
balance=sender_acct.balance
|
|
292
|
-
data=sender_acct.data
|
|
293
|
-
|
|
291
|
+
balance=sender_acct.balance - amount - fee,
|
|
292
|
+
data=sender_acct.data,
|
|
293
|
+
counter=sender_acct.counter + 1,
|
|
294
294
|
)
|
|
295
295
|
)
|
|
296
296
|
|
|
@@ -298,14 +298,14 @@ class Block:
|
|
|
298
298
|
if recip_pk == TREASURY:
|
|
299
299
|
treasury = self.accounts.get_account(TREASURY)
|
|
300
300
|
|
|
301
|
-
trie = PatriciaTrie(node_get=None, root_hash=treasury.data
|
|
301
|
+
trie = PatriciaTrie(node_get=None, root_hash=treasury.data)
|
|
302
302
|
stake_bytes = trie.get(sender_pk) or b""
|
|
303
303
|
current_stake = int.from_bytes(stake_bytes, "big") if stake_bytes else 0
|
|
304
304
|
|
|
305
305
|
if amount > 0:
|
|
306
306
|
# stake **deposit**
|
|
307
307
|
trie.put(sender_pk, (current_stake + amount).to_bytes(32, "big"))
|
|
308
|
-
new_treas_bal = treasury.balance
|
|
308
|
+
new_treas_bal = treasury.balance + amount
|
|
309
309
|
else:
|
|
310
310
|
# stake **withdrawal**
|
|
311
311
|
if current_stake == 0:
|
|
@@ -315,13 +315,13 @@ class Block:
|
|
|
315
315
|
self.accounts.set_account(
|
|
316
316
|
sender_pk,
|
|
317
317
|
Account.create(
|
|
318
|
-
balance=sender_after.balance
|
|
319
|
-
data=sender_after.data
|
|
320
|
-
|
|
318
|
+
balance=sender_after.balance + current_stake,
|
|
319
|
+
data=sender_after.data,
|
|
320
|
+
counter=sender_after.counter,
|
|
321
321
|
)
|
|
322
322
|
)
|
|
323
323
|
trie.delete(sender_pk)
|
|
324
|
-
new_treas_bal = treasury.balance
|
|
324
|
+
new_treas_bal = treasury.balance # treasury balance unchanged
|
|
325
325
|
|
|
326
326
|
# write back treasury with new trie root
|
|
327
327
|
self.accounts.set_account(
|
|
@@ -329,7 +329,7 @@ class Block:
|
|
|
329
329
|
Account.create(
|
|
330
330
|
balance=new_treas_bal,
|
|
331
331
|
data=trie.root_hash,
|
|
332
|
-
|
|
332
|
+
counter=treasury.counter,
|
|
333
333
|
)
|
|
334
334
|
)
|
|
335
335
|
|
|
@@ -338,9 +338,9 @@ class Block:
|
|
|
338
338
|
self.accounts.set_account(
|
|
339
339
|
recip_pk,
|
|
340
340
|
Account.create(
|
|
341
|
-
balance=recip_acct.balance
|
|
342
|
-
data=recip_acct.data
|
|
343
|
-
|
|
341
|
+
balance=recip_acct.balance + amount,
|
|
342
|
+
data=recip_acct.data,
|
|
343
|
+
counter=recip_acct.counter,
|
|
344
344
|
)
|
|
345
345
|
)
|
|
346
346
|
|
|
@@ -417,7 +417,7 @@ class Block:
|
|
|
417
417
|
v_acct = dummy.accounts.get_account(self.validator_pk) or Account.create(0,b"",0)
|
|
418
418
|
dummy.accounts.set_account(
|
|
419
419
|
self.validator_pk,
|
|
420
|
-
Account.create(v_acct.balance
|
|
420
|
+
Account.create(v_acct.balance+rew, v_acct.data, v_acct.counter)
|
|
421
421
|
)
|
|
422
422
|
|
|
423
423
|
if dummy.accounts.root_hash != self.accounts_hash:
|