Require a real Electrum quorum and allow certificate pinning (B-84, ELC-01, ELC-03, ELC-14, ELC-19, ARC-20, OPS-12)
An empty ELECTRUM_FALLBACK_SERVERS (the .env.example default) silently made every corroboration pass, and with fallbacks the majority was taken over whoever answered, so one reachable fallback decided alone. TLS certificates were never checked. - _corroborate_majority passes only with at least 2 answers (MIN_QUORUM_RESPONSES) and a strict majority of the answers; no other server configured, or fewer than 2 answering, fails. ALLOW_NO_QUORUM=true keeps the old reduced rule. - config.validate_quorum_servers, called from the lifespan after parse_endpoints, refuses to start with fewer than 2 fallback servers unless ALLOW_NO_QUORUM=true (which logs a warning at every start). - Optional pinning: `host:port[:mode]:pin=<sha256>` per fallback and ELECTRUM_CERT_PIN for the primary; ElectrumClient compares the SHA-256 of the DER certificate after the TLS handshake and a mismatch fails like an unreachable server. A pin on a plaintext entry is a startup error. - .env.example now ships the four bootstrap servers and documents both new variables; docs/setup.md, spec, storico and BUGS.md updated. - Tests changed because the spec changed: the two "no other servers configured -> True" corroboration tests now assert that outcome only under allow_no_quorum. Note: a dev .env with fewer than 2 fallback servers will now refuse to start. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd6a91e93d
commit
13cb66cc06
18 files changed
+451
-125
No files matched your search
+7
-3
@@ -22,7 +22,7 @@ from app.api.routes.users import router as users_router
|
||||
from app.api.routes.withdrawals import router as withdrawals_router
|
||||
from app.auth.routes import router as auth_router
|
||||
from app.api.errors import ApiError
|
||||
from app.config import settings, validate_runtime_secrets
|
||||
from app.config import settings, validate_quorum_servers, validate_runtime_secrets
|
||||
from app.db.base import AsyncSessionLocal
|
||||
from app.deposits.reconcile import DepositReconciler
|
||||
from app.electrum.client import ElectrumClient, ElectrumEndpoint, parse_endpoints
|
||||
@@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _make_electrum_client(endpoint: ElectrumEndpoint) -> ElectrumClient:
|
||||
return ElectrumClient(endpoint.host, endpoint.port, endpoint.use_ssl)
|
||||
return ElectrumClient(endpoint.host, endpoint.port, endpoint.use_ssl, endpoint.pin)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -50,10 +50,14 @@ async def lifespan(app: FastAPI):
|
||||
settings.electrum_port,
|
||||
settings.electrum_use_ssl,
|
||||
settings.electrum_fallback_servers,
|
||||
settings.electrum_cert_pin,
|
||||
)
|
||||
validate_quorum_servers(endpoints) # B-84: refuses to serve without a usable quorum
|
||||
logger.info("Electrum endpoints (in rotation order): %s", ", ".join(str(e) for e in endpoints))
|
||||
|
||||
listener = ElectrumListener(_make_electrum_client, AsyncSessionLocal, endpoints)
|
||||
listener = ElectrumListener(
|
||||
_make_electrum_client, AsyncSessionLocal, endpoints, allow_no_quorum=settings.allow_no_quorum
|
||||
)
|
||||
app.state.electrum_listener = listener
|
||||
app.state.user_locks = UserLocks()
|
||||
|
||||
|
||||
Reference in new issue
Block a user