Task
Implement the mirror-coin lifecycle in dig-node: create one per (store, root) it serves per epoch,
reclaim the previous epoch's, and reclaim when it stops serving — signing automatically, under a
scoped key.
Per (store, root), not per store. The legacy had one coin per store; roots are now broadcast
individually, so a publisher can fund the latest root and not the legacy one if they choose.
The naming chain, so the legacy source is readable
server coin (legacy code) → collateral coin (interim) → mirror coin (canonical). grep -rniE "mirror" across all three legacy src/ trees returns zero hits — the type is ServerCoin; "mirror" is the puzzle name one layer down in DataLayer-Driver.
§908 carve-out — the node signs this automatically
User directive: "the node MUST sign this on the users behalf, managing these mirror coins behind the scenes, and creating the spends automatically is a hard requirement. Its impossible for the user to approve all the transactions."
Scoped to mirror coins. §908 stands for everything else. The shape to lock first (§1.10): which key signs (a dedicated, separately-derived key with a bounded working balance — not the master seed, or the carve-out is total), what bounds the authority (per-epoch cap, the epoch's required amount per (store, root), destination restricted to the mirror puzzle), what the user accepts and when, and how revocation reclaims what is locked.
The legacy state machine, measured — replicate the shape, not the bugs
ensureServerCoinExists (dig-chia-sdk/src/blockchain/ServerCoin.ts:285-363), driven by a 60-second job (sync_stores.ts:326-333), not a weekly timer — the weekly cadence is emergent from comparing the computed epoch to the cached one:
- coin exists, synced, root matches → no-op (
:306)
- coin exists, synced, root moved → update the local record only, no chain write (
:311)
- coin exists, not synced → melt it (
:323), stated reason: "helps prevent penalties for not having a valid peer registered"
- no coin, synced → create (
:334)
- not synced → return without creating (
:327) — never advertise a store you cannot serve
It is a re-create per epoch, not a renew. Nothing tops up or extends.
The epoch clock
calculateEpochAndRound (ServerCoin.ts:500-529): 7-day wall-clock windows, hard-coded 7*24*60*60*1000, 1-based, genesis 2024-09-03T00:00Z. Rounds are 10 minutes, 1008 per epoch. No chain input.
dig-epoch is NOT this. It is L2 epoch geometry anchored to L1 block heights with BlockProduction/Checkpoint/Finalization phases — a different notion. Using it here would silently change the cadence. If it is adopted anyway, it leaves the parked L2 island and needs the same release-first treatment as dig-mirror-coin.
Bugs NOT to inherit — this is the highest-value part of the brief
-
meltOutdatedEpochs had no in-repo callers, but it WORKED — it was an operational step.
Corrected by the user directly: "no meltOutdatedEpochs worked when that legacy ran. We just had to
locate the old coins and spend them in that version." An earlier version of this ticket called it
dead code; that was wrong, and the error is worth keeping visible — "no callers" was turned into
"never happened" without asking what ran outside the repo.
The requirement survives, and it binds harder here than it did there: dig-node has no operator.
Nobody will locate and spend the old coins by hand. So reclaiming epoch n-1 when creating epoch
n is an acceptance criterion, not an optimisation — stranded collateral is real user money that
never comes back.
-
Unbounded retry under the sync mutex (:71-78 + sync_stores.ts:253) — recursion on a substring match, no cap, no backoff. One underfunded store stalls synchronisation of every other store, forever.
-
IP-keyed records (:116) with a 7-day IP refresh (ip_refresh.ts:41-48) — a dynamic IP strands collateral at every address ever held. Key on launcher id + owner puzzle hash.
-
The local .json was authoritative and unrecoverable if lost. Use query::list (keyed on owner puzzle hash, ownership from the lineage proof) as the source of truth.
-
waitForConfirmation waited on the wrong coin (:347) — it waits for the funding coin to be spent, which a competing spend satisfies identically. It never confirmed the mirror coin was created.
-
Reclaim requires XCH for its own fee (:136), and meltUntrackedStoreCoins (:424-438) has no per-coin try/catch — the first failure aborts the whole sweep. A wallet at zero can be unable to advertise and unable to recover what it already locked.
-
Melt/create churn on sync flap (:321-323) — two fees and two confirmation waits per round trip.
Amount
The amount is derived per epoch, not fixed. equilibrium x multiplier - handicap, computed from
chain data and independently reproducible by every node — specified in
https://github.com/DIG-Network/dig_ecosystem/issues/3173 and implemented in dig-mirror-collateral.
Call that crate; never re-derive the formula — its clamps are structural in the code and absent from
the formula as written, so re-deriving reintroduces holes.
$DIG has 3 decimals, so whole $DIG x 1,000 = CAT mojos. At genesis the schedule starts at 1.000 $DIG
per (store, root) and rises toward 5.000 as the handicap decays. Legacy locked 0.0003 XCH — a
different asset entirely; do not let it drift back.
Hard constraints
- Never hand-roll a spend bundle (§4.1) —
dig-mirror-coin builds them.
- Every automated spend writes to the audit record (dig-node#376). A spend reaching chain with no entry is invisible money movement.
- BLOCKED on
dig-mirror-coin reaching the ceiling and publishing (dig-mirror-coin#3). Adopting it at chia 0.26/0.30 would re-split dig-node, which just reached 0.36.1.
Evidence (§2.6)
A person watching a (store, root) get collateralised on a real machine: the epoch's required amount
locked, the coin resolvable on chain, and the previous epoch's coin reclaimed at rollover. Not a green
suite.
Parent
https://github.com/DIG-Network/dig_ecosystem/issues/3166
Task
Implement the mirror-coin lifecycle in dig-node: create one per (store, root) it serves per epoch,
reclaim the previous epoch's, and reclaim when it stops serving — signing automatically, under a
scoped key.
Per (store, root), not per store. The legacy had one coin per store; roots are now broadcast
individually, so a publisher can fund the latest root and not the legacy one if they choose.
The naming chain, so the legacy source is readable
server coin (legacy code) → collateral coin (interim) → mirror coin (canonical).
grep -rniE "mirror"across all three legacysrc/trees returns zero hits — the type isServerCoin; "mirror" is the puzzle name one layer down inDataLayer-Driver.§908 carve-out — the node signs this automatically
User directive: "the node MUST sign this on the users behalf, managing these mirror coins behind the scenes, and creating the spends automatically is a hard requirement. Its impossible for the user to approve all the transactions."
Scoped to mirror coins. §908 stands for everything else. The shape to lock first (§1.10): which key signs (a dedicated, separately-derived key with a bounded working balance — not the master seed, or the carve-out is total), what bounds the authority (per-epoch cap, the epoch's required amount per (store, root), destination restricted to the mirror puzzle), what the user accepts and when, and how revocation reclaims what is locked.
The legacy state machine, measured — replicate the shape, not the bugs
ensureServerCoinExists(dig-chia-sdk/src/blockchain/ServerCoin.ts:285-363), driven by a 60-second job (sync_stores.ts:326-333), not a weekly timer — the weekly cadence is emergent from comparing the computed epoch to the cached one::306):311):323), stated reason: "helps prevent penalties for not having a valid peer registered":334):327) — never advertise a store you cannot serveIt is a re-create per epoch, not a renew. Nothing tops up or extends.
The epoch clock
calculateEpochAndRound(ServerCoin.ts:500-529): 7-day wall-clock windows, hard-coded7*24*60*60*1000, 1-based, genesis 2024-09-03T00:00Z. Rounds are 10 minutes, 1008 per epoch. No chain input.dig-epochis NOT this. It is L2 epoch geometry anchored to L1 block heights with BlockProduction/Checkpoint/Finalization phases — a different notion. Using it here would silently change the cadence. If it is adopted anyway, it leaves the parked L2 island and needs the same release-first treatment asdig-mirror-coin.Bugs NOT to inherit — this is the highest-value part of the brief
meltOutdatedEpochshad no in-repo callers, but it WORKED — it was an operational step.Corrected by the user directly: "no meltOutdatedEpochs worked when that legacy ran. We just had to
locate the old coins and spend them in that version." An earlier version of this ticket called it
dead code; that was wrong, and the error is worth keeping visible — "no callers" was turned into
"never happened" without asking what ran outside the repo.
The requirement survives, and it binds harder here than it did there: dig-node has no operator.
Nobody will locate and spend the old coins by hand. So reclaiming epoch n-1 when creating epoch
n is an acceptance criterion, not an optimisation — stranded collateral is real user money that
never comes back.
Unbounded retry under the sync mutex (
:71-78+sync_stores.ts:253) — recursion on a substring match, no cap, no backoff. One underfunded store stalls synchronisation of every other store, forever.IP-keyed records (
:116) with a 7-day IP refresh (ip_refresh.ts:41-48) — a dynamic IP strands collateral at every address ever held. Key on launcher id + owner puzzle hash.The local
.jsonwas authoritative and unrecoverable if lost. Usequery::list(keyed on owner puzzle hash, ownership from the lineage proof) as the source of truth.waitForConfirmationwaited on the wrong coin (:347) — it waits for the funding coin to be spent, which a competing spend satisfies identically. It never confirmed the mirror coin was created.Reclaim requires XCH for its own fee (
:136), andmeltUntrackedStoreCoins(:424-438) has no per-coin try/catch — the first failure aborts the whole sweep. A wallet at zero can be unable to advertise and unable to recover what it already locked.Melt/create churn on sync flap (
:321-323) — two fees and two confirmation waits per round trip.Amount
The amount is derived per epoch, not fixed.
equilibrium x multiplier - handicap, computed fromchain data and independently reproducible by every node — specified in
https://github.com/DIG-Network/dig_ecosystem/issues/3173 and implemented in
dig-mirror-collateral.Call that crate; never re-derive the formula — its clamps are structural in the code and absent from
the formula as written, so re-deriving reintroduces holes.
$DIG has 3 decimals, so whole $DIG x 1,000 = CAT mojos. At genesis the schedule starts at 1.000 $DIG
per (store, root) and rises toward 5.000 as the handicap decays. Legacy locked 0.0003 XCH — a
different asset entirely; do not let it drift back.
Hard constraints
dig-mirror-coinbuilds them.dig-mirror-coinreaching the ceiling and publishing (dig-mirror-coin#3). Adopting it at chia 0.26/0.30 would re-split dig-node, which just reached 0.36.1.Evidence (§2.6)
A person watching a (store, root) get collateralised on a real machine: the epoch's required amount
locked, the coin resolvable on chain, and the previous epoch's coin reclaimed at rollover. Not a green
suite.
Parent
https://github.com/DIG-Network/dig_ecosystem/issues/3166