astreum 0.1.18__py3-none-any.whl → 0.1.20__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/node/__init__.py +390 -418
- astreum/node/storage/__init__.py +0 -13
- astreum/node/storage/merkle.py +65 -65
- astreum/node/validation/__init__.py +0 -84
- astreum/node/validation/_block/__init__.py +0 -12
- astreum/node/validation/block.py +10 -19
- {astreum-0.1.18.dist-info → astreum-0.1.20.dist-info}/METADATA +1 -1
- {astreum-0.1.18.dist-info → astreum-0.1.20.dist-info}/RECORD +11 -12
- {astreum-0.1.18.dist-info → astreum-0.1.20.dist-info}/WHEEL +1 -1
- astreum/node/validation/state.py +0 -230
- {astreum-0.1.18.dist-info → astreum-0.1.20.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.1.18.dist-info → astreum-0.1.20.dist-info}/top_level.txt +0 -0
astreum/node/__init__.py
CHANGED
|
@@ -1,475 +1,447 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
from typing import List
|
|
4
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
from .relay import Relay, Topic
|
|
8
|
+
from ..machine import AstreumMachine
|
|
9
|
+
from .utils import hash_data
|
|
10
|
+
from .validation.block import Block
|
|
11
|
+
from .storage.storage import Storage
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# from .validation.state import (
|
|
31
|
-
# add_block_to_state,
|
|
32
|
-
# validate_and_apply_block,
|
|
33
|
-
# create_account_state,
|
|
34
|
-
# get_validator_for_slot,
|
|
35
|
-
# select_best_chain,
|
|
36
|
-
# compare_chains,
|
|
37
|
-
# get_validator_set
|
|
38
|
-
# )
|
|
39
|
-
# from .validation.adapter import BlockAdapter, TransactionAdapter
|
|
40
|
-
|
|
41
|
-
# class Node:
|
|
42
|
-
# def __init__(self, config: dict):
|
|
43
|
-
# # Ensure config is a dictionary, but allow it to be None
|
|
44
|
-
# self.config = config if config is not None else {}
|
|
45
|
-
|
|
46
|
-
# # Handle validation key if provided
|
|
47
|
-
# self.validation_private_key = None
|
|
48
|
-
# self.validation_public_key = None
|
|
49
|
-
# self.is_validator = False
|
|
50
|
-
|
|
51
|
-
# # Extract validation private key from config
|
|
52
|
-
# if 'validation_private_key' in self.config:
|
|
53
|
-
# try:
|
|
54
|
-
# key_bytes = bytes.fromhex(self.config['validation_private_key'])
|
|
55
|
-
# self.validation_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(key_bytes)
|
|
56
|
-
# self.validation_public_key = self.validation_private_key.public_key()
|
|
57
|
-
# self.is_validator = True
|
|
13
|
+
class Node:
|
|
14
|
+
def __init__(self, config: dict):
|
|
15
|
+
# Ensure config is a dictionary, but allow it to be None
|
|
16
|
+
self.config = config if config is not None else {}
|
|
17
|
+
|
|
18
|
+
# Handle validation key if provided
|
|
19
|
+
self.validation_private_key = None
|
|
20
|
+
self.validation_public_key = None
|
|
21
|
+
self.is_validator = False
|
|
22
|
+
|
|
23
|
+
# Extract validation private key from config
|
|
24
|
+
if 'validation_private_key' in self.config:
|
|
25
|
+
try:
|
|
26
|
+
key_bytes = bytes.fromhex(self.config['validation_private_key'])
|
|
27
|
+
self.validation_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(key_bytes)
|
|
28
|
+
self.validation_public_key = self.validation_private_key.public_key()
|
|
29
|
+
self.is_validator = True
|
|
58
30
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
|
|
31
|
+
# Set validation_route to True in config so relay will join validation route
|
|
32
|
+
self.config['validation_route'] = True
|
|
33
|
+
print(f"Node is configured as a validator with validation key")
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(f"Error loading validation private key: {e}")
|
|
36
|
+
|
|
37
|
+
# Initialize relay with our config
|
|
38
|
+
self.relay = Relay(self.config)
|
|
39
|
+
|
|
40
|
+
# Get the node_id from relay
|
|
41
|
+
self.node_id = self.relay.node_id
|
|
42
|
+
|
|
43
|
+
# Initialize storage
|
|
44
|
+
self.storage = Storage(self.config)
|
|
45
|
+
self.storage.node = self # Set the storage node reference to self
|
|
46
|
+
|
|
47
|
+
# Initialize blockchain state
|
|
48
|
+
self.blockchain = create_account_state(self.config)
|
|
49
|
+
|
|
50
|
+
# Store our validator info if we're a validator
|
|
51
|
+
if self.is_validator and self.validation_public_key:
|
|
52
|
+
self.validator_address = self.validation_public_key.public_bytes(
|
|
53
|
+
encoding=serialization.Encoding.Raw,
|
|
54
|
+
format=serialization.PublicFormat.Raw
|
|
55
|
+
)
|
|
56
|
+
self.validator_private_bytes = self.validation_private_key.private_bytes(
|
|
57
|
+
encoding=serialization.Encoding.Raw,
|
|
58
|
+
format=serialization.PrivateFormat.Raw,
|
|
59
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
60
|
+
)
|
|
61
|
+
print(f"Registered validator with address: {self.validator_address.hex()}")
|
|
62
|
+
else:
|
|
63
|
+
self.validator_address = None
|
|
64
|
+
self.validator_private_bytes = None
|
|
65
|
+
|
|
66
|
+
# Latest block of the chain this node is following
|
|
67
|
+
self.latest_block = None
|
|
68
|
+
self.followed_chain_id = self.config.get('followed_chain_id', None)
|
|
69
|
+
|
|
70
|
+
# Initialize machine
|
|
71
|
+
self.machine = AstreumMachine(node=self)
|
|
72
|
+
|
|
73
|
+
# Register message handlers
|
|
74
|
+
self.relay.message_handlers[Topic.PEER_ROUTE] = self._handle_peer_route
|
|
75
|
+
self.relay.message_handlers[Topic.PING] = self._handle_ping
|
|
76
|
+
self.relay.message_handlers[Topic.PONG] = self._handle_pong
|
|
77
|
+
self.relay.message_handlers[Topic.OBJECT_REQUEST] = self._handle_object_request
|
|
78
|
+
self.relay.message_handlers[Topic.OBJECT_RESPONSE] = self._handle_object_response
|
|
79
|
+
self.relay.message_handlers[Topic.ROUTE_REQUEST] = self._handle_route_request
|
|
80
|
+
self.relay.message_handlers[Topic.ROUTE] = self._handle_route
|
|
81
|
+
self.relay.message_handlers[Topic.LATEST_BLOCK_REQUEST] = self._handle_latest_block_request
|
|
82
|
+
self.relay.message_handlers[Topic.LATEST_BLOCK] = self._handle_latest_block
|
|
83
|
+
self.relay.message_handlers[Topic.TRANSACTION] = self._handle_transaction
|
|
84
|
+
self.relay.message_handlers[Topic.BLOCK_REQUEST] = self._handle_block_request
|
|
85
|
+
self.relay.message_handlers[Topic.BLOCK_RESPONSE] = self._handle_block_response
|
|
86
|
+
|
|
87
|
+
# Initialize latest block from storage if available
|
|
88
|
+
self._initialize_latest_block()
|
|
89
|
+
|
|
90
|
+
# Candidate chains that might be adopted
|
|
91
|
+
self.candidate_chains = {} # chain_id -> {'latest_block': block, 'timestamp': time.time()}
|
|
92
|
+
self.pending_blocks = {} # block_hash -> {'block': block, 'timestamp': time.time()}
|
|
93
|
+
|
|
94
|
+
# Threads for validation and chain monitoring
|
|
95
|
+
self.running = False
|
|
96
|
+
self.main_chain_validation_thread = None
|
|
97
|
+
self.candidate_chain_validation_thread = None
|
|
98
|
+
|
|
99
|
+
# Pending transactions for a block
|
|
100
|
+
self.pending_transactions = {} # tx_hash -> {'transaction': tx, 'timestamp': time.time()}
|
|
101
|
+
|
|
102
|
+
# Last block production attempt time
|
|
103
|
+
self.last_block_attempt_time = 0
|
|
132
104
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
105
|
+
def start(self):
|
|
106
|
+
"""Start the node."""
|
|
107
|
+
self.running = True
|
|
108
|
+
|
|
109
|
+
# Start relay
|
|
110
|
+
self.relay.start()
|
|
111
|
+
|
|
112
|
+
# Start chain monitoring thread
|
|
113
|
+
self.main_chain_validation_thread = threading.Thread(
|
|
114
|
+
target=self._main_chain_validation_loop,
|
|
115
|
+
name="MainChainValidation"
|
|
116
|
+
)
|
|
117
|
+
self.main_chain_validation_thread.daemon = True
|
|
118
|
+
self.main_chain_validation_thread.start()
|
|
119
|
+
|
|
120
|
+
self.candidate_chain_validation_thread = threading.Thread(
|
|
121
|
+
target=self._candidate_chain_validation_loop,
|
|
122
|
+
name="CandidateChainValidation"
|
|
123
|
+
)
|
|
124
|
+
self.candidate_chain_validation_thread.daemon = True
|
|
125
|
+
self.candidate_chain_validation_thread.start()
|
|
126
|
+
|
|
127
|
+
# Set up recurring block query tasks
|
|
128
|
+
main_query_thread = threading.Thread(
|
|
129
|
+
target=self._block_query_loop,
|
|
130
|
+
args=('main',),
|
|
131
|
+
daemon=True
|
|
132
|
+
)
|
|
133
|
+
main_query_thread.start()
|
|
134
|
+
|
|
135
|
+
validation_query_thread = threading.Thread(
|
|
136
|
+
target=self._block_query_loop,
|
|
137
|
+
args=('validation',),
|
|
138
|
+
daemon=True
|
|
139
|
+
)
|
|
140
|
+
validation_query_thread.start()
|
|
141
|
+
|
|
142
|
+
print(f"Node started with ID {self.node_id.hex()}")
|
|
171
143
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
144
|
+
def stop(self):
|
|
145
|
+
"""Stop the node and all its services."""
|
|
146
|
+
self.running = False
|
|
175
147
|
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
148
|
+
# Stop all threads
|
|
149
|
+
if self.main_chain_validation_thread and self.main_chain_validation_thread.is_alive():
|
|
150
|
+
self.main_chain_validation_thread.join(timeout=1.0)
|
|
179
151
|
|
|
180
|
-
|
|
181
|
-
|
|
152
|
+
if self.candidate_chain_validation_thread and self.candidate_chain_validation_thread.is_alive():
|
|
153
|
+
self.candidate_chain_validation_thread.join(timeout=1.0)
|
|
182
154
|
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
155
|
+
# Stop relay last
|
|
156
|
+
if self.relay:
|
|
157
|
+
self.relay.stop()
|
|
186
158
|
|
|
187
|
-
|
|
159
|
+
print("Node stopped")
|
|
188
160
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
161
|
+
def _main_chain_validation_loop(self):
|
|
162
|
+
"""
|
|
163
|
+
Main validation loop for the primary blockchain.
|
|
164
|
+
This thread prioritizes validating blocks on the main chain we're following.
|
|
165
|
+
"""
|
|
166
|
+
while self.running:
|
|
167
|
+
try:
|
|
168
|
+
# Update latest block if we don't have one yet
|
|
169
|
+
if not self.latest_block and hasattr(self.blockchain, 'get_latest_block'):
|
|
170
|
+
self.latest_block = self.blockchain.get_latest_block()
|
|
199
171
|
|
|
200
|
-
#
|
|
201
|
-
|
|
172
|
+
# Process any blocks that extend our main chain immediately
|
|
173
|
+
self._process_main_chain_blocks()
|
|
202
174
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
175
|
+
# Attempt block production if we are a validator
|
|
176
|
+
if self.is_validator and self.validator_address:
|
|
177
|
+
self._attempt_block_production()
|
|
206
178
|
|
|
207
|
-
#
|
|
208
|
-
|
|
179
|
+
# Cleanup old items
|
|
180
|
+
self._prune_pending_items()
|
|
209
181
|
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
182
|
+
# Sleep to prevent high CPU usage
|
|
183
|
+
time.sleep(0.1) # Short sleep for main chain validation
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"Error in main chain validation loop: {e}")
|
|
186
|
+
time.sleep(1) # Longer sleep on error
|
|
215
187
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
#
|
|
225
|
-
|
|
188
|
+
def _candidate_chain_validation_loop(self):
|
|
189
|
+
"""
|
|
190
|
+
Validation loop for candidate chains (potential forks).
|
|
191
|
+
This thread handles validation of blocks from alternate chains
|
|
192
|
+
without slowing down the main chain processing.
|
|
193
|
+
"""
|
|
194
|
+
while self.running:
|
|
195
|
+
try:
|
|
196
|
+
# Process candidate chains
|
|
197
|
+
self._evaluate_candidate_chains()
|
|
226
198
|
|
|
227
|
-
#
|
|
228
|
-
|
|
199
|
+
# Prune old candidate chains
|
|
200
|
+
self._prune_candidate_chains()
|
|
229
201
|
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
202
|
+
# Sleep longer for candidate chain validation (lower priority)
|
|
203
|
+
time.sleep(1) # Longer sleep for candidate chain validation
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"Error in candidate chain validation loop: {e}")
|
|
206
|
+
time.sleep(2) # Even longer sleep on error
|
|
235
207
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
208
|
+
def _prune_pending_items(self):
|
|
209
|
+
"""Remove old pending blocks and transactions."""
|
|
210
|
+
current_time = time.time()
|
|
211
|
+
|
|
212
|
+
# Prune old pending blocks (older than 1 hour)
|
|
213
|
+
blocks_to_remove = [
|
|
214
|
+
block_hash for block_hash, data in self.pending_blocks.items()
|
|
215
|
+
if current_time - data['timestamp'] > 3600 # 1 hour
|
|
216
|
+
]
|
|
217
|
+
for block_hash in blocks_to_remove:
|
|
218
|
+
del self.pending_blocks[block_hash]
|
|
247
219
|
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
220
|
+
# Prune old pending transactions (older than 30 minutes)
|
|
221
|
+
txs_to_remove = [
|
|
222
|
+
tx_hash for tx_hash, data in self.pending_transactions.items()
|
|
223
|
+
if current_time - data['timestamp'] > 1800 # 30 minutes
|
|
224
|
+
]
|
|
225
|
+
for tx_hash in txs_to_remove:
|
|
226
|
+
del self.pending_transactions[tx_hash]
|
|
255
227
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
228
|
+
def _process_main_chain_blocks(self):
|
|
229
|
+
"""
|
|
230
|
+
Process blocks that extend our current main chain.
|
|
231
|
+
Prioritizes blocks that build on our latest block.
|
|
232
|
+
"""
|
|
233
|
+
# Skip if we don't have a latest block yet
|
|
234
|
+
if not self.latest_block:
|
|
235
|
+
return
|
|
264
236
|
|
|
265
|
-
#
|
|
266
|
-
|
|
237
|
+
# Get the hash of our latest block
|
|
238
|
+
latest_hash = self.latest_block.get_hash()
|
|
267
239
|
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
240
|
+
# Find any pending blocks that build on our latest block
|
|
241
|
+
main_chain_blocks = []
|
|
242
|
+
for block_hash, data in list(self.pending_blocks.items()):
|
|
243
|
+
block = data['block']
|
|
272
244
|
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
245
|
+
# Check if this block extends our latest block
|
|
246
|
+
if block.previous == latest_hash:
|
|
247
|
+
main_chain_blocks.append(block)
|
|
276
248
|
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
249
|
+
# Process found blocks
|
|
250
|
+
for block in main_chain_blocks:
|
|
251
|
+
self._validate_and_process_main_chain_block(block)
|
|
280
252
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
#
|
|
290
|
-
|
|
253
|
+
def _validate_and_process_main_chain_block(self, block: Block):
|
|
254
|
+
"""
|
|
255
|
+
Validate and process a block that extends our main chain.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
block: Block to validate and process
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
# Validate block
|
|
262
|
+
is_valid = validate_block(block, self.blockchain.get_accounts_at_block(block.previous), self.blockchain.get_blocks())
|
|
291
263
|
|
|
292
|
-
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
264
|
+
if is_valid:
|
|
265
|
+
# Apply block to our state
|
|
266
|
+
success = validate_and_apply_block(self.blockchain, block)
|
|
267
|
+
if success:
|
|
268
|
+
print(f"Applied valid block {block.number} to blockchain state")
|
|
269
|
+
self._update_latest_block(block)
|
|
270
|
+
blocks_to_remove = [block.get_hash()]
|
|
271
|
+
for block_hash in blocks_to_remove:
|
|
272
|
+
if block_hash in self.pending_blocks:
|
|
273
|
+
del self.pending_blocks[block_hash]
|
|
274
|
+
print(f"Added block {block.number} to blockchain")
|
|
275
|
+
return True
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f"Error validating main chain block {block.number}: {e}")
|
|
306
278
|
|
|
307
|
-
|
|
279
|
+
return False
|
|
308
280
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
#
|
|
315
|
-
|
|
316
|
-
|
|
281
|
+
def _evaluate_candidate_chains(self):
|
|
282
|
+
"""
|
|
283
|
+
Evaluate candidate chains to determine if any should become our main chain.
|
|
284
|
+
This will validate pending blocks and look for chains with higher cumulative difficulty.
|
|
285
|
+
"""
|
|
286
|
+
# Skip if no candidate chains
|
|
287
|
+
if not self.candidate_chains:
|
|
288
|
+
return
|
|
317
289
|
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
290
|
+
# For each candidate chain, validate blocks and calculate metrics
|
|
291
|
+
for chain_id, data in list(self.candidate_chains.items()):
|
|
292
|
+
latest_candidate_block = data['latest_block']
|
|
321
293
|
|
|
322
|
-
#
|
|
323
|
-
|
|
294
|
+
# Build the chain backwards
|
|
295
|
+
chain_blocks = self._build_chain_from_latest(latest_candidate_block)
|
|
324
296
|
|
|
325
|
-
#
|
|
326
|
-
|
|
327
|
-
|
|
297
|
+
# Skip if we couldn't build a complete chain
|
|
298
|
+
if not chain_blocks:
|
|
299
|
+
continue
|
|
328
300
|
|
|
329
|
-
#
|
|
330
|
-
|
|
301
|
+
# Validate the entire chain
|
|
302
|
+
valid_chain = self._validate_candidate_chain(chain_blocks)
|
|
331
303
|
|
|
332
|
-
#
|
|
333
|
-
|
|
334
|
-
|
|
304
|
+
# If valid and better than our current chain, switch to it
|
|
305
|
+
if valid_chain and self._is_better_chain(chain_blocks):
|
|
306
|
+
self._switch_to_new_chain(chain_blocks)
|
|
335
307
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
308
|
+
def _build_chain_from_latest(self, latest_block: Block) -> List[Block]:
|
|
309
|
+
"""
|
|
310
|
+
Build a chain from the latest block back to a known point in our blockchain.
|
|
339
311
|
|
|
340
|
-
|
|
341
|
-
|
|
312
|
+
Args:
|
|
313
|
+
latest_block: Latest block in the candidate chain
|
|
342
314
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
#
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
#
|
|
356
|
-
|
|
357
|
-
|
|
315
|
+
Returns:
|
|
316
|
+
List of blocks in the chain, ordered from oldest to newest
|
|
317
|
+
"""
|
|
318
|
+
chain_blocks = [latest_block]
|
|
319
|
+
current_block = latest_block
|
|
320
|
+
|
|
321
|
+
# Track visited blocks to avoid cycles
|
|
322
|
+
visited = {current_block.get_hash()}
|
|
323
|
+
|
|
324
|
+
# Build chain backwards until we either:
|
|
325
|
+
# 1. Find a block in our main chain
|
|
326
|
+
# 2. Run out of blocks
|
|
327
|
+
# 3. Detect a cycle
|
|
328
|
+
while current_block.number > 0:
|
|
329
|
+
previous_hash = current_block.previous
|
|
358
330
|
|
|
359
|
-
#
|
|
360
|
-
|
|
361
|
-
#
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
331
|
+
# Check if we have this block in our blockchain
|
|
332
|
+
if hasattr(self.blockchain, 'has_block') and self.blockchain.has_block(previous_hash):
|
|
333
|
+
# Found connection to our main chain
|
|
334
|
+
previous_block = self.blockchain.get_block(previous_hash)
|
|
335
|
+
chain_blocks.insert(0, previous_block)
|
|
336
|
+
break
|
|
365
337
|
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
338
|
+
# Check if block is in pending blocks
|
|
339
|
+
elif previous_hash in self.pending_blocks:
|
|
340
|
+
previous_block = self.pending_blocks[previous_hash]['block']
|
|
369
341
|
|
|
370
|
-
#
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
342
|
+
# Check for cycles
|
|
343
|
+
if previous_hash in visited:
|
|
344
|
+
print(f"Cycle detected in candidate chain at block {previous_block.number}")
|
|
345
|
+
return []
|
|
374
346
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
#
|
|
380
|
-
|
|
381
|
-
|
|
347
|
+
visited.add(previous_hash)
|
|
348
|
+
chain_blocks.insert(0, previous_block)
|
|
349
|
+
current_block = previous_block
|
|
350
|
+
else:
|
|
351
|
+
# Missing block, cannot validate the chain
|
|
352
|
+
print(f"Missing block {previous_hash.hex()} in candidate chain")
|
|
353
|
+
return []
|
|
382
354
|
|
|
383
|
-
|
|
355
|
+
return chain_blocks
|
|
384
356
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
357
|
+
def _validate_candidate_chain(self, chain_blocks: List[Block]) -> bool:
|
|
358
|
+
"""
|
|
359
|
+
Validate a candidate chain of blocks.
|
|
388
360
|
|
|
389
|
-
|
|
390
|
-
|
|
361
|
+
Args:
|
|
362
|
+
chain_blocks: List of blocks in the chain (oldest to newest)
|
|
391
363
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
#
|
|
396
|
-
|
|
397
|
-
#
|
|
398
|
-
|
|
399
|
-
|
|
364
|
+
Returns:
|
|
365
|
+
True if the chain is valid, False otherwise
|
|
366
|
+
"""
|
|
367
|
+
# Validate each block in the chain
|
|
368
|
+
for i, block in enumerate(chain_blocks):
|
|
369
|
+
# Skip first block, it's either genesis or a block we already have
|
|
370
|
+
if i == 0:
|
|
371
|
+
continue
|
|
400
372
|
|
|
401
|
-
#
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
373
|
+
# Validate block connections
|
|
374
|
+
if block.previous != chain_blocks[i-1].get_hash():
|
|
375
|
+
print(f"Invalid chain: block {block.number} does not reference previous block")
|
|
376
|
+
return False
|
|
405
377
|
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
378
|
+
# Validate block
|
|
379
|
+
is_valid = validate_block(block, self.blockchain.get_accounts_at_block(block.previous), self.blockchain.get_blocks())
|
|
380
|
+
if not is_valid:
|
|
381
|
+
print(f"Invalid chain: block {block.number} is invalid")
|
|
382
|
+
return False
|
|
411
383
|
|
|
412
|
-
|
|
384
|
+
return True
|
|
413
385
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
386
|
+
def _is_better_chain(self, chain_blocks: List[Block]) -> bool:
|
|
387
|
+
"""
|
|
388
|
+
Determine if a candidate chain is better than our current chain.
|
|
417
389
|
|
|
418
|
-
|
|
419
|
-
|
|
390
|
+
Args:
|
|
391
|
+
chain_blocks: List of blocks in the candidate chain
|
|
420
392
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
#
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
#
|
|
428
|
-
|
|
429
|
-
|
|
393
|
+
Returns:
|
|
394
|
+
True if the candidate chain is better, False otherwise
|
|
395
|
+
"""
|
|
396
|
+
# Get the latest block from the candidate chain
|
|
397
|
+
candidate_latest = chain_blocks[-1]
|
|
398
|
+
|
|
399
|
+
# If we don't have a latest block, any valid chain is better
|
|
400
|
+
if not self.latest_block:
|
|
401
|
+
return True
|
|
430
402
|
|
|
431
|
-
#
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
403
|
+
# Compare block numbers (longest chain rule)
|
|
404
|
+
if candidate_latest.number > self.latest_block.number:
|
|
405
|
+
print(f"Candidate chain is longer: {candidate_latest.number} vs {self.latest_block.number}")
|
|
406
|
+
return True
|
|
435
407
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
#
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
def _switch_to_new_chain(self, chain_blocks: List[Block]):
|
|
411
|
+
"""
|
|
412
|
+
Switch to a new chain by adding all blocks to our blockchain.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
chain_blocks: List of blocks in the chain (oldest to newest)
|
|
416
|
+
"""
|
|
417
|
+
# Find the point where the chains diverge
|
|
418
|
+
divergence_point = 0
|
|
419
|
+
for i, block in enumerate(chain_blocks):
|
|
420
|
+
# Check if we have this block in our blockchain
|
|
421
|
+
if hasattr(self.blockchain, 'has_block') and self.blockchain.has_block(block.get_hash()):
|
|
422
|
+
divergence_point = i + 1
|
|
423
|
+
else:
|
|
424
|
+
break
|
|
453
425
|
|
|
454
|
-
#
|
|
455
|
-
|
|
456
|
-
|
|
426
|
+
# Add all blocks after the divergence point
|
|
427
|
+
for i in range(divergence_point, len(chain_blocks)):
|
|
428
|
+
block = chain_blocks[i]
|
|
457
429
|
|
|
458
|
-
#
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
430
|
+
# Add block to blockchain
|
|
431
|
+
if hasattr(self.blockchain, 'add_block'):
|
|
432
|
+
try:
|
|
433
|
+
self.blockchain.add_block(block)
|
|
462
434
|
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
435
|
+
# Remove from pending blocks
|
|
436
|
+
block_hash = block.get_hash()
|
|
437
|
+
if block_hash in self.pending_blocks:
|
|
438
|
+
del self.pending_blocks[block_hash]
|
|
467
439
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
440
|
+
print(f"Added block {block.number} to blockchain")
|
|
441
|
+
except Exception as e:
|
|
442
|
+
print(f"Error adding block {block.number} to blockchain: {e}")
|
|
443
|
+
return
|
|
472
444
|
|
|
473
|
-
#
|
|
474
|
-
|
|
475
|
-
|
|
445
|
+
# Update latest block
|
|
446
|
+
self._update_latest_block(chain_blocks[-1])
|
|
447
|
+
print(f"Switched to new chain, latest block: {self.latest_block.number}")
|