Follow and record chain reorganisations, pausing on deep ones (B-85, ELC-09, ELC-22, ELC-23, ELC-24, AUD-38, DAT-69)
A reorganisation was neither recorded nor acted upon, and a competing header at the tip's own height was ignored for as long as the tip stood, leaving the listener on an orphan. - A header at the height we hold with a different hash is followed through the same fork-point search as any other reorg (ELC-09) instead of being ignored; it still has to bring valid work and a connected chain. - ElectrumListener._record_reorg writes `chain_reorg_detected` (depth, fork height, old and new tip height and hash) before the window moves, so a failed write ends the session and the next server's announcement of the same fork is recorded then. Depth 1 is a transaction of its own; depth >= 2 also pauses the lottery with paused_reason 'reorg' and writes `lottery_paused` in the same transaction (begin_immediate first). - Docs: ELC-09/22/23/24, AUD-03/11/38, DAT-30/69, OPS-30, RND-04, ADR-26/52 lose their B-85 "da implementare" lines. Tests changed because the spec changed: test_apply_header_ignores_a_competing_header_at_ the_current_tip_height encoded the B-64 behaviour that ELC-09 now reverses; it is replaced by test_a_competing_header_at_the_tip_height_is_a_reorg_to_follow and the depth tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca50ef62f3
commit
50b68d2957
16 files changed
+254
-111
No files matched your search
+42
-18
@@ -7,6 +7,8 @@ from embit.transaction import Transaction
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from app.audit.log import write_audit_log
|
||||
from app.db.base import begin_immediate
|
||||
from app.db.models import User
|
||||
from app.deposits.service import (
|
||||
credit_confirmed_utxos,
|
||||
@@ -28,6 +30,7 @@ from app.rounds.draw import (
|
||||
header_prev_hash,
|
||||
)
|
||||
from app.rounds.events import broadcaster
|
||||
from app.rounds.pause import pause_automatically
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,6 +55,10 @@ _RESUBSCRIBE_CONCURRENCY = 20
|
||||
# a hostile server. Also the deepest fork it will follow.
|
||||
_HEADER_WINDOW = 10
|
||||
|
||||
# A reorganisation at least this deep pauses the lottery (B-85, ELC-24): one block is
|
||||
# ordinary on any chain, two or more is worth a look before a new round opens.
|
||||
_REORG_PAUSE_DEPTH = 2
|
||||
|
||||
# `blockchain.block.headers` serves at most this many headers per request (ELC-12).
|
||||
_MAX_HEADERS_PER_REQUEST = 2016
|
||||
|
||||
@@ -339,14 +346,11 @@ class ElectrumListener:
|
||||
if self.tip_header_hex is not None and height == self.tip_height:
|
||||
if header_hex == self.tip_header_hex:
|
||||
return # the same block announced again
|
||||
# B-64: a second header for the height we hold one for — a reorg at the tip, or a
|
||||
# server disagreeing with the rest. Not replaced under us (B-85 follows it,
|
||||
# ELC-09); corroborate_header refuses to seed a draw from an orphan anyway.
|
||||
logger.warning(
|
||||
"ignoring a competing header at the current tip height %s "
|
||||
"(reorg at the tip, or a server disagreeing with the rest)",
|
||||
height,
|
||||
)
|
||||
# B-85 (ELC-09): another block at the height we hold — a reorg at the tip. It is
|
||||
# followed like any other fork (it must bring the work and the chain to show for
|
||||
# it), not ignored: ignoring it left us on an orphan for as long as the tip
|
||||
# stood, and a draw seeded from it would have been refused by the quorum.
|
||||
await self._follow_reorg(height, header_hex, client)
|
||||
return
|
||||
|
||||
if not header_meets_its_own_target(header_hex):
|
||||
@@ -460,17 +464,37 @@ class ElectrumListener:
|
||||
raise HeaderValidationError(f"the server's branch does not end at the header it announced at height {height}")
|
||||
self._check_chained(branch, header_hex_to_block_hash(self._window[fork_height]))
|
||||
old_height, old_hex = self.tip_height, self.tip_header_hex
|
||||
self._extend_window([(fork_height + 1 + i, h) for i, h in enumerate(branch)])
|
||||
await self._reorg_followed(old_height, old_hex, fork_height)
|
||||
|
||||
async def _reorg_followed(self, old_height: int, old_hex: str, fork_height: int) -> None:
|
||||
logger.warning(
|
||||
"followed a chain reorganisation: tip %s -> %s, fork at height %s (depth %s)",
|
||||
old_height,
|
||||
self.tip_height,
|
||||
fork_height,
|
||||
old_height - fork_height,
|
||||
# Recorded *before* the window moves: if the write fails the session ends, the next
|
||||
# server announces the same fork against an unchanged window, and the event is
|
||||
# written then — a reorg is never followed without a trace.
|
||||
await self._record_reorg(
|
||||
depth=old_height - fork_height,
|
||||
fork_height=fork_height,
|
||||
old_tip_height=old_height,
|
||||
old_tip_hash=header_hex_to_block_hash(old_hex),
|
||||
new_tip_height=height,
|
||||
new_tip_hash=header_hex_to_block_hash(header_hex),
|
||||
)
|
||||
self._extend_window([(fork_height + 1 + i, h) for i, h in enumerate(branch)])
|
||||
|
||||
async def _record_reorg(self, **payload) -> None:
|
||||
"""B-85 (ELC-24, AUD-38): the audit row of a followed reorganisation; a depth of 2
|
||||
or more also pauses the lottery (`paused_reason = 'reorg'`), in the same transaction
|
||||
as the row and as the `lottery_paused` one. Depth 1 is only observed: a one-block
|
||||
reorganisation is ordinary on any chain and touches no deposit."""
|
||||
logger.warning(
|
||||
"followed a chain reorganisation of depth %s: fork at height %s, tip %s -> %s",
|
||||
payload["depth"],
|
||||
payload["fork_height"],
|
||||
payload["old_tip_height"],
|
||||
payload["new_tip_height"],
|
||||
)
|
||||
async with self._session_factory() as session:
|
||||
await begin_immediate(session)
|
||||
await write_audit_log(session, "chain_reorg_detected", payload)
|
||||
if payload["depth"] >= _REORG_PAUSE_DEPTH:
|
||||
await pause_automatically(session, "reorg")
|
||||
await session.commit()
|
||||
|
||||
async def _corroborate_majority(
|
||||
self,
|
||||
|
||||
Reference in new issue
Block a user