astreum 0.2.40__py3-none-any.whl → 0.2.41__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,3 +1,5 @@
1
+ from .account import Account
2
+ from .accounts import Accounts
1
3
  from .block import Block
2
4
  from .chain import Chain
3
5
  from .fork import Fork
@@ -12,5 +14,7 @@ __all__ = [
12
14
  "Fork",
13
15
  "Receipt",
14
16
  "Transaction",
17
+ "Account",
18
+ "Accounts",
15
19
  "consensus_setup",
16
20
  ]
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Callable, List, Optional, Tuple
5
+
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
65
+
66
+
67
+ @dataclass
68
+ class Account:
69
+ _balance: int
70
+ _data: bytes
71
+ _nonce: int
72
+ hash: bytes = ZERO32
73
+ atoms: List[Atom] = field(default_factory=list)
74
+
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
+ @classmethod
92
+ def create(cls, balance: int, data: bytes, nonce: int) -> "Account":
93
+ account_hash, atoms = cls._encode(balance, data, nonce)
94
+ return cls(
95
+ _balance=int(balance),
96
+ _data=bytes(data),
97
+ _nonce=int(nonce),
98
+ hash=account_hash,
99
+ atoms=atoms,
100
+ )
101
+
102
+ @classmethod
103
+ def from_atom(cls, source: Any, account_id: bytes) -> "Account":
104
+ storage_get = _resolve_storage_get(source)
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)")
113
+
114
+ entries = _read_list_entries(storage_get, outer_value.next)
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)
120
+ if type_atom is None or type_atom.data != b"account":
121
+ raise ValueError("not an account (type mismatch)")
122
+
123
+ body_list = storage_get(body_id)
124
+ if body_list is None or body_list.data != b"list":
125
+ raise ValueError("malformed account body (type)")
126
+
127
+ body_value = storage_get(body_list.next)
128
+ if body_value is None:
129
+ raise ValueError("malformed account body (value)")
130
+
131
+ field_ids = _read_list_entries(storage_get, body_value.next)
132
+ if len(field_ids) < 3:
133
+ field_ids.extend([ZERO32] * (3 - len(field_ids)))
134
+
135
+ def _read_field(field_id: bytes) -> bytes:
136
+ if not field_id or field_id == ZERO32:
137
+ return b""
138
+ atom = storage_get(field_id)
139
+ return atom.data if atom is not None else b""
140
+
141
+ balance_bytes = _read_field(field_ids[0])
142
+ data_bytes = _read_field(field_ids[1])
143
+ nonce_bytes = _read_field(field_ids[2])
144
+
145
+ account = cls.create(
146
+ balance=_be_bytes_to_int(balance_bytes),
147
+ data=data_bytes,
148
+ nonce=_be_bytes_to_int(nonce_bytes),
149
+ )
150
+ if account.hash != account_id:
151
+ raise ValueError("account hash mismatch while decoding")
152
+ return account
153
+
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
+ def to_atom(self) -> Tuple[bytes, List[Atom]]:
167
+ account_hash, atoms = self._encode(self._balance, self._data, self._nonce)
168
+ self.hash = account_hash
169
+ self.atoms = atoms
170
+ return account_hash, list(atoms)
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, Iterable, Optional, Tuple
5
+
6
+ from .._storage.atom import Atom
7
+ from .._storage.patricia import PatriciaTrie
8
+ from .account import Account
9
+
10
+
11
+ class Accounts:
12
+ def __init__(
13
+ self,
14
+ root_hash: Optional[bytes] = None,
15
+ ) -> None:
16
+ self._trie = PatriciaTrie(root_hash=root_hash)
17
+ 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
+
23
+ @property
24
+ def root_hash(self) -> Optional[bytes]:
25
+ return self._trie.root_hash
26
+
27
+ def _resolve_node(self, node: Optional[Any]) -> Any:
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
+
40
+ cached = self._cache.get(address)
41
+ if cached is not None:
42
+ return cached
43
+
44
+ storage_node = self._resolve_node(node)
45
+ account_id: Optional[bytes] = self._trie.get(storage_node, address)
46
+ if account_id is None:
47
+ return None
48
+
49
+ account = Account.from_atom(storage_node, account_id)
50
+ self._cache[address] = account
51
+ return account
52
+
53
+ 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
+ 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()}
@@ -1,5 +1,5 @@
1
1
 
2
- from typing import Callable, List, Optional, Tuple, TYPE_CHECKING
2
+ from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING
3
3
 
4
4
  from .._storage.atom import Atom, ZERO32
5
5
 
@@ -181,7 +181,14 @@ class Block:
181
181
  return self.hash, atoms_acc
182
182
 
183
183
  @classmethod
184
- def from_atom(cls, storage_get: Callable[[bytes], Optional[Atom]], block_id: bytes) -> "Block":
184
+ def from_atom(cls, source: Any, block_id: bytes) -> "Block":
185
+ storage_get: Optional[Callable[[bytes], Optional[Atom]]]
186
+ if callable(source):
187
+ storage_get = source
188
+ else:
189
+ storage_get = getattr(source, "_local_get", None)
190
+ if not callable(storage_get):
191
+ raise TypeError("Block.from_atom requires a node with '_local_get' or a callable storage getter")
185
192
  # 1) Expect main list
186
193
  main_typ = storage_get(block_id)
187
194
  if main_typ is None or main_typ.data != b"list":
@@ -1,66 +1,66 @@
1
1
  # chain.py
2
- from typing import Callable, Dict, Optional
3
- from .block import Block
4
- from .._storage.atom import ZERO32, Atom
5
-
6
- class Chain:
7
- def __init__(self, head_block: Block):
8
- self.head_block = head_block
9
- self.validated_upto_block = None
10
- # Root (genesis) hash for this chain; set by validation setup when known
11
- self.root: Optional[bytes] = None
12
- # Fork position: the head hash of the default/current fork for this chain
13
- self.fork_position: Optional[bytes] = getattr(head_block, "hash", None)
14
- # Mark the first malicious block encountered during validation; None means not found
15
- self.malicious_block_hash: Optional[bytes] = None
16
-
17
- def validate(self, storage_get: Callable[[bytes], Atom]) -> Block:
18
- """Validate the chain from head to genesis and return the root block.
19
-
20
- Incorporates per-block validation (signature on body and timestamp
21
- monotonicity). Uses a simple cache to avoid duplicate Atom fetches and
22
- duplicate block decoding during the backward walk.
23
- """
24
- # Atom and Block caches for this validation pass
25
- atom_cache: Dict[bytes, Optional[Atom]] = {}
26
- block_cache: Dict[bytes, Block] = {}
27
-
28
- def get_cached(k: bytes) -> Optional[Atom]:
29
- if k in atom_cache:
30
- return atom_cache[k]
31
- a = storage_get(k)
32
- atom_cache[k] = a
33
- return a
34
-
35
- def load_block(bid: bytes) -> Block:
36
- if bid in block_cache:
37
- return block_cache[bid]
38
- b = Block.from_atom(get_cached, bid) # type: ignore[arg-type]
39
- block_cache[bid] = b
40
- return b
41
-
42
- blk = self.head_block
43
- # Ensure head is in cache if it has a hash
44
- if getattr(blk, "hash", None):
45
- block_cache[blk.hash] = blk # type: ignore[attr-defined]
46
-
47
- # Walk back, validating each block
48
- while True:
49
- # Validate current block (signature over body, timestamp rule)
50
- try:
51
- blk.validate(get_cached) # may decode previous but uses cached atoms
52
- except Exception:
53
- # record first failure point then propagate
54
- self.malicious_block_hash = getattr(blk, "hash", None)
55
- raise
56
-
57
- prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
58
- if prev_hash == ZERO32:
59
- break
60
- # Move to previous block using cache-aware loader
61
- prev_blk = load_block(prev_hash)
62
- blk.previous_block = prev_blk # cache the object for any downstream use
63
- blk = prev_blk
64
-
65
- self.validated_upto_block = blk
66
- return blk
2
+ from typing import Callable, Dict, Optional
3
+ from .block import Block
4
+ from .._storage.atom import ZERO32, Atom
5
+
6
+ class Chain:
7
+ def __init__(self, head_block: Block):
8
+ self.head_block = head_block
9
+ self.validated_upto_block = None
10
+ # Root (genesis) hash for this chain; set by validation setup when known
11
+ self.root: Optional[bytes] = None
12
+ # Fork position: the head hash of the default/current fork for this chain
13
+ self.fork_position: Optional[bytes] = getattr(head_block, "hash", None)
14
+ # Mark the first malicious block encountered during validation; None means not found
15
+ self.malicious_block_hash: Optional[bytes] = None
16
+
17
+ def validate(self, storage_get: Callable[[bytes], Atom]) -> Block:
18
+ """Validate the chain from head to genesis and return the root block.
19
+
20
+ Incorporates per-block validation (signature on body and timestamp
21
+ monotonicity). Uses a simple cache to avoid duplicate Atom fetches and
22
+ duplicate block decoding during the backward walk.
23
+ """
24
+ # Atom and Block caches for this validation pass
25
+ atom_cache: Dict[bytes, Optional[Atom]] = {}
26
+ block_cache: Dict[bytes, Block] = {}
27
+
28
+ def get_cached(k: bytes) -> Optional[Atom]:
29
+ if k in atom_cache:
30
+ return atom_cache[k]
31
+ a = storage_get(k)
32
+ atom_cache[k] = a
33
+ return a
34
+
35
+ def load_block(bid: bytes) -> Block:
36
+ if bid in block_cache:
37
+ return block_cache[bid]
38
+ b = Block.from_atom(get_cached, bid)
39
+ block_cache[bid] = b
40
+ return b
41
+
42
+ blk = self.head_block
43
+ # Ensure head is in cache if it has a hash
44
+ if getattr(blk, "hash", None):
45
+ block_cache[blk.hash] = blk # type: ignore[attr-defined]
46
+
47
+ # Walk back, validating each block
48
+ while True:
49
+ # Validate current block (signature over body, timestamp rule)
50
+ try:
51
+ blk.validate(get_cached) # may decode previous but uses cached atoms
52
+ except Exception:
53
+ # record first failure point then propagate
54
+ self.malicious_block_hash = getattr(blk, "hash", None)
55
+ raise
56
+
57
+ prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
58
+ if prev_hash == ZERO32:
59
+ break
60
+ # Move to previous block using cache-aware loader
61
+ prev_blk = load_block(prev_hash)
62
+ blk.previous_block = prev_blk # cache the object for any downstream use
63
+ blk = prev_blk
64
+
65
+ self.validated_upto_block = blk
66
+ return blk
@@ -1,100 +1,100 @@
1
- from __future__ import annotations
2
-
1
+ from __future__ import annotations
2
+
3
3
  from typing import Optional, Set, Any, Callable, Dict
4
- from .block import Block
5
- from .._storage.atom import ZERO32, Atom
6
-
7
-
8
- class Fork:
9
- """A branch head within a Chain (same root).
10
-
11
- - head: current tip block id (bytes)
12
- - peers: identifiers (e.g., peer pubkey objects) following this head
13
- - root: genesis block id for this chain (optional)
14
- - validated_upto: earliest verified ancestor (optional)
15
- - chain_fork_position: the chain's fork anchor relevant to this fork
16
- """
17
-
18
- def __init__(
19
- self,
20
- head: bytes,
21
- ) -> None:
22
- self.head: bytes = head
23
- self.peers: Set[Any] = set()
24
- self.root: Optional[bytes] = None
25
- self.validated_upto: Optional[bytes] = None
26
- self.chain_fork_position: Optional[bytes] = None
27
- # Mark the first block found malicious during validation; None means not found
28
- self.malicious_block_hash: Optional[bytes] = None
29
-
30
- def add_peer(self, peer_id: Any) -> None:
31
- self.peers.add(peer_id)
32
-
33
- def remove_peer(self, peer_id: Any) -> None:
34
- self.peers.discard(peer_id)
35
-
36
- def validate(
37
- self,
38
- storage_get: Callable[[bytes], Optional[object]],
39
- stop_heads: Optional[Set[bytes]] = None,
40
- ) -> bool:
41
- """Validate only up to the chain fork position, not genesis.
42
-
43
- Returns True if self.head descends from self.chain_fork_position (or if
44
- chain_fork_position is None/equals head), and updates validated_upto to
45
- that anchor. If stop_heads is provided, returns True early if ancestry
46
- reaches any of those heads, setting validated_upto to the matched head.
47
- Returns False if ancestry cannot be confirmed.
48
- """
49
- if self.chain_fork_position is None or self.chain_fork_position == self.head:
50
- self.validated_upto = self.head
51
- return True
52
- # Caches to avoid double fetching/decoding
53
- atom_cache: Dict[bytes, Optional[Atom]] = {}
54
- block_cache: Dict[bytes, Block] = {}
55
-
56
- def get_cached(k: bytes) -> Optional[Atom]:
57
- if k in atom_cache:
58
- return atom_cache[k]
59
- a = storage_get(k) # type: ignore[call-arg]
60
- atom_cache[k] = a # may be None if missing
61
- return a
62
-
63
- def load_block(bid: bytes) -> Optional[Block]:
64
- if bid in block_cache:
65
- return block_cache[bid]
66
- try:
67
- b = Block.from_atom(get_cached, bid) # type: ignore[arg-type]
68
- except Exception:
69
- return None
70
- block_cache[bid] = b
71
- return b
72
-
73
- blk = load_block(self.head)
74
- if blk is None:
75
- # Missing head data: unverifiable, not malicious
76
- return False
77
- # Walk up to fork anchor, validating each block signature + timestamp
78
- while True:
79
- try:
80
- blk.validate(get_cached) # type: ignore[arg-type]
81
- except Exception:
82
- # mark the first failure point
83
- self.malicious_block_hash = blk.hash
84
- return False
85
-
86
- # Early-exit if we met another known fork head
87
- if stop_heads and blk.hash in stop_heads:
88
- self.validated_upto = blk.hash
89
- return True
90
-
91
- if blk.hash == self.chain_fork_position:
92
- self.validated_upto = blk.hash
93
- return True
94
-
95
- prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
96
- nxt = load_block(prev_hash)
97
- if nxt is None:
98
- return False
99
- blk.previous_block = nxt # cache for future use
100
- blk = nxt
4
+ from .block import Block
5
+ from .._storage.atom import ZERO32, Atom
6
+
7
+
8
+ class Fork:
9
+ """A branch head within a Chain (same root).
10
+
11
+ - head: current tip block id (bytes)
12
+ - peers: identifiers (e.g., peer pubkey objects) following this head
13
+ - root: genesis block id for this chain (optional)
14
+ - validated_upto: earliest verified ancestor (optional)
15
+ - chain_fork_position: the chain's fork anchor relevant to this fork
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ head: bytes,
21
+ ) -> None:
22
+ self.head: bytes = head
23
+ self.peers: Set[Any] = set()
24
+ self.root: Optional[bytes] = None
25
+ self.validated_upto: Optional[bytes] = None
26
+ self.chain_fork_position: Optional[bytes] = None
27
+ # Mark the first block found malicious during validation; None means not found
28
+ self.malicious_block_hash: Optional[bytes] = None
29
+
30
+ def add_peer(self, peer_id: Any) -> None:
31
+ self.peers.add(peer_id)
32
+
33
+ def remove_peer(self, peer_id: Any) -> None:
34
+ self.peers.discard(peer_id)
35
+
36
+ def validate(
37
+ self,
38
+ storage_get: Callable[[bytes], Optional[object]],
39
+ stop_heads: Optional[Set[bytes]] = None,
40
+ ) -> bool:
41
+ """Validate only up to the chain fork position, not genesis.
42
+
43
+ Returns True if self.head descends from self.chain_fork_position (or if
44
+ chain_fork_position is None/equals head), and updates validated_upto to
45
+ that anchor. If stop_heads is provided, returns True early if ancestry
46
+ reaches any of those heads, setting validated_upto to the matched head.
47
+ Returns False if ancestry cannot be confirmed.
48
+ """
49
+ if self.chain_fork_position is None or self.chain_fork_position == self.head:
50
+ self.validated_upto = self.head
51
+ return True
52
+ # Caches to avoid double fetching/decoding
53
+ atom_cache: Dict[bytes, Optional[Atom]] = {}
54
+ block_cache: Dict[bytes, Block] = {}
55
+
56
+ def get_cached(k: bytes) -> Optional[Atom]:
57
+ if k in atom_cache:
58
+ return atom_cache[k]
59
+ a = storage_get(k) # type: ignore[call-arg]
60
+ atom_cache[k] = a # may be None if missing
61
+ return a
62
+
63
+ def load_block(bid: bytes) -> Optional[Block]:
64
+ if bid in block_cache:
65
+ return block_cache[bid]
66
+ try:
67
+ b = Block.from_atom(get_cached, bid)
68
+ except Exception:
69
+ return None
70
+ block_cache[bid] = b
71
+ return b
72
+
73
+ blk = load_block(self.head)
74
+ if blk is None:
75
+ # Missing head data: unverifiable, not malicious
76
+ return False
77
+ # Walk up to fork anchor, validating each block signature + timestamp
78
+ while True:
79
+ try:
80
+ blk.validate(get_cached) # type: ignore[arg-type]
81
+ except Exception:
82
+ # mark the first failure point
83
+ self.malicious_block_hash = blk.hash
84
+ return False
85
+
86
+ # Early-exit if we met another known fork head
87
+ if stop_heads and blk.hash in stop_heads:
88
+ self.validated_upto = blk.hash
89
+ return True
90
+
91
+ if blk.hash == self.chain_fork_position:
92
+ self.validated_upto = blk.hash
93
+ return True
94
+
95
+ prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
96
+ nxt = load_block(prev_hash)
97
+ if nxt is None:
98
+ return False
99
+ blk.previous_block = nxt # cache for future use
100
+ blk = nxt