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.
@@ -3,43 +3,11 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Any, List, Optional, Tuple
5
5
 
6
- from .._storage.atom import Atom, ZERO32
7
- from .receipt import Receipt, STATUS_SUCCESS
8
-
9
-
10
- def _int_to_be_bytes(value: Optional[int]) -> bytes:
11
- if value is None:
12
- return b""
13
- value = int(value)
14
- if value == 0:
15
- return b"\x00"
16
- size = (value.bit_length() + 7) // 8
17
- return value.to_bytes(size, "big")
18
-
19
-
20
- def _be_bytes_to_int(data: Optional[bytes]) -> int:
21
- if not data:
22
- return 0
23
- return int.from_bytes(data, "big")
24
-
25
-
26
- def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
27
- atoms: List[Atom] = []
28
- next_hash = ZERO32
29
- chain: List[Atom] = []
30
- for child_id in reversed(child_ids):
31
- elem = Atom.from_data(data=child_id, next_hash=next_hash)
32
- next_hash = elem.object_id()
33
- chain.append(elem)
34
-
35
- chain.reverse()
36
- list_value = Atom.from_data(data=len(child_ids).to_bytes(8, "little"), next_hash=next_hash)
37
- list_type = Atom.from_data(data=b"list", next_hash=list_value.object_id())
38
- atoms.extend(chain)
39
- atoms.append(list_value)
40
- atoms.append(list_type)
41
- return list_type.object_id(), atoms
42
-
6
+ from .._storage.atom import Atom, AtomKind, ZERO32
7
+ from ..utils.integer import bytes_to_int, int_to_bytes
8
+ from .account import Account
9
+ from .genesis import TREASURY_ADDRESS
10
+ from .receipt import STATUS_FAILED, Receipt, STATUS_SUCCESS
43
11
 
44
12
  @dataclass
45
13
  class Transaction:
@@ -57,31 +25,46 @@ class Transaction:
57
25
  acc: List[Atom] = []
58
26
 
59
27
  def emit(payload: bytes) -> None:
60
- atom = Atom.from_data(data=payload)
28
+ atom = Atom.from_data(data=payload, kind=AtomKind.BYTES)
61
29
  body_child_ids.append(atom.object_id())
62
30
  acc.append(atom)
63
31
 
64
- emit(_int_to_be_bytes(self.amount))
65
- emit(_int_to_be_bytes(self.counter))
32
+ emit(int_to_bytes(self.amount))
33
+ emit(int_to_bytes(self.counter))
66
34
  emit(bytes(self.data))
67
35
  emit(bytes(self.recipient))
68
36
  emit(bytes(self.sender))
69
37
 
70
- body_id, body_atoms = _make_list(body_child_ids)
38
+ # Build the linked list of body entry references.
39
+ body_atoms: List[Atom] = []
40
+ body_head = ZERO32
41
+ for child_id in reversed(body_child_ids):
42
+ node = Atom.from_data(data=child_id, next_hash=body_head, kind=AtomKind.BYTES)
43
+ body_head = node.object_id()
44
+ body_atoms.append(node)
45
+ body_atoms.reverse()
71
46
  acc.extend(body_atoms)
72
47
 
73
- type_atom = Atom.from_data(data=b"transaction", next_hash=body_id)
74
- signature_atom = Atom.from_data(data=bytes(self.signature))
48
+ body_list_atom = Atom.from_data(data=body_head, kind=AtomKind.LIST)
49
+ acc.append(body_list_atom)
50
+ body_list_id = body_list_atom.object_id()
75
51
 
76
- top_list_id, top_atoms = _make_list(
77
- [type_atom.object_id(), body_id, signature_atom.object_id()]
52
+ signature_atom = Atom.from_data(
53
+ data=bytes(self.signature),
54
+ next_hash=body_list_id,
55
+ kind=AtomKind.BYTES,
56
+ )
57
+ type_atom = Atom.from_data(
58
+ data=b"transaction",
59
+ next_hash=signature_atom.object_id(),
60
+ kind=AtomKind.SYMBOL,
78
61
  )
79
62
 
80
- atoms: List[Atom] = acc
81
- atoms.append(type_atom)
82
- atoms.append(signature_atom)
83
- atoms.extend(top_atoms)
84
- return top_list_id, atoms
63
+ acc.append(signature_atom)
64
+ acc.append(type_atom)
65
+
66
+ self.hash = type_atom.object_id()
67
+ return self.hash, acc
85
68
 
86
69
  @classmethod
87
70
  def from_atom(
@@ -89,83 +72,86 @@ class Transaction:
89
72
  node: Any,
90
73
  transaction_id: bytes,
91
74
  ) -> Transaction:
92
- storage_get = node._local_get
75
+ storage_get = getattr(node, "storage_get", None)
93
76
  if not callable(storage_get):
94
77
  raise NotImplementedError("node does not expose a storage getter")
95
78
 
96
- top_type_atom = storage_get(transaction_id)
97
- if top_type_atom is None or top_type_atom.data != b"list":
98
- raise ValueError("not a transaction (outer list missing)")
99
-
100
- top_value_atom = storage_get(top_type_atom.next)
101
- if top_value_atom is None:
102
- raise ValueError("malformed transaction (outer value missing)")
103
-
104
- head = top_value_atom.next
105
- first_elem = storage_get(head)
106
- if first_elem is None:
107
- raise ValueError("malformed transaction (type element missing)")
108
-
109
- type_atom_id = first_elem.data
110
- type_atom = storage_get(type_atom_id)
111
- if type_atom is None or type_atom.data != b"transaction":
112
- raise ValueError("not a transaction (type mismatch)")
113
-
114
- def read_list_entries(start: bytes) -> List[bytes]:
79
+ def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
80
+ kind_value = getattr(atom, "kind", None)
81
+ if isinstance(kind_value, AtomKind):
82
+ return kind_value
83
+ if isinstance(kind_value, int):
84
+ try:
85
+ return AtomKind(kind_value)
86
+ except ValueError:
87
+ return None
88
+ return None
89
+
90
+ def _require_atom(
91
+ atom_id: Optional[bytes],
92
+ context: str,
93
+ expected_kind: Optional[AtomKind] = None,
94
+ ) -> Atom:
95
+ if not atom_id or atom_id == ZERO32:
96
+ raise ValueError(f"missing {context}")
97
+ atom = storage_get(atom_id)
98
+ if atom is None:
99
+ raise ValueError(f"missing {context}")
100
+ if expected_kind is not None:
101
+ kind = _atom_kind(atom)
102
+ if kind is not expected_kind:
103
+ raise ValueError(f"malformed {context}")
104
+ return atom
105
+
106
+ def _read_list(head_id: Optional[bytes], context: str) -> List[bytes]:
115
107
  entries: List[bytes] = []
116
- current = start if start != ZERO32 else b""
108
+ current = head_id if head_id and head_id != ZERO32 else None
117
109
  while current:
118
- elem = storage_get(current)
119
- if elem is None:
120
- break
121
- entries.append(elem.data)
122
- nxt = elem.next
123
- current = nxt if nxt != ZERO32 else b""
110
+ node = storage_get(current)
111
+ if node is None:
112
+ raise ValueError(f"missing list node while decoding {context}")
113
+ node_kind = _atom_kind(node)
114
+ if node_kind is not AtomKind.BYTES:
115
+ raise ValueError(f"invalid list node while decoding {context}")
116
+ if len(node.data) != len(ZERO32):
117
+ raise ValueError(f"malformed list entry while decoding {context}")
118
+ entries.append(node.data)
119
+ nxt = node.next
120
+ current = nxt if nxt and nxt != ZERO32 else None
124
121
  return entries
125
122
 
126
- remainder_entries = read_list_entries(first_elem.next)
127
- if len(remainder_entries) < 2:
128
- raise ValueError("malformed transaction (body/signature missing)")
129
-
130
- body_id, signature_atom_id = remainder_entries[0], remainder_entries[1]
131
-
132
- body_type_atom = storage_get(body_id)
133
- if body_type_atom is None or body_type_atom.data != b"list":
134
- raise ValueError("malformed transaction body (type)")
135
-
136
- body_value_atom = storage_get(body_type_atom.next)
137
- if body_value_atom is None:
138
- raise ValueError("malformed transaction body (value)")
139
-
140
- body_entries = read_list_entries(body_value_atom.next)
141
- if len(body_entries) < 5:
142
- body_entries.extend([ZERO32] * (5 - len(body_entries)))
143
-
144
- def read_detail_bytes(entry_id: bytes) -> bytes:
145
- if entry_id == ZERO32:
146
- return b""
147
- elem = storage_get(entry_id)
148
- if elem is None:
123
+ def _read_detail_bytes(entry_id: Optional[bytes]) -> bytes:
124
+ if not entry_id or entry_id == ZERO32:
149
125
  return b""
150
- detail_atom = storage_get(elem.data)
126
+ detail_atom = storage_get(entry_id)
151
127
  return detail_atom.data if detail_atom is not None else b""
152
128
 
153
- amount_bytes = read_detail_bytes(body_entries[0])
154
- counter_bytes = read_detail_bytes(body_entries[1])
155
- data_bytes = read_detail_bytes(body_entries[2])
156
- recipient_bytes = read_detail_bytes(body_entries[3])
157
- sender_bytes = read_detail_bytes(body_entries[4])
129
+ type_atom = _require_atom(transaction_id, "transaction type atom", AtomKind.SYMBOL)
130
+ if type_atom.data != b"transaction":
131
+ raise ValueError("not a transaction (type atom payload)")
132
+
133
+ signature_atom = _require_atom(type_atom.next, "transaction signature atom", AtomKind.BYTES)
134
+ body_list_atom = _require_atom(signature_atom.next, "transaction body list atom", AtomKind.LIST)
135
+ if body_list_atom.next and body_list_atom.next != ZERO32:
136
+ raise ValueError("malformed transaction (body list tail)")
137
+
138
+ body_entry_ids = _read_list(body_list_atom.data, "transaction body")
139
+ if len(body_entry_ids) < 5:
140
+ body_entry_ids.extend([ZERO32] * (5 - len(body_entry_ids)))
158
141
 
159
- signature_atom = storage_get(signature_atom_id)
160
- signature_bytes = signature_atom.data if signature_atom is not None else b""
142
+ amount_bytes = _read_detail_bytes(body_entry_ids[0])
143
+ counter_bytes = _read_detail_bytes(body_entry_ids[1])
144
+ data_bytes = _read_detail_bytes(body_entry_ids[2])
145
+ recipient_bytes = _read_detail_bytes(body_entry_ids[3])
146
+ sender_bytes = _read_detail_bytes(body_entry_ids[4])
161
147
 
162
148
  return cls(
163
- amount=_be_bytes_to_int(amount_bytes),
164
- counter=_be_bytes_to_int(counter_bytes),
149
+ amount=bytes_to_int(amount_bytes),
150
+ counter=bytes_to_int(counter_bytes),
165
151
  data=data_bytes,
166
152
  recipient=recipient_bytes,
167
153
  sender=sender_bytes,
168
- signature=signature_bytes,
154
+ signature=signature_atom.data,
169
155
  hash=bytes(transaction_id),
170
156
  )
171
157
 
@@ -174,9 +160,50 @@ def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None
174
160
  """Apply transaction to the candidate block. Override downstream."""
175
161
  transaction = Transaction.from_atom(node, transaction_hash)
176
162
 
177
- if block.transactions is None:
178
- block.transactions = []
179
- block.transactions.append(transaction)
163
+ accounts = block.accounts
164
+
165
+ sender_account = accounts.get_account(address=transaction.sender, node=node)
166
+
167
+ if sender_account is None:
168
+ return
169
+
170
+ tx_cost = 1 + transaction.amount
171
+
172
+ if sender_account.balance < tx_cost:
173
+ low_sender_balance_receipt = Receipt(
174
+ transaction_hash=transaction_hash,
175
+ cost=0,
176
+ logs=b"low sender balance",
177
+ status=STATUS_FAILED
178
+ )
179
+ low_sender_balance_receipt.atomize()
180
+ block.receipts.append(receipt)
181
+ block.transactions.append(transaction)
182
+ return
183
+
184
+ recipient_account = accounts.get_account(address=transaction.recipient, node=node)
185
+
186
+ if recipient_account is None:
187
+ recipient_account = Account.create()
188
+
189
+ if transaction.recipient == TREASURY_ADDRESS:
190
+ stake_trie = recipient_account.data
191
+ existing_stake = stake_trie.get(node, transaction.sender)
192
+ current_stake = bytes_to_int(existing_stake)
193
+ new_stake = current_stake + transaction.amount
194
+ stake_trie.put(node, transaction.sender, int_to_bytes(new_stake))
195
+ recipient_account.data_hash = stake_trie.root_hash or ZERO32
196
+ recipient_account.balance += transaction.amount
197
+ else:
198
+ recipient_account.balance += transaction.amount
199
+
200
+ sender_account.balance -= tx_cost
201
+
202
+ block.accounts.set_account(address=sender_account)
203
+
204
+ block.accounts.set_account(address=recipient_account)
205
+
206
+ block.transactions.append(transaction_hash)
180
207
 
181
208
  receipt = Receipt(
182
209
  transaction_hash=bytes(transaction_hash),
@@ -185,8 +212,4 @@ def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None
185
212
  status=STATUS_SUCCESS,
186
213
  )
187
214
  receipt.atomize()
188
- if block.receipts is None:
189
- block.receipts = []
190
215
  block.receipts.append(receipt)
191
-
192
- # Downstream implementations can extend this to apply state changes.
@@ -88,7 +88,7 @@ def make_validation_worker(
88
88
  new_block.timestamp = max(int(now), min_allowed)
89
89
 
90
90
  # atomize block
91
- new_block_hash, _ = new_block.to_atom()
91
+ new_block_hash, new_block_atoms = new_block.to_atom()
92
92
  # put as own latest block hash
93
93
  node.latest_block_hash = new_block_hash
94
94
 
@@ -117,6 +117,9 @@ def make_validation_worker(
117
117
  except Exception:
118
118
  pass
119
119
 
120
- # store the new block and receipts
120
+ # upload block atoms
121
+
122
+ # upload receipt atoms
123
+ # upload account atoms
121
124
 
122
125
  return _validation_worker
@@ -17,7 +17,7 @@ def _process_peers_latest_block(
17
17
  fk.head for fk in node.forks.values() if fk.head != latest_block_hash
18
18
  }
19
19
 
20
- new_fork.validate(storage_get=node._local_get, stop_heads=current_fork_heads)
20
+ new_fork.validate(storage_get=node.storage_get, stop_heads=current_fork_heads)
21
21
 
22
22
  if new_fork.validated_upto and new_fork.validated_upto in node.forks:
23
23
  ref = node.forks[new_fork.validated_upto]
@@ -1,37 +1,190 @@
1
- from typing import List, Optional
2
-
3
-
4
- class Expr:
5
- class ListExpr:
6
- def __init__(self, elements: List['Expr']):
7
- self.elements = elements
8
-
9
- def __repr__(self):
10
- if not self.elements:
11
- return "()"
12
- inner = " ".join(str(e) for e in self.elements)
13
- return f"({inner} list)"
14
-
15
- class Symbol:
16
- def __init__(self, value: str):
17
- self.value = value
18
-
19
- def __repr__(self):
20
- return f"({self.value} symbol)"
21
-
22
- class Bytes:
23
- def __init__(self, value: bytes):
24
- self.value = value
25
-
26
- def __repr__(self):
27
- return f"({self.value} bytes)"
28
-
29
- class Error:
30
- def __init__(self, topic: str, origin: Optional['Expr'] = None):
31
- self.topic = topic
32
- self.origin = origin
33
-
34
- def __repr__(self):
35
- if self.origin is None:
36
- return f'({self.topic} error)'
37
- return f'({self.origin} {self.topic} error)'
1
+ from typing import Any, List, Optional, Tuple
2
+
3
+ from .._storage.atom import Atom, AtomKind
4
+
5
+ ZERO32 = b"\x00" * 32
6
+ ERROR_SYMBOL = "error"
7
+
8
+
9
+ class Expr:
10
+ class ListExpr:
11
+ def __init__(self, elements: List['Expr']):
12
+ self.elements = elements
13
+
14
+ def __repr__(self):
15
+ if not self.elements:
16
+ return "()"
17
+ inner = " ".join(str(e) for e in self.elements)
18
+ return f"({inner})"
19
+
20
+ def to_atoms(self):
21
+ return Expr.to_atoms(self)
22
+
23
+ class Symbol:
24
+ def __init__(self, value: str):
25
+ self.value = value
26
+
27
+ def __repr__(self):
28
+ return f"{self.value}"
29
+
30
+ def to_atoms(self):
31
+ return Expr.to_atoms(self)
32
+
33
+ class Bytes:
34
+ def __init__(self, value: bytes):
35
+ self.value = value
36
+
37
+ def __repr__(self):
38
+ int_value = int.from_bytes(self.value, "big") if self.value else 0
39
+ return f"{int_value}"
40
+
41
+ def to_atoms(self):
42
+ return Expr.to_atoms(self)
43
+ @classmethod
44
+ def from_atoms(cls, node: Any, root_hash: bytes) -> "Expr":
45
+ """Rebuild an expression tree from stored atoms."""
46
+ if not isinstance(root_hash, (bytes, bytearray)):
47
+ raise TypeError("root hash must be bytes-like")
48
+
49
+ storage_get = getattr(node, "storage_get", None)
50
+ if not callable(storage_get):
51
+ raise TypeError("node must provide a callable 'storage_get'")
52
+
53
+ expr_id = bytes(root_hash)
54
+
55
+ def _require(atom_id: Optional[bytes], context: str):
56
+ if not atom_id:
57
+ raise ValueError(f"missing atom id while decoding {context}")
58
+ atom = storage_get(atom_id)
59
+ if atom is None:
60
+ raise ValueError(f"missing atom data while decoding {context}")
61
+ return atom
62
+
63
+ def _atom_kind(atom: Any) -> Optional[AtomKind]:
64
+ kind_value = getattr(atom, "kind", None)
65
+ if isinstance(kind_value, AtomKind):
66
+ return kind_value
67
+ if isinstance(kind_value, int):
68
+ try:
69
+ return AtomKind(kind_value)
70
+ except ValueError:
71
+ return None
72
+ return None
73
+
74
+ type_atom = _require(expr_id, "expression atom")
75
+
76
+ kind_enum = _atom_kind(type_atom)
77
+ if kind_enum is None:
78
+ raise ValueError("expression atom missing kind")
79
+
80
+ if kind_enum is AtomKind.SYMBOL:
81
+ try:
82
+ return cls.Symbol(type_atom.data.decode("utf-8"))
83
+ except UnicodeDecodeError as exc:
84
+ raise ValueError("symbol atom is not valid utf-8") from exc
85
+
86
+ if kind_enum is AtomKind.BYTES:
87
+ return cls.Bytes(type_atom.data)
88
+
89
+ if kind_enum is AtomKind.LIST:
90
+ # Empty list sentinel: zero-length payload and no next pointer.
91
+ if len(type_atom.data) == 0 and type_atom.next == ZERO32:
92
+ return cls.ListExpr([])
93
+
94
+ elements: List[Expr] = []
95
+ current_atom = type_atom
96
+ idx = 0
97
+ while True:
98
+ child_hash = current_atom.data
99
+ if not child_hash:
100
+ raise ValueError("list element missing child hash")
101
+ if len(child_hash) != len(ZERO32):
102
+ raise ValueError("list element hash has unexpected length")
103
+ child_expr = cls.from_atoms(node, child_hash)
104
+ elements.append(child_expr)
105
+ next_id = current_atom.next
106
+ if not next_id or next_id == ZERO32:
107
+ break
108
+ next_atom = _require(next_id, f"list element {idx}")
109
+ next_kind = _atom_kind(next_atom)
110
+ if next_kind is not AtomKind.LIST:
111
+ raise ValueError("list chain contains non-list atom")
112
+ current_atom = next_atom
113
+ idx += 1
114
+ return cls.ListExpr(elements)
115
+
116
+ raise ValueError(f"unknown expression kind: {kind_enum}")
117
+
118
+ @staticmethod
119
+ def to_atoms(e: "Expr") -> Tuple[bytes, List[Atom]]:
120
+ def symbol(value: str) -> Tuple[bytes, List[Atom]]:
121
+ atom = Atom.from_data(data=value.encode("utf-8"), kind=AtomKind.SYMBOL)
122
+ return atom.object_id(), [atom]
123
+
124
+ def bytes_value(data: bytes) -> Tuple[bytes, List[Atom]]:
125
+ atom = Atom.from_data(data=data, kind=AtomKind.BYTES)
126
+ return atom.object_id(), [atom]
127
+
128
+ def lst(items: List["Expr"]) -> Tuple[bytes, List[Atom]]:
129
+ acc: List[Atom] = []
130
+ child_hashes: List[bytes] = []
131
+ for it in items:
132
+ h, atoms = Expr.to_atoms(it)
133
+ acc.extend(atoms)
134
+ child_hashes.append(h)
135
+ next_hash = ZERO32
136
+ elem_atoms: List[Atom] = []
137
+ for h in reversed(child_hashes):
138
+ a = Atom.from_data(h, next_hash, kind=AtomKind.LIST)
139
+ next_hash = a.object_id()
140
+ elem_atoms.append(a)
141
+ elem_atoms.reverse()
142
+ if elem_atoms:
143
+ head = elem_atoms[0].object_id()
144
+ else:
145
+ empty_atom = Atom.from_data(data=b"", next_hash=ZERO32, kind=AtomKind.LIST)
146
+ elem_atoms = [empty_atom]
147
+ head = empty_atom.object_id()
148
+ return head, acc + elem_atoms
149
+
150
+ if isinstance(e, Expr.Symbol):
151
+ return symbol(e.value)
152
+ if isinstance(e, Expr.Bytes):
153
+ return bytes_value(e.value)
154
+ if isinstance(e, Expr.ListExpr):
155
+ return lst(e.elements)
156
+ raise TypeError("unknown Expr variant")
157
+
158
+ def _expr_generate_id(expr) -> bytes:
159
+ expr_id, _ = Expr.to_atoms(expr)
160
+ return expr_id
161
+
162
+
163
+ def _expr_cached_id(expr) -> bytes:
164
+ cached = getattr(expr, "_cached_id", None)
165
+ if cached is None:
166
+ cached = _expr_generate_id(expr)
167
+ setattr(expr, "_cached_id", cached)
168
+ return cached
169
+
170
+
171
+ for _expr_cls in (Expr.ListExpr, Expr.Symbol, Expr.Bytes):
172
+ _expr_cls.generate_id = _expr_generate_id # type: ignore[attr-defined]
173
+ _expr_cls.id = property(_expr_cached_id) # type: ignore[attr-defined]
174
+
175
+
176
+ def error_expr(topic: str, message: str) -> Expr.ListExpr:
177
+ """Encode an error as (error <topic-bytes> <message-bytes>)."""
178
+ try:
179
+ topic_bytes = topic.encode("utf-8")
180
+ except UnicodeEncodeError as exc:
181
+ raise ValueError("error topic must be valid utf-8") from exc
182
+ try:
183
+ message_bytes = message.encode("utf-8")
184
+ except UnicodeEncodeError as exc:
185
+ raise ValueError("error message must be valid utf-8") from exc
186
+ return Expr.ListExpr([
187
+ Expr.Symbol(ERROR_SYMBOL),
188
+ Expr.Bytes(topic_bytes),
189
+ Expr.Bytes(message_bytes),
190
+ ])