Mint epoch mechanics: height-named units, records, manual rotation lever #16

Closed
opened 2026-08-12 18:07:55 +01:00 by vnprc-agent · 1 comment
Collaborator

Give the mint epoch mechanics: open and close mining epochs with height-named currency units, persist epoch records, and hold provisional-epoch quotes unpaid until their boundary confirms — all exercisable by hand before any trigger exists. Design: docs/EPOCH_DESIGN.md.

Primary goals:

  • open_epoch(height, block_hash, source) — register the new unit's payment processor entry (via #15), create its keyset, write the epoch record (height, block hash, reward amount, state), and switch quote stamping to the new unit. Unit is hash_<pool>_<height> — pool = compressed pubkey from mint config, lowercase hex — with a deterministic suffix on derivation-index collision.
  • Genesis — first boot records the current chain height and opens the first epoch, final immediately, no backing reward; restarts reuse the persisted current epoch. Clean slate: existing dev/test mint state is blown away, no migration.
  • Quote lifecycle branch — quotes created while the current epoch is final are paid at creation (today's behavior); quotes in a provisional epoch are created unpaid, invisible to sweeping and unmintable through existing CDK state machinery. Finality bulk-pays them (idempotent — payment id is the share's header hash) and retires the previous epoch's registration.
  • Manual lever — just rotate-epoch hitting a localhost admin endpoint that calls open_epoch with source=manual; manual epochs have no chain boundary and are final immediately. This is the demo and the test harness for everything above.

Current state and specifics

The mint has exactly one hardcoded unit (CurrencyUnit::Custom("hash") in roles/mint/src/lib/mint_manager/setup.rs), created by builder bootstrap; no rotation code exists anywhere. The SV2 quote path hardcodes the unit at creation and immediately marks quotes paid (roles/mint/src/lib/sv2_connection/quote_processing.rs) — the stamping and the pay-now-or-defer branch both live there. Mint::rotate_keyset handles a brand-new unit at the pinned revision (non-empty amounts vector required). Epoch records need a small mint-side store; keep it separate from CDK's own schema. Bump the ehash quote TTL well past the confirmation window (expiry is advisory at the pin; do not rely on that).

Witness

On the dev stack: just rotate-epoch → /v1/keysets shows the new unit's keyset active, the epoch record exists, and new quotes stamp the new unit while old-epoch quotes still mint. A second rotation repeats it. Restart reuses the current epoch (no re-genesis).

Scope

In: mint-side epoch orchestration, records, the quote-path branch, the admin lever. Out: reward detection and finality/dissolve transitions driven by the chain (#18 — this issue provides the functions it calls), sweeper multi-unit handling (#17), settlement.

Part of #13. Gated on #15.

Give the mint epoch mechanics: open and close mining epochs with height-named currency units, persist epoch records, and hold provisional-epoch quotes unpaid until their boundary confirms — all exercisable by hand before any trigger exists. Design: `docs/EPOCH_DESIGN.md`. Primary goals: - **`open_epoch(height, block_hash, source)`** — register the new unit's payment processor entry (via #15), create its keyset, write the epoch record (height, block hash, reward amount, state), and switch quote stamping to the new unit. Unit is `hash_<pool>_<height>` — pool = compressed pubkey from mint config, lowercase hex — with a deterministic suffix on derivation-index collision. - **Genesis** — first boot records the current chain height and opens the first epoch, final immediately, no backing reward; restarts reuse the persisted current epoch. Clean slate: existing dev/test mint state is blown away, no migration. - **Quote lifecycle branch** — quotes created while the current epoch is final are paid at creation (today's behavior); quotes in a provisional epoch are created unpaid, invisible to sweeping and unmintable through existing CDK state machinery. Finality bulk-pays them (idempotent — payment id is the share's header hash) and retires the previous epoch's registration. - **Manual lever** — `just rotate-epoch` hitting a localhost admin endpoint that calls `open_epoch` with source=manual; manual epochs have no chain boundary and are final immediately. This is the demo and the test harness for everything above. ### Current state and specifics The mint has exactly one hardcoded unit (`CurrencyUnit::Custom("hash")` in `roles/mint/src/lib/mint_manager/setup.rs`), created by builder bootstrap; no rotation code exists anywhere. The SV2 quote path hardcodes the unit at creation and immediately marks quotes paid (`roles/mint/src/lib/sv2_connection/quote_processing.rs`) — the stamping and the pay-now-or-defer branch both live there. `Mint::rotate_keyset` handles a brand-new unit at the pinned revision (non-empty amounts vector required). Epoch records need a small mint-side store; keep it separate from CDK's own schema. Bump the ehash quote TTL well past the confirmation window (expiry is advisory at the pin; do not rely on that). ### Witness On the dev stack: `just rotate-epoch` → `/v1/keysets` shows the new unit's keyset active, the epoch record exists, and new quotes stamp the new unit while old-epoch quotes still mint. A second rotation repeats it. Restart reuses the current epoch (no re-genesis). ### Scope In: mint-side epoch orchestration, records, the quote-path branch, the admin lever. Out: reward detection and finality/dissolve transitions driven by the chain (#18 — this issue provides the functions it calls), sweeper multi-unit handling (#17), settlement. Part of #13. Gated on #15.
Author
Collaborator

Done, landed on master as vnprc/hashpool#33.

The mint opens a genesis epoch at the current chain height on first boot, every epoch is its own currency unit hash_<pool-pubkey>_<height> with its own keyset, quotes stamp the current epoch's unit, and just rotate-epoch closes the current epoch and opens the next. Records persist in a file-backed store beside the mint database, so a restart resumes rather than re-genesising. The static hash unit is gone from mint setup.

Two faults were found and fixed during the work, both only by running the stack:

  • Closing an epoch originally used deregister, which permanently stranded outstanding paid-but-unissued quotes in that epoch. It now uses retire (see vnprc/hashpool#15).
  • That protection is in-memory only, so a restart stranded every closed epoch's outstanding quotes again. Resume now rebuilds the state for every epoch that can still matter, then retires all but the resumable chain.

Verified on a live dev stack: genesis at the real chain height, two rotations, a mint restart, and five rewards from an already-closed epoch minted afterwards. The quotable/resolvable split is observable — /v1/keysets lists every epoch while /v1/info nut04 and nut05 each list exactly one method, the current epoch.

Not exercised: the provisional/dissolve lifecycle. Genesis and manual epochs are final immediately, so nothing creates a provisional epoch until vnprc/hashpool#18 lands. resumable_chain and the unpaid-quote branch in quote_processing.rs therefore exist on master but have only ever run against unit-test fixtures.

Open hardening follow-ups from this work: vnprc/hashpool#34, vnprc/hashpool#35, vnprc/hashpool#36.

Done, landed on master as vnprc/hashpool#33. The mint opens a genesis epoch at the current chain height on first boot, every epoch is its own currency unit `hash_<pool-pubkey>_<height>` with its own keyset, quotes stamp the current epoch's unit, and `just rotate-epoch` closes the current epoch and opens the next. Records persist in a file-backed store beside the mint database, so a restart resumes rather than re-genesising. The static `hash` unit is gone from mint setup. Two faults were found and fixed during the work, both only by running the stack: - Closing an epoch originally used deregister, which permanently stranded outstanding paid-but-unissued quotes in that epoch. It now uses retire (see vnprc/hashpool#15). - That protection is in-memory only, so a restart stranded every closed epoch's outstanding quotes again. Resume now rebuilds the state for every epoch that can still matter, then retires all but the resumable chain. Verified on a live dev stack: genesis at the real chain height, two rotations, a mint restart, and five rewards from an already-closed epoch minted afterwards. The quotable/resolvable split is observable — `/v1/keysets` lists every epoch while `/v1/info` nut04 and nut05 each list exactly one method, the current epoch. Not exercised: the provisional/dissolve lifecycle. Genesis and manual epochs are final immediately, so nothing creates a provisional epoch until vnprc/hashpool#18 lands. `resumable_chain` and the unpaid-quote branch in `quote_processing.rs` therefore exist on master but have only ever run against unit-test fixtures. Open hardening follow-ups from this work: vnprc/hashpool#34, vnprc/hashpool#35, vnprc/hashpool#36.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#16
No description provided.