brawny 0.1.13__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.
- brawny/__init__.py +106 -0
- brawny/_context.py +232 -0
- brawny/_rpc/__init__.py +38 -0
- brawny/_rpc/broadcast.py +172 -0
- brawny/_rpc/clients.py +98 -0
- brawny/_rpc/context.py +49 -0
- brawny/_rpc/errors.py +252 -0
- brawny/_rpc/gas.py +158 -0
- brawny/_rpc/manager.py +982 -0
- brawny/_rpc/selector.py +156 -0
- brawny/accounts.py +534 -0
- brawny/alerts/__init__.py +132 -0
- brawny/alerts/abi_resolver.py +530 -0
- brawny/alerts/base.py +152 -0
- brawny/alerts/context.py +271 -0
- brawny/alerts/contracts.py +635 -0
- brawny/alerts/encoded_call.py +201 -0
- brawny/alerts/errors.py +267 -0
- brawny/alerts/events.py +680 -0
- brawny/alerts/function_caller.py +364 -0
- brawny/alerts/health.py +185 -0
- brawny/alerts/routing.py +118 -0
- brawny/alerts/send.py +364 -0
- brawny/api.py +660 -0
- brawny/chain.py +93 -0
- brawny/cli/__init__.py +16 -0
- brawny/cli/app.py +17 -0
- brawny/cli/bootstrap.py +37 -0
- brawny/cli/commands/__init__.py +41 -0
- brawny/cli/commands/abi.py +93 -0
- brawny/cli/commands/accounts.py +632 -0
- brawny/cli/commands/console.py +495 -0
- brawny/cli/commands/contract.py +139 -0
- brawny/cli/commands/health.py +112 -0
- brawny/cli/commands/init_project.py +86 -0
- brawny/cli/commands/intents.py +130 -0
- brawny/cli/commands/job_dev.py +254 -0
- brawny/cli/commands/jobs.py +308 -0
- brawny/cli/commands/logs.py +87 -0
- brawny/cli/commands/maintenance.py +182 -0
- brawny/cli/commands/migrate.py +51 -0
- brawny/cli/commands/networks.py +253 -0
- brawny/cli/commands/run.py +249 -0
- brawny/cli/commands/script.py +209 -0
- brawny/cli/commands/signer.py +248 -0
- brawny/cli/helpers.py +265 -0
- brawny/cli_templates.py +1445 -0
- brawny/config/__init__.py +74 -0
- brawny/config/models.py +404 -0
- brawny/config/parser.py +633 -0
- brawny/config/routing.py +55 -0
- brawny/config/validation.py +246 -0
- brawny/daemon/__init__.py +14 -0
- brawny/daemon/context.py +69 -0
- brawny/daemon/core.py +702 -0
- brawny/daemon/loops.py +327 -0
- brawny/db/__init__.py +78 -0
- brawny/db/base.py +986 -0
- brawny/db/base_new.py +165 -0
- brawny/db/circuit_breaker.py +97 -0
- brawny/db/global_cache.py +298 -0
- brawny/db/mappers.py +182 -0
- brawny/db/migrate.py +349 -0
- brawny/db/migrations/001_init.sql +186 -0
- brawny/db/migrations/002_add_included_block.sql +7 -0
- brawny/db/migrations/003_add_broadcast_at.sql +10 -0
- brawny/db/migrations/004_broadcast_binding.sql +20 -0
- brawny/db/migrations/005_add_retry_after.sql +9 -0
- brawny/db/migrations/006_add_retry_count_column.sql +11 -0
- brawny/db/migrations/007_add_gap_tracking.sql +18 -0
- brawny/db/migrations/008_add_transactions.sql +72 -0
- brawny/db/migrations/009_add_intent_metadata.sql +5 -0
- brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
- brawny/db/migrations/011_add_job_logs.sql +24 -0
- brawny/db/migrations/012_add_claimed_by.sql +5 -0
- brawny/db/ops/__init__.py +29 -0
- brawny/db/ops/attempts.py +108 -0
- brawny/db/ops/blocks.py +83 -0
- brawny/db/ops/cache.py +93 -0
- brawny/db/ops/intents.py +296 -0
- brawny/db/ops/jobs.py +110 -0
- brawny/db/ops/logs.py +97 -0
- brawny/db/ops/nonces.py +322 -0
- brawny/db/postgres.py +2535 -0
- brawny/db/postgres_new.py +196 -0
- brawny/db/queries.py +584 -0
- brawny/db/sqlite.py +2733 -0
- brawny/db/sqlite_new.py +191 -0
- brawny/history.py +126 -0
- brawny/interfaces.py +136 -0
- brawny/invariants.py +155 -0
- brawny/jobs/__init__.py +26 -0
- brawny/jobs/base.py +287 -0
- brawny/jobs/discovery.py +233 -0
- brawny/jobs/job_validation.py +111 -0
- brawny/jobs/kv.py +125 -0
- brawny/jobs/registry.py +283 -0
- brawny/keystore.py +484 -0
- brawny/lifecycle.py +551 -0
- brawny/logging.py +290 -0
- brawny/metrics.py +594 -0
- brawny/model/__init__.py +53 -0
- brawny/model/contexts.py +319 -0
- brawny/model/enums.py +70 -0
- brawny/model/errors.py +194 -0
- brawny/model/events.py +93 -0
- brawny/model/startup.py +20 -0
- brawny/model/types.py +483 -0
- brawny/networks/__init__.py +96 -0
- brawny/networks/config.py +269 -0
- brawny/networks/manager.py +423 -0
- brawny/obs/__init__.py +67 -0
- brawny/obs/emit.py +158 -0
- brawny/obs/health.py +175 -0
- brawny/obs/heartbeat.py +133 -0
- brawny/reconciliation.py +108 -0
- brawny/scheduler/__init__.py +19 -0
- brawny/scheduler/poller.py +472 -0
- brawny/scheduler/reorg.py +632 -0
- brawny/scheduler/runner.py +708 -0
- brawny/scheduler/shutdown.py +371 -0
- brawny/script_tx.py +297 -0
- brawny/scripting.py +251 -0
- brawny/startup.py +76 -0
- brawny/telegram.py +393 -0
- brawny/testing.py +108 -0
- brawny/tx/__init__.py +41 -0
- brawny/tx/executor.py +1071 -0
- brawny/tx/fees.py +50 -0
- brawny/tx/intent.py +423 -0
- brawny/tx/monitor.py +628 -0
- brawny/tx/nonce.py +498 -0
- brawny/tx/replacement.py +456 -0
- brawny/tx/utils.py +26 -0
- brawny/utils.py +205 -0
- brawny/validation.py +69 -0
- brawny-0.1.13.dist-info/METADATA +156 -0
- brawny-0.1.13.dist-info/RECORD +141 -0
- brawny-0.1.13.dist-info/WHEEL +5 -0
- brawny-0.1.13.dist-info/entry_points.txt +2 -0
- brawny-0.1.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Block poller for continuous block processing.
|
|
2
|
+
|
|
3
|
+
Implements the polling loop from SPEC 5:
|
|
4
|
+
- HTTP poll head block (eth_blockNumber)
|
|
5
|
+
- Process sequentially from last_processed+1 up to head
|
|
6
|
+
- Limit catchup blocks per iteration
|
|
7
|
+
- Sleep poll_interval_seconds between iterations
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from threading import Event, Thread
|
|
15
|
+
from typing import TYPE_CHECKING, Callable
|
|
16
|
+
|
|
17
|
+
from brawny.alerts.health import health_alert
|
|
18
|
+
from brawny.invariants import collect_invariants
|
|
19
|
+
from brawny.logging import LogEvents, get_logger
|
|
20
|
+
from brawny.metrics import (
|
|
21
|
+
BLOCK_PROCESSING_SECONDS,
|
|
22
|
+
BLOCKS_PROCESSED,
|
|
23
|
+
BLOCK_PROCESSING_LAG_SECONDS,
|
|
24
|
+
LAST_BLOCK_TIMESTAMP,
|
|
25
|
+
LAST_BLOCK_PROCESSED_TIMESTAMP,
|
|
26
|
+
LAST_PROCESSED_BLOCK,
|
|
27
|
+
OLDEST_PENDING_INTENT_AGE_SECONDS,
|
|
28
|
+
PENDING_INTENTS,
|
|
29
|
+
get_metrics,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Collect invariants every N blocks to avoid overhead
|
|
33
|
+
INVARIANT_COLLECTION_INTERVAL_BLOCKS = 6
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from brawny.config import Config
|
|
37
|
+
from brawny.db.base import Database
|
|
38
|
+
from brawny.model.types import BlockInfo
|
|
39
|
+
from brawny._rpc.manager import RPCManager
|
|
40
|
+
from brawny.scheduler.reorg import ReorgDetector
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class PollResult:
|
|
47
|
+
"""Result of a poll iteration."""
|
|
48
|
+
|
|
49
|
+
blocks_processed: int
|
|
50
|
+
head_block: int
|
|
51
|
+
last_processed: int
|
|
52
|
+
reorg_detected: bool = False
|
|
53
|
+
reorg_depth: int = 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BlockPoller:
|
|
57
|
+
"""Block poller with configurable interval.
|
|
58
|
+
|
|
59
|
+
Always starts at chain head (live-head mode). No historical catchup.
|
|
60
|
+
Downtime means missed block evaluations by design.
|
|
61
|
+
|
|
62
|
+
Provides the main polling loop that:
|
|
63
|
+
1. Gets head block from RPC
|
|
64
|
+
2. Processes blocks sequentially from last processed (or head on startup)
|
|
65
|
+
3. Calls block handler for each block
|
|
66
|
+
4. Sleeps between iterations
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
db: Database,
|
|
72
|
+
rpc: RPCManager,
|
|
73
|
+
config: Config,
|
|
74
|
+
block_handler: Callable[[BlockInfo], None],
|
|
75
|
+
reorg_detector: "ReorgDetector | None" = None,
|
|
76
|
+
health_send_fn: Callable[..., None] | None = None,
|
|
77
|
+
health_chat_id: str | None = None,
|
|
78
|
+
health_cooldown: int = 1800,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Initialize block poller.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
db: Database connection
|
|
84
|
+
rpc: RPC manager
|
|
85
|
+
config: Application configuration
|
|
86
|
+
block_handler: Callback for processing each block
|
|
87
|
+
reorg_detector: Optional reorg detector
|
|
88
|
+
health_send_fn: Optional health alert send function
|
|
89
|
+
health_chat_id: Optional health alert chat ID
|
|
90
|
+
health_cooldown: Health alert cooldown in seconds
|
|
91
|
+
"""
|
|
92
|
+
self._db = db
|
|
93
|
+
self._rpc = rpc
|
|
94
|
+
self._config = config
|
|
95
|
+
self._block_handler = block_handler
|
|
96
|
+
self._reorg_detector = reorg_detector
|
|
97
|
+
self._chain_id = config.chain_id
|
|
98
|
+
|
|
99
|
+
# Health alerting
|
|
100
|
+
self._health_send_fn = health_send_fn
|
|
101
|
+
self._health_chat_id = health_chat_id
|
|
102
|
+
self._health_cooldown = health_cooldown
|
|
103
|
+
|
|
104
|
+
# Polling state
|
|
105
|
+
self._running = False
|
|
106
|
+
self._stop_event = Event()
|
|
107
|
+
self._poll_thread: Thread | None = None
|
|
108
|
+
|
|
109
|
+
# Session state - will be initialized on first poll
|
|
110
|
+
self._session_last_processed: int | None = None
|
|
111
|
+
|
|
112
|
+
# Invariant collection tick counter
|
|
113
|
+
self._invariant_tick_count = 0
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_running(self) -> bool:
|
|
117
|
+
"""Check if poller is running."""
|
|
118
|
+
return self._running
|
|
119
|
+
|
|
120
|
+
def start(self, blocking: bool = True) -> None:
|
|
121
|
+
"""Start the polling loop.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
blocking: If True, run in current thread. Otherwise spawn thread.
|
|
125
|
+
"""
|
|
126
|
+
self._running = True
|
|
127
|
+
self._stop_event.clear()
|
|
128
|
+
|
|
129
|
+
if blocking:
|
|
130
|
+
self._poll_loop()
|
|
131
|
+
else:
|
|
132
|
+
self._poll_thread = Thread(target=self._poll_loop, daemon=True)
|
|
133
|
+
self._poll_thread.start()
|
|
134
|
+
|
|
135
|
+
def stop(self, timeout: float | None = None) -> bool:
|
|
136
|
+
"""Stop the polling loop.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
timeout: Max seconds to wait for clean stop
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if stopped cleanly, False if timed out
|
|
143
|
+
"""
|
|
144
|
+
self._stop_event.set()
|
|
145
|
+
self._running = False
|
|
146
|
+
|
|
147
|
+
if self._poll_thread and self._poll_thread.is_alive():
|
|
148
|
+
self._poll_thread.join(timeout=timeout)
|
|
149
|
+
return not self._poll_thread.is_alive()
|
|
150
|
+
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
def wait(self, timeout: float | None = None) -> bool:
|
|
154
|
+
"""Wait for current block processing to complete.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
timeout: Max seconds to wait
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if completed within timeout
|
|
161
|
+
"""
|
|
162
|
+
if self._poll_thread:
|
|
163
|
+
self._poll_thread.join(timeout=timeout)
|
|
164
|
+
return not self._poll_thread.is_alive()
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
def _poll_loop(self) -> None:
|
|
168
|
+
"""Main polling loop."""
|
|
169
|
+
logger.info(
|
|
170
|
+
"poller.started",
|
|
171
|
+
chain_id=self._chain_id,
|
|
172
|
+
poll_interval=self._config.poll_interval_seconds,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
while not self._stop_event.is_set():
|
|
176
|
+
try:
|
|
177
|
+
result = self._poll_once()
|
|
178
|
+
|
|
179
|
+
if result.blocks_processed > 0:
|
|
180
|
+
logger.debug(
|
|
181
|
+
"poller.iteration",
|
|
182
|
+
blocks_processed=result.blocks_processed,
|
|
183
|
+
head=result.head_block,
|
|
184
|
+
last_processed=result.last_processed,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(
|
|
189
|
+
"poller.error",
|
|
190
|
+
error=str(e),
|
|
191
|
+
exc_info=True,
|
|
192
|
+
)
|
|
193
|
+
health_alert(
|
|
194
|
+
component="brawny.scheduler.poller",
|
|
195
|
+
chain_id=self._chain_id,
|
|
196
|
+
error=e,
|
|
197
|
+
action="Check RPC connectivity",
|
|
198
|
+
db_dialect=self._db.dialect,
|
|
199
|
+
send_fn=self._health_send_fn,
|
|
200
|
+
health_chat_id=self._health_chat_id,
|
|
201
|
+
cooldown_seconds=self._health_cooldown,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Sleep between iterations
|
|
205
|
+
if not self._stop_event.wait(timeout=self._config.poll_interval_seconds):
|
|
206
|
+
continue
|
|
207
|
+
else:
|
|
208
|
+
break # Stop event was set
|
|
209
|
+
|
|
210
|
+
logger.info("poller.stopped")
|
|
211
|
+
|
|
212
|
+
def _poll_once(self) -> PollResult:
|
|
213
|
+
"""Execute one poll iteration.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Poll result with blocks processed
|
|
217
|
+
"""
|
|
218
|
+
# Get head block (bounded by RPC timeout)
|
|
219
|
+
timeout = min(5.0, float(self._config.rpc_timeout_seconds))
|
|
220
|
+
head_block = self._rpc.get_block_number(timeout=timeout)
|
|
221
|
+
|
|
222
|
+
# Determine starting point
|
|
223
|
+
if self._session_last_processed is None:
|
|
224
|
+
# First poll of session - always start at chain head (no catchup)
|
|
225
|
+
last_processed = head_block - 1
|
|
226
|
+
logger.info(
|
|
227
|
+
"poller.starting_at_head",
|
|
228
|
+
chain_id=self._chain_id,
|
|
229
|
+
head_block=head_block,
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
last_processed = self._session_last_processed
|
|
233
|
+
|
|
234
|
+
# Calculate how many blocks to process
|
|
235
|
+
blocks_to_process = head_block - last_processed
|
|
236
|
+
|
|
237
|
+
if blocks_to_process <= 0:
|
|
238
|
+
return PollResult(
|
|
239
|
+
blocks_processed=0,
|
|
240
|
+
head_block=head_block,
|
|
241
|
+
last_processed=last_processed,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Process blocks sequentially
|
|
245
|
+
blocks_processed = 0
|
|
246
|
+
for block_number in range(last_processed + 1, last_processed + blocks_to_process + 1):
|
|
247
|
+
if self._stop_event.is_set():
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
if self._reorg_detector:
|
|
252
|
+
reorg_result = self._reorg_detector.check(block_number)
|
|
253
|
+
if reorg_result.reorg_detected:
|
|
254
|
+
if reorg_result.pause:
|
|
255
|
+
logger.error(
|
|
256
|
+
"poller.reorg_pause",
|
|
257
|
+
chain_id=self._chain_id,
|
|
258
|
+
reason=reorg_result.rewind_reason,
|
|
259
|
+
)
|
|
260
|
+
self._stop_event.set()
|
|
261
|
+
self._running = False
|
|
262
|
+
return PollResult(
|
|
263
|
+
blocks_processed=0,
|
|
264
|
+
head_block=head_block,
|
|
265
|
+
last_processed=last_processed,
|
|
266
|
+
reorg_detected=True,
|
|
267
|
+
reorg_depth=reorg_result.reorg_depth,
|
|
268
|
+
)
|
|
269
|
+
if reorg_result.last_good_height is None or reorg_result.last_good_height < 0:
|
|
270
|
+
self._reorg_detector.handle_deep_reorg()
|
|
271
|
+
if self._config.deep_reorg_pause:
|
|
272
|
+
logger.error(
|
|
273
|
+
"poller.deep_reorg_pause",
|
|
274
|
+
chain_id=self._chain_id,
|
|
275
|
+
)
|
|
276
|
+
self._stop_event.set()
|
|
277
|
+
self._running = False
|
|
278
|
+
# Deep reorg - reset to start fresh at head on next poll
|
|
279
|
+
self._session_last_processed = None
|
|
280
|
+
return PollResult(
|
|
281
|
+
blocks_processed=0,
|
|
282
|
+
head_block=head_block,
|
|
283
|
+
last_processed=last_processed,
|
|
284
|
+
reorg_detected=True,
|
|
285
|
+
reorg_depth=reorg_result.reorg_depth,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
rewind_result = self._reorg_detector.rewind(reorg_result)
|
|
289
|
+
new_last = (
|
|
290
|
+
rewind_result.last_good_height
|
|
291
|
+
if rewind_result.last_good_height is not None
|
|
292
|
+
else last_processed
|
|
293
|
+
)
|
|
294
|
+
self._session_last_processed = new_last
|
|
295
|
+
return PollResult(
|
|
296
|
+
blocks_processed=0,
|
|
297
|
+
head_block=head_block,
|
|
298
|
+
last_processed=new_last,
|
|
299
|
+
reorg_detected=True,
|
|
300
|
+
reorg_depth=reorg_result.reorg_depth,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Fetch block with retries for transient RPC issues
|
|
304
|
+
block_info = None
|
|
305
|
+
for retry in range(3):
|
|
306
|
+
block_info = self._fetch_block_info(block_number)
|
|
307
|
+
if block_info is not None:
|
|
308
|
+
break
|
|
309
|
+
# Exponential backoff: 0.5s, 1s, 2s
|
|
310
|
+
backoff = 0.5 * (2**retry)
|
|
311
|
+
logger.debug(
|
|
312
|
+
"poller.block_fetch_retry",
|
|
313
|
+
block_number=block_number,
|
|
314
|
+
retry=retry + 1,
|
|
315
|
+
backoff_seconds=backoff,
|
|
316
|
+
)
|
|
317
|
+
time.sleep(backoff)
|
|
318
|
+
|
|
319
|
+
if block_info is None:
|
|
320
|
+
logger.warning(
|
|
321
|
+
"poller.block_not_found_after_retries",
|
|
322
|
+
block_number=block_number,
|
|
323
|
+
retries=3,
|
|
324
|
+
)
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
# Log block ingestion start
|
|
328
|
+
logger.debug(
|
|
329
|
+
LogEvents.BLOCK_INGEST_START,
|
|
330
|
+
block_number=block_number,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Call the block handler (this is where job evaluation happens)
|
|
334
|
+
metrics = get_metrics()
|
|
335
|
+
start_time = time.perf_counter()
|
|
336
|
+
self._block_handler(block_info)
|
|
337
|
+
duration = time.perf_counter() - start_time
|
|
338
|
+
metrics.histogram(BLOCK_PROCESSING_SECONDS).observe(
|
|
339
|
+
duration,
|
|
340
|
+
chain_id=self._chain_id,
|
|
341
|
+
)
|
|
342
|
+
metrics.counter(BLOCKS_PROCESSED).inc(chain_id=self._chain_id)
|
|
343
|
+
metrics.gauge(LAST_PROCESSED_BLOCK).set(
|
|
344
|
+
block_info.block_number,
|
|
345
|
+
chain_id=self._chain_id,
|
|
346
|
+
)
|
|
347
|
+
pending_count = self._db.get_pending_intent_count(chain_id=self._chain_id)
|
|
348
|
+
metrics.gauge(PENDING_INTENTS).set(
|
|
349
|
+
pending_count,
|
|
350
|
+
chain_id=self._chain_id,
|
|
351
|
+
)
|
|
352
|
+
oldest_age = self._db.get_oldest_pending_intent_age(chain_id=self._chain_id)
|
|
353
|
+
if oldest_age is not None:
|
|
354
|
+
metrics.gauge(OLDEST_PENDING_INTENT_AGE_SECONDS).set(
|
|
355
|
+
oldest_age,
|
|
356
|
+
chain_id=self._chain_id,
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
metrics.gauge(OLDEST_PENDING_INTENT_AGE_SECONDS).set(
|
|
360
|
+
0,
|
|
361
|
+
chain_id=self._chain_id,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Update block state and hash history
|
|
365
|
+
self._db.upsert_block_state(
|
|
366
|
+
self._chain_id,
|
|
367
|
+
block_number,
|
|
368
|
+
block_info.block_hash,
|
|
369
|
+
)
|
|
370
|
+
self._db.insert_block_hash(
|
|
371
|
+
self._chain_id,
|
|
372
|
+
block_number,
|
|
373
|
+
block_info.block_hash,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Cleanup old block hashes
|
|
377
|
+
self._db.cleanup_old_block_hashes(
|
|
378
|
+
self._chain_id,
|
|
379
|
+
self._config.block_hash_history_size,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Emit timestamp after all DB commits are complete
|
|
383
|
+
metrics.gauge(LAST_BLOCK_PROCESSED_TIMESTAMP).set(
|
|
384
|
+
time.time(),
|
|
385
|
+
chain_id=self._chain_id,
|
|
386
|
+
)
|
|
387
|
+
metrics.gauge(LAST_BLOCK_TIMESTAMP).set(
|
|
388
|
+
block_info.timestamp,
|
|
389
|
+
chain_id=self._chain_id,
|
|
390
|
+
)
|
|
391
|
+
metrics.gauge(BLOCK_PROCESSING_LAG_SECONDS).set(
|
|
392
|
+
time.time() - float(block_info.timestamp),
|
|
393
|
+
chain_id=self._chain_id,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Collect invariants every N blocks
|
|
397
|
+
self._invariant_tick_count += 1
|
|
398
|
+
if self._invariant_tick_count >= INVARIANT_COLLECTION_INTERVAL_BLOCKS:
|
|
399
|
+
self._invariant_tick_count = 0
|
|
400
|
+
try:
|
|
401
|
+
collect_invariants(
|
|
402
|
+
self._db,
|
|
403
|
+
self._chain_id,
|
|
404
|
+
health_send_fn=self._health_send_fn,
|
|
405
|
+
health_chat_id=self._health_chat_id,
|
|
406
|
+
health_cooldown=self._health_cooldown,
|
|
407
|
+
)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.error(
|
|
410
|
+
"invariants.collection_failed",
|
|
411
|
+
error=str(e),
|
|
412
|
+
exc_info=True,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
logger.debug(
|
|
416
|
+
LogEvents.BLOCK_INGEST_DONE,
|
|
417
|
+
block_number=block_number,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
blocks_processed += 1
|
|
421
|
+
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.error(
|
|
424
|
+
"poller.block_error",
|
|
425
|
+
block_number=block_number,
|
|
426
|
+
error=str(e),
|
|
427
|
+
)
|
|
428
|
+
break
|
|
429
|
+
|
|
430
|
+
# Update session state
|
|
431
|
+
self._session_last_processed = last_processed + blocks_processed
|
|
432
|
+
|
|
433
|
+
return PollResult(
|
|
434
|
+
blocks_processed=blocks_processed,
|
|
435
|
+
head_block=head_block,
|
|
436
|
+
last_processed=last_processed + blocks_processed,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def _fetch_block_info(self, block_number: int) -> BlockInfo | None:
|
|
440
|
+
"""Fetch block info from RPC.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
block_number: Block number to fetch
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
BlockInfo or None if not found
|
|
447
|
+
"""
|
|
448
|
+
from brawny.model.types import BlockInfo
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
block = self._rpc.get_block(block_number)
|
|
452
|
+
if block is None:
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
return BlockInfo(
|
|
456
|
+
chain_id=self._chain_id,
|
|
457
|
+
block_number=block["number"],
|
|
458
|
+
block_hash=f"0x{block['hash'].hex()}" if isinstance(block["hash"], bytes) else block["hash"],
|
|
459
|
+
timestamp=block["timestamp"],
|
|
460
|
+
)
|
|
461
|
+
except Exception:
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
def poll_once(self) -> PollResult:
|
|
465
|
+
"""Public method to run a single poll iteration.
|
|
466
|
+
|
|
467
|
+
Useful for testing and one-shot processing.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Poll result
|
|
471
|
+
"""
|
|
472
|
+
return self._poll_once()
|