proxy wallet: sweep quotes across per-epoch currency units #31

Merged
vnprc merged 2 commits from agent/multi-unit-sweeper into agent/nutxx-pubkey-lookup 2026-10-09 06:08:04 +01:00
Collaborator

The proxy's quote sweeper now mints ecash in every per-epoch currency unit, not just the one its wallet was built with. Before this, the first epoch rotation silently cost every later epoch all of its mining rewards: the sweeper fetched the new unit's quotes, stored them, and never minted them. What proves it: on a live dev stack, mining filled the genesis epoch to a balance of 2752, two rotations then minted in the new units, and a backlog of 55 previously stuck rewards drained in a single pass. If this is wrong, revert the two commits — the wallet DB schema is untouched, so rollback is safe.

Replaces vnprc/hashpool#23, closed only so the history could be squashed; the forge refuses force-push on an open pull request branch. The tree is byte-identical to the state verified there.

Merge after vnprc/hashpool#21.

In plain English

Once the mint issues a separate kind of ecash per mining epoch, the wallet has to collect all of them, and it could only collect the first. This teaches it to collect every kind it sees, and to work through a backlog in bounded batches so one bad reward cannot block the rest.

Two bugs found by running the real stack are fixed here, because without them a new epoch cannot be collected at all. First, on first seeing a new kind of ecash the wallet never asked the mint for the matching key, so every attempt failed and rewards piled up uncollectable forever. Second, a failed attempt left the reward marked "in progress" with nothing to ever clear it, so it could never be retried.

Worth knowing: all 32 tests on the original branch passed while both bugs were live. They covered the planning logic, not the actual collection call.

Commits

  • f1f4e994 re-pins cdk for Wallet::cleanup_orphaned_quote_reservations, the narrow cleanup that never resumes a saga. Pin only, so it builds alone.
  • 4453ffff is the sweeper: per-unit wallet handles, unioned discovery, bounded per-pass work, the keyset refresh, and the reservation recovery.

Risk

R2. Per-unit wallet handles share one sqlite localstore and one metadata cache; the wallet schema and NUT-13 keyset counters are unit- or keyset-scoped, and units are swept sequentially within a pass, so there is no cross-unit contention. Discovery no longer depends on the pubkey lookup alone: a lookup-endpoint failure leaves every DB-known unit sweepable and visible in the reconcile log. A unit whose keyset refresh fails is skipped for that pass and retried on the next, so a persistent mint outage defers that unit rather than leaking reservations behind a keyset it could not look up. The reconcile log format changed to a per-unit summary; anything scraping the old string needs updating.

Residual risk that review could not settle and running it did: the per-chunk network paths are exercised by the live witness, not by unit tests.

Validation

  • cargo check -p translator_sv2 clean; cargo test -p translator_sv2 37 passed, 0 failed, re-run by the integrator. Each commit builds independently.
  • MSRV lock pins intact: time 0.3.41, serde_with 3.17.0, darling 0.21.3, simple_asn1 0.6.3 against rustc 1.86.0.
  • Live dev stack, integrated with vnprc/hashpool#21 and vnprc/hashpool#24: shares mined, ehash minted in the genesis epoch, two rotations performed, the mint restarted, and 55 previously wedged rewards cleared and minted in one pass once the fixes were in — including five from an epoch that had already been closed.
  • Three cold cross-family review passes. The first found that clearing stuck rewards also resumed the faucet's in-flight transfers and that a failed key lookup was ignored before reserving rewards; the second found the first round's fix had traded that hazard for a permanent leak on the shared wallet; the third resolved it by exposing a narrow cleanup in cdk rather than working around it. Comment bloat was cut on each pass.

Not run: the end-to-end rotation witness against the reward trigger of vnprc/hashpool#18, which is unstarted.

The proxy's quote sweeper now mints ecash in every per-epoch currency unit, not just the one its wallet was built with. Before this, the first epoch rotation silently cost every later epoch all of its mining rewards: the sweeper fetched the new unit's quotes, stored them, and never minted them. What proves it: on a live dev stack, mining filled the genesis epoch to a balance of 2752, two rotations then minted in the new units, and a backlog of 55 previously stuck rewards drained in a single pass. If this is wrong, revert the two commits — the wallet DB schema is untouched, so rollback is safe. Replaces vnprc/hashpool#23, closed only so the history could be squashed; the forge refuses force-push on an open pull request branch. The tree is byte-identical to the state verified there. Merge after vnprc/hashpool#21. ## In plain English Once the mint issues a separate kind of ecash per mining epoch, the wallet has to collect all of them, and it could only collect the first. This teaches it to collect every kind it sees, and to work through a backlog in bounded batches so one bad reward cannot block the rest. Two bugs found by running the real stack are fixed here, because without them a new epoch cannot be collected at all. First, on first seeing a new kind of ecash the wallet never asked the mint for the matching key, so every attempt failed and rewards piled up uncollectable forever. Second, a failed attempt left the reward marked "in progress" with nothing to ever clear it, so it could never be retried. Worth knowing: all 32 tests on the original branch passed while both bugs were live. They covered the planning logic, not the actual collection call. ## Commits - `f1f4e994` re-pins cdk for `Wallet::cleanup_orphaned_quote_reservations`, the narrow cleanup that never resumes a saga. Pin only, so it builds alone. - `4453ffff` is the sweeper: per-unit wallet handles, unioned discovery, bounded per-pass work, the keyset refresh, and the reservation recovery. ## Risk R2. Per-unit wallet handles share one sqlite localstore and one metadata cache; the wallet schema and NUT-13 keyset counters are unit- or keyset-scoped, and units are swept sequentially within a pass, so there is no cross-unit contention. Discovery no longer depends on the pubkey lookup alone: a lookup-endpoint failure leaves every DB-known unit sweepable and visible in the reconcile log. A unit whose keyset refresh fails is skipped for that pass and retried on the next, so a persistent mint outage defers that unit rather than leaking reservations behind a keyset it could not look up. The reconcile log format changed to a per-unit summary; anything scraping the old string needs updating. Residual risk that review could not settle and running it did: the per-chunk network paths are exercised by the live witness, not by unit tests. ## Validation - `cargo check -p translator_sv2` clean; `cargo test -p translator_sv2` 37 passed, 0 failed, re-run by the integrator. Each commit builds independently. - MSRV lock pins intact: `time` 0.3.41, `serde_with` 3.17.0, `darling` 0.21.3, `simple_asn1` 0.6.3 against rustc 1.86.0. - Live dev stack, integrated with vnprc/hashpool#21 and vnprc/hashpool#24: shares mined, ehash minted in the genesis epoch, two rotations performed, the mint restarted, and 55 previously wedged rewards cleared and minted in one pass once the fixes were in — including five from an epoch that had already been closed. - Three cold cross-family review passes. The first found that clearing stuck rewards also resumed the faucet's in-flight transfers and that a failed key lookup was ignored before reserving rewards; the second found the first round's fix had traded that hazard for a permanent leak on the shared wallet; the third resolved it by exposing a narrow cleanup in cdk rather than working around it. Comment bloat was cut on each pass. Not run: the end-to-end rotation witness against the reward trigger of vnprc/hashpool#18, which is unstarted.
The sweeper needs `Wallet::cleanup_orphaned_quote_reservations`, which clears
only orphaned quote reservations and never resumes a saga. The previously
pinned revision exposed no such entry point: the only public recovery method
also resumed Swap, Send and Receive sagas, which cannot safely run on a wallet
handle the faucet is also driving.

Pin moves in the four declared sites and resolves in roles/Cargo.lock. The
MSRV-pinned transitive deps are unchanged: time 0.3.41, serde_with 3.17.0,
darling 0.21.3, simple_asn1 0.6.3.
The mint issues a distinct currency unit per mining epoch, but the sweeper only
minted the one unit its wallet was built with. After the first rotation it
fetched the new unit's quotes, stored them, and silently never minted them, so
every epoch after the first lost all of its mining rewards.

The sweeper now mints every unit it sees. A per-unit wallet handle is built on
first sight from one shared localstore, seed and mint metadata cache, and each
unit runs the same sweep: discover, status-check, then P2PK-locked batch mint.
Discovery unions the unit-agnostic pubkey lookup with a unit-blind read of the
local store's unissued quotes, so a failing lookup endpoint cannot hide a
backlog. Work is bounded per unit per pass -- at most 200 quotes selected,
locally-mintable first, processed in chunks of 50 -- so one bad quote id wedges
at most its own chunk and a rotation backlog drains over successive passes.

Two faults found by running the full dev stack are fixed here, because the
sweeper cannot mint a new epoch's unit without them:

The shared mint metadata cache is keyed by mint, not by unit, and the default
load policy treats "populated and within TTL" as fresh. A brand-new epoch unit
therefore read as cached while its keyset was absent, and every mint attempt
failed with "Unknown Keyset" forever. The first sight of a unit now forces one
refresh, and a unit whose refresh fails is skipped for the pass rather than
having quotes reserved behind a keyset that could not be looked up.

A failed batch mint leaves the quote reserved in the local store with nothing
to release it, wedging a paid quote behind "Quote already in use by another
operation". Each pass now clears orphaned reservations for the unit first, via
cdk's narrow cleanup, which never resumes or compensates a saga and so is safe
on the base unit's handle even though the faucet drives its own Send saga over
it. The count of cleared reservations is logged when non-zero.

The faucet, web UI and quote notification path keep using the single base-unit
wallet exactly as before. The wallet DB schema is untouched.

Closes vnprc/hashpool#17. Closes vnprc/hashpool#29. Refs vnprc/hashpool#13.
vnprc approved these changes 2026-10-09 06:06:57 +01:00
vnprc merged commit 4453ffff6b into agent/nutxx-pubkey-lookup 2026-10-09 06:08:04 +01:00
vnprc deleted branch agent/multi-unit-sweeper 2026-10-09 06:08:04 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
vnprc/hashpool!31
No description provided.