Cap participants per round and give the payout its own input limit (B-52)

The payout has to spend one pool UTXO per bet, so reusing MAX_TX_INPUTS (50)
for it made any round past ~50 players unpayable: select_utxos raised
too_many_inputs, the round stayed "paying_out" retrying every 60s forever, and
since no new round may open while one is active, the whole lottery stopped with
the pool stuck. The cap was being enforced on the payout side, i.e. discovered
once the money was already committed and there was no way back.

Two halves:

- select_utxos takes the cap as a parameter. Bets and withdrawals keep
  MAX_TX_INPUTS = 50, which protects a user from a fee that eats into the amount
  they are moving; the payout uses MAX_PAYOUT_TX_INPUTS = 500, where that
  argument doesn't apply — 400 inputs at 1 sat/vB cost ~0.00027 PLM out of the
  winner's 70% share. What actually bounds it is relay policy: 500 inputs is
  ~34 kvB against the 100 kvB standardness limit, and signing that many measures
  ~0.4s, once per round, inside a background task.

- place_bet refuses the 401st bet with a new round_full error (translated into
  all 7 languages), so "a round can always be paid out" is an invariant checked
  before any money moves. MAX_PARTICIPANTS_PER_ROUND sits below the input cap to
  leave the payout headroom for pool change from earlier rounds, and counts every
  participant row rather than only confirmed ones, since a failed bet frees a slot.

A round already wedged past the old cap now pays out on the next retry tick.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 16:21:33 +02:00
co-authored by Claude Opus 5
parent 99d7a1ee00
commit 025754c860
10 changed files with 311 additions and 28 deletions
+6 -2
View File
@@ -365,8 +365,12 @@ class RoundScheduler:
fee_rate_sat_vb=fee_rate,
)
except InsufficientFundsError as exc:
# Includes the B-48 "too_many_inputs" case: the pool holds enough, but spread
# over more UTXOs than one transaction may spend, so /admin has to say which.
# Includes the "too_many_inputs" case: the pool holds enough, but spread over
# more UTXOs than one transaction may spend, so /admin has to say which. Since
# B-52 that means MAX_PAYOUT_TX_INPUTS, and participants are capped below it at
# bet time (bets/service.py), so reaching it now takes pool change accumulated
# over many rounds rather than one busy round — an operator consolidation job,
# not a dead end for the bets of the round in progress.
reason = "insufficient pool UTXOs" if exc.code == "insufficient_balance" else exc.code
logger.exception("round %s payout failed: %s", round_id, reason)
await self._log_payout_failure(round_id, winner_user_id, reason)