astreum 0.2.61__py3-none-any.whl → 0.3.9__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/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +89 -0
- astreum/communication/handlers/object_request.py +176 -0
- astreum/communication/handlers/object_response.py +115 -0
- astreum/communication/handlers/ping.py +34 -0
- astreum/communication/handlers/route_request.py +76 -0
- astreum/communication/handlers/route_response.py +53 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/communication/models/message.py +124 -0
- astreum/communication/models/peer.py +51 -0
- astreum/{_communication → communication/models}/route.py +7 -12
- astreum/communication/processors/__init__.py +0 -0
- astreum/communication/processors/incoming.py +98 -0
- astreum/communication/processors/outgoing.py +20 -0
- astreum/communication/setup.py +166 -0
- astreum/communication/start.py +37 -0
- astreum/{_communication → communication}/util.py +7 -0
- astreum/consensus/__init__.py +20 -0
- astreum/consensus/genesis.py +66 -0
- astreum/consensus/models/__init__.py +0 -0
- astreum/consensus/models/account.py +84 -0
- astreum/consensus/models/accounts.py +72 -0
- astreum/consensus/models/block.py +364 -0
- astreum/{_consensus → consensus/models}/chain.py +7 -7
- astreum/{_consensus → consensus/models}/fork.py +8 -8
- astreum/consensus/models/receipt.py +98 -0
- astreum/{_consensus → consensus/models}/transaction.py +76 -78
- astreum/{_consensus → consensus}/setup.py +18 -50
- astreum/consensus/start.py +67 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +19 -1
- astreum/consensus/workers/validation.py +307 -0
- astreum/{_consensus → consensus}/workers/verify.py +29 -2
- astreum/crypto/chacha20poly1305.py +74 -0
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
- astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum/machine/models/__init__.py +0 -0
- astreum/machine/models/environment.py +31 -0
- astreum/{_lispeum → machine/models}/expression.py +36 -8
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +78 -767
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +183 -0
- astreum/storage/actions/set.py +178 -0
- astreum/{_storage → storage/models}/atom.py +55 -57
- astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
- astreum/storage/requests.py +28 -0
- astreum/storage/setup.py +22 -15
- astreum/utils/config.py +48 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
- astreum-0.3.9.dist-info/RECORD +71 -0
- astreum/_communication/message.py +0 -101
- astreum/_communication/peer.py +0 -23
- astreum/_communication/setup.py +0 -322
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -95
- astreum/_consensus/accounts.py +0 -38
- astreum/_consensus/block.py +0 -311
- astreum/_consensus/genesis.py +0 -72
- astreum/_consensus/receipt.py +0 -136
- astreum/_consensus/workers/validation.py +0 -125
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -198
- astreum/_storage/__init__.py +0 -7
- astreum/_storage/setup.py +0 -35
- astreum/format.py +0 -75
- astreum/models/block.py +0 -441
- astreum/models/merkle.py +0 -205
- astreum/models/patricia.py +0 -393
- astreum/storage/object.py +0 -68
- astreum-0.2.61.dist-info/RECORD +0 -57
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- /astreum/{_lispeum → machine}/parser.py +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, List, Tuple
|
|
5
|
+
|
|
6
|
+
from ...storage.models.atom import Atom, ZERO32, AtomKind
|
|
7
|
+
from ...storage.models.trie import Trie
|
|
8
|
+
from ...utils.integer import bytes_to_int, int_to_bytes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Account:
|
|
13
|
+
balance: int
|
|
14
|
+
code_hash: bytes
|
|
15
|
+
counter: int
|
|
16
|
+
data_hash: bytes
|
|
17
|
+
data: Trie
|
|
18
|
+
atom_hash: bytes = ZERO32
|
|
19
|
+
atoms: List[Atom] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def create(cls, balance: int = 0, data_hash: bytes = ZERO32, code_hash: bytes = ZERO32, counter: int = 0) -> "Account":
|
|
23
|
+
account = cls(
|
|
24
|
+
balance=int(balance),
|
|
25
|
+
code_hash=bytes(code_hash),
|
|
26
|
+
counter=int(counter),
|
|
27
|
+
data_hash=bytes(data_hash),
|
|
28
|
+
data=Trie(root_hash=bytes(data_hash)),
|
|
29
|
+
)
|
|
30
|
+
atom_hash, atoms = account.to_atom()
|
|
31
|
+
account.atom_hash = atom_hash
|
|
32
|
+
account.atoms = atoms
|
|
33
|
+
return account
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_atom(cls, node: Any, root_id: bytes) -> "Account":
|
|
37
|
+
|
|
38
|
+
account_atoms = node.get_atom_list_from_storage(root_hash=root_id)
|
|
39
|
+
|
|
40
|
+
if account_atoms is None or len(account_atoms) != 5:
|
|
41
|
+
raise ValueError("malformed account atom list")
|
|
42
|
+
|
|
43
|
+
type_atom, balance_atom, code_atom, counter_atom, data_atom = account_atoms
|
|
44
|
+
|
|
45
|
+
if type_atom.data != b"account":
|
|
46
|
+
raise ValueError("not an account (type mismatch)")
|
|
47
|
+
|
|
48
|
+
account = cls.create(
|
|
49
|
+
balance=bytes_to_int(balance_atom.data),
|
|
50
|
+
data_hash=data_atom.data,
|
|
51
|
+
counter=bytes_to_int(counter_atom.data),
|
|
52
|
+
code_hash=code_atom.data,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return account
|
|
56
|
+
|
|
57
|
+
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
58
|
+
data_atom = Atom(
|
|
59
|
+
data=bytes(self.data_hash),
|
|
60
|
+
kind=AtomKind.LIST,
|
|
61
|
+
)
|
|
62
|
+
counter_atom = Atom(
|
|
63
|
+
data=int_to_bytes(self.counter),
|
|
64
|
+
next_id=data_atom.object_id(),
|
|
65
|
+
kind=AtomKind.BYTES,
|
|
66
|
+
)
|
|
67
|
+
code_atom = Atom(
|
|
68
|
+
data=bytes(self.code_hash),
|
|
69
|
+
next_id=counter_atom.object_id(),
|
|
70
|
+
kind=AtomKind.LIST,
|
|
71
|
+
)
|
|
72
|
+
balance_atom = Atom(
|
|
73
|
+
data=int_to_bytes(self.balance),
|
|
74
|
+
next_id=code_atom.object_id(),
|
|
75
|
+
kind=AtomKind.BYTES,
|
|
76
|
+
)
|
|
77
|
+
type_atom = Atom(
|
|
78
|
+
data=b"account",
|
|
79
|
+
next_id=balance_atom.object_id(),
|
|
80
|
+
kind=AtomKind.SYMBOL,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
atoms = [data_atom, counter_atom, code_atom, balance_atom, type_atom]
|
|
84
|
+
return type_atom.object_id(), list(atoms)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from ...storage.models.atom import Atom, ZERO32
|
|
6
|
+
from ...storage.models.trie import Trie
|
|
7
|
+
from .account import Account
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Accounts:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
root_hash: Optional[bytes] = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
self._trie = Trie(root_hash=root_hash)
|
|
16
|
+
self._cache: Dict[bytes, Account] = {}
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def root_hash(self) -> Optional[bytes]:
|
|
20
|
+
return self._trie.root_hash
|
|
21
|
+
|
|
22
|
+
def get_account(self, address: bytes, node: Optional[Any] = None) -> Optional[Account]:
|
|
23
|
+
cached = self._cache.get(address)
|
|
24
|
+
if cached is not None:
|
|
25
|
+
return cached
|
|
26
|
+
|
|
27
|
+
if node is None:
|
|
28
|
+
raise ValueError("Accounts requires a node reference for trie access")
|
|
29
|
+
|
|
30
|
+
account_id: Optional[bytes] = self._trie.get(node, address)
|
|
31
|
+
if account_id is None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
account = Account.from_atom(node, account_id)
|
|
35
|
+
self._cache[address] = account
|
|
36
|
+
return account
|
|
37
|
+
|
|
38
|
+
def set_account(self, address: bytes, account: Account) -> None:
|
|
39
|
+
self._cache[address] = account
|
|
40
|
+
|
|
41
|
+
def update_trie(self, node: Any) -> List[Atom]:
|
|
42
|
+
"""
|
|
43
|
+
Serialise cached accounts, ensure their associated data tries are materialised,
|
|
44
|
+
and return all atoms that must be stored (data tries, account records, and the
|
|
45
|
+
accounts trie nodes themselves).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def _node_atoms(trie: Trie) -> List[Atom]:
|
|
49
|
+
emitted: List[Atom] = []
|
|
50
|
+
if not trie.nodes:
|
|
51
|
+
return emitted
|
|
52
|
+
for node_hash in sorted(trie.nodes.keys()):
|
|
53
|
+
trie_node = trie.nodes[node_hash]
|
|
54
|
+
head_hash, atoms = trie_node.to_atoms()
|
|
55
|
+
if head_hash != node_hash:
|
|
56
|
+
continue
|
|
57
|
+
emitted.extend(atoms)
|
|
58
|
+
return emitted
|
|
59
|
+
|
|
60
|
+
data_atoms: List[Atom] = []
|
|
61
|
+
account_atoms: List[Atom] = []
|
|
62
|
+
|
|
63
|
+
for address, account in self._cache.items():
|
|
64
|
+
account.data_hash = account.data.root_hash or ZERO32
|
|
65
|
+
data_atoms.extend(_node_atoms(account.data))
|
|
66
|
+
|
|
67
|
+
account_id, atoms = account.to_atom()
|
|
68
|
+
self._trie.put(node, address, account_id)
|
|
69
|
+
account_atoms.extend(atoms)
|
|
70
|
+
|
|
71
|
+
trie_atoms = _node_atoms(self._trie)
|
|
72
|
+
return data_atoms + account_atoms + trie_atoms
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ...storage.models.atom import Atom, AtomKind, ZERO32, hash_bytes
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ...storage.models.trie import Trie
|
|
8
|
+
from .transaction import Transaction
|
|
9
|
+
from .receipt import Receipt
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
11
|
+
from cryptography.exceptions import InvalidSignature
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _int_to_be_bytes(n: Optional[int]) -> bytes:
|
|
15
|
+
if n is None:
|
|
16
|
+
return b""
|
|
17
|
+
n = int(n)
|
|
18
|
+
if n == 0:
|
|
19
|
+
return b"\x00"
|
|
20
|
+
size = (n.bit_length() + 7) // 8
|
|
21
|
+
return n.to_bytes(size, "big")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _be_bytes_to_int(b: Optional[bytes]) -> int:
|
|
25
|
+
if not b:
|
|
26
|
+
return 0
|
|
27
|
+
return int.from_bytes(b, "big")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Block:
|
|
31
|
+
"""Validation Block representation using Atom storage.
|
|
32
|
+
|
|
33
|
+
Top-level encoding:
|
|
34
|
+
block_id = type_atom.object_id()
|
|
35
|
+
chain: type_atom --next--> signature_atom --next--> body_list_atom --next--> ZERO32
|
|
36
|
+
where: type_atom = Atom(kind=AtomKind.SYMBOL, data=b"block")
|
|
37
|
+
signature_atom = Atom(kind=AtomKind.BYTES, data=<signature-bytes>)
|
|
38
|
+
body_list_atom = Atom(kind=AtomKind.LIST, data=<body_head_id>)
|
|
39
|
+
|
|
40
|
+
Details order in body_list:
|
|
41
|
+
0: chain (byte)
|
|
42
|
+
1: previous_block_hash (bytes)
|
|
43
|
+
2: number (int -> big-endian bytes)
|
|
44
|
+
3: timestamp (int -> big-endian bytes)
|
|
45
|
+
4: accounts_hash (bytes)
|
|
46
|
+
5: transactions_total_fees (int -> big-endian bytes)
|
|
47
|
+
6: transactions_hash (bytes)
|
|
48
|
+
7: receipts_hash (bytes)
|
|
49
|
+
8: delay_difficulty (int -> big-endian bytes)
|
|
50
|
+
9: validator_public_key (bytes)
|
|
51
|
+
10: nonce (int -> big-endian bytes)
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- "body tree" is represented here by the body_list id (self.body_hash), not
|
|
55
|
+
embedded again as a field to avoid circular references.
|
|
56
|
+
- "signature" is a field on the class but is not required for validation
|
|
57
|
+
navigation; include it in the instance but it is not encoded in atoms
|
|
58
|
+
unless explicitly provided via details extension in the future.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# essential identifiers
|
|
62
|
+
atom_hash: Optional[bytes]
|
|
63
|
+
chain_id: int
|
|
64
|
+
previous_block_hash: bytes
|
|
65
|
+
previous_block: Optional["Block"]
|
|
66
|
+
|
|
67
|
+
# block details
|
|
68
|
+
number: int
|
|
69
|
+
timestamp: Optional[int]
|
|
70
|
+
accounts_hash: Optional[bytes]
|
|
71
|
+
transactions_total_fees: Optional[int]
|
|
72
|
+
transactions_hash: Optional[bytes]
|
|
73
|
+
receipts_hash: Optional[bytes]
|
|
74
|
+
delay_difficulty: Optional[int]
|
|
75
|
+
validator_public_key: Optional[bytes]
|
|
76
|
+
nonce: Optional[int]
|
|
77
|
+
|
|
78
|
+
# additional
|
|
79
|
+
body_hash: Optional[bytes]
|
|
80
|
+
signature: Optional[bytes]
|
|
81
|
+
|
|
82
|
+
# structures
|
|
83
|
+
accounts: Optional["Trie"]
|
|
84
|
+
transactions: Optional[List["Transaction"]]
|
|
85
|
+
receipts: Optional[List["Receipt"]]
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
chain_id: int,
|
|
91
|
+
previous_block_hash: bytes,
|
|
92
|
+
previous_block: Optional["Block"],
|
|
93
|
+
number: int,
|
|
94
|
+
timestamp: Optional[int],
|
|
95
|
+
accounts_hash: Optional[bytes],
|
|
96
|
+
transactions_total_fees: Optional[int],
|
|
97
|
+
transactions_hash: Optional[bytes],
|
|
98
|
+
receipts_hash: Optional[bytes],
|
|
99
|
+
delay_difficulty: Optional[int],
|
|
100
|
+
validator_public_key: Optional[bytes],
|
|
101
|
+
nonce: Optional[int] = None,
|
|
102
|
+
signature: Optional[bytes] = None,
|
|
103
|
+
atom_hash: Optional[bytes] = None,
|
|
104
|
+
body_hash: Optional[bytes] = None,
|
|
105
|
+
accounts: Optional["Trie"] = None,
|
|
106
|
+
transactions: Optional[List["Transaction"]] = None,
|
|
107
|
+
receipts: Optional[List["Receipt"]] = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
self.atom_hash = atom_hash
|
|
110
|
+
self.chain_id = chain_id
|
|
111
|
+
self.previous_block_hash = previous_block_hash
|
|
112
|
+
self.previous_block = previous_block
|
|
113
|
+
self.number = number
|
|
114
|
+
self.timestamp = timestamp
|
|
115
|
+
self.accounts_hash = accounts_hash
|
|
116
|
+
self.transactions_total_fees = transactions_total_fees
|
|
117
|
+
self.transactions_hash = transactions_hash
|
|
118
|
+
self.receipts_hash = receipts_hash
|
|
119
|
+
self.delay_difficulty = delay_difficulty
|
|
120
|
+
self.validator_public_key = validator_public_key
|
|
121
|
+
self.nonce = nonce
|
|
122
|
+
self.body_hash = body_hash
|
|
123
|
+
self.signature = signature
|
|
124
|
+
self.accounts = accounts
|
|
125
|
+
self.transactions = transactions
|
|
126
|
+
self.receipts = receipts
|
|
127
|
+
|
|
128
|
+
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
129
|
+
# Build body details as direct byte atoms, in defined order
|
|
130
|
+
detail_payloads: List[bytes] = []
|
|
131
|
+
block_atoms: List[Atom] = []
|
|
132
|
+
|
|
133
|
+
def _emit(detail_bytes: bytes) -> None:
|
|
134
|
+
detail_payloads.append(detail_bytes)
|
|
135
|
+
|
|
136
|
+
# 0: chain
|
|
137
|
+
_emit(_int_to_be_bytes(self.chain_id))
|
|
138
|
+
# 1: previous_block_hash
|
|
139
|
+
_emit(self.previous_block_hash)
|
|
140
|
+
# 2: number
|
|
141
|
+
_emit(_int_to_be_bytes(self.number))
|
|
142
|
+
# 3: timestamp
|
|
143
|
+
_emit(_int_to_be_bytes(self.timestamp))
|
|
144
|
+
# 4: accounts_hash
|
|
145
|
+
_emit(self.accounts_hash or b"")
|
|
146
|
+
# 5: transactions_total_fees
|
|
147
|
+
_emit(_int_to_be_bytes(self.transactions_total_fees))
|
|
148
|
+
# 6: transactions_hash
|
|
149
|
+
_emit(self.transactions_hash or b"")
|
|
150
|
+
# 7: receipts_hash
|
|
151
|
+
_emit(self.receipts_hash or b"")
|
|
152
|
+
# 8: delay_difficulty
|
|
153
|
+
_emit(_int_to_be_bytes(self.delay_difficulty))
|
|
154
|
+
# 9: validator_public_key
|
|
155
|
+
_emit(self.validator_public_key or b"")
|
|
156
|
+
# 10: nonce
|
|
157
|
+
_emit(_int_to_be_bytes(self.nonce))
|
|
158
|
+
|
|
159
|
+
# Build body list chain directly from detail atoms
|
|
160
|
+
body_head = ZERO32
|
|
161
|
+
detail_atoms: List[Atom] = []
|
|
162
|
+
for payload in reversed(detail_payloads):
|
|
163
|
+
atom = Atom(data=payload, next_id=body_head, kind=AtomKind.BYTES)
|
|
164
|
+
detail_atoms.append(atom)
|
|
165
|
+
body_head = atom.object_id()
|
|
166
|
+
detail_atoms.reverse()
|
|
167
|
+
|
|
168
|
+
block_atoms.extend(detail_atoms)
|
|
169
|
+
|
|
170
|
+
body_list_atom = Atom(data=body_head, kind=AtomKind.LIST)
|
|
171
|
+
self.body_hash = body_list_atom.object_id()
|
|
172
|
+
|
|
173
|
+
# Signature atom links to body list atom; type atom links to signature atom
|
|
174
|
+
sig_atom = Atom(
|
|
175
|
+
data=bytes(self.signature or b""),
|
|
176
|
+
next_id=self.body_hash,
|
|
177
|
+
kind=AtomKind.BYTES,
|
|
178
|
+
)
|
|
179
|
+
type_atom = Atom(data=b"block", next_id=sig_atom.object_id(), kind=AtomKind.SYMBOL)
|
|
180
|
+
|
|
181
|
+
block_atoms.append(body_list_atom)
|
|
182
|
+
block_atoms.append(sig_atom)
|
|
183
|
+
block_atoms.append(type_atom)
|
|
184
|
+
|
|
185
|
+
self.atom_hash = type_atom.object_id()
|
|
186
|
+
return self.atom_hash, block_atoms
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def from_atom(cls, node: Any, block_id: bytes) -> "Block":
|
|
190
|
+
|
|
191
|
+
block_header = node.get_atom_list_from_storage(block_id)
|
|
192
|
+
if block_header is None or len(block_header) != 3:
|
|
193
|
+
raise ValueError("malformed block atom chain")
|
|
194
|
+
type_atom, sig_atom, body_list_atom = block_header
|
|
195
|
+
|
|
196
|
+
if type_atom.kind is not AtomKind.SYMBOL or type_atom.data != b"block":
|
|
197
|
+
raise ValueError("not a block (type atom payload)")
|
|
198
|
+
if sig_atom.kind is not AtomKind.BYTES:
|
|
199
|
+
raise ValueError("malformed block (signature atom kind)")
|
|
200
|
+
if body_list_atom.kind is not AtomKind.LIST:
|
|
201
|
+
raise ValueError("malformed block (body list atom kind)")
|
|
202
|
+
if body_list_atom.next_id != ZERO32:
|
|
203
|
+
raise ValueError("malformed block (body list tail)")
|
|
204
|
+
|
|
205
|
+
detail_atoms = node.get_atom_list_from_storage(body_list_atom.data)
|
|
206
|
+
if detail_atoms is None:
|
|
207
|
+
raise ValueError("missing block body list nodes")
|
|
208
|
+
|
|
209
|
+
if len(detail_atoms) != 11:
|
|
210
|
+
raise ValueError("block body must contain exactly 11 detail entries")
|
|
211
|
+
|
|
212
|
+
detail_values: List[bytes] = []
|
|
213
|
+
for detail_atom in detail_atoms:
|
|
214
|
+
if detail_atom.kind is not AtomKind.BYTES:
|
|
215
|
+
raise ValueError("block body detail atoms must be bytes")
|
|
216
|
+
detail_values.append(detail_atom.data)
|
|
217
|
+
|
|
218
|
+
(
|
|
219
|
+
chain_bytes,
|
|
220
|
+
prev_bytes,
|
|
221
|
+
number_bytes,
|
|
222
|
+
timestamp_bytes,
|
|
223
|
+
accounts_bytes,
|
|
224
|
+
fees_bytes,
|
|
225
|
+
transactions_bytes,
|
|
226
|
+
receipts_bytes,
|
|
227
|
+
delay_diff_bytes,
|
|
228
|
+
validator_bytes,
|
|
229
|
+
nonce_bytes,
|
|
230
|
+
) = detail_values
|
|
231
|
+
|
|
232
|
+
return cls(
|
|
233
|
+
chain_id=_be_bytes_to_int(chain_bytes),
|
|
234
|
+
previous_block_hash=prev_bytes or ZERO32,
|
|
235
|
+
previous_block=None,
|
|
236
|
+
number=_be_bytes_to_int(number_bytes),
|
|
237
|
+
timestamp=_be_bytes_to_int(timestamp_bytes),
|
|
238
|
+
accounts_hash=accounts_bytes or None,
|
|
239
|
+
transactions_total_fees=_be_bytes_to_int(fees_bytes),
|
|
240
|
+
transactions_hash=transactions_bytes or None,
|
|
241
|
+
receipts_hash=receipts_bytes or None,
|
|
242
|
+
delay_difficulty=_be_bytes_to_int(delay_diff_bytes),
|
|
243
|
+
validator_public_key=validator_bytes or None,
|
|
244
|
+
nonce=_be_bytes_to_int(nonce_bytes),
|
|
245
|
+
signature=sig_atom.data if sig_atom is not None else None,
|
|
246
|
+
atom_hash=block_id,
|
|
247
|
+
body_hash=body_list_atom.object_id(),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def validate(self, storage_get: Callable[[bytes], Optional[Atom]]) -> bool:
|
|
251
|
+
"""Validate this block against storage.
|
|
252
|
+
|
|
253
|
+
Checks:
|
|
254
|
+
- Signature: signature must verify over the body list id using the
|
|
255
|
+
validator's public key.
|
|
256
|
+
- Timestamp monotonicity: if previous block exists (not ZERO32), this
|
|
257
|
+
block's timestamp must be >= previous.timestamp + 1.
|
|
258
|
+
"""
|
|
259
|
+
# Unverifiable if critical fields are missing
|
|
260
|
+
if not self.body_hash:
|
|
261
|
+
return False
|
|
262
|
+
if not self.signature:
|
|
263
|
+
return False
|
|
264
|
+
if not self.validator_public_key:
|
|
265
|
+
return False
|
|
266
|
+
if self.timestamp is None:
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
# 1) Signature check over body hash
|
|
270
|
+
try:
|
|
271
|
+
pub = Ed25519PublicKey.from_public_bytes(bytes(self.validator_public_key))
|
|
272
|
+
pub.verify(self.signature, self.body_hash)
|
|
273
|
+
except InvalidSignature as e:
|
|
274
|
+
raise ValueError("invalid signature") from e
|
|
275
|
+
|
|
276
|
+
# 2) Timestamp monotonicity against previous block
|
|
277
|
+
prev_ts: Optional[int] = None
|
|
278
|
+
prev_hash = self.previous_block_hash or ZERO32
|
|
279
|
+
|
|
280
|
+
if self.previous_block is not None:
|
|
281
|
+
prev_ts = int(self.previous_block.timestamp or 0)
|
|
282
|
+
prev_hash = self.previous_block.atom_hash or prev_hash or ZERO32
|
|
283
|
+
|
|
284
|
+
if prev_hash and prev_hash != ZERO32 and prev_ts is None:
|
|
285
|
+
# If previous block cannot be loaded, treat as unverifiable, not malicious
|
|
286
|
+
try:
|
|
287
|
+
prev = Block.from_atom(storage_get, prev_hash)
|
|
288
|
+
except Exception:
|
|
289
|
+
return False
|
|
290
|
+
prev_ts = int(prev.timestamp or 0)
|
|
291
|
+
|
|
292
|
+
if prev_hash and prev_hash != ZERO32:
|
|
293
|
+
if prev_ts is None:
|
|
294
|
+
return False
|
|
295
|
+
cur_ts = int(self.timestamp or 0)
|
|
296
|
+
if cur_ts < prev_ts + 1:
|
|
297
|
+
raise ValueError("timestamp must be at least prev+1")
|
|
298
|
+
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def _leading_zero_bits(buf: bytes) -> int:
|
|
303
|
+
"""Return the number of leading zero bits in the provided buffer."""
|
|
304
|
+
zeros = 0
|
|
305
|
+
for byte in buf:
|
|
306
|
+
if byte == 0:
|
|
307
|
+
zeros += 8
|
|
308
|
+
continue
|
|
309
|
+
zeros += 8 - int(byte).bit_length()
|
|
310
|
+
break
|
|
311
|
+
return zeros
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def calculate_delay_difficulty(
|
|
315
|
+
*,
|
|
316
|
+
previous_timestamp: Optional[int],
|
|
317
|
+
current_timestamp: Optional[int],
|
|
318
|
+
previous_difficulty: Optional[int],
|
|
319
|
+
target_spacing: int = 2,
|
|
320
|
+
) -> int:
|
|
321
|
+
"""
|
|
322
|
+
Adjust the delay difficulty based on how quickly the previous block was produced.
|
|
323
|
+
|
|
324
|
+
The previous block difficulty is increased if the spacing is below the target,
|
|
325
|
+
decreased if above, and returned unchanged when the target spacing is met.
|
|
326
|
+
"""
|
|
327
|
+
base_difficulty = max(1, int(previous_difficulty or 1))
|
|
328
|
+
if previous_timestamp is None or current_timestamp is None:
|
|
329
|
+
return base_difficulty
|
|
330
|
+
|
|
331
|
+
spacing = max(0, int(current_timestamp) - int(previous_timestamp))
|
|
332
|
+
if spacing <= 1:
|
|
333
|
+
adjusted = base_difficulty * 1.618
|
|
334
|
+
elif spacing == target_spacing:
|
|
335
|
+
adjusted = float(base_difficulty)
|
|
336
|
+
elif spacing > target_spacing:
|
|
337
|
+
adjusted = base_difficulty * 0.618
|
|
338
|
+
else:
|
|
339
|
+
adjusted = float(base_difficulty)
|
|
340
|
+
|
|
341
|
+
return max(1, int(round(adjusted)))
|
|
342
|
+
|
|
343
|
+
def generate_nonce(
|
|
344
|
+
self,
|
|
345
|
+
*,
|
|
346
|
+
difficulty: int,
|
|
347
|
+
) -> int:
|
|
348
|
+
"""
|
|
349
|
+
Find a nonce that yields a block hash with the required leading zero bits.
|
|
350
|
+
|
|
351
|
+
The search starts from the current nonce and iterates until the target
|
|
352
|
+
difficulty is met.
|
|
353
|
+
"""
|
|
354
|
+
target = max(1, int(difficulty))
|
|
355
|
+
start = int(self.nonce or 0)
|
|
356
|
+
nonce = start
|
|
357
|
+
while True:
|
|
358
|
+
self.nonce = nonce
|
|
359
|
+
block_hash, _ = self.to_atom()
|
|
360
|
+
leading_zeros = self._leading_zero_bits(block_hash)
|
|
361
|
+
if leading_zeros >= target:
|
|
362
|
+
self.atom_hash = block_hash
|
|
363
|
+
return nonce
|
|
364
|
+
nonce += 1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# chain.py
|
|
1
|
+
# chain.py
|
|
2
2
|
from typing import Callable, Dict, Optional
|
|
3
3
|
from .block import Block
|
|
4
|
-
from
|
|
4
|
+
from ...storage.models.atom import ZERO32, Atom
|
|
5
5
|
|
|
6
6
|
class Chain:
|
|
7
7
|
def __init__(self, head_block: Block):
|
|
@@ -10,7 +10,7 @@ class Chain:
|
|
|
10
10
|
# Root (genesis) hash for this chain; set by validation setup when known
|
|
11
11
|
self.root: Optional[bytes] = None
|
|
12
12
|
# Fork position: the head hash of the default/current fork for this chain
|
|
13
|
-
self.fork_position: Optional[bytes] = getattr(head_block, "
|
|
13
|
+
self.fork_position: Optional[bytes] = getattr(head_block, "atom_hash", None)
|
|
14
14
|
# Mark the first malicious block encountered during validation; None means not found
|
|
15
15
|
self.malicious_block_hash: Optional[bytes] = None
|
|
16
16
|
|
|
@@ -35,14 +35,14 @@ class Chain:
|
|
|
35
35
|
def load_block(bid: bytes) -> Block:
|
|
36
36
|
if bid in block_cache:
|
|
37
37
|
return block_cache[bid]
|
|
38
|
-
b = Block.from_atom(get_cached, bid)
|
|
38
|
+
b = Block.from_atom(get_cached, bid)
|
|
39
39
|
block_cache[bid] = b
|
|
40
40
|
return b
|
|
41
41
|
|
|
42
42
|
blk = self.head_block
|
|
43
43
|
# Ensure head is in cache if it has a hash
|
|
44
|
-
if getattr(blk, "
|
|
45
|
-
block_cache[blk.
|
|
44
|
+
if getattr(blk, "atom_hash", None):
|
|
45
|
+
block_cache[blk.atom_hash] = blk # type: ignore[attr-defined]
|
|
46
46
|
|
|
47
47
|
# Walk back, validating each block
|
|
48
48
|
while True:
|
|
@@ -51,7 +51,7 @@ class Chain:
|
|
|
51
51
|
blk.validate(get_cached) # may decode previous but uses cached atoms
|
|
52
52
|
except Exception:
|
|
53
53
|
# record first failure point then propagate
|
|
54
|
-
self.malicious_block_hash = getattr(blk, "
|
|
54
|
+
self.malicious_block_hash = getattr(blk, "atom_hash", None)
|
|
55
55
|
raise
|
|
56
56
|
|
|
57
57
|
prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Optional, Set, Any, Callable, Dict
|
|
3
|
+
from typing import Optional, Set, Any, Callable, Dict
|
|
4
4
|
from .block import Block
|
|
5
|
-
from
|
|
5
|
+
from ...storage.models.atom import ZERO32, Atom
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Fork:
|
|
@@ -64,7 +64,7 @@ class Fork:
|
|
|
64
64
|
if bid in block_cache:
|
|
65
65
|
return block_cache[bid]
|
|
66
66
|
try:
|
|
67
|
-
b = Block.from_atom(get_cached, bid)
|
|
67
|
+
b = Block.from_atom(get_cached, bid)
|
|
68
68
|
except Exception:
|
|
69
69
|
return None
|
|
70
70
|
block_cache[bid] = b
|
|
@@ -80,16 +80,16 @@ class Fork:
|
|
|
80
80
|
blk.validate(get_cached) # type: ignore[arg-type]
|
|
81
81
|
except Exception:
|
|
82
82
|
# mark the first failure point
|
|
83
|
-
self.malicious_block_hash = blk.
|
|
83
|
+
self.malicious_block_hash = blk.atom_hash
|
|
84
84
|
return False
|
|
85
85
|
|
|
86
86
|
# Early-exit if we met another known fork head
|
|
87
|
-
if stop_heads and blk.
|
|
88
|
-
self.validated_upto = blk.
|
|
87
|
+
if stop_heads and blk.atom_hash in stop_heads:
|
|
88
|
+
self.validated_upto = blk.atom_hash
|
|
89
89
|
return True
|
|
90
90
|
|
|
91
|
-
if blk.
|
|
92
|
-
self.validated_upto = blk.
|
|
91
|
+
if blk.atom_hash == self.chain_fork_position:
|
|
92
|
+
self.validated_upto = blk.atom_hash
|
|
93
93
|
return True
|
|
94
94
|
|
|
95
95
|
prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
|