astreum 0.2.29__py3-none-any.whl → 0.2.61__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astreum/__init__.py +9 -1
- astreum/_communication/__init__.py +11 -0
- astreum/{models → _communication}/message.py +101 -64
- astreum/_communication/peer.py +23 -0
- astreum/_communication/ping.py +33 -0
- astreum/_communication/route.py +95 -0
- astreum/_communication/setup.py +322 -0
- astreum/_communication/util.py +42 -0
- astreum/_consensus/__init__.py +20 -0
- astreum/_consensus/account.py +95 -0
- astreum/_consensus/accounts.py +38 -0
- astreum/_consensus/block.py +311 -0
- astreum/_consensus/chain.py +66 -0
- astreum/_consensus/fork.py +100 -0
- astreum/_consensus/genesis.py +72 -0
- astreum/_consensus/receipt.py +136 -0
- astreum/_consensus/setup.py +115 -0
- astreum/_consensus/transaction.py +215 -0
- astreum/_consensus/workers/__init__.py +9 -0
- astreum/_consensus/workers/discovery.py +48 -0
- astreum/_consensus/workers/validation.py +125 -0
- astreum/_consensus/workers/verify.py +63 -0
- astreum/_lispeum/__init__.py +16 -0
- astreum/_lispeum/environment.py +13 -0
- astreum/_lispeum/expression.py +190 -0
- astreum/_lispeum/high_evaluation.py +236 -0
- astreum/_lispeum/low_evaluation.py +123 -0
- astreum/_lispeum/meter.py +18 -0
- astreum/_lispeum/parser.py +51 -0
- astreum/_lispeum/tokenizer.py +22 -0
- astreum/_node.py +198 -0
- astreum/_storage/__init__.py +7 -0
- astreum/_storage/atom.py +109 -0
- astreum/_storage/patricia.py +478 -0
- astreum/_storage/setup.py +35 -0
- astreum/models/block.py +48 -39
- astreum/node.py +755 -563
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/METADATA +50 -14
- astreum-0.2.61.dist-info/RECORD +57 -0
- astreum/lispeum/__init__.py +0 -2
- 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.29.dist-info/RECORD +0 -33
- {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
- {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, List
|
|
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 ZERO32
|
|
11
|
+
from .._storage.patricia import PatriciaTrie
|
|
12
|
+
from ..utils.integer import int_to_bytes
|
|
13
|
+
|
|
14
|
+
TREASURY_ADDRESS = b"\x01" * 32
|
|
15
|
+
BURN_ADDRESS = b"\x00" * 32
|
|
16
|
+
def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
|
|
17
|
+
validator_pk = bytes(validator_public_key)
|
|
18
|
+
|
|
19
|
+
if len(validator_pk) != 32:
|
|
20
|
+
raise ValueError("validator_public_key must be 32 bytes")
|
|
21
|
+
|
|
22
|
+
# 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
|
|
23
|
+
stake_trie = PatriciaTrie()
|
|
24
|
+
stake_amount = int_to_bytes(1)
|
|
25
|
+
stake_trie.put(storage_node=node, key=validator_pk, value=stake_amount)
|
|
26
|
+
stake_root = stake_trie.root_hash
|
|
27
|
+
|
|
28
|
+
# 2. Account trie with treasury, burn, and validator accounts.
|
|
29
|
+
accounts_trie = PatriciaTrie()
|
|
30
|
+
|
|
31
|
+
treasury_account = Account.create(balance=1, data=stake_root, counter=0)
|
|
32
|
+
accounts_trie.put(storage_node=node, key=TREASURY_ADDRESS, value=treasury_account.hash)
|
|
33
|
+
|
|
34
|
+
burn_account = Account.create(balance=0, data=b"", counter=0)
|
|
35
|
+
accounts_trie.put(storage_node=node, key=BURN_ADDRESS, value=burn_account.hash)
|
|
36
|
+
|
|
37
|
+
validator_account = Account.create(balance=0, data=b"", counter=0)
|
|
38
|
+
accounts_trie.put(storage_node=node, key=validator_pk, value=validator_account.hash)
|
|
39
|
+
|
|
40
|
+
accounts_root = accounts_trie.root_hash
|
|
41
|
+
if accounts_root is None:
|
|
42
|
+
raise ValueError("genesis accounts trie is empty")
|
|
43
|
+
|
|
44
|
+
# 3. Assemble block metadata.
|
|
45
|
+
block = Block()
|
|
46
|
+
block.previous_block_hash = ZERO32
|
|
47
|
+
block.number = 0
|
|
48
|
+
block.timestamp = 0
|
|
49
|
+
block.accounts_hash = accounts_root
|
|
50
|
+
block.accounts = accounts_trie
|
|
51
|
+
block.transactions_total_fees = 0
|
|
52
|
+
block.transactions_hash = ZERO32
|
|
53
|
+
block.receipts_hash = ZERO32
|
|
54
|
+
block.delay_difficulty = 0
|
|
55
|
+
block.delay_output = b""
|
|
56
|
+
block.validator_public_key = validator_pk
|
|
57
|
+
block.transactions = []
|
|
58
|
+
block.receipts = []
|
|
59
|
+
|
|
60
|
+
# 4. Sign the block body with the validator secret key.
|
|
61
|
+
block.signature = b""
|
|
62
|
+
block.to_atom()
|
|
63
|
+
|
|
64
|
+
if block.body_hash is None:
|
|
65
|
+
raise ValueError("failed to materialise genesis block body")
|
|
66
|
+
|
|
67
|
+
secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
|
|
68
|
+
block.signature = secret.sign(block.body_hash)
|
|
69
|
+
block_hash, _ = block.to_atom()
|
|
70
|
+
|
|
71
|
+
block.hash = block_hash
|
|
72
|
+
return block
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Callable, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from .._storage.atom import Atom, AtomKind, ZERO32
|
|
7
|
+
|
|
8
|
+
STATUS_SUCCESS = 0
|
|
9
|
+
STATUS_FAILED = 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _int_to_be_bytes(value: Optional[int]) -> bytes:
|
|
13
|
+
if value is None:
|
|
14
|
+
return b""
|
|
15
|
+
value = int(value)
|
|
16
|
+
if value == 0:
|
|
17
|
+
return b"\x00"
|
|
18
|
+
size = (value.bit_length() + 7) // 8
|
|
19
|
+
return value.to_bytes(size, "big")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _be_bytes_to_int(data: Optional[bytes]) -> int:
|
|
23
|
+
if not data:
|
|
24
|
+
return 0
|
|
25
|
+
return int.from_bytes(data, "big")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Receipt:
|
|
30
|
+
transaction_hash: bytes = ZERO32
|
|
31
|
+
cost: int = 0
|
|
32
|
+
logs: bytes = b""
|
|
33
|
+
status: int = 0
|
|
34
|
+
hash: bytes = ZERO32
|
|
35
|
+
atoms: List[Atom] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
38
|
+
"""Serialise the receipt into Atom storage."""
|
|
39
|
+
if self.status not in (STATUS_SUCCESS, STATUS_FAILED):
|
|
40
|
+
raise ValueError("unsupported receipt status")
|
|
41
|
+
|
|
42
|
+
detail_specs = [
|
|
43
|
+
(bytes(self.transaction_hash), AtomKind.LIST),
|
|
44
|
+
(_int_to_be_bytes(self.status), AtomKind.BYTES),
|
|
45
|
+
(_int_to_be_bytes(self.cost), AtomKind.BYTES),
|
|
46
|
+
(bytes(self.logs), AtomKind.BYTES),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
detail_atoms: List[Atom] = []
|
|
50
|
+
next_hash = ZERO32
|
|
51
|
+
for payload, kind in reversed(detail_specs):
|
|
52
|
+
atom = Atom.from_data(data=payload, next_hash=next_hash, kind=kind)
|
|
53
|
+
detail_atoms.append(atom)
|
|
54
|
+
next_hash = atom.object_id()
|
|
55
|
+
detail_atoms.reverse()
|
|
56
|
+
|
|
57
|
+
type_atom = Atom.from_data(
|
|
58
|
+
data=b"receipt",
|
|
59
|
+
next_hash=next_hash,
|
|
60
|
+
kind=AtomKind.SYMBOL,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.hash = type_atom.object_id()
|
|
64
|
+
atoms = detail_atoms + [type_atom]
|
|
65
|
+
return self.hash, atoms
|
|
66
|
+
|
|
67
|
+
def atomize(self) -> Tuple[bytes, List[Atom]]:
|
|
68
|
+
"""Generate atoms for this receipt and cache them."""
|
|
69
|
+
receipt_id, atoms = self.to_atom()
|
|
70
|
+
self.hash = receipt_id
|
|
71
|
+
self.atoms = atoms
|
|
72
|
+
return receipt_id, atoms
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_atom(
|
|
76
|
+
cls,
|
|
77
|
+
storage_get: Callable[[bytes], Optional[Atom]],
|
|
78
|
+
receipt_id: bytes,
|
|
79
|
+
) -> Receipt:
|
|
80
|
+
"""Materialise a Receipt from Atom storage."""
|
|
81
|
+
def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
|
|
82
|
+
kind_value = getattr(atom, "kind", None)
|
|
83
|
+
if isinstance(kind_value, AtomKind):
|
|
84
|
+
return kind_value
|
|
85
|
+
if isinstance(kind_value, int):
|
|
86
|
+
try:
|
|
87
|
+
return AtomKind(kind_value)
|
|
88
|
+
except ValueError:
|
|
89
|
+
return None
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
type_atom = storage_get(receipt_id)
|
|
93
|
+
if type_atom is None:
|
|
94
|
+
raise ValueError("missing receipt type atom")
|
|
95
|
+
if _atom_kind(type_atom) is not AtomKind.SYMBOL:
|
|
96
|
+
raise ValueError("malformed receipt (type kind)")
|
|
97
|
+
if type_atom.data != b"receipt":
|
|
98
|
+
raise ValueError("not a receipt (type payload)")
|
|
99
|
+
|
|
100
|
+
details: List[Atom] = []
|
|
101
|
+
current = type_atom.next
|
|
102
|
+
while current and current != ZERO32 and len(details) < 4:
|
|
103
|
+
atom = storage_get(current)
|
|
104
|
+
if atom is None:
|
|
105
|
+
raise ValueError("missing receipt detail atom")
|
|
106
|
+
details.append(atom)
|
|
107
|
+
current = atom.next
|
|
108
|
+
|
|
109
|
+
if current and current != ZERO32:
|
|
110
|
+
raise ValueError("too many receipt fields")
|
|
111
|
+
if len(details) != 4:
|
|
112
|
+
raise ValueError("incomplete receipt fields")
|
|
113
|
+
|
|
114
|
+
tx_atom, status_atom, cost_atom, logs_atom = details
|
|
115
|
+
|
|
116
|
+
if _atom_kind(tx_atom) is not AtomKind.LIST:
|
|
117
|
+
raise ValueError("receipt transaction hash must be list-kind")
|
|
118
|
+
if any(_atom_kind(atom) is not AtomKind.BYTES for atom in [status_atom, cost_atom, logs_atom]):
|
|
119
|
+
raise ValueError("receipt detail atoms must be bytes-kind")
|
|
120
|
+
|
|
121
|
+
transaction_hash_bytes = tx_atom.data or ZERO32
|
|
122
|
+
status_bytes = status_atom.data
|
|
123
|
+
cost_bytes = cost_atom.data
|
|
124
|
+
logs_bytes = logs_atom.data
|
|
125
|
+
|
|
126
|
+
status_value = _be_bytes_to_int(status_bytes)
|
|
127
|
+
if status_value not in (STATUS_SUCCESS, STATUS_FAILED):
|
|
128
|
+
raise ValueError("unsupported receipt status")
|
|
129
|
+
|
|
130
|
+
return cls(
|
|
131
|
+
transaction_hash=transaction_hash_bytes or ZERO32,
|
|
132
|
+
cost=_be_bytes_to_int(cost_bytes),
|
|
133
|
+
logs=logs_bytes,
|
|
134
|
+
status=status_value,
|
|
135
|
+
hash=bytes(receipt_id),
|
|
136
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from queue import Queue
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from .workers import (
|
|
8
|
+
make_discovery_worker,
|
|
9
|
+
make_validation_worker,
|
|
10
|
+
make_verify_worker,
|
|
11
|
+
)
|
|
12
|
+
from .genesis import create_genesis_block
|
|
13
|
+
from ..utils.bytes import hex_to_bytes
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def current_validator(node: Any) -> bytes:
|
|
17
|
+
"""Return the current validator identifier. Override downstream."""
|
|
18
|
+
raise NotImplementedError("current_validator must be implemented by the host node")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
|
|
22
|
+
config = config or {}
|
|
23
|
+
|
|
24
|
+
# Shared state
|
|
25
|
+
node.validation_lock = getattr(node, "validation_lock", threading.RLock())
|
|
26
|
+
|
|
27
|
+
# Public maps per your spec
|
|
28
|
+
# - chains: Dict[root, Chain]
|
|
29
|
+
# - forks: Dict[head, Fork]
|
|
30
|
+
node.chains = getattr(node, "chains", {})
|
|
31
|
+
node.forks = getattr(node, "forks", {})
|
|
32
|
+
|
|
33
|
+
node.latest_block_hash = None
|
|
34
|
+
latest_block_hex = config.get("latest_block_hash")
|
|
35
|
+
if latest_block_hex is not None:
|
|
36
|
+
node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
|
|
37
|
+
node.latest_block = None
|
|
38
|
+
|
|
39
|
+
# Pending transactions queue (hash-only entries)
|
|
40
|
+
node._validation_transaction_queue = getattr(
|
|
41
|
+
node, "_validation_transaction_queue", Queue()
|
|
42
|
+
)
|
|
43
|
+
# Single work queue of grouped items: (latest_block_hash, set(peer_ids))
|
|
44
|
+
node._validation_verify_queue = getattr(
|
|
45
|
+
node, "_validation_verify_queue", Queue()
|
|
46
|
+
)
|
|
47
|
+
node._validation_stop_event = getattr(
|
|
48
|
+
node, "_validation_stop_event", threading.Event()
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def enqueue_transaction_hash(tx_hash: bytes) -> None:
|
|
52
|
+
"""Schedule a transaction hash for validation processing."""
|
|
53
|
+
if not isinstance(tx_hash, (bytes, bytearray)):
|
|
54
|
+
raise TypeError("transaction hash must be bytes-like")
|
|
55
|
+
node._validation_transaction_queue.put(bytes(tx_hash))
|
|
56
|
+
|
|
57
|
+
node.enqueue_transaction_hash = enqueue_transaction_hash
|
|
58
|
+
|
|
59
|
+
verify_worker = make_verify_worker(node)
|
|
60
|
+
validation_worker = make_validation_worker(
|
|
61
|
+
node, current_validator=current_validator
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Start workers as daemons
|
|
65
|
+
discovery_worker = make_discovery_worker(node)
|
|
66
|
+
node.consensus_discovery_thread = threading.Thread(
|
|
67
|
+
target=discovery_worker, daemon=True, name="consensus-discovery"
|
|
68
|
+
)
|
|
69
|
+
node.consensus_verify_thread = threading.Thread(
|
|
70
|
+
target=verify_worker, daemon=True, name="consensus-verify"
|
|
71
|
+
)
|
|
72
|
+
node.consensus_validation_thread = threading.Thread(
|
|
73
|
+
target=validation_worker, daemon=True, name="consensus-validation"
|
|
74
|
+
)
|
|
75
|
+
node.consensus_discovery_thread.start()
|
|
76
|
+
node.consensus_verify_thread.start()
|
|
77
|
+
|
|
78
|
+
validator_secret_hex = config.get("validation_secret_key")
|
|
79
|
+
if validator_secret_hex:
|
|
80
|
+
validator_secret_bytes = hex_to_bytes(validator_secret_hex, expected_length=32)
|
|
81
|
+
try:
|
|
82
|
+
from cryptography.hazmat.primitives import serialization
|
|
83
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
84
|
+
|
|
85
|
+
validator_private = ed25519.Ed25519PrivateKey.from_private_bytes(
|
|
86
|
+
validator_secret_bytes
|
|
87
|
+
)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
raise ValueError("invalid validation_secret_key") from exc
|
|
90
|
+
|
|
91
|
+
validator_public_bytes = validator_private.public_key().public_bytes(
|
|
92
|
+
encoding=serialization.Encoding.Raw,
|
|
93
|
+
format=serialization.PublicFormat.Raw,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
node.validation_secret_key = validator_private
|
|
97
|
+
node.validation_public_key = validator_public_bytes
|
|
98
|
+
|
|
99
|
+
if node.latest_block_hash is None:
|
|
100
|
+
genesis_block = create_genesis_block(
|
|
101
|
+
node,
|
|
102
|
+
validator_public_key=validator_public_bytes,
|
|
103
|
+
validator_secret_key=validator_secret_bytes,
|
|
104
|
+
)
|
|
105
|
+
genesis_hash, genesis_atoms = genesis_block.to_atom()
|
|
106
|
+
if hasattr(node, "_local_set"):
|
|
107
|
+
for atom in genesis_atoms:
|
|
108
|
+
try:
|
|
109
|
+
node._local_set(atom.object_id(), atom)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
node.latest_block_hash = genesis_hash
|
|
113
|
+
node.latest_block = genesis_block
|
|
114
|
+
|
|
115
|
+
node.consensus_validation_thread.start()
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from .._storage.atom import Atom, AtomKind, ZERO32
|
|
7
|
+
from ..utils.integer import bytes_to_int, int_to_bytes
|
|
8
|
+
from .account import Account
|
|
9
|
+
from .genesis import TREASURY_ADDRESS
|
|
10
|
+
from .receipt import STATUS_FAILED, Receipt, STATUS_SUCCESS
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Transaction:
|
|
14
|
+
amount: int
|
|
15
|
+
counter: int
|
|
16
|
+
data: bytes = b""
|
|
17
|
+
recipient: bytes = b""
|
|
18
|
+
sender: bytes = b""
|
|
19
|
+
signature: bytes = b""
|
|
20
|
+
hash: bytes = ZERO32
|
|
21
|
+
|
|
22
|
+
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
23
|
+
"""Serialise the transaction, returning (object_id, atoms)."""
|
|
24
|
+
body_child_ids: List[bytes] = []
|
|
25
|
+
acc: List[Atom] = []
|
|
26
|
+
|
|
27
|
+
def emit(payload: bytes) -> None:
|
|
28
|
+
atom = Atom.from_data(data=payload, kind=AtomKind.BYTES)
|
|
29
|
+
body_child_ids.append(atom.object_id())
|
|
30
|
+
acc.append(atom)
|
|
31
|
+
|
|
32
|
+
emit(int_to_bytes(self.amount))
|
|
33
|
+
emit(int_to_bytes(self.counter))
|
|
34
|
+
emit(bytes(self.data))
|
|
35
|
+
emit(bytes(self.recipient))
|
|
36
|
+
emit(bytes(self.sender))
|
|
37
|
+
|
|
38
|
+
# Build the linked list of body entry references.
|
|
39
|
+
body_atoms: List[Atom] = []
|
|
40
|
+
body_head = ZERO32
|
|
41
|
+
for child_id in reversed(body_child_ids):
|
|
42
|
+
node = Atom.from_data(data=child_id, next_hash=body_head, kind=AtomKind.BYTES)
|
|
43
|
+
body_head = node.object_id()
|
|
44
|
+
body_atoms.append(node)
|
|
45
|
+
body_atoms.reverse()
|
|
46
|
+
acc.extend(body_atoms)
|
|
47
|
+
|
|
48
|
+
body_list_atom = Atom.from_data(data=body_head, kind=AtomKind.LIST)
|
|
49
|
+
acc.append(body_list_atom)
|
|
50
|
+
body_list_id = body_list_atom.object_id()
|
|
51
|
+
|
|
52
|
+
signature_atom = Atom.from_data(
|
|
53
|
+
data=bytes(self.signature),
|
|
54
|
+
next_hash=body_list_id,
|
|
55
|
+
kind=AtomKind.BYTES,
|
|
56
|
+
)
|
|
57
|
+
type_atom = Atom.from_data(
|
|
58
|
+
data=b"transaction",
|
|
59
|
+
next_hash=signature_atom.object_id(),
|
|
60
|
+
kind=AtomKind.SYMBOL,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
acc.append(signature_atom)
|
|
64
|
+
acc.append(type_atom)
|
|
65
|
+
|
|
66
|
+
self.hash = type_atom.object_id()
|
|
67
|
+
return self.hash, acc
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_atom(
|
|
71
|
+
cls,
|
|
72
|
+
node: Any,
|
|
73
|
+
transaction_id: bytes,
|
|
74
|
+
) -> Transaction:
|
|
75
|
+
storage_get = getattr(node, "storage_get", None)
|
|
76
|
+
if not callable(storage_get):
|
|
77
|
+
raise NotImplementedError("node does not expose a storage getter")
|
|
78
|
+
|
|
79
|
+
def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
|
|
80
|
+
kind_value = getattr(atom, "kind", None)
|
|
81
|
+
if isinstance(kind_value, AtomKind):
|
|
82
|
+
return kind_value
|
|
83
|
+
if isinstance(kind_value, int):
|
|
84
|
+
try:
|
|
85
|
+
return AtomKind(kind_value)
|
|
86
|
+
except ValueError:
|
|
87
|
+
return None
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def _require_atom(
|
|
91
|
+
atom_id: Optional[bytes],
|
|
92
|
+
context: str,
|
|
93
|
+
expected_kind: Optional[AtomKind] = None,
|
|
94
|
+
) -> Atom:
|
|
95
|
+
if not atom_id or atom_id == ZERO32:
|
|
96
|
+
raise ValueError(f"missing {context}")
|
|
97
|
+
atom = storage_get(atom_id)
|
|
98
|
+
if atom is None:
|
|
99
|
+
raise ValueError(f"missing {context}")
|
|
100
|
+
if expected_kind is not None:
|
|
101
|
+
kind = _atom_kind(atom)
|
|
102
|
+
if kind is not expected_kind:
|
|
103
|
+
raise ValueError(f"malformed {context}")
|
|
104
|
+
return atom
|
|
105
|
+
|
|
106
|
+
def _read_list(head_id: Optional[bytes], context: str) -> List[bytes]:
|
|
107
|
+
entries: List[bytes] = []
|
|
108
|
+
current = head_id if head_id and head_id != ZERO32 else None
|
|
109
|
+
while current:
|
|
110
|
+
node = storage_get(current)
|
|
111
|
+
if node is None:
|
|
112
|
+
raise ValueError(f"missing list node while decoding {context}")
|
|
113
|
+
node_kind = _atom_kind(node)
|
|
114
|
+
if node_kind is not AtomKind.BYTES:
|
|
115
|
+
raise ValueError(f"invalid list node while decoding {context}")
|
|
116
|
+
if len(node.data) != len(ZERO32):
|
|
117
|
+
raise ValueError(f"malformed list entry while decoding {context}")
|
|
118
|
+
entries.append(node.data)
|
|
119
|
+
nxt = node.next
|
|
120
|
+
current = nxt if nxt and nxt != ZERO32 else None
|
|
121
|
+
return entries
|
|
122
|
+
|
|
123
|
+
def _read_detail_bytes(entry_id: Optional[bytes]) -> bytes:
|
|
124
|
+
if not entry_id or entry_id == ZERO32:
|
|
125
|
+
return b""
|
|
126
|
+
detail_atom = storage_get(entry_id)
|
|
127
|
+
return detail_atom.data if detail_atom is not None else b""
|
|
128
|
+
|
|
129
|
+
type_atom = _require_atom(transaction_id, "transaction type atom", AtomKind.SYMBOL)
|
|
130
|
+
if type_atom.data != b"transaction":
|
|
131
|
+
raise ValueError("not a transaction (type atom payload)")
|
|
132
|
+
|
|
133
|
+
signature_atom = _require_atom(type_atom.next, "transaction signature atom", AtomKind.BYTES)
|
|
134
|
+
body_list_atom = _require_atom(signature_atom.next, "transaction body list atom", AtomKind.LIST)
|
|
135
|
+
if body_list_atom.next and body_list_atom.next != ZERO32:
|
|
136
|
+
raise ValueError("malformed transaction (body list tail)")
|
|
137
|
+
|
|
138
|
+
body_entry_ids = _read_list(body_list_atom.data, "transaction body")
|
|
139
|
+
if len(body_entry_ids) < 5:
|
|
140
|
+
body_entry_ids.extend([ZERO32] * (5 - len(body_entry_ids)))
|
|
141
|
+
|
|
142
|
+
amount_bytes = _read_detail_bytes(body_entry_ids[0])
|
|
143
|
+
counter_bytes = _read_detail_bytes(body_entry_ids[1])
|
|
144
|
+
data_bytes = _read_detail_bytes(body_entry_ids[2])
|
|
145
|
+
recipient_bytes = _read_detail_bytes(body_entry_ids[3])
|
|
146
|
+
sender_bytes = _read_detail_bytes(body_entry_ids[4])
|
|
147
|
+
|
|
148
|
+
return cls(
|
|
149
|
+
amount=bytes_to_int(amount_bytes),
|
|
150
|
+
counter=bytes_to_int(counter_bytes),
|
|
151
|
+
data=data_bytes,
|
|
152
|
+
recipient=recipient_bytes,
|
|
153
|
+
sender=sender_bytes,
|
|
154
|
+
signature=signature_atom.data,
|
|
155
|
+
hash=bytes(transaction_id),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None:
|
|
160
|
+
"""Apply transaction to the candidate block. Override downstream."""
|
|
161
|
+
transaction = Transaction.from_atom(node, transaction_hash)
|
|
162
|
+
|
|
163
|
+
accounts = block.accounts
|
|
164
|
+
|
|
165
|
+
sender_account = accounts.get_account(address=transaction.sender, node=node)
|
|
166
|
+
|
|
167
|
+
if sender_account is None:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
tx_cost = 1 + transaction.amount
|
|
171
|
+
|
|
172
|
+
if sender_account.balance < tx_cost:
|
|
173
|
+
low_sender_balance_receipt = Receipt(
|
|
174
|
+
transaction_hash=transaction_hash,
|
|
175
|
+
cost=0,
|
|
176
|
+
logs=b"low sender balance",
|
|
177
|
+
status=STATUS_FAILED
|
|
178
|
+
)
|
|
179
|
+
low_sender_balance_receipt.atomize()
|
|
180
|
+
block.receipts.append(receipt)
|
|
181
|
+
block.transactions.append(transaction)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
recipient_account = accounts.get_account(address=transaction.recipient, node=node)
|
|
185
|
+
|
|
186
|
+
if recipient_account is None:
|
|
187
|
+
recipient_account = Account.create()
|
|
188
|
+
|
|
189
|
+
if transaction.recipient == TREASURY_ADDRESS:
|
|
190
|
+
stake_trie = recipient_account.data
|
|
191
|
+
existing_stake = stake_trie.get(node, transaction.sender)
|
|
192
|
+
current_stake = bytes_to_int(existing_stake)
|
|
193
|
+
new_stake = current_stake + transaction.amount
|
|
194
|
+
stake_trie.put(node, transaction.sender, int_to_bytes(new_stake))
|
|
195
|
+
recipient_account.data_hash = stake_trie.root_hash or ZERO32
|
|
196
|
+
recipient_account.balance += transaction.amount
|
|
197
|
+
else:
|
|
198
|
+
recipient_account.balance += transaction.amount
|
|
199
|
+
|
|
200
|
+
sender_account.balance -= tx_cost
|
|
201
|
+
|
|
202
|
+
block.accounts.set_account(address=sender_account)
|
|
203
|
+
|
|
204
|
+
block.accounts.set_account(address=recipient_account)
|
|
205
|
+
|
|
206
|
+
block.transactions.append(transaction_hash)
|
|
207
|
+
|
|
208
|
+
receipt = Receipt(
|
|
209
|
+
transaction_hash=bytes(transaction_hash),
|
|
210
|
+
cost=0,
|
|
211
|
+
logs=b"",
|
|
212
|
+
status=STATUS_SUCCESS,
|
|
213
|
+
)
|
|
214
|
+
receipt.atomize()
|
|
215
|
+
block.receipts.append(receipt)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker thread factories for the consensus subsystem.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .discovery import make_discovery_worker
|
|
6
|
+
from .validation import make_validation_worker
|
|
7
|
+
from .verify import make_verify_worker
|
|
8
|
+
|
|
9
|
+
__all__ = ["make_discovery_worker", "make_verify_worker", "make_validation_worker"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from queue import Empty
|
|
5
|
+
from typing import Any, Dict, Set, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_discovery_worker(node: Any):
|
|
9
|
+
"""
|
|
10
|
+
Build the discovery worker bound to the given node.
|
|
11
|
+
|
|
12
|
+
The returned callable mirrors the previous inline worker in ``setup.py``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def _discovery_worker() -> None:
|
|
16
|
+
stop = node._validation_stop_event
|
|
17
|
+
while not stop.is_set():
|
|
18
|
+
try:
|
|
19
|
+
peers = getattr(node, "peers", None)
|
|
20
|
+
if isinstance(peers, dict):
|
|
21
|
+
pairs: list[Tuple[Any, bytes]] = [
|
|
22
|
+
(peer_id, bytes(latest))
|
|
23
|
+
for peer_id, peer in list(peers.items())
|
|
24
|
+
if isinstance(
|
|
25
|
+
(latest := getattr(peer, "latest_block", None)),
|
|
26
|
+
(bytes, bytearray),
|
|
27
|
+
)
|
|
28
|
+
and latest
|
|
29
|
+
]
|
|
30
|
+
latest_keys: Set[bytes] = {hb for _, hb in pairs}
|
|
31
|
+
grouped: Dict[bytes, set[Any]] = {
|
|
32
|
+
hb: {pid for pid, phb in pairs if phb == hb}
|
|
33
|
+
for hb in latest_keys
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
while True:
|
|
38
|
+
node._validation_verify_queue.get_nowait()
|
|
39
|
+
except Empty:
|
|
40
|
+
pass
|
|
41
|
+
for latest_b, peer_set in grouped.items():
|
|
42
|
+
node._validation_verify_queue.put((latest_b, peer_set))
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
finally:
|
|
46
|
+
time.sleep(0.5)
|
|
47
|
+
|
|
48
|
+
return _discovery_worker
|