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/_communication/peer.py
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
2
|
+
from cryptography.hazmat.primitives import serialization
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
class Peer:
|
|
7
|
+
shared_key: bytes
|
|
8
|
+
timestamp: datetime
|
|
9
|
+
latest_block: bytes
|
|
10
|
+
address: Optional[Tuple[str, int]]
|
|
11
|
+
public_key: X25519PublicKey
|
|
12
|
+
public_key_bytes: bytes
|
|
13
|
+
|
|
14
|
+
def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
|
|
15
|
+
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
16
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
17
|
+
self.latest_block = b""
|
|
18
|
+
self.address = None
|
|
19
|
+
self.public_key = peer_pub_key
|
|
20
|
+
self.public_key_bytes = peer_pub_key.public_bytes(
|
|
21
|
+
encoding=serialization.Encoding.Raw,
|
|
22
|
+
format=serialization.PublicFormat.Raw,
|
|
23
|
+
)
|
astreum/_communication/route.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from typing import Dict, List, Union
|
|
1
|
+
from typing import Dict, List, Optional, Union
|
|
2
2
|
from cryptography.hazmat.primitives import serialization
|
|
3
3
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
4
|
+
from .peer import Peer
|
|
4
5
|
|
|
5
6
|
PeerKey = Union[X25519PublicKey, bytes, bytearray]
|
|
6
7
|
|
|
@@ -15,7 +16,7 @@ class Route:
|
|
|
15
16
|
self.buckets: Dict[int, List[bytes]] = {
|
|
16
17
|
i: [] for i in range(len(self.relay_public_key_bytes) * 8)
|
|
17
18
|
}
|
|
18
|
-
self.peers = {}
|
|
19
|
+
self.peers: Dict[bytes, Peer] = {}
|
|
19
20
|
|
|
20
21
|
@staticmethod
|
|
21
22
|
def _matching_leading_bits(a: bytes, b: bytes) -> int:
|
|
@@ -38,13 +39,21 @@ class Route:
|
|
|
38
39
|
return key_bytes
|
|
39
40
|
raise TypeError("peer_public_key must be raw bytes or X25519PublicKey")
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _xor_distance(a: bytes, b: bytes) -> int:
|
|
44
|
+
if len(a) != len(b):
|
|
45
|
+
raise ValueError("xor distance requires equal-length operands")
|
|
46
|
+
return int.from_bytes(bytes(x ^ y for x, y in zip(a, b)), "big", signed=False)
|
|
47
|
+
|
|
48
|
+
def add_peer(self, peer_public_key: PeerKey, peer: Optional[Peer] = None):
|
|
42
49
|
peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
|
|
43
50
|
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
44
51
|
if len(self.buckets[bucket_idx]) < self.bucket_size:
|
|
45
52
|
bucket = self.buckets[bucket_idx]
|
|
46
53
|
if peer_public_key_bytes not in bucket:
|
|
47
54
|
bucket.append(peer_public_key_bytes)
|
|
55
|
+
if peer is not None:
|
|
56
|
+
self.peers[peer_public_key_bytes] = peer
|
|
48
57
|
|
|
49
58
|
def remove_peer(self, peer_public_key: PeerKey):
|
|
50
59
|
peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
|
|
@@ -56,3 +65,31 @@ class Route:
|
|
|
56
65
|
bucket.remove(peer_public_key_bytes)
|
|
57
66
|
except ValueError:
|
|
58
67
|
pass
|
|
68
|
+
self.peers.pop(peer_public_key_bytes, None)
|
|
69
|
+
|
|
70
|
+
def closest_peer_for_hash(self, target_hash: bytes) -> Optional[Peer]:
|
|
71
|
+
"""Return the peer with the minimal XOR distance to ``target_hash``."""
|
|
72
|
+
if not isinstance(target_hash, (bytes, bytearray)):
|
|
73
|
+
raise TypeError("target_hash must be bytes-like")
|
|
74
|
+
|
|
75
|
+
target = bytes(target_hash)
|
|
76
|
+
if len(target) != len(self.relay_public_key_bytes):
|
|
77
|
+
raise ValueError("target_hash must match peer key length (32 bytes)")
|
|
78
|
+
|
|
79
|
+
closest_key: Optional[bytes] = None
|
|
80
|
+
closest_distance: Optional[int] = None
|
|
81
|
+
|
|
82
|
+
for bucket in self.buckets.values():
|
|
83
|
+
for peer_key in bucket:
|
|
84
|
+
try:
|
|
85
|
+
distance = self._xor_distance(target, peer_key)
|
|
86
|
+
except ValueError:
|
|
87
|
+
continue
|
|
88
|
+
if closest_distance is None or distance < closest_distance:
|
|
89
|
+
closest_distance = distance
|
|
90
|
+
closest_key = peer_key
|
|
91
|
+
|
|
92
|
+
if closest_key is None:
|
|
93
|
+
return None
|
|
94
|
+
peer = self.peers.get(closest_key)
|
|
95
|
+
return peer
|
astreum/_communication/setup.py
CHANGED
|
@@ -95,9 +95,10 @@ def process_incoming_messages(node: "Node") -> None:
|
|
|
95
95
|
peer = Peer(node.relay_secret_key, sender_key)
|
|
96
96
|
except Exception:
|
|
97
97
|
continue
|
|
98
|
-
|
|
98
|
+
peer.address = address_key
|
|
99
|
+
|
|
99
100
|
node.peers[sender_public_key_bytes] = peer
|
|
100
|
-
node.peer_route.add_peer(sender_public_key_bytes)
|
|
101
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
101
102
|
|
|
102
103
|
response = Message(handshake=True, sender=node.relay_public_key)
|
|
103
104
|
node.outgoing_queue.put((response.to_bytes(), address_key))
|
|
@@ -105,17 +106,25 @@ def process_incoming_messages(node: "Node") -> None:
|
|
|
105
106
|
|
|
106
107
|
elif old_key_bytes == sender_public_key_bytes:
|
|
107
108
|
# existing mapping with same key -> nothing to change
|
|
108
|
-
|
|
109
|
+
peer = node.peers.get(sender_public_key_bytes)
|
|
110
|
+
if peer is not None:
|
|
111
|
+
peer.address = address_key
|
|
109
112
|
|
|
110
113
|
else:
|
|
111
114
|
# address reused with a different key -> replace peer
|
|
112
115
|
node.peers.pop(old_key_bytes, None)
|
|
116
|
+
try:
|
|
117
|
+
node.peer_route.remove_peer(old_key_bytes)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
113
120
|
try:
|
|
114
121
|
peer = Peer(node.relay_secret_key, sender_key)
|
|
115
122
|
except Exception:
|
|
116
123
|
continue
|
|
117
|
-
|
|
124
|
+
peer.address = address_key
|
|
125
|
+
|
|
118
126
|
node.peers[sender_public_key_bytes] = peer
|
|
127
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
119
128
|
|
|
120
129
|
match message.topic:
|
|
121
130
|
case MessageTopic.PING:
|
|
@@ -164,6 +173,65 @@ def process_incoming_messages(node: "Node") -> None:
|
|
|
164
173
|
if node.validation_secret_key is None:
|
|
165
174
|
continue
|
|
166
175
|
node._validation_transaction_queue.put(message.content)
|
|
176
|
+
|
|
177
|
+
case MessageTopic.STORAGE_REQUEST:
|
|
178
|
+
payload = message.content
|
|
179
|
+
if len(payload) < 32:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
atom_id = payload[:32]
|
|
183
|
+
provider_bytes = payload[32:]
|
|
184
|
+
if not provider_bytes:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
provider_str = provider_bytes.decode("utf-8")
|
|
189
|
+
except UnicodeDecodeError:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
host, port = addr[0], int(addr[1])
|
|
194
|
+
except Exception:
|
|
195
|
+
continue
|
|
196
|
+
address_key = (host, port)
|
|
197
|
+
sender_key_bytes = node.addresses.get(address_key)
|
|
198
|
+
if sender_key_bytes is None:
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
local_key_bytes = node.relay_public_key.public_bytes(
|
|
203
|
+
encoding=serialization.Encoding.Raw,
|
|
204
|
+
format=serialization.PublicFormat.Raw,
|
|
205
|
+
)
|
|
206
|
+
except Exception:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
def xor_distance(target: bytes, key: bytes) -> int:
|
|
210
|
+
return int.from_bytes(
|
|
211
|
+
bytes(a ^ b for a, b in zip(target, key)),
|
|
212
|
+
byteorder="big",
|
|
213
|
+
signed=False,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self_distance = xor_distance(atom_id, local_key_bytes)
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
closest_peer = node.peer_route.closest_peer_for_hash(atom_id)
|
|
220
|
+
except Exception:
|
|
221
|
+
closest_peer = None
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
closest_peer is not None
|
|
225
|
+
and closest_peer.public_key_bytes != sender_key_bytes
|
|
226
|
+
):
|
|
227
|
+
closest_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
|
|
228
|
+
if closest_distance < self_distance:
|
|
229
|
+
target_addr = closest_peer.address
|
|
230
|
+
if target_addr is not None and target_addr != addr:
|
|
231
|
+
node.outgoing_queue.put((message.to_bytes(), target_addr))
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
node.storage_index[atom_id] = provider_str.strip()
|
|
167
235
|
|
|
168
236
|
case _:
|
|
169
237
|
continue
|
astreum/_consensus/account.py
CHANGED
|
@@ -1,170 +1,95 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from .._storage.atom import Atom, ZERO32
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _int_to_be_bytes(value: int) -> bytes:
|
|
10
|
-
value = int(value)
|
|
11
|
-
if value < 0:
|
|
12
|
-
raise ValueError("account integers must be non-negative")
|
|
13
|
-
if value == 0:
|
|
14
|
-
return b"\x00"
|
|
15
|
-
size = (value.bit_length() + 7) // 8
|
|
16
|
-
return value.to_bytes(size, "big")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _be_bytes_to_int(data: Optional[bytes]) -> int:
|
|
20
|
-
if not data:
|
|
21
|
-
return 0
|
|
22
|
-
return int.from_bytes(data, "big")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
26
|
-
next_hash = ZERO32
|
|
27
|
-
elements: List[Atom] = []
|
|
28
|
-
for child_id in reversed(child_ids):
|
|
29
|
-
elem = Atom.from_data(data=child_id, next_hash=next_hash)
|
|
30
|
-
next_hash = elem.object_id()
|
|
31
|
-
elements.append(elem)
|
|
32
|
-
elements.reverse()
|
|
33
|
-
value_atom = Atom.from_data(
|
|
34
|
-
data=len(child_ids).to_bytes(8, "little"),
|
|
35
|
-
next_hash=next_hash,
|
|
36
|
-
)
|
|
37
|
-
type_atom = Atom.from_data(data=b"list", next_hash=value_atom.object_id())
|
|
38
|
-
atoms = elements + [value_atom, type_atom]
|
|
39
|
-
return type_atom.object_id(), atoms
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _resolve_storage_get(source: Any) -> Callable[[bytes], Optional[Atom]]:
|
|
43
|
-
if callable(source):
|
|
44
|
-
return source
|
|
45
|
-
getter = getattr(source, "_local_get", None)
|
|
46
|
-
if callable(getter):
|
|
47
|
-
return getter
|
|
48
|
-
raise TypeError("Account.from_atom needs a callable storage getter or node with '_local_get'")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _read_list_entries(
|
|
52
|
-
storage_get: Callable[[bytes], Optional[Atom]],
|
|
53
|
-
start: bytes,
|
|
54
|
-
) -> List[bytes]:
|
|
55
|
-
entries: List[bytes] = []
|
|
56
|
-
current = start if start and start != ZERO32 else b""
|
|
57
|
-
while current:
|
|
58
|
-
elem = storage_get(current)
|
|
59
|
-
if elem is None:
|
|
60
|
-
break
|
|
61
|
-
entries.append(elem.data)
|
|
62
|
-
nxt = elem.next
|
|
63
|
-
current = nxt if nxt and nxt != ZERO32 else b""
|
|
64
|
-
return entries
|
|
7
|
+
from .._storage.patricia import PatriciaTrie
|
|
8
|
+
from ..utils.integer import bytes_to_int, int_to_bytes
|
|
65
9
|
|
|
66
10
|
|
|
67
11
|
@dataclass
|
|
68
12
|
class Account:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
13
|
+
balance: int
|
|
14
|
+
code: bytes
|
|
15
|
+
counter: int
|
|
16
|
+
data_hash: bytes
|
|
17
|
+
data: PatriciaTrie
|
|
72
18
|
hash: bytes = ZERO32
|
|
19
|
+
body_hash: bytes = ZERO32
|
|
73
20
|
atoms: List[Atom] = field(default_factory=list)
|
|
74
21
|
|
|
75
|
-
@staticmethod
|
|
76
|
-
def _encode(balance: int, data: bytes, nonce: int) -> Tuple[bytes, List[Atom]]:
|
|
77
|
-
balance_atom = Atom.from_data(data=_int_to_be_bytes(balance))
|
|
78
|
-
data_atom = Atom.from_data(data=bytes(data))
|
|
79
|
-
nonce_atom = Atom.from_data(data=_int_to_be_bytes(nonce))
|
|
80
|
-
|
|
81
|
-
field_atoms = [balance_atom, data_atom, nonce_atom]
|
|
82
|
-
field_ids = [a.object_id() for a in field_atoms]
|
|
83
|
-
|
|
84
|
-
body_id, body_atoms = _make_list(field_ids)
|
|
85
|
-
type_atom = Atom.from_data(data=b"account", next_hash=body_id)
|
|
86
|
-
top_id, top_atoms = _make_list([type_atom.object_id(), body_id])
|
|
87
|
-
|
|
88
|
-
atoms = field_atoms + body_atoms + [type_atom] + top_atoms
|
|
89
|
-
return top_id, atoms
|
|
90
|
-
|
|
91
22
|
@classmethod
|
|
92
|
-
def create(cls, balance: int,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
atoms=atoms,
|
|
23
|
+
def create(cls, balance: int = 0, data_hash: bytes = ZERO32, code: bytes = ZERO32, counter: int = 0) -> "Account":
|
|
24
|
+
account = cls(
|
|
25
|
+
balance=int(balance),
|
|
26
|
+
code=bytes(code),
|
|
27
|
+
counter=int(counter),
|
|
28
|
+
data_hash=bytes(data_hash),
|
|
29
|
+
data=PatriciaTrie(root_hash=bytes(data_hash)),
|
|
100
30
|
)
|
|
31
|
+
account.to_atom()
|
|
32
|
+
return account
|
|
101
33
|
|
|
102
34
|
@classmethod
|
|
103
|
-
def from_atom(cls,
|
|
104
|
-
storage_get =
|
|
105
|
-
|
|
106
|
-
outer_list = storage_get(account_id)
|
|
107
|
-
if outer_list is None or outer_list.data != b"list":
|
|
108
|
-
raise ValueError("not an account (outer list missing)")
|
|
109
|
-
|
|
110
|
-
outer_value = storage_get(outer_list.next)
|
|
111
|
-
if outer_value is None:
|
|
112
|
-
raise ValueError("malformed account (outer value missing)")
|
|
35
|
+
def from_atom(cls, node: Any, account_id: bytes) -> "Account":
|
|
36
|
+
storage_get = node.storage_get
|
|
113
37
|
|
|
114
|
-
|
|
115
|
-
if len(entries) < 2:
|
|
116
|
-
raise ValueError("malformed account (type/body missing)")
|
|
117
|
-
|
|
118
|
-
type_atom_id, body_id = entries[0], entries[1]
|
|
119
|
-
type_atom = storage_get(type_atom_id)
|
|
38
|
+
type_atom = storage_get(account_id)
|
|
120
39
|
if type_atom is None or type_atom.data != b"account":
|
|
121
40
|
raise ValueError("not an account (type mismatch)")
|
|
122
41
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
42
|
+
def _read_atom(atom_id: Optional[bytes]) -> Optional[Atom]:
|
|
43
|
+
if not atom_id or atom_id == ZERO32:
|
|
44
|
+
return None
|
|
45
|
+
return storage_get(atom_id)
|
|
126
46
|
|
|
127
|
-
|
|
128
|
-
if
|
|
129
|
-
raise ValueError("malformed account
|
|
47
|
+
balance_atom = _read_atom(type_atom.next)
|
|
48
|
+
if balance_atom is None:
|
|
49
|
+
raise ValueError("malformed account (balance missing)")
|
|
130
50
|
|
|
131
|
-
|
|
132
|
-
if
|
|
133
|
-
|
|
51
|
+
code_atom = _read_atom(balance_atom.next)
|
|
52
|
+
if code_atom is None:
|
|
53
|
+
raise ValueError("malformed account (code missing)")
|
|
134
54
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
atom = storage_get(field_id)
|
|
139
|
-
return atom.data if atom is not None else b""
|
|
55
|
+
counter_atom = _read_atom(code_atom.next)
|
|
56
|
+
if counter_atom is None:
|
|
57
|
+
raise ValueError("malformed account (counter missing)")
|
|
140
58
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
59
|
+
data_atom = _read_atom(counter_atom.next)
|
|
60
|
+
if data_atom is None:
|
|
61
|
+
raise ValueError("malformed account (data missing)")
|
|
144
62
|
|
|
145
63
|
account = cls.create(
|
|
146
|
-
balance=
|
|
147
|
-
data
|
|
148
|
-
|
|
64
|
+
balance=bytes_to_int(balance_atom.data),
|
|
65
|
+
data_hash=data_atom.data,
|
|
66
|
+
counter=bytes_to_int(counter_atom.data),
|
|
67
|
+
code=code_atom.data,
|
|
149
68
|
)
|
|
150
69
|
if account.hash != account_id:
|
|
151
70
|
raise ValueError("account hash mismatch while decoding")
|
|
152
71
|
return account
|
|
153
72
|
|
|
154
|
-
def balance(self) -> int:
|
|
155
|
-
return self._balance
|
|
156
|
-
|
|
157
|
-
def data(self) -> bytes:
|
|
158
|
-
return self._data
|
|
159
|
-
|
|
160
|
-
def nonce(self) -> int:
|
|
161
|
-
return self._nonce
|
|
162
|
-
|
|
163
|
-
def body_hash(self) -> bytes:
|
|
164
|
-
return self.hash
|
|
165
|
-
|
|
166
73
|
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
167
|
-
|
|
74
|
+
# Build a single forward chain: account -> balance -> code -> counter -> data.
|
|
75
|
+
data_atom = Atom.from_data(data=bytes(self.data_hash))
|
|
76
|
+
counter_atom = Atom.from_data(
|
|
77
|
+
data=int_to_bytes(self.counter),
|
|
78
|
+
next_hash=data_atom.object_id(),
|
|
79
|
+
)
|
|
80
|
+
code_atom = Atom.from_data(
|
|
81
|
+
data=bytes(self.code),
|
|
82
|
+
next_hash=counter_atom.object_id(),
|
|
83
|
+
)
|
|
84
|
+
balance_atom = Atom.from_data(
|
|
85
|
+
data=int_to_bytes(self.balance),
|
|
86
|
+
next_hash=code_atom.object_id(),
|
|
87
|
+
)
|
|
88
|
+
type_atom = Atom.from_data(data=b"account", next_hash=balance_atom.object_id())
|
|
89
|
+
|
|
90
|
+
atoms = [data_atom, counter_atom, code_atom, balance_atom, type_atom]
|
|
91
|
+
account_hash = type_atom.object_id()
|
|
168
92
|
self.hash = account_hash
|
|
93
|
+
self.body_hash = account_hash
|
|
169
94
|
self.atoms = atoms
|
|
170
95
|
return account_hash, list(atoms)
|
astreum/_consensus/accounts.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
4
|
-
from typing import Any, Dict,
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
5
4
|
|
|
6
|
-
from .._storage.atom import Atom
|
|
7
5
|
from .._storage.patricia import PatriciaTrie
|
|
8
6
|
from .account import Account
|
|
9
7
|
|
|
@@ -15,53 +13,26 @@ class Accounts:
|
|
|
15
13
|
) -> None:
|
|
16
14
|
self._trie = PatriciaTrie(root_hash=root_hash)
|
|
17
15
|
self._cache: Dict[bytes, Account] = {}
|
|
18
|
-
self._staged: Dict[bytes, Account] = {}
|
|
19
|
-
self._staged_hashes: Dict[bytes, bytes] = {}
|
|
20
|
-
self._staged_atoms: Dict[bytes, Iterable[Atom]] = {}
|
|
21
|
-
self._node: Optional[Any] = None
|
|
22
16
|
|
|
23
17
|
@property
|
|
24
18
|
def root_hash(self) -> Optional[bytes]:
|
|
25
19
|
return self._trie.root_hash
|
|
26
20
|
|
|
27
|
-
def
|
|
28
|
-
if node is not None:
|
|
29
|
-
if self._node is None:
|
|
30
|
-
self._node = node
|
|
31
|
-
return node
|
|
32
|
-
if self._node is None:
|
|
33
|
-
raise ValueError("Accounts requires a node reference for trie access")
|
|
34
|
-
return self._node
|
|
35
|
-
|
|
36
|
-
def get_account(self, address: bytes, *, node: Optional[Any] = None) -> Optional[Account]:
|
|
37
|
-
if address in self._staged:
|
|
38
|
-
return self._staged[address]
|
|
39
|
-
|
|
21
|
+
def get_account(self, address: bytes, node: Optional[Any] = None) -> Optional[Account]:
|
|
40
22
|
cached = self._cache.get(address)
|
|
41
23
|
if cached is not None:
|
|
42
24
|
return cached
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
26
|
+
if node is None:
|
|
27
|
+
raise ValueError("Accounts requires a node reference for trie access")
|
|
28
|
+
|
|
29
|
+
account_id: Optional[bytes] = self._trie.get(node, address)
|
|
46
30
|
if account_id is None:
|
|
47
31
|
return None
|
|
48
32
|
|
|
49
|
-
account = Account.from_atom(
|
|
33
|
+
account = Account.from_atom(node, account_id)
|
|
50
34
|
self._cache[address] = account
|
|
51
35
|
return account
|
|
52
36
|
|
|
53
37
|
def set_account(self, address: bytes, account: Account) -> None:
|
|
54
|
-
account_hash, atoms = account.to_atom()
|
|
55
|
-
self._staged[address] = account
|
|
56
|
-
self._staged_hashes[address] = account_hash
|
|
57
|
-
self._staged_atoms[address] = tuple(atoms)
|
|
58
38
|
self._cache[address] = account
|
|
59
|
-
|
|
60
|
-
def staged_items(self) -> Iterable[Tuple[bytes, Account]]:
|
|
61
|
-
return self._staged.items()
|
|
62
|
-
|
|
63
|
-
def staged_hashes(self) -> Dict[bytes, bytes]:
|
|
64
|
-
return dict(self._staged_hashes)
|
|
65
|
-
|
|
66
|
-
def staged_atoms(self) -> Dict[bytes, Iterable[Atom]]:
|
|
67
|
-
return {addr: tuple(atoms) for addr, atoms in self._staged_atoms.items()}
|