mint epoch mechanics: height-named units, records, manual rotation lever #24
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/epoch-mechanics"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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>_<height>with its own keyset, quotes stamp the current epoch's unit, andjust rotate-epochcloses the current epoch and opens the next by hand. No trigger yet — rotation is manual/genesis only (the reward watcher is #18); no wallet changes (the multi-unit sweeper is #17, a sibling branch). The legacy statichashunit is gone from mint setup — units are runtime state now, so this is a clean-slate change for dev databases (just clean cashu). If it is wrong, revert; master's static-unit mint is untouched by this branch until merge.Implements #16. Part of #13. Stacked on #21 (pubkey-lookup); pins cdk at the dynamic-registration branch (see Validation for the push dependency).
Design anchors
docs/EPOCH_DESIGN.md— this PR implements: the model (naming incl. deterministic collision suffix, mint-owned identity, genesis-at-startup, one-epoch-per-(pool,height)), the quote lifecycle branch (final epoch → pay at creation, exactly the historical behavior; provisional epoch → created unpaid, pays at finality — dormant until #18 since genesis/manual epochs are final immediately), retire-previous-epoch-at-successor-finality, and the 7-day quote TTL. Epoch records live in a file-backed store (epochs.jsonbeside the mint DB) — single writer, atomic rename, the last non-dissolved record is current.Risk
pool_pubkey— both fail loud if missing. Prod config carries placeholder TODOs for the operator.127.0.0.1:3339default) — rotation is not reachable off-box.Validation
cargo check -p mintclean at the final pin;cargo test -p mint17+5 tests green (new epoch tests: pool-pubkey validation incl. case normalization and uncompressed rejection, suffix determinism, store round-trip/current-selection/dissolved-exclusion/height-counting).hash_<pool>_5, source=genesis, final); two same-height manual rotations produced_5_1and_5_2(the deterministic suffix rule); after three more blocks, rotation produced_8;epochs.jsonrecords all four with correct height/unit/state/source; mint restart logsresuming persisted epoch … height=8with no re-genesis;/v1/keysetslists all four epoch units, each keyset active within its own unit (old epochs stay mintable for outstanding quotes by design — quote creation for them is what retires).listen_host = "localhost"binds IPv6::1on this machine, so probes must uselocalhost, not127.0.0.1(the rest of the dev stack also useslocalhost, which is why this was never visible). The admin lever binds127.0.0.1explicitly.Adversarial review findings (read-only pass; fixes in flight on this branch):
BLOCKER — restart never re-registers the resumed epoch's processor.
roles/mint/src/lib/epoch/mod.rsresume branch returns withoutregister_payment_processor; the cdk processor map is in-memory only, so after any restart with an existingepochs.json, every MintQuoteRequest fails "Unit unsupported" until a manual rotation. First boot works (genesis registers); the smoke's restart check read only DB-backed surfaces (resume log, /v1/keysets) so this was invisible. Secondary: first rotation after restart can't deregister the previous unit (not in the map), so stale NUT-04/05 entries accumulate. Fix: resume path re-registers via a helper shared withopen_epoch, treating duplicate-register as success.should-fix —
just clean cashu(the PR's prescribed clean-slate step) leavesepochs.jsonbehind: fresh DB + stale store = resumed epoch with no keyset/settings, quoting dead, manualrmto recover. Clean must removeepochs.json(+.json.tmp).should-fix — the derivation-collision retry is dead code: the match is
contains("collision")but cdk's error displays "collided" (UnitStringCollision). A real 31-bit collision would brick genesis/rotation for that (pool,height) instead of suffixing. Match the typed variant; cap the loop.should-fix — rotation deregisters the previous unit before swapping
current; a quote handled in that window reads the old unit and gets "Unit unsupported" (share bounced, pool doesn't retry). Narrow under manual rotation, armed the moment #18 automates it. Also matches the design doc better to retire at successor finality: reorder to persist + swap before deregister.should-fix (latent, armed by #18) — pay-or-defer does two separate reads (unit at creation, finality after): a rotation between them strands a final-epoch quote unpaid forever. Needs one snapshot read returning (unit, is_final).
should-fix —
open_epochfailure ordering: persist failure leaves memory/disk diverged andcurrentpointing at a just-deregistered unit; crash between register and persist leaves settings naming a unit the store doesn't know. Same reorder + rollback the in-memory push on persist failure.should-fix — the "60s retry then fail-loud" genesis claim holds only when bitcoind fails fast: MiniRpcClient has no request timeout, so a hung RPC blocks boot forever, silently. Wrap each attempt in a timeout.
nits — admin lever is unauthenticated and loopback-by-convention (an
0.0.0.0typo exposes rotation;just rotate-epochhardcodes the port, ignoring config); prod config ships the generator-point placeholder as a valid pool_pubkey (consider refusing it at startup); store persist has no fsync (power loss can truncate the file; parse failure on boot; deleting it silently re-genesises), and blocking fs calls run under the async mutex.Body-truth: test counts and smoke narrative reproduced exactly; "old quotes stay mintable" verified true at the pin (cdk
e583e73cmints from recorded payments); "clean-slate viajust clean cashu" is FALSE as shipped (second finding); the cdk pin is now fetchable from github (the owner pushed), so the buildability caveat is moot. Merging #24 without #23 leaves dev-stack sweeping dead (epoch-unit quotes vshash-unit wallet) — disclosed, but merge them together.Verdict: design and rotation mechanics sound, body honest; not mergeable until the blocker + clean-cashu fix land. Fix commits follow on this branch.
Review findings addressed in the commit above: restart re-registration (blocker) — witnessed by a fresh-process resume against existing epoch state plus a successful post-restart rotation (
_8_1, previous unit retired);just clean cashunow removesepochs.json; collision retry matches the typedUnitStringCollisionwith a 32-attempt cap; rotation persists and swaps current before deregistering the previous unit; the quote path takes one snapshot read for unit + pay decision; store append rolls back memory on persist failure; RPC attempts have a 10s hard timeout. Not addressed (recorded nits): admin-lever auth/binding hardening, generator-point placeholder refusal in prod config, store fsync. cargo check clean; 17+5 tests green.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.