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.
- astreum/_consensus/__init__.py +4 -0
- astreum/_consensus/account.py +170 -0
- astreum/_consensus/accounts.py +67 -0
- astreum/_consensus/block.py +9 -2
- astreum/_consensus/chain.py +65 -65
- astreum/_consensus/fork.py +99 -99
- astreum/_consensus/genesis.py +141 -0
- astreum/_consensus/receipt.py +11 -1
- astreum/_consensus/setup.py +15 -152
- astreum/_consensus/transaction.py +27 -3
- astreum/_consensus/workers/__init__.py +9 -0
- astreum/_consensus/workers/discovery.py +48 -0
- astreum/_consensus/workers/validation.py +122 -0
- astreum/_consensus/workers/verify.py +63 -0
- astreum/_storage/atom.py +24 -7
- astreum/models/block.py +2 -2
- astreum/node.py +755 -753
- {astreum-0.2.40.dist-info → astreum-0.2.41.dist-info}/METADATA +1 -1
- {astreum-0.2.40.dist-info → astreum-0.2.41.dist-info}/RECORD +22 -28
- astreum/lispeum/__init__.py +0 -0
- astreum/lispeum/environment.py +0 -40
- astreum/lispeum/expression.py +0 -86
- astreum/lispeum/parser.py +0 -41
- astreum/lispeum/tokenizer.py +0 -52
- astreum/models/account.py +0 -91
- astreum/models/accounts.py +0 -34
- astreum/models/transaction.py +0 -106
- astreum/relay/__init__.py +0 -0
- astreum/relay/peer.py +0 -9
- astreum/relay/route.py +0 -25
- astreum/relay/setup.py +0 -58
- {astreum-0.2.40.dist-info → astreum-0.2.41.dist-info}/WHEEL +0 -0
- {astreum-0.2.40.dist-info → astreum-0.2.41.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.40.dist-info → astreum-0.2.41.dist-info}/top_level.txt +0 -0
astreum/_consensus/__init__.py
CHANGED
|
@@ -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()}
|
astreum/_consensus/block.py
CHANGED
|
@@ -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,
|
|
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":
|
astreum/_consensus/chain.py
CHANGED
|
@@ -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)
|
|
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
|
astreum/_consensus/fork.py
CHANGED
|
@@ -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)
|
|
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
|