Skip to content

Pool top-ups spend BZZ without passing through the purchase ceiling #334

Description

@crtahlin

The gap

STAMP_POOL_MAX_PURCHASES_PER_HOUR was deliberately placed inside _purchase_stamp rather than in the replenish loop, so that no code path could miss it — the scheduled check, the immediate replenishment after an acquire, and anything added later all pass through it.

The pool's top-up path does not. app/services/stamp_pool.py:976 calls swarm_api.extend_postage_stamp directly:

await swarm_api.extend_postage_stamp(batch_id, amount)

Extending a batch spends BZZ exactly as buying one does. It is the only spending path in the gateway with no bound of any kind:

path bound
POST /stamps/ per-request ceiling + daily budget (#102)
PATCH /stamps/{id}/extend per-request ceiling + daily budget (#102)
POST /pool/acquire daily allowance per origin
pool purchase STAMP_POOL_MAX_PURCHASES_PER_HOUR
pool top-up none

How bad is it

Not urgent. This path is driven by the scheduler, not by callers — nothing external triggers it, so it cannot be provoked. The amount comes from STAMP_POOL_TOPUP_HOURS and the batch count is small, so ordinary operation costs little.

What it is exposed to is a configuration or logic error: a topup interval that fires more often than intended, a TTL calculation that thinks every batch needs extending on every pass, or a retry loop around a failing extend. The purchase ceiling exists because exactly that happened — 82 batches bought against a target of five (#271), with the bulk never explained. A ceiling does not need the cause, which was the argument for adding it. The same argument applies here.

Suggested fix

Route the top-up through the same accounting as _purchase_stamp, or give _extend_stamp its own hourly ceiling and report refusals in GET /api/v1/pool/status the way purchase refusals already are.

Whichever way, the property worth preserving is the one the purchase ceiling was designed around: it should be impossible to add a new spending path that skips the bound, rather than something a future author has to remember.

Related: #271 (the incident the purchase ceiling came from), #102 (bounds on the caller-facing spending paths).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcostAffects BZZ or running costp3Low priority — someday

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions