mint: run mining epochs with per-epoch currency units #33

Merged
vnprc merged 2 commits from agent/epoch-mechanics into master 2026-10-09 06:30:42 +01:00
Collaborator

The mint now owns mining epochs: each epoch is its own currency unit with its own keyset, named after the pool and the block height that opened it, so a reward stays identifiable as belonging to the epoch that earned it. just rotate-epoch advances by hand; there is no automatic trigger yet. What proves it: on a live dev stack it opened a genesis epoch at the real chain height, minted ecash through two rotations, survived a mint restart, and let five rewards from an already-closed epoch still be collected. If this is wrong, revert the two commits — dev databases need just clean cashu either way, since units are runtime state now.

Replaces vnprc/hashpool#24, which the forge auto-closed when its base branch merged and was deleted. Rebased onto master, so it no longer carries a cdk pin of its own; master's pin already has everything it needs.

Merges to master. vnprc/hashpool#18, the automatic reward trigger, is unstarted and follows.

In plain English

Closing an epoch has to stop new rewards without destroying rewards already earned, and getting that distinction wrong loses real money. It was wrong twice, and both are fixed here.

Closing an epoch originally tore it down completely, so any reward already earned but not yet collected in that epoch became permanently uncollectable — earned, paid for, impossible to claim. That is fixed by using a gentler "retire" that closes the epoch to new rewards while keeping it recognisable for old ones, which is what the companion cdk change exists to provide.

Then restarting the mint brought the problem straight back, because that "still recognisable" state lived only in memory. A deploy or a crash would have quietly destroyed money. Resume now rebuilds it for every epoch that could still matter, not just the current one.

The second fault only appeared because the mint happened to get restarted during testing. It was not on anyone's list.

Commits

  • 95a79fdc adds a getblockcount request to the mini RPC client, so the mint can name an epoch after the block that opened it. Additive, builds alone.
  • eb140b5f is the epoch mechanics: the store, naming, genesis and rotation, the resume rebuild, and the quote-path snapshot.

Twenty-plus commits became two, grouped as the standalone RPC addition the epoch code depends on, then the epoch work itself. The pin commits that cluttered the old history are gone: master already pins the right cdk revision.

What a reviewer should look hardest at

Retire versus deregister. Because ehash is a custom payment method, cdk re-consults the processor map even for an already-paid quote, so a deregistered unit returns UnsupportedUnit forever. retire removes the quote-creation settings and keeps the map entry. A cdk-side test pins this by mutation: making retire drop the map entry makes a paid quote fail to mint.

Resume ordering. Registration happens before retirement, so a closed epoch is briefly quotable. That is safe only because resume completes before the admin, HTTP and SV2 listeners bind — verified in main.rs — and moving the call later would break it. A comment records that precondition.

Failing closed. Any retirement failure aborts startup. Serving with a closed epoch still accepting quotes is worse than not serving, and that asymmetry against the register loop is deliberate.

The restore set. Resume registers the current epoch, any closed epoch still owed ecash by cdk's amount_paid > amount_issued predicate, and the chain of epochs a cascade of dissolves could hand the current role to. The 7-day quote TTL is deliberately not used as the cutoff: docs/EPOCH_DESIGN.md says expiry is advisory and unenforced at mint time, so a quote past it can still be outstanding.

Risk

R2. No cdk pin change; master's revision already carries the dynamic processor registration, retire, and the exhaustive settings removal this depends on. MSRV lock pins unchanged (time 0.3.41, serde_with 3.17.0, darling 0.21.3, simple_asn1 0.6.3). Mint startup now requires bitcoind RPC and a configured pool_pubkey, both failing loud, with each RPC attempt under a hard timeout so a hung endpoint cannot block boot forever. The admin lever binds loopback only. Resume retains one processor map entry per restored epoch, bounded by the owed-ecash predicate rather than growing with lifetime epoch count.

Recorded open nits, not addressed here: the admin lever is unauthenticated and loopback-by-convention, prod config ships the generator-point placeholder as a valid-looking pool_pubkey, and the epoch store has no fsync.

Validation

  • cargo check -p rpc_sv2 clean on the first commit alone; cargo test -p mint 24 unit plus 5 integration, 0 failed, re-run by the integrator on master's pin.
  • Live dev stack: genesis epoch at the real chain height with its own keyset and the ehash route registered, ecash minted, two manual rotations, mint restarted, restored_epochs=3 on resume, and Unit unsupported for a closed epoch went from 107 occurrences to zero. Five rewards from a closed epoch then minted.
  • Three cold cross-family review passes. The first found four problems including the quotable-closed-epoch one; the second found two more, both caused by the first round's fixes; the third returned no correctness findings, having checked empty, singleton, all-provisional and trailing-dissolved stores. Comment bloat was cut on every pass.

Correcting an earlier claim on the old pull request: its smoke test established that a closed epoch's keysets still existed and read that as proof the rewards were still collectable. Existing keysets do not mean minting works, and minting was broken. That check was measuring the wrong thing and passing.

Not run: the end-to-end witness against the automatic reward trigger of vnprc/hashpool#18, which is unstarted, and the provisional/dissolve path, which is unreachable until it lands.

The mint now owns mining epochs: each epoch is its own currency unit with its own keyset, named after the pool and the block height that opened it, so a reward stays identifiable as belonging to the epoch that earned it. `just rotate-epoch` advances by hand; there is no automatic trigger yet. What proves it: on a live dev stack it opened a genesis epoch at the real chain height, minted ecash through two rotations, survived a mint restart, and let five rewards from an already-closed epoch still be collected. If this is wrong, revert the two commits — dev databases need `just clean cashu` either way, since units are runtime state now. Replaces vnprc/hashpool#24, which the forge auto-closed when its base branch merged and was deleted. Rebased onto master, so it no longer carries a cdk pin of its own; master's pin already has everything it needs. Merges to master. vnprc/hashpool#18, the automatic reward trigger, is unstarted and follows. ## In plain English Closing an epoch has to stop new rewards without destroying rewards already earned, and getting that distinction wrong loses real money. It was wrong twice, and both are fixed here. Closing an epoch originally tore it down completely, so any reward already earned but not yet collected in that epoch became permanently uncollectable — earned, paid for, impossible to claim. That is fixed by using a gentler "retire" that closes the epoch to new rewards while keeping it recognisable for old ones, which is what the companion cdk change exists to provide. Then restarting the mint brought the problem straight back, because that "still recognisable" state lived only in memory. A deploy or a crash would have quietly destroyed money. Resume now rebuilds it for every epoch that could still matter, not just the current one. The second fault only appeared because the mint happened to get restarted during testing. It was not on anyone's list. ## Commits - `95a79fdc` adds a `getblockcount` request to the mini RPC client, so the mint can name an epoch after the block that opened it. Additive, builds alone. - `eb140b5f` is the epoch mechanics: the store, naming, genesis and rotation, the resume rebuild, and the quote-path snapshot. Twenty-plus commits became two, grouped as the standalone RPC addition the epoch code depends on, then the epoch work itself. The pin commits that cluttered the old history are gone: master already pins the right cdk revision. ## What a reviewer should look hardest at **Retire versus deregister.** Because `ehash` is a custom payment method, cdk re-consults the processor map even for an already-paid quote, so a deregistered unit returns `UnsupportedUnit` forever. `retire` removes the quote-creation settings and keeps the map entry. A cdk-side test pins this by mutation: making retire drop the map entry makes a paid quote fail to mint. **Resume ordering.** Registration happens before retirement, so a closed epoch is briefly quotable. That is safe only because resume completes before the admin, HTTP and SV2 listeners bind — verified in `main.rs` — and moving the call later would break it. A comment records that precondition. **Failing closed.** Any retirement failure aborts startup. Serving with a closed epoch still accepting quotes is worse than not serving, and that asymmetry against the register loop is deliberate. **The restore set.** Resume registers the current epoch, any closed epoch still owed ecash by cdk's `amount_paid > amount_issued` predicate, and the chain of epochs a cascade of dissolves could hand the current role to. The 7-day quote TTL is deliberately not used as the cutoff: `docs/EPOCH_DESIGN.md` says expiry is advisory and unenforced at mint time, so a quote past it can still be outstanding. ## Risk R2. No cdk pin change; master's revision already carries the dynamic processor registration, retire, and the exhaustive settings removal this depends on. MSRV lock pins unchanged (`time` 0.3.41, `serde_with` 3.17.0, `darling` 0.21.3, `simple_asn1` 0.6.3). Mint startup now requires bitcoind RPC and a configured `pool_pubkey`, both failing loud, with each RPC attempt under a hard timeout so a hung endpoint cannot block boot forever. The admin lever binds loopback only. Resume retains one processor map entry per restored epoch, bounded by the owed-ecash predicate rather than growing with lifetime epoch count. Recorded open nits, not addressed here: the admin lever is unauthenticated and loopback-by-convention, prod config ships the generator-point placeholder as a valid-looking `pool_pubkey`, and the epoch store has no fsync. ## Validation - `cargo check -p rpc_sv2` clean on the first commit alone; `cargo test -p mint` 24 unit plus 5 integration, 0 failed, re-run by the integrator on master's pin. - Live dev stack: genesis epoch at the real chain height with its own keyset and the `ehash` route registered, ecash minted, two manual rotations, mint restarted, `restored_epochs=3` on resume, and `Unit unsupported` for a closed epoch went from 107 occurrences to zero. Five rewards from a closed epoch then minted. - Three cold cross-family review passes. The first found four problems including the quotable-closed-epoch one; the second found two more, both caused by the first round's fixes; the third returned no correctness findings, having checked empty, singleton, all-provisional and trailing-dissolved stores. Comment bloat was cut on every pass. Correcting an earlier claim on the old pull request: its smoke test established that a closed epoch's keysets still existed and read that as proof the rewards were still collectable. Existing keysets do not mean minting works, and minting was broken. That check was measuring the wrong thing and passing. Not run: the end-to-end witness against the automatic reward trigger of vnprc/hashpool#18, which is unstarted, and the provisional/dissolve path, which is unreachable until it lands.
The mint needs the chain height to name an epoch after the block that opened
it. Additive; no existing caller changes.
The mint now owns mining epochs. On first boot it opens a genesis epoch at the
current chain height; every epoch is its own currency unit named
`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 by hand. There is no automatic trigger yet. Records live in a file-backed
store beside the mint database, so a restart resumes rather than re-genesising.
The legacy static `hash` unit is gone from mint setup, since units are runtime
state now, which makes this a clean-slate change for dev databases.

Closing an epoch must stop new rewards without destroying earned ones, and
getting that wrong loses money. It uses `retire_payment_processor`, which
removes the quote-creation settings while keeping the processor map entry, so
outstanding quotes in a closed epoch stay resolvable and mintable. A full
`deregister` would drop that entry and strand them: because `ehash` is a custom
payment method, cdk re-consults the processor map even for an already-paid
quote, so a deregistered unit returns `UnsupportedUnit` forever and the holder
can never issue.

That protection is in-memory only, so a restart has to rebuild it. Resume
registers every epoch that could still matter -- the current one, any closed
epoch still owed ecash by cdk's own `amount_paid > amount_issued` predicate,
and the chain of epochs a cascade of dissolves could hand the current role to
-- then retires all but that chain. Registration happens before retirement,
which leaves a closed epoch briefly quotable; that is safe only because resume
completes before the mint's listeners bind, and moving it later would break it.
Every retirement must succeed or startup aborts, since serving with a closed
epoch still accepting quotes is worse than not serving.

Rotation persists and swaps the current epoch before retiring the previous one,
so the quote path never observes a retired unit as current. The quote path
takes a single snapshot of (unit, is-final), so a rotation between "which unit"
and "pay now" cannot tear.

Implements vnprc/hashpool#16. Closes vnprc/hashpool#30. Refs vnprc/hashpool#13.
vnprc approved these changes 2026-10-09 06:30:37 +01:00
vnprc merged commit eb140b5fc9 into master 2026-10-09 06:30:42 +01:00
vnprc deleted branch agent/epoch-mechanics 2026-10-09 06:30:42 +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!33
No description provided.