brawny 0.1.13__py3-none-any.whl → 0.1.22__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 +2 -0
- brawny/_context.py +5 -5
- brawny/_rpc/__init__.py +36 -12
- brawny/_rpc/broadcast.py +14 -13
- brawny/_rpc/caller.py +243 -0
- brawny/_rpc/client.py +539 -0
- brawny/_rpc/clients.py +11 -11
- brawny/_rpc/context.py +23 -0
- brawny/_rpc/errors.py +465 -31
- brawny/_rpc/gas.py +7 -6
- brawny/_rpc/pool.py +18 -0
- brawny/_rpc/retry.py +266 -0
- brawny/_rpc/retry_policy.py +81 -0
- brawny/accounts.py +28 -9
- brawny/alerts/__init__.py +15 -18
- brawny/alerts/abi_resolver.py +212 -36
- brawny/alerts/base.py +2 -2
- brawny/alerts/contracts.py +77 -10
- brawny/alerts/errors.py +30 -3
- brawny/alerts/events.py +38 -5
- brawny/alerts/health.py +19 -13
- brawny/alerts/send.py +513 -55
- brawny/api.py +39 -11
- brawny/assets/AGENTS.md +325 -0
- brawny/async_runtime.py +48 -0
- brawny/chain.py +3 -3
- brawny/cli/commands/__init__.py +2 -0
- brawny/cli/commands/console.py +69 -19
- brawny/cli/commands/contract.py +2 -2
- brawny/cli/commands/controls.py +121 -0
- brawny/cli/commands/health.py +2 -2
- brawny/cli/commands/job_dev.py +6 -5
- brawny/cli/commands/jobs.py +99 -2
- brawny/cli/commands/maintenance.py +13 -29
- brawny/cli/commands/migrate.py +1 -0
- brawny/cli/commands/run.py +10 -3
- brawny/cli/commands/script.py +8 -3
- brawny/cli/commands/signer.py +143 -26
- brawny/cli/helpers.py +0 -3
- brawny/cli_templates.py +25 -349
- brawny/config/__init__.py +4 -1
- brawny/config/models.py +43 -57
- brawny/config/parser.py +268 -57
- brawny/config/validation.py +52 -15
- brawny/daemon/context.py +4 -2
- brawny/daemon/core.py +185 -63
- brawny/daemon/loops.py +166 -98
- brawny/daemon/supervisor.py +261 -0
- brawny/db/__init__.py +14 -26
- brawny/db/base.py +248 -151
- brawny/db/global_cache.py +11 -1
- brawny/db/migrate.py +175 -28
- brawny/db/migrations/001_init.sql +4 -3
- brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
- brawny/db/migrations/011_add_job_logs.sql +1 -2
- brawny/db/migrations/012_add_claimed_by.sql +2 -2
- brawny/db/migrations/013_attempt_unique.sql +10 -0
- brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
- brawny/db/migrations/015_add_signer_alias.sql +14 -0
- brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
- brawny/db/migrations/017_add_job_drain.sql +6 -0
- brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
- brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
- brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
- brawny/db/ops/__init__.py +3 -25
- brawny/db/ops/logs.py +1 -2
- brawny/db/queries.py +47 -91
- brawny/db/serialized.py +65 -0
- brawny/db/sqlite/__init__.py +1001 -0
- brawny/db/sqlite/connection.py +231 -0
- brawny/db/sqlite/execute.py +116 -0
- brawny/db/sqlite/mappers.py +190 -0
- brawny/db/sqlite/repos/attempts.py +372 -0
- brawny/db/sqlite/repos/block_state.py +102 -0
- brawny/db/sqlite/repos/cache.py +104 -0
- brawny/db/sqlite/repos/intents.py +1021 -0
- brawny/db/sqlite/repos/jobs.py +200 -0
- brawny/db/sqlite/repos/maintenance.py +182 -0
- brawny/db/sqlite/repos/signers_nonces.py +566 -0
- brawny/db/sqlite/tx.py +119 -0
- brawny/http.py +194 -0
- brawny/invariants.py +11 -24
- brawny/jobs/base.py +8 -0
- brawny/jobs/job_validation.py +2 -1
- brawny/keystore.py +83 -7
- brawny/lifecycle.py +64 -12
- brawny/logging.py +0 -2
- brawny/metrics.py +84 -12
- brawny/model/contexts.py +111 -9
- brawny/model/enums.py +1 -0
- brawny/model/errors.py +18 -0
- brawny/model/types.py +47 -131
- brawny/network_guard.py +133 -0
- brawny/networks/__init__.py +5 -5
- brawny/networks/config.py +1 -7
- brawny/networks/manager.py +14 -11
- brawny/runtime_controls.py +74 -0
- brawny/scheduler/poller.py +11 -7
- brawny/scheduler/reorg.py +95 -39
- brawny/scheduler/runner.py +442 -168
- brawny/scheduler/shutdown.py +3 -3
- brawny/script_tx.py +3 -3
- brawny/telegram.py +53 -7
- brawny/testing.py +1 -0
- brawny/timeout.py +38 -0
- brawny/tx/executor.py +922 -308
- brawny/tx/intent.py +54 -16
- brawny/tx/monitor.py +31 -12
- brawny/tx/nonce.py +212 -90
- brawny/tx/replacement.py +69 -18
- brawny/tx/retry_policy.py +24 -0
- brawny/tx/stages/types.py +75 -0
- brawny/types.py +18 -0
- brawny/utils.py +41 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
- brawny-0.1.22.dist-info/RECORD +163 -0
- brawny/_rpc/manager.py +0 -982
- brawny/_rpc/selector.py +0 -156
- brawny/db/base_new.py +0 -165
- brawny/db/mappers.py +0 -182
- brawny/db/migrations/008_add_transactions.sql +0 -72
- brawny/db/ops/attempts.py +0 -108
- brawny/db/ops/blocks.py +0 -83
- brawny/db/ops/cache.py +0 -93
- brawny/db/ops/intents.py +0 -296
- brawny/db/ops/jobs.py +0 -110
- brawny/db/ops/nonces.py +0 -322
- brawny/db/postgres.py +0 -2535
- brawny/db/postgres_new.py +0 -196
- brawny/db/sqlite.py +0 -2733
- brawny/db/sqlite_new.py +0 -191
- brawny-0.1.13.dist-info/RECORD +0 -141
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/top_level.txt +0 -0
brawny/scheduler/reorg.py
CHANGED
|
@@ -9,6 +9,7 @@ Implements reorg detection from SPEC 5.2:
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import asyncio
|
|
12
13
|
from dataclasses import dataclass, replace
|
|
13
14
|
from typing import TYPE_CHECKING, Callable
|
|
14
15
|
|
|
@@ -16,12 +17,14 @@ from brawny.alerts.health import health_alert
|
|
|
16
17
|
from brawny.logging import LogEvents, get_logger
|
|
17
18
|
from brawny.metrics import REORGS_DETECTED, get_metrics
|
|
18
19
|
from brawny.model.enums import AttemptStatus, IntentStatus, NonceStatus
|
|
20
|
+
from brawny.model.errors import DatabaseError
|
|
19
21
|
from brawny.tx.intent import transition_intent
|
|
22
|
+
from brawny._rpc.errors import RPCError
|
|
20
23
|
|
|
21
24
|
if TYPE_CHECKING:
|
|
22
25
|
from brawny.db.base import Database
|
|
23
26
|
from brawny.lifecycle import LifecycleDispatcher
|
|
24
|
-
from brawny._rpc.
|
|
27
|
+
from brawny._rpc.clients import ReadClient
|
|
25
28
|
|
|
26
29
|
logger = get_logger(__name__)
|
|
27
30
|
|
|
@@ -59,7 +62,7 @@ class ReorgDetector:
|
|
|
59
62
|
def __init__(
|
|
60
63
|
self,
|
|
61
64
|
db: Database,
|
|
62
|
-
rpc:
|
|
65
|
+
rpc: ReadClient,
|
|
63
66
|
chain_id: int,
|
|
64
67
|
reorg_depth: int = 32,
|
|
65
68
|
block_hash_history_size: int = 256,
|
|
@@ -91,14 +94,14 @@ class ReorgDetector:
|
|
|
91
94
|
self._health_chat_id = health_chat_id
|
|
92
95
|
self._health_cooldown = health_cooldown
|
|
93
96
|
|
|
94
|
-
def check(self, current_block: int) -> ReorgResult:
|
|
97
|
+
def check(self, current_block: int) -> ReorgResult | None:
|
|
95
98
|
"""Check for reorg at the current block height.
|
|
96
99
|
|
|
97
100
|
Args:
|
|
98
101
|
current_block: Current block being processed
|
|
99
102
|
|
|
100
103
|
Returns:
|
|
101
|
-
ReorgResult with detection status
|
|
104
|
+
ReorgResult with detection status, or None if probing fails
|
|
102
105
|
"""
|
|
103
106
|
# Get block state
|
|
104
107
|
block_state = self._db.get_block_state(self._chain_id)
|
|
@@ -126,6 +129,15 @@ class ReorgDetector:
|
|
|
126
129
|
return ReorgResult(reorg_detected=False)
|
|
127
130
|
anchor_missing = True
|
|
128
131
|
elif anchor_height >= history_min:
|
|
132
|
+
if history_max is not None and history_max - history_min < self._reorg_depth:
|
|
133
|
+
logger.warning(
|
|
134
|
+
"reorg.history_insufficient",
|
|
135
|
+
anchor_height=anchor_height,
|
|
136
|
+
history_min=history_min,
|
|
137
|
+
history_max=history_max,
|
|
138
|
+
reorg_depth=self._reorg_depth,
|
|
139
|
+
)
|
|
140
|
+
return ReorgResult(reorg_detected=False)
|
|
129
141
|
# Expected history missing -> possible corruption
|
|
130
142
|
logger.error(
|
|
131
143
|
"reorg.history_missing",
|
|
@@ -170,10 +182,13 @@ class ReorgDetector:
|
|
|
170
182
|
chain_hash = chain_hash.hex()
|
|
171
183
|
if not chain_hash.startswith("0x"):
|
|
172
184
|
chain_hash = f"0x{chain_hash}"
|
|
173
|
-
except
|
|
185
|
+
except asyncio.CancelledError:
|
|
186
|
+
raise
|
|
187
|
+
except RPCError as e:
|
|
174
188
|
logger.warning(
|
|
175
189
|
"reorg.check_failed",
|
|
176
190
|
anchor_height=anchor_height,
|
|
191
|
+
chain_id=self._chain_id,
|
|
177
192
|
error=str(e),
|
|
178
193
|
)
|
|
179
194
|
return ReorgResult(reorg_detected=False)
|
|
@@ -188,6 +203,11 @@ class ReorgDetector:
|
|
|
188
203
|
|
|
189
204
|
# Reorg detected!
|
|
190
205
|
rewind_reason = "missing_history" if anchor_missing else "anchor_mismatch"
|
|
206
|
+
# Find last good height via binary search
|
|
207
|
+
last_good_height = self._find_last_good_height(anchor_height, last_processed)
|
|
208
|
+
if last_good_height is None:
|
|
209
|
+
return None
|
|
210
|
+
|
|
191
211
|
logger.warning(
|
|
192
212
|
LogEvents.BLOCK_REORG_DETECTED,
|
|
193
213
|
anchor_height=anchor_height,
|
|
@@ -198,9 +218,6 @@ class ReorgDetector:
|
|
|
198
218
|
metrics.counter(REORGS_DETECTED).inc(
|
|
199
219
|
chain_id=self._chain_id,
|
|
200
220
|
)
|
|
201
|
-
|
|
202
|
-
# Find last good height via binary search
|
|
203
|
-
last_good_height = self._find_last_good_height(anchor_height, last_processed)
|
|
204
221
|
oldest = history_min
|
|
205
222
|
if oldest is not None and last_good_height < oldest:
|
|
206
223
|
finality_floor = max(0, last_processed - self._finality_confirmations)
|
|
@@ -271,7 +288,7 @@ class ReorgDetector:
|
|
|
271
288
|
last_processed=last_processed,
|
|
272
289
|
)
|
|
273
290
|
|
|
274
|
-
def _find_last_good_height(self, low: int, high: int) -> int:
|
|
291
|
+
def _find_last_good_height(self, low: int, high: int) -> int | None:
|
|
275
292
|
"""Binary search to find last matching block height.
|
|
276
293
|
|
|
277
294
|
Args:
|
|
@@ -279,29 +296,33 @@ class ReorgDetector:
|
|
|
279
296
|
high: Upper bound (known bad)
|
|
280
297
|
|
|
281
298
|
Returns:
|
|
282
|
-
Last good block height
|
|
299
|
+
Last good block height, or None if probing fails
|
|
283
300
|
"""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
mid = None
|
|
302
|
+
left = None
|
|
303
|
+
right = None
|
|
287
304
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
305
|
+
try:
|
|
306
|
+
oldest = self._db.get_oldest_block_in_history(self._chain_id)
|
|
307
|
+
if oldest is None:
|
|
308
|
+
return low
|
|
292
309
|
|
|
293
|
-
|
|
310
|
+
# Start from the known bad anchor and search forward
|
|
311
|
+
# We need to find where the chain diverged
|
|
312
|
+
left = max(oldest, low)
|
|
313
|
+
right = high
|
|
294
314
|
|
|
295
|
-
|
|
296
|
-
mid = (left + right) // 2
|
|
315
|
+
last_good = left - 1 # Assume nothing matches if search fails
|
|
297
316
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
while left <= right:
|
|
318
|
+
mid = (left + right) // 2
|
|
319
|
+
|
|
320
|
+
stored = self._db.get_block_hash_at_height(self._chain_id, mid)
|
|
321
|
+
if stored is None:
|
|
322
|
+
# No history here, move right
|
|
323
|
+
left = mid + 1
|
|
324
|
+
continue
|
|
303
325
|
|
|
304
|
-
try:
|
|
305
326
|
block = self._rpc.get_block(mid)
|
|
306
327
|
if block is None:
|
|
307
328
|
left = mid + 1
|
|
@@ -312,19 +333,41 @@ class ReorgDetector:
|
|
|
312
333
|
chain_hash = chain_hash.hex()
|
|
313
334
|
if not chain_hash.startswith("0x"):
|
|
314
335
|
chain_hash = f"0x{chain_hash}"
|
|
315
|
-
except Exception:
|
|
316
|
-
left = mid + 1
|
|
317
|
-
continue
|
|
318
336
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
337
|
+
if stored.lower() == chain_hash.lower():
|
|
338
|
+
# Match - reorg is after this point
|
|
339
|
+
last_good = mid
|
|
340
|
+
left = mid + 1
|
|
341
|
+
else:
|
|
342
|
+
# Mismatch - reorg is at or before this point
|
|
343
|
+
right = mid - 1
|
|
344
|
+
|
|
345
|
+
return last_good
|
|
346
|
+
except asyncio.CancelledError:
|
|
347
|
+
raise
|
|
348
|
+
except (DatabaseError, RPCError):
|
|
349
|
+
endpoint = getattr(self._rpc, "endpoint", None)
|
|
350
|
+
if endpoint is None:
|
|
351
|
+
endpoint = getattr(self._rpc, "url", None)
|
|
352
|
+
|
|
353
|
+
log_fields = {
|
|
354
|
+
"chain_id": self._chain_id,
|
|
355
|
+
"low": low,
|
|
356
|
+
"high": high,
|
|
357
|
+
"endpoint": endpoint,
|
|
358
|
+
}
|
|
359
|
+
if mid is None:
|
|
360
|
+
log_fields["left"] = left
|
|
361
|
+
log_fields["right"] = right
|
|
323
362
|
else:
|
|
324
|
-
|
|
325
|
-
right = mid - 1
|
|
363
|
+
log_fields["mid"] = mid
|
|
326
364
|
|
|
327
|
-
|
|
365
|
+
logger.warning(
|
|
366
|
+
"reorg.probe_failed",
|
|
367
|
+
exc_info=True,
|
|
368
|
+
**log_fields,
|
|
369
|
+
)
|
|
370
|
+
return None
|
|
328
371
|
def rewind(self, reorg_result: ReorgResult) -> ReorgResult:
|
|
329
372
|
"""Rewind state using the centralized recovery contract."""
|
|
330
373
|
recovery = ReorgRecovery(
|
|
@@ -392,7 +435,7 @@ class ReorgRecovery:
|
|
|
392
435
|
def __init__(
|
|
393
436
|
self,
|
|
394
437
|
db: Database,
|
|
395
|
-
rpc:
|
|
438
|
+
rpc: ReadClient,
|
|
396
439
|
chain_id: int,
|
|
397
440
|
lifecycle: "LifecycleDispatcher | None" = None,
|
|
398
441
|
finality_confirmations: int = 0,
|
|
@@ -453,7 +496,15 @@ class ReorgRecovery:
|
|
|
453
496
|
rewind_hash = block["hash"]
|
|
454
497
|
if isinstance(rewind_hash, bytes):
|
|
455
498
|
rewind_hash = rewind_hash.hex()
|
|
456
|
-
except
|
|
499
|
+
except asyncio.CancelledError:
|
|
500
|
+
raise
|
|
501
|
+
except RPCError as e:
|
|
502
|
+
logger.warning(
|
|
503
|
+
"reorg.rewind_hash_fetch_failed",
|
|
504
|
+
chain_id=self._chain_id,
|
|
505
|
+
to_height=to_height,
|
|
506
|
+
error=str(e),
|
|
507
|
+
)
|
|
457
508
|
rewind_hash = None
|
|
458
509
|
|
|
459
510
|
if rewind_hash is None:
|
|
@@ -467,11 +518,15 @@ class ReorgRecovery:
|
|
|
467
518
|
|
|
468
519
|
intents_reverted, attempts_reverted = self._revert_reorged_intents(to_height)
|
|
469
520
|
self._assert_no_confirmed_above(to_height)
|
|
521
|
+
except asyncio.CancelledError:
|
|
522
|
+
raise
|
|
470
523
|
except Exception as e:
|
|
471
524
|
logger.error(
|
|
472
525
|
"reorg.rewind_failed",
|
|
526
|
+
chain_id=self._chain_id,
|
|
473
527
|
to_height=to_height,
|
|
474
528
|
error=str(e)[:200],
|
|
529
|
+
exc_info=True,
|
|
475
530
|
)
|
|
476
531
|
health_alert(
|
|
477
532
|
component="brawny.scheduler.reorg",
|
|
@@ -564,10 +619,11 @@ class ReorgRecovery:
|
|
|
564
619
|
NonceStatus.IN_FLIGHT.value,
|
|
565
620
|
intent_id=intent.intent_id,
|
|
566
621
|
)
|
|
567
|
-
except
|
|
622
|
+
except DatabaseError as e:
|
|
568
623
|
logger.warning(
|
|
569
624
|
"reorg.nonce_reconcile_failed",
|
|
570
625
|
intent_id=str(intent.intent_id),
|
|
626
|
+
chain_id=self._chain_id,
|
|
571
627
|
nonce=attempt.nonce,
|
|
572
628
|
error=str(e)[:200],
|
|
573
629
|
)
|