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.
@@ -1,77 +1,18 @@
1
-
1
+
2
2
  from __future__ import annotations
3
3
 
4
- from typing import Any, Iterable, List, Optional, Tuple
4
+ from typing import Any, List
5
5
 
6
6
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
7
 
8
8
  from .account import Account
9
9
  from .block import Block
10
- from .._storage.atom import Atom, ZERO32
11
- from .._storage.patricia import PatriciaTrie, PatriciaNode
10
+ from .._storage.atom import ZERO32
11
+ from .._storage.patricia import PatriciaTrie
12
+ from ..utils.integer import int_to_bytes
12
13
 
13
14
  TREASURY_ADDRESS = b"\x01" * 32
14
15
  BURN_ADDRESS = b"\x00" * 32
15
-
16
-
17
- def _int_to_be_bytes(value: int) -> bytes:
18
- if value < 0:
19
- raise ValueError("integer fields in genesis must be non-negative")
20
- if value == 0:
21
- return b"\x00"
22
- length = (value.bit_length() + 7) // 8
23
- return value.to_bytes(length, "big")
24
-
25
-
26
- def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
27
- next_hash = ZERO32
28
- chain: List[Atom] = []
29
- for child_id in reversed(child_ids):
30
- elem = Atom.from_data(data=child_id, next_hash=next_hash)
31
- next_hash = elem.object_id()
32
- chain.append(elem)
33
- chain.reverse()
34
-
35
- value_atom = Atom.from_data(
36
- data=len(child_ids).to_bytes(8, "little"),
37
- next_hash=next_hash,
38
- )
39
- type_atom = Atom.from_data(data=b"list", next_hash=value_atom.object_id())
40
- atoms = chain + [value_atom, type_atom]
41
- return type_atom.object_id(), atoms
42
-
43
-
44
- def _store_atoms(node: Any, atoms: Iterable[Atom]) -> None:
45
- setter = getattr(node, "_local_set", None)
46
- if not callable(setter):
47
- raise TypeError("node must expose '_local_set(object_id, atom)'")
48
- for atom in atoms:
49
- setter(atom.object_id(), atom)
50
-
51
-
52
- def _persist_trie(trie: PatriciaTrie, node: Any) -> None:
53
- for patricia_node in trie.nodes.values():
54
- _, atoms = patricia_node.to_atoms()
55
- _store_atoms(node, atoms)
56
-
57
-
58
- if not hasattr(PatriciaNode, "to_bytes"):
59
- def _patricia_node_to_bytes(self: PatriciaNode) -> bytes: # type: ignore[no-redef]
60
- fields = [
61
- bytes([self.key_len]) + self.key,
62
- self.child_0 or ZERO32,
63
- self.child_1 or ZERO32,
64
- self.value or b"",
65
- ]
66
- encoded: List[bytes] = []
67
- for field in fields:
68
- encoded.append(len(field).to_bytes(4, "big"))
69
- encoded.append(field)
70
- return b"".join(encoded)
71
-
72
- PatriciaNode.to_bytes = _patricia_node_to_bytes # type: ignore[attr-defined]
73
-
74
-
75
16
  def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
76
17
  validator_pk = bytes(validator_public_key)
77
18
 
@@ -80,30 +21,21 @@ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secre
80
21
 
81
22
  # 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
82
23
  stake_trie = PatriciaTrie()
83
- stake_amount = (1).to_bytes(32, "big")
84
- stake_trie.put(node, validator_pk, stake_amount)
85
- _persist_trie(stake_trie, node)
86
- stake_root = stake_trie.root_hash or ZERO32
24
+ stake_amount = int_to_bytes(1)
25
+ stake_trie.put(storage_node=node, key=validator_pk, value=stake_amount)
26
+ stake_root = stake_trie.root_hash
87
27
 
88
28
  # 2. Account trie with treasury, burn, and validator accounts.
89
29
  accounts_trie = PatriciaTrie()
90
30
 
91
- treasury_account = Account.create(balance=1, data=stake_root, nonce=0)
92
- treasury_account_id, treasury_atoms = treasury_account.to_atom()
93
- _store_atoms(node, treasury_atoms)
94
- accounts_trie.put(node, TREASURY_ADDRESS, treasury_account_id)
95
-
96
- burn_account = Account.create(balance=0, data=b"", nonce=0)
97
- burn_account_id, burn_atoms = burn_account.to_atom()
98
- _store_atoms(node, burn_atoms)
99
- accounts_trie.put(node, BURN_ADDRESS, burn_account_id)
31
+ treasury_account = Account.create(balance=1, data=stake_root, counter=0)
32
+ accounts_trie.put(storage_node=node, key=TREASURY_ADDRESS, value=treasury_account.hash)
100
33
 
101
- validator_account = Account.create(balance=0, data=b"", nonce=0)
102
- validator_account_id, validator_atoms = validator_account.to_atom()
103
- _store_atoms(node, validator_atoms)
104
- accounts_trie.put(node, validator_pk, validator_account_id)
34
+ burn_account = Account.create(balance=0, data=b"", counter=0)
35
+ accounts_trie.put(storage_node=node, key=BURN_ADDRESS, value=burn_account.hash)
105
36
 
106
- _persist_trie(accounts_trie, node)
37
+ validator_account = Account.create(balance=0, data=b"", counter=0)
38
+ accounts_trie.put(storage_node=node, key=validator_pk, value=validator_account.hash)
107
39
 
108
40
  accounts_root = accounts_trie.root_hash
109
41
  if accounts_root is None:
@@ -134,8 +66,7 @@ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secre
134
66
 
135
67
  secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
136
68
  block.signature = secret.sign(block.body_hash)
137
- block_hash, block_atoms = block.to_atom()
138
- _store_atoms(node, block_atoms)
69
+ block_hash, _ = block.to_atom()
139
70
 
140
71
  block.hash = block_hash
141
72
  return block
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field
4
4
  from typing import Callable, List, Optional, Tuple
5
5
 
6
- from .._storage.atom import Atom, ZERO32
6
+ from .._storage.atom import Atom, AtomKind, ZERO32
7
7
 
8
8
  STATUS_SUCCESS = 0
9
9
  STATUS_FAILED = 1
@@ -25,52 +25,6 @@ def _be_bytes_to_int(data: Optional[bytes]) -> int:
25
25
  return int.from_bytes(data, "big")
26
26
 
27
27
 
28
- def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
29
- atoms: List[Atom] = []
30
- next_hash = ZERO32
31
- elements: List[Atom] = []
32
- for child_id in reversed(child_ids):
33
- elem = Atom.from_data(data=child_id, next_hash=next_hash)
34
- next_hash = elem.object_id()
35
- elements.append(elem)
36
- elements.reverse()
37
- list_value = Atom.from_data(data=len(child_ids).to_bytes(8, "little"), next_hash=next_hash)
38
- list_type = Atom.from_data(data=b"list", next_hash=list_value.object_id())
39
- atoms.extend(elements)
40
- atoms.append(list_value)
41
- atoms.append(list_type)
42
- return list_type.object_id(), atoms
43
-
44
-
45
- def _read_list_entries(
46
- storage_get: Callable[[bytes], Optional[Atom]], start: bytes
47
- ) -> List[bytes]:
48
- entries: List[bytes] = []
49
- current = start if start and start != ZERO32 else b""
50
- while current:
51
- elem = storage_get(current)
52
- if elem is None:
53
- break
54
- entries.append(elem.data)
55
- nxt = elem.next
56
- current = nxt if nxt and nxt != ZERO32 else b""
57
- return entries
58
-
59
-
60
- def _read_payload_bytes(
61
- storage_get: Callable[[bytes], Optional[Atom]], object_id: bytes
62
- ) -> bytes:
63
- if not object_id or object_id == ZERO32:
64
- return b""
65
- atom = storage_get(object_id)
66
- if atom is None:
67
- return b""
68
- if atom.data == b"bytes":
69
- value_atom = storage_get(atom.next)
70
- return value_atom.data if value_atom is not None else b""
71
- return atom.data
72
-
73
-
74
28
  @dataclass
75
29
  class Receipt:
76
30
  transaction_hash: bytes = ZERO32
@@ -85,31 +39,30 @@ class Receipt:
85
39
  if self.status not in (STATUS_SUCCESS, STATUS_FAILED):
86
40
  raise ValueError("unsupported receipt status")
87
41
 
88
- atoms: List[Atom] = []
89
-
90
- tx_atom = Atom.from_data(data=bytes(self.transaction_hash))
91
- status_atom = Atom.from_data(data=_int_to_be_bytes(self.status))
92
- cost_atom = Atom.from_data(data=_int_to_be_bytes(self.cost))
93
- logs_atom = Atom.from_data(data=bytes(self.logs))
94
-
95
- atoms.extend([tx_atom, status_atom, cost_atom, logs_atom])
96
-
97
- body_child_ids = [
98
- tx_atom.object_id(),
99
- status_atom.object_id(),
100
- cost_atom.object_id(),
101
- logs_atom.object_id(),
42
+ detail_specs = [
43
+ (bytes(self.transaction_hash), AtomKind.LIST),
44
+ (_int_to_be_bytes(self.status), AtomKind.BYTES),
45
+ (_int_to_be_bytes(self.cost), AtomKind.BYTES),
46
+ (bytes(self.logs), AtomKind.BYTES),
102
47
  ]
103
- body_id, body_atoms = _make_list(body_child_ids)
104
- atoms.extend(body_atoms)
105
-
106
- type_atom = Atom.from_data(data=b"receipt", next_hash=body_id)
107
- atoms.append(type_atom)
108
48
 
109
- top_list_id, top_atoms = _make_list([type_atom.object_id(), body_id])
110
- atoms.extend(top_atoms)
49
+ detail_atoms: List[Atom] = []
50
+ next_hash = ZERO32
51
+ for payload, kind in reversed(detail_specs):
52
+ atom = Atom.from_data(data=payload, next_hash=next_hash, kind=kind)
53
+ detail_atoms.append(atom)
54
+ next_hash = atom.object_id()
55
+ detail_atoms.reverse()
56
+
57
+ type_atom = Atom.from_data(
58
+ data=b"receipt",
59
+ next_hash=next_hash,
60
+ kind=AtomKind.SYMBOL,
61
+ )
111
62
 
112
- return top_list_id, atoms
63
+ self.hash = type_atom.object_id()
64
+ atoms = detail_atoms + [type_atom]
65
+ return self.hash, atoms
113
66
 
114
67
  def atomize(self) -> Tuple[bytes, List[Atom]]:
115
68
  """Generate atoms for this receipt and cache them."""
@@ -125,45 +78,51 @@ class Receipt:
125
78
  receipt_id: bytes,
126
79
  ) -> Receipt:
127
80
  """Materialise a Receipt from Atom storage."""
128
- top_type_atom = storage_get(receipt_id)
129
- if top_type_atom is None or top_type_atom.data != b"list":
130
- raise ValueError("not a receipt (outer list missing)")
131
-
132
- top_value_atom = storage_get(top_type_atom.next)
133
- if top_value_atom is None:
134
- raise ValueError("malformed receipt (outer value missing)")
135
-
136
- head = top_value_atom.next
137
- first_elem = storage_get(head) if head else None
138
- if first_elem is None:
139
- raise ValueError("malformed receipt (type element missing)")
140
-
141
- type_atom_id = first_elem.data
142
- type_atom = storage_get(type_atom_id)
143
- if type_atom is None or type_atom.data != b"receipt":
144
- raise ValueError("not a receipt (type mismatch)")
145
-
146
- remainder_entries = _read_list_entries(storage_get, first_elem.next)
147
- if not remainder_entries:
148
- raise ValueError("malformed receipt (body missing)")
149
- body_id = remainder_entries[0]
150
-
151
- body_type_atom = storage_get(body_id)
152
- if body_type_atom is None or body_type_atom.data != b"list":
153
- raise ValueError("malformed receipt body (type)")
154
-
155
- body_value_atom = storage_get(body_type_atom.next)
156
- if body_value_atom is None:
157
- raise ValueError("malformed receipt body (value)")
158
-
159
- body_entries = _read_list_entries(storage_get, body_value_atom.next)
160
- if len(body_entries) < 4:
161
- body_entries.extend([ZERO32] * (4 - len(body_entries)))
162
-
163
- transaction_hash_bytes = _read_payload_bytes(storage_get, body_entries[0])
164
- status_bytes = _read_payload_bytes(storage_get, body_entries[1])
165
- cost_bytes = _read_payload_bytes(storage_get, body_entries[2])
166
- logs_bytes = _read_payload_bytes(storage_get, body_entries[3])
81
+ def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
82
+ kind_value = getattr(atom, "kind", None)
83
+ if isinstance(kind_value, AtomKind):
84
+ return kind_value
85
+ if isinstance(kind_value, int):
86
+ try:
87
+ return AtomKind(kind_value)
88
+ except ValueError:
89
+ return None
90
+ return None
91
+
92
+ type_atom = storage_get(receipt_id)
93
+ if type_atom is None:
94
+ raise ValueError("missing receipt type atom")
95
+ if _atom_kind(type_atom) is not AtomKind.SYMBOL:
96
+ raise ValueError("malformed receipt (type kind)")
97
+ if type_atom.data != b"receipt":
98
+ raise ValueError("not a receipt (type payload)")
99
+
100
+ details: List[Atom] = []
101
+ current = type_atom.next
102
+ while current and current != ZERO32 and len(details) < 4:
103
+ atom = storage_get(current)
104
+ if atom is None:
105
+ raise ValueError("missing receipt detail atom")
106
+ details.append(atom)
107
+ current = atom.next
108
+
109
+ if current and current != ZERO32:
110
+ raise ValueError("too many receipt fields")
111
+ if len(details) != 4:
112
+ raise ValueError("incomplete receipt fields")
113
+
114
+ tx_atom, status_atom, cost_atom, logs_atom = details
115
+
116
+ if _atom_kind(tx_atom) is not AtomKind.LIST:
117
+ raise ValueError("receipt transaction hash must be list-kind")
118
+ if any(_atom_kind(atom) is not AtomKind.BYTES for atom in [status_atom, cost_atom, logs_atom]):
119
+ raise ValueError("receipt detail atoms must be bytes-kind")
120
+
121
+ transaction_hash_bytes = tx_atom.data or ZERO32
122
+ status_bytes = status_atom.data
123
+ cost_bytes = cost_atom.data
124
+ logs_bytes = logs_atom.data
125
+
167
126
  status_value = _be_bytes_to_int(status_bytes)
168
127
  if status_value not in (STATUS_SUCCESS, STATUS_FAILED):
169
128
  raise ValueError("unsupported receipt status")
@@ -2,13 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import threading
4
4
  from queue import Queue
5
- from typing import Any
5
+ from typing import Any, Optional
6
6
 
7
7
  from .workers import (
8
8
  make_discovery_worker,
9
9
  make_validation_worker,
10
10
  make_verify_worker,
11
11
  )
12
+ from .genesis import create_genesis_block
13
+ from ..utils.bytes import hex_to_bytes
12
14
 
13
15
 
14
16
  def current_validator(node: Any) -> bytes:
@@ -16,7 +18,9 @@ def current_validator(node: Any) -> bytes:
16
18
  raise NotImplementedError("current_validator must be implemented by the host node")
17
19
 
18
20
 
19
- def consensus_setup(node: Any) -> None:
21
+ def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
22
+ config = config or {}
23
+
20
24
  # Shared state
21
25
  node.validation_lock = getattr(node, "validation_lock", threading.RLock())
22
26
 
@@ -26,6 +30,12 @@ def consensus_setup(node: Any) -> None:
26
30
  node.chains = getattr(node, "chains", {})
27
31
  node.forks = getattr(node, "forks", {})
28
32
 
33
+ node.latest_block_hash = None
34
+ latest_block_hex = config.get("latest_block_hash")
35
+ if latest_block_hex is not None:
36
+ node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
37
+ node.latest_block = None
38
+
29
39
  # Pending transactions queue (hash-only entries)
30
40
  node._validation_transaction_queue = getattr(
31
41
  node, "_validation_transaction_queue", Queue()
@@ -64,5 +74,42 @@ def consensus_setup(node: Any) -> None:
64
74
  )
65
75
  node.consensus_discovery_thread.start()
66
76
  node.consensus_verify_thread.start()
67
- if getattr(node, "validation_secret_key", None):
77
+
78
+ validator_secret_hex = config.get("validation_secret_key")
79
+ if validator_secret_hex:
80
+ validator_secret_bytes = hex_to_bytes(validator_secret_hex, expected_length=32)
81
+ try:
82
+ from cryptography.hazmat.primitives import serialization
83
+ from cryptography.hazmat.primitives.asymmetric import ed25519
84
+
85
+ validator_private = ed25519.Ed25519PrivateKey.from_private_bytes(
86
+ validator_secret_bytes
87
+ )
88
+ except Exception as exc:
89
+ raise ValueError("invalid validation_secret_key") from exc
90
+
91
+ validator_public_bytes = validator_private.public_key().public_bytes(
92
+ encoding=serialization.Encoding.Raw,
93
+ format=serialization.PublicFormat.Raw,
94
+ )
95
+
96
+ node.validation_secret_key = validator_private
97
+ node.validation_public_key = validator_public_bytes
98
+
99
+ if node.latest_block_hash is None:
100
+ genesis_block = create_genesis_block(
101
+ node,
102
+ validator_public_key=validator_public_bytes,
103
+ validator_secret_key=validator_secret_bytes,
104
+ )
105
+ genesis_hash, genesis_atoms = genesis_block.to_atom()
106
+ if hasattr(node, "_local_set"):
107
+ for atom in genesis_atoms:
108
+ try:
109
+ node._local_set(atom.object_id(), atom)
110
+ except Exception:
111
+ pass
112
+ node.latest_block_hash = genesis_hash
113
+ node.latest_block = genesis_block
114
+
68
115
  node.consensus_validation_thread.start()