astreum 0.2.40__py3-none-any.whl → 0.2.42__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.
Potentially problematic release.
This version of astreum might be problematic. Click here for more details.
- astreum/_consensus/__init__.py +4 -0
- astreum/_consensus/account.py +95 -0
- astreum/_consensus/accounts.py +38 -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 +71 -23
- 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 +22 -22
- astreum/node.py +755 -753
- astreum/utils/integer.py +25 -0
- {astreum-0.2.40.dist-info → astreum-0.2.42.dist-info}/METADATA +1 -1
- {astreum-0.2.40.dist-info → astreum-0.2.42.dist-info}/RECORD +23 -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.42.dist-info}/WHEEL +0 -0
- {astreum-0.2.40.dist-info → astreum-0.2.42.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.40.dist-info → astreum-0.2.42.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,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from .._storage.atom import Atom, ZERO32
|
|
7
|
+
from .._storage.patricia import PatriciaTrie
|
|
8
|
+
from ..utils.integer import bytes_to_int, int_to_bytes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Account:
|
|
13
|
+
balance: int
|
|
14
|
+
code: bytes
|
|
15
|
+
counter: int
|
|
16
|
+
data_hash: bytes
|
|
17
|
+
data: PatriciaTrie
|
|
18
|
+
hash: bytes = ZERO32
|
|
19
|
+
body_hash: bytes = ZERO32
|
|
20
|
+
atoms: List[Atom] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
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)),
|
|
30
|
+
)
|
|
31
|
+
account.to_atom()
|
|
32
|
+
return account
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_atom(cls, node: Any, account_id: bytes) -> "Account":
|
|
36
|
+
storage_get = node._local_get
|
|
37
|
+
|
|
38
|
+
type_atom = storage_get(account_id)
|
|
39
|
+
if type_atom is None or type_atom.data != b"account":
|
|
40
|
+
raise ValueError("not an account (type mismatch)")
|
|
41
|
+
|
|
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)
|
|
46
|
+
|
|
47
|
+
balance_atom = _read_atom(type_atom.next)
|
|
48
|
+
if balance_atom is None:
|
|
49
|
+
raise ValueError("malformed account (balance missing)")
|
|
50
|
+
|
|
51
|
+
code_atom = _read_atom(balance_atom.next)
|
|
52
|
+
if code_atom is None:
|
|
53
|
+
raise ValueError("malformed account (code missing)")
|
|
54
|
+
|
|
55
|
+
counter_atom = _read_atom(code_atom.next)
|
|
56
|
+
if counter_atom is None:
|
|
57
|
+
raise ValueError("malformed account (counter missing)")
|
|
58
|
+
|
|
59
|
+
data_atom = _read_atom(counter_atom.next)
|
|
60
|
+
if data_atom is None:
|
|
61
|
+
raise ValueError("malformed account (data missing)")
|
|
62
|
+
|
|
63
|
+
account = cls.create(
|
|
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,
|
|
68
|
+
)
|
|
69
|
+
if account.hash != account_id:
|
|
70
|
+
raise ValueError("account hash mismatch while decoding")
|
|
71
|
+
return account
|
|
72
|
+
|
|
73
|
+
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
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()
|
|
92
|
+
self.hash = account_hash
|
|
93
|
+
self.body_hash = account_hash
|
|
94
|
+
self.atoms = atoms
|
|
95
|
+
return account_hash, list(atoms)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from .._storage.patricia import PatriciaTrie
|
|
6
|
+
from .account import Account
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Accounts:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
root_hash: Optional[bytes] = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
self._trie = PatriciaTrie(root_hash=root_hash)
|
|
15
|
+
self._cache: Dict[bytes, Account] = {}
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def root_hash(self) -> Optional[bytes]:
|
|
19
|
+
return self._trie.root_hash
|
|
20
|
+
|
|
21
|
+
def get_account(self, address: bytes, node: Optional[Any] = None) -> Optional[Account]:
|
|
22
|
+
cached = self._cache.get(address)
|
|
23
|
+
if cached is not None:
|
|
24
|
+
return cached
|
|
25
|
+
|
|
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)
|
|
30
|
+
if account_id is None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
account = Account.from_atom(node, account_id)
|
|
34
|
+
self._cache[address] = account
|
|
35
|
+
return account
|
|
36
|
+
|
|
37
|
+
def set_account(self, address: bytes, account: Account) -> None:
|
|
38
|
+
self._cache[address] = account
|
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
|
astreum/_consensus/genesis.py
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Iterable, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
7
|
+
|
|
8
|
+
from .account import Account
|
|
9
|
+
from .block import Block
|
|
10
|
+
from .._storage.atom import Atom, ZERO32
|
|
11
|
+
from .._storage.patricia import PatriciaTrie, PatriciaNode
|
|
12
|
+
|
|
13
|
+
TREASURY_ADDRESS = b"\x01" * 32
|
|
14
|
+
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
|
+
def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
|
|
76
|
+
validator_pk = bytes(validator_public_key)
|
|
77
|
+
|
|
78
|
+
if len(validator_pk) != 32:
|
|
79
|
+
raise ValueError("validator_public_key must be 32 bytes")
|
|
80
|
+
|
|
81
|
+
# 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
|
|
82
|
+
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
|
|
87
|
+
|
|
88
|
+
# 2. Account trie with treasury, burn, and validator accounts.
|
|
89
|
+
accounts_trie = PatriciaTrie()
|
|
90
|
+
|
|
91
|
+
treasury_account = Account.create(balance=1, data=stake_root, counter=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"", counter=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)
|
|
100
|
+
|
|
101
|
+
validator_account = Account.create(balance=0, data=b"", counter=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)
|
|
105
|
+
|
|
106
|
+
_persist_trie(accounts_trie, node)
|
|
107
|
+
|
|
108
|
+
accounts_root = accounts_trie.root_hash
|
|
109
|
+
if accounts_root is None:
|
|
110
|
+
raise ValueError("genesis accounts trie is empty")
|
|
111
|
+
|
|
112
|
+
# 3. Assemble block metadata.
|
|
113
|
+
block = Block()
|
|
114
|
+
block.previous_block_hash = ZERO32
|
|
115
|
+
block.number = 0
|
|
116
|
+
block.timestamp = 0
|
|
117
|
+
block.accounts_hash = accounts_root
|
|
118
|
+
block.accounts = accounts_trie
|
|
119
|
+
block.transactions_total_fees = 0
|
|
120
|
+
block.transactions_hash = ZERO32
|
|
121
|
+
block.receipts_hash = ZERO32
|
|
122
|
+
block.delay_difficulty = 0
|
|
123
|
+
block.delay_output = b""
|
|
124
|
+
block.validator_public_key = validator_pk
|
|
125
|
+
block.transactions = []
|
|
126
|
+
block.receipts = []
|
|
127
|
+
|
|
128
|
+
# 4. Sign the block body with the validator secret key.
|
|
129
|
+
block.signature = b""
|
|
130
|
+
block.to_atom()
|
|
131
|
+
|
|
132
|
+
if block.body_hash is None:
|
|
133
|
+
raise ValueError("failed to materialise genesis block body")
|
|
134
|
+
|
|
135
|
+
secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
|
|
136
|
+
block.signature = secret.sign(block.body_hash)
|
|
137
|
+
block_hash, block_atoms = block.to_atom()
|
|
138
|
+
_store_atoms(node, block_atoms)
|
|
139
|
+
|
|
140
|
+
block.hash = block_hash
|
|
141
|
+
return block
|
astreum/_consensus/receipt.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Callable, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from .._storage.atom import Atom, ZERO32
|
|
@@ -77,6 +77,8 @@ class Receipt:
|
|
|
77
77
|
cost: int = 0
|
|
78
78
|
logs: bytes = b""
|
|
79
79
|
status: int = 0
|
|
80
|
+
hash: bytes = ZERO32
|
|
81
|
+
atoms: List[Atom] = field(default_factory=list)
|
|
80
82
|
|
|
81
83
|
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
82
84
|
"""Serialise the receipt into Atom storage."""
|
|
@@ -109,6 +111,13 @@ class Receipt:
|
|
|
109
111
|
|
|
110
112
|
return top_list_id, atoms
|
|
111
113
|
|
|
114
|
+
def atomize(self) -> Tuple[bytes, List[Atom]]:
|
|
115
|
+
"""Generate atoms for this receipt and cache them."""
|
|
116
|
+
receipt_id, atoms = self.to_atom()
|
|
117
|
+
self.hash = receipt_id
|
|
118
|
+
self.atoms = atoms
|
|
119
|
+
return receipt_id, atoms
|
|
120
|
+
|
|
112
121
|
@classmethod
|
|
113
122
|
def from_atom(
|
|
114
123
|
cls,
|
|
@@ -164,4 +173,5 @@ class Receipt:
|
|
|
164
173
|
cost=_be_bytes_to_int(cost_bytes),
|
|
165
174
|
logs=logs_bytes,
|
|
166
175
|
status=status_value,
|
|
176
|
+
hash=bytes(receipt_id),
|
|
167
177
|
)
|