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.
Files changed (135) hide show
  1. brawny/__init__.py +2 -0
  2. brawny/_context.py +5 -5
  3. brawny/_rpc/__init__.py +36 -12
  4. brawny/_rpc/broadcast.py +14 -13
  5. brawny/_rpc/caller.py +243 -0
  6. brawny/_rpc/client.py +539 -0
  7. brawny/_rpc/clients.py +11 -11
  8. brawny/_rpc/context.py +23 -0
  9. brawny/_rpc/errors.py +465 -31
  10. brawny/_rpc/gas.py +7 -6
  11. brawny/_rpc/pool.py +18 -0
  12. brawny/_rpc/retry.py +266 -0
  13. brawny/_rpc/retry_policy.py +81 -0
  14. brawny/accounts.py +28 -9
  15. brawny/alerts/__init__.py +15 -18
  16. brawny/alerts/abi_resolver.py +212 -36
  17. brawny/alerts/base.py +2 -2
  18. brawny/alerts/contracts.py +77 -10
  19. brawny/alerts/errors.py +30 -3
  20. brawny/alerts/events.py +38 -5
  21. brawny/alerts/health.py +19 -13
  22. brawny/alerts/send.py +513 -55
  23. brawny/api.py +39 -11
  24. brawny/assets/AGENTS.md +325 -0
  25. brawny/async_runtime.py +48 -0
  26. brawny/chain.py +3 -3
  27. brawny/cli/commands/__init__.py +2 -0
  28. brawny/cli/commands/console.py +69 -19
  29. brawny/cli/commands/contract.py +2 -2
  30. brawny/cli/commands/controls.py +121 -0
  31. brawny/cli/commands/health.py +2 -2
  32. brawny/cli/commands/job_dev.py +6 -5
  33. brawny/cli/commands/jobs.py +99 -2
  34. brawny/cli/commands/maintenance.py +13 -29
  35. brawny/cli/commands/migrate.py +1 -0
  36. brawny/cli/commands/run.py +10 -3
  37. brawny/cli/commands/script.py +8 -3
  38. brawny/cli/commands/signer.py +143 -26
  39. brawny/cli/helpers.py +0 -3
  40. brawny/cli_templates.py +25 -349
  41. brawny/config/__init__.py +4 -1
  42. brawny/config/models.py +43 -57
  43. brawny/config/parser.py +268 -57
  44. brawny/config/validation.py +52 -15
  45. brawny/daemon/context.py +4 -2
  46. brawny/daemon/core.py +185 -63
  47. brawny/daemon/loops.py +166 -98
  48. brawny/daemon/supervisor.py +261 -0
  49. brawny/db/__init__.py +14 -26
  50. brawny/db/base.py +248 -151
  51. brawny/db/global_cache.py +11 -1
  52. brawny/db/migrate.py +175 -28
  53. brawny/db/migrations/001_init.sql +4 -3
  54. brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
  55. brawny/db/migrations/011_add_job_logs.sql +1 -2
  56. brawny/db/migrations/012_add_claimed_by.sql +2 -2
  57. brawny/db/migrations/013_attempt_unique.sql +10 -0
  58. brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
  59. brawny/db/migrations/015_add_signer_alias.sql +14 -0
  60. brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
  61. brawny/db/migrations/017_add_job_drain.sql +6 -0
  62. brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
  63. brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
  64. brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
  65. brawny/db/ops/__init__.py +3 -25
  66. brawny/db/ops/logs.py +1 -2
  67. brawny/db/queries.py +47 -91
  68. brawny/db/serialized.py +65 -0
  69. brawny/db/sqlite/__init__.py +1001 -0
  70. brawny/db/sqlite/connection.py +231 -0
  71. brawny/db/sqlite/execute.py +116 -0
  72. brawny/db/sqlite/mappers.py +190 -0
  73. brawny/db/sqlite/repos/attempts.py +372 -0
  74. brawny/db/sqlite/repos/block_state.py +102 -0
  75. brawny/db/sqlite/repos/cache.py +104 -0
  76. brawny/db/sqlite/repos/intents.py +1021 -0
  77. brawny/db/sqlite/repos/jobs.py +200 -0
  78. brawny/db/sqlite/repos/maintenance.py +182 -0
  79. brawny/db/sqlite/repos/signers_nonces.py +566 -0
  80. brawny/db/sqlite/tx.py +119 -0
  81. brawny/http.py +194 -0
  82. brawny/invariants.py +11 -24
  83. brawny/jobs/base.py +8 -0
  84. brawny/jobs/job_validation.py +2 -1
  85. brawny/keystore.py +83 -7
  86. brawny/lifecycle.py +64 -12
  87. brawny/logging.py +0 -2
  88. brawny/metrics.py +84 -12
  89. brawny/model/contexts.py +111 -9
  90. brawny/model/enums.py +1 -0
  91. brawny/model/errors.py +18 -0
  92. brawny/model/types.py +47 -131
  93. brawny/network_guard.py +133 -0
  94. brawny/networks/__init__.py +5 -5
  95. brawny/networks/config.py +1 -7
  96. brawny/networks/manager.py +14 -11
  97. brawny/runtime_controls.py +74 -0
  98. brawny/scheduler/poller.py +11 -7
  99. brawny/scheduler/reorg.py +95 -39
  100. brawny/scheduler/runner.py +442 -168
  101. brawny/scheduler/shutdown.py +3 -3
  102. brawny/script_tx.py +3 -3
  103. brawny/telegram.py +53 -7
  104. brawny/testing.py +1 -0
  105. brawny/timeout.py +38 -0
  106. brawny/tx/executor.py +922 -308
  107. brawny/tx/intent.py +54 -16
  108. brawny/tx/monitor.py +31 -12
  109. brawny/tx/nonce.py +212 -90
  110. brawny/tx/replacement.py +69 -18
  111. brawny/tx/retry_policy.py +24 -0
  112. brawny/tx/stages/types.py +75 -0
  113. brawny/types.py +18 -0
  114. brawny/utils.py +41 -0
  115. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
  116. brawny-0.1.22.dist-info/RECORD +163 -0
  117. brawny/_rpc/manager.py +0 -982
  118. brawny/_rpc/selector.py +0 -156
  119. brawny/db/base_new.py +0 -165
  120. brawny/db/mappers.py +0 -182
  121. brawny/db/migrations/008_add_transactions.sql +0 -72
  122. brawny/db/ops/attempts.py +0 -108
  123. brawny/db/ops/blocks.py +0 -83
  124. brawny/db/ops/cache.py +0 -93
  125. brawny/db/ops/intents.py +0 -296
  126. brawny/db/ops/jobs.py +0 -110
  127. brawny/db/ops/nonces.py +0 -322
  128. brawny/db/postgres.py +0 -2535
  129. brawny/db/postgres_new.py +0 -196
  130. brawny/db/sqlite.py +0 -2733
  131. brawny/db/sqlite_new.py +0 -191
  132. brawny-0.1.13.dist-info/RECORD +0 -141
  133. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
  134. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
  135. {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.manager import RPCManager
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: RPCManager,
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 Exception as e:
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
- oldest = self._db.get_oldest_block_in_history(self._chain_id)
285
- if oldest is None:
286
- return low
301
+ mid = None
302
+ left = None
303
+ right = None
287
304
 
288
- # Start from the known bad anchor and search forward
289
- # We need to find where the chain diverged
290
- left = max(oldest, low)
291
- right = high
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
- last_good = left - 1 # Assume nothing matches if search fails
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
- while left <= right:
296
- mid = (left + right) // 2
315
+ last_good = left - 1 # Assume nothing matches if search fails
297
316
 
298
- stored = self._db.get_block_hash_at_height(self._chain_id, mid)
299
- if stored is None:
300
- # No history here, move right
301
- left = mid + 1
302
- continue
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
- if stored.lower() == chain_hash.lower():
320
- # Match - reorg is after this point
321
- last_good = mid
322
- left = mid + 1
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
- # Mismatch - reorg is at or before this point
325
- right = mid - 1
363
+ log_fields["mid"] = mid
326
364
 
327
- return last_good
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: RPCManager,
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 Exception:
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 Exception as e:
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
  )