astreum 0.2.41__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/_communication/message.py +1 -0
- astreum/_communication/peer.py +23 -11
- astreum/_communication/route.py +40 -3
- astreum/_communication/setup.py +72 -4
- astreum/_consensus/account.py +62 -137
- astreum/_consensus/accounts.py +7 -36
- astreum/_consensus/block.py +311 -328
- astreum/_consensus/genesis.py +15 -84
- astreum/_consensus/receipt.py +67 -108
- astreum/_consensus/setup.py +50 -3
- astreum/_consensus/transaction.py +141 -118
- astreum/_consensus/workers/validation.py +5 -2
- astreum/_consensus/workers/verify.py +1 -1
- astreum/_lispeum/expression.py +190 -37
- astreum/_lispeum/high_evaluation.py +232 -173
- astreum/_lispeum/low_evaluation.py +21 -21
- astreum/_lispeum/parser.py +26 -31
- astreum/_node.py +154 -14
- astreum/_storage/__init__.py +7 -5
- astreum/_storage/atom.py +88 -96
- astreum/_storage/patricia.py +51 -16
- astreum/_storage/setup.py +35 -0
- astreum/models/block.py +20 -20
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/METADATA +14 -1
- astreum-0.2.61.dist-info/RECORD +57 -0
- astreum-0.2.41.dist-info/RECORD +0 -53
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
astreum/_consensus/genesis.py
CHANGED
|
@@ -1,77 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, List
|
|
5
5
|
|
|
6
6
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
7
7
|
|
|
8
8
|
from .account import Account
|
|
9
9
|
from .block import Block
|
|
10
|
-
from .._storage.atom import
|
|
11
|
-
from .._storage.patricia import PatriciaTrie
|
|
10
|
+
from .._storage.atom import ZERO32
|
|
11
|
+
from .._storage.patricia import PatriciaTrie
|
|
12
|
+
from ..utils.integer import int_to_bytes
|
|
12
13
|
|
|
13
14
|
TREASURY_ADDRESS = b"\x01" * 32
|
|
14
15
|
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
16
|
def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
|
|
76
17
|
validator_pk = bytes(validator_public_key)
|
|
77
18
|
|
|
@@ -80,30 +21,21 @@ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secre
|
|
|
80
21
|
|
|
81
22
|
# 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
|
|
82
23
|
stake_trie = PatriciaTrie()
|
|
83
|
-
stake_amount = (1)
|
|
84
|
-
stake_trie.put(node, validator_pk, stake_amount)
|
|
85
|
-
|
|
86
|
-
stake_root = stake_trie.root_hash or ZERO32
|
|
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
|
|
87
27
|
|
|
88
28
|
# 2. Account trie with treasury, burn, and validator accounts.
|
|
89
29
|
accounts_trie = PatriciaTrie()
|
|
90
30
|
|
|
91
|
-
treasury_account = Account.create(balance=1, data=stake_root,
|
|
92
|
-
|
|
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"", nonce=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)
|
|
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)
|
|
100
33
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
_store_atoms(node, validator_atoms)
|
|
104
|
-
accounts_trie.put(node, validator_pk, validator_account_id)
|
|
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)
|
|
105
36
|
|
|
106
|
-
|
|
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)
|
|
107
39
|
|
|
108
40
|
accounts_root = accounts_trie.root_hash
|
|
109
41
|
if accounts_root is None:
|
|
@@ -134,8 +66,7 @@ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secre
|
|
|
134
66
|
|
|
135
67
|
secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
|
|
136
68
|
block.signature = secret.sign(block.body_hash)
|
|
137
|
-
block_hash,
|
|
138
|
-
_store_atoms(node, block_atoms)
|
|
69
|
+
block_hash, _ = block.to_atom()
|
|
139
70
|
|
|
140
71
|
block.hash = block_hash
|
|
141
72
|
return block
|
astreum/_consensus/receipt.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Callable, List, Optional, Tuple
|
|
5
5
|
|
|
6
|
-
from .._storage.atom import Atom, ZERO32
|
|
6
|
+
from .._storage.atom import Atom, AtomKind, ZERO32
|
|
7
7
|
|
|
8
8
|
STATUS_SUCCESS = 0
|
|
9
9
|
STATUS_FAILED = 1
|
|
@@ -25,52 +25,6 @@ def _be_bytes_to_int(data: Optional[bytes]) -> int:
|
|
|
25
25
|
return int.from_bytes(data, "big")
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
29
|
-
atoms: List[Atom] = []
|
|
30
|
-
next_hash = ZERO32
|
|
31
|
-
elements: List[Atom] = []
|
|
32
|
-
for child_id in reversed(child_ids):
|
|
33
|
-
elem = Atom.from_data(data=child_id, next_hash=next_hash)
|
|
34
|
-
next_hash = elem.object_id()
|
|
35
|
-
elements.append(elem)
|
|
36
|
-
elements.reverse()
|
|
37
|
-
list_value = Atom.from_data(data=len(child_ids).to_bytes(8, "little"), next_hash=next_hash)
|
|
38
|
-
list_type = Atom.from_data(data=b"list", next_hash=list_value.object_id())
|
|
39
|
-
atoms.extend(elements)
|
|
40
|
-
atoms.append(list_value)
|
|
41
|
-
atoms.append(list_type)
|
|
42
|
-
return list_type.object_id(), atoms
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _read_list_entries(
|
|
46
|
-
storage_get: Callable[[bytes], Optional[Atom]], start: bytes
|
|
47
|
-
) -> List[bytes]:
|
|
48
|
-
entries: List[bytes] = []
|
|
49
|
-
current = start if start and start != ZERO32 else b""
|
|
50
|
-
while current:
|
|
51
|
-
elem = storage_get(current)
|
|
52
|
-
if elem is None:
|
|
53
|
-
break
|
|
54
|
-
entries.append(elem.data)
|
|
55
|
-
nxt = elem.next
|
|
56
|
-
current = nxt if nxt and nxt != ZERO32 else b""
|
|
57
|
-
return entries
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _read_payload_bytes(
|
|
61
|
-
storage_get: Callable[[bytes], Optional[Atom]], object_id: bytes
|
|
62
|
-
) -> bytes:
|
|
63
|
-
if not object_id or object_id == ZERO32:
|
|
64
|
-
return b""
|
|
65
|
-
atom = storage_get(object_id)
|
|
66
|
-
if atom is None:
|
|
67
|
-
return b""
|
|
68
|
-
if atom.data == b"bytes":
|
|
69
|
-
value_atom = storage_get(atom.next)
|
|
70
|
-
return value_atom.data if value_atom is not None else b""
|
|
71
|
-
return atom.data
|
|
72
|
-
|
|
73
|
-
|
|
74
28
|
@dataclass
|
|
75
29
|
class Receipt:
|
|
76
30
|
transaction_hash: bytes = ZERO32
|
|
@@ -85,31 +39,30 @@ class Receipt:
|
|
|
85
39
|
if self.status not in (STATUS_SUCCESS, STATUS_FAILED):
|
|
86
40
|
raise ValueError("unsupported receipt status")
|
|
87
41
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
logs_atom = Atom.from_data(data=bytes(self.logs))
|
|
94
|
-
|
|
95
|
-
atoms.extend([tx_atom, status_atom, cost_atom, logs_atom])
|
|
96
|
-
|
|
97
|
-
body_child_ids = [
|
|
98
|
-
tx_atom.object_id(),
|
|
99
|
-
status_atom.object_id(),
|
|
100
|
-
cost_atom.object_id(),
|
|
101
|
-
logs_atom.object_id(),
|
|
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),
|
|
102
47
|
]
|
|
103
|
-
body_id, body_atoms = _make_list(body_child_ids)
|
|
104
|
-
atoms.extend(body_atoms)
|
|
105
|
-
|
|
106
|
-
type_atom = Atom.from_data(data=b"receipt", next_hash=body_id)
|
|
107
|
-
atoms.append(type_atom)
|
|
108
48
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
)
|
|
111
62
|
|
|
112
|
-
|
|
63
|
+
self.hash = type_atom.object_id()
|
|
64
|
+
atoms = detail_atoms + [type_atom]
|
|
65
|
+
return self.hash, atoms
|
|
113
66
|
|
|
114
67
|
def atomize(self) -> Tuple[bytes, List[Atom]]:
|
|
115
68
|
"""Generate atoms for this receipt and cache them."""
|
|
@@ -125,45 +78,51 @@ class Receipt:
|
|
|
125
78
|
receipt_id: bytes,
|
|
126
79
|
) -> Receipt:
|
|
127
80
|
"""Materialise a Receipt from Atom storage."""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
type_atom
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if
|
|
157
|
-
raise ValueError("
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
|
|
167
126
|
status_value = _be_bytes_to_int(status_bytes)
|
|
168
127
|
if status_value not in (STATUS_SUCCESS, STATUS_FAILED):
|
|
169
128
|
raise ValueError("unsupported receipt status")
|
astreum/_consensus/setup.py
CHANGED
|
@@ -2,13 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
4
|
from queue import Queue
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
from .workers import (
|
|
8
8
|
make_discovery_worker,
|
|
9
9
|
make_validation_worker,
|
|
10
10
|
make_verify_worker,
|
|
11
11
|
)
|
|
12
|
+
from .genesis import create_genesis_block
|
|
13
|
+
from ..utils.bytes import hex_to_bytes
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def current_validator(node: Any) -> bytes:
|
|
@@ -16,7 +18,9 @@ def current_validator(node: Any) -> bytes:
|
|
|
16
18
|
raise NotImplementedError("current_validator must be implemented by the host node")
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
def consensus_setup(node: Any) -> None:
|
|
21
|
+
def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
|
|
22
|
+
config = config or {}
|
|
23
|
+
|
|
20
24
|
# Shared state
|
|
21
25
|
node.validation_lock = getattr(node, "validation_lock", threading.RLock())
|
|
22
26
|
|
|
@@ -26,6 +30,12 @@ def consensus_setup(node: Any) -> None:
|
|
|
26
30
|
node.chains = getattr(node, "chains", {})
|
|
27
31
|
node.forks = getattr(node, "forks", {})
|
|
28
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
|
+
|
|
29
39
|
# Pending transactions queue (hash-only entries)
|
|
30
40
|
node._validation_transaction_queue = getattr(
|
|
31
41
|
node, "_validation_transaction_queue", Queue()
|
|
@@ -64,5 +74,42 @@ def consensus_setup(node: Any) -> None:
|
|
|
64
74
|
)
|
|
65
75
|
node.consensus_discovery_thread.start()
|
|
66
76
|
node.consensus_verify_thread.start()
|
|
67
|
-
|
|
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
|
+
|
|
68
115
|
node.consensus_validation_thread.start()
|