Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

restore.gno

24.32 Kb · 598 lines
  1package nslogic
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7	"time"
  8
  9	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
 10)
 11
 12/*
 13REBUILDING THE REGISTRY ON A NEW CHAIN.
 14
 15WHY THIS EXISTS. gno.land replaces testnets rather than preserving them:
 16topaz-1 was superseded by sapphire-1 on twenty-four hours' notice and
 17every name registered on it went to zero. tools/snapshot.js keeps a
 18complete off-chain copy of the vault for exactly that morning. This is
 19the other half — the path that turns a snapshot back into a live
 20registry, so that people who paid for a name get that name back rather
 21than an apology.
 22
 23THIS IS A BACKDOOR, AND IT IS TREATED AS ONE.
 24
 25A function that can mint any name, to any address, with any registration
 26date, is precisely the thing that would make every name here worthless
 27if it could be reached whenever the admin felt like it. Ownership means
 28nothing if history is editable on demand. So the capability is not
 29merely admin-gated — admin-gated is what everything else here already
 30is, and it is not enough for this. It is a ONE-SHOT:
 31
 32  - it starts closed, and on a chain where nothing needs restoring it is
 33    sealed immediately and can never be used at all;
 34  - opening it is a deliberate, separate, announced act with a deadline
 35    attached, so forgetting to close it is not the same as leaving it
 36    open forever;
 37  - sealing is IRREVERSIBLE. Sealed cannot be reopened. There is no
 38    argument that unseals it, including a good one;
 39  - it refuses to touch a name that already exists. It fills gaps in an
 40    empty registry; it cannot rewrite a live one.
 41
 42WHERE THE SEAL LIVES, AND WHY IT IS NOT HERE. This realm holds no state
 43at all — that is the whole reason prices and names survive a logic
 44upgrade untouched. If the seal lived in this realm's variables it would
 45reset to "never opened" the moment a new generation was deployed, which
 46would make "irreversible" a lie with a two-hour shelf life. It lives in
 47the vault's global slot instead, so a new logic generation inherits a
 48seal it cannot talk its way out of.
 49
 50WHAT THAT DOES AND DOES NOT GUARANTEE. It is a real barrier against
 51every accident and against this realm being reached later for something
 52it was not built for. It is not a barrier against the admin themselves,
 53who can deploy a further logic realm and have the vault trust it — but
 54that is already total control by definition, and no flag in a realm the
 55admin can replace was ever going to change it. What the seal gives is
 56the thing that actually matters in practice: the state is public, it is
 57checkable by anybody at any time through RestoreStatus, and moving it
 58backwards leaves a permanent trace on chain.
 59
 60WHAT IT DELIBERATELY DOES NOT RESTORE. Marketplace listings and open
 61votes. Those live in their own realms, are intentions rather than facts,
 62and are re-creatable by the people who made them in one transaction
 63each. Re-materialising somebody's month-old asking price as though they
 64had just set it would be inventing a decision on their behalf.
 65*/
 66
 67// The vault key holding the state of this whole mechanism.
 68const keyRestore = "restore"
 69
 70// Restore states, stored as a string in the vault's global slot.
 71const (
 72	restoreNever  = ""       // never opened; the normal state forever
 73	restoreSealed = "sealed" // closed permanently; cannot be reopened
 74	restoreOpen   = "open:"  // followed by the unix second it lapses
 75)
 76
 77// The longest a restore window may be left open. A restore is a day of
 78// work, not a season, and an unbounded window is just "open forever"
 79// spelled differently — the failure here is nobody remembering to seal
 80// it, so the deadline seals it for them.
 81const maxRestoreDays = 30
 82
 83// RestoreStatus reports the state of the restore mechanism in a form a
 84// person can read, and is deliberately PUBLIC AND UNAUTHENTICATED.
 85// Anybody at all can check whether this registry currently has a door in
 86// it, without asking us and without trusting the answer we would give.
 87//
 88// Returns the state ("never", "open", "expired" or "sealed"), the unix
 89// second an open window lapses (0 otherwise), and how many records have
 90// been restored in the lifetime of this vault.
 91func RestoreStatus() (state string, deadline int64, restored int64) {
 92	st, d := readRestoreState(nsdata.GetGlobal(keyRestore), time.Now().Unix())
 93	return st, d, restoredCount()
 94}
 95
 96// readRestoreState is the state machine on its own, with the clock passed
 97// in. Split out so the one piece of logic that decides whether the door
 98// is open can be tested exhaustively without a chain under it.
 99func readRestoreState(raw string, now int64) (state string, deadline int64) {
100	switch {
101	case raw == restoreSealed:
102		return "sealed", 0
103	case strings.HasPrefix(raw, restoreOpen):
104		d, err := strconv.ParseInt(raw[len(restoreOpen):], 10, 64)
105		if err != nil || d <= 0 {
106			// An unparseable deadline is read as NO permission. The safe
107			// reading of a door whose lock is broken is "shut" — a corrupt
108			// value must never widen what somebody is allowed to do.
109			return "sealed", 0
110		}
111		if now >= d {
112			return "expired", d
113		}
114		return "open", d
115	}
116	return "never", 0
117}
118
119const keyRestoredCount = "restored"
120
121func restoredCount() int64 {
122	if v := nsdata.GetGlobal(keyRestoredCount); v != "" {
123		if n, err := strconv.ParseInt(v, 10, 64); err == nil {
124			return n
125		}
126	}
127	return 0
128}
129
130/*
131THE RESTORE KEYS GO THROUGH THE VAULT'S RESERVED DOOR, and every refusal
132panics.
133
134They used to go through SetGlobalRecord, written for the first vault,
135which had no reserved keys. This vault refuses "restore" and "restored"
136there — and the three call sites threw the error away. So SealRestore
137returned success and sealed nothing, OpenRestore returned success and
138opened nothing (every Restore* call then said "never been opened", in the
139middle of a paused migration), and the counter never moved. A returned
140error in gno does not revert anything; ignoring one is how a control
141reads as live while doing nothing.
142
143SetReservedGlobalRecord is the machinery's own door, and the vault now
144refuses to change a seal through it as well: a sealed restore stays
145sealed whatever any logic realm asks, so a future logic generation
146cannot talk its way out of it either.
147*/
148func setRestoreKey(cur realm, key, value string) {
149	if err := nsdata.SetReservedGlobalRecord(cross(cur), key, value); err != nil {
150		panic("nslogic: " + err.Error())
151	}
152}
153
154func bumpRestored(cur realm, n int64) {
155	setRestoreKey(cur, keyRestoredCount, strconv.FormatInt(restoredCount()+n, 10))
156}
157
158/*
159assertNotRestoring CLOSES REGISTRATION WHILE A RESTORE WINDOW IS OPEN
160(re-audit 2026-10-16, M-4). The window opens on an empty registry, and the
161snapshots it replays are committed to the repository, so the list of who
162held what is public: anybody could register a holder's name before its
163batch arrived — at the genesis price, before the price rules were back —
164and the holder never got it back, while the batch holding it reverted and
165stopped the run. Every door that creates a name, or makes a free one
166sellable, passes this: RegisterName and RegisterNameRef, RedeemCredit and
167RegisterFor, GrantFreeName, UpgradeFreeName (and the lab's RegisterLabName).
168Sealed, or lapsed, the registry sells again.
169*/
170func assertNotRestoring() {
171	if st, _ := readRestoreState(nsdata.GetGlobal(keyRestore), time.Now().Unix()); st == "open" {
172		panic("nslogic: the registry is being restored; registration reopens once the restore is sealed")
173	}
174}
175
176// assertRestoring is the gate every Restore* function passes through.
177func assertRestoring(cur realm) {
178	assertNoSend(cur)
179	assertIsAdmin(cur)
180	state, _, _ := RestoreStatus()
181	switch state {
182	case "open":
183		return
184	case "sealed":
185		panic("nslogic: restore was sealed and cannot be reopened. This is " +
186			"not recoverable by design — a registry whose history can be " +
187			"rewritten on request does not have owners, it has guests.")
188	case "expired":
189		panic("nslogic: the restore window has lapsed. Deploy a fresh vault " +
190			"and open a new window there; an expired one does not reopen.")
191	}
192	panic("nslogic: restore has never been opened. Call OpenRestore first, " +
193		"and mean it.")
194}
195
196// OpenRestore opens the window, once, for a bounded number of days.
197//
198// Only ever valid on a chain where the registry is EMPTY because the
199// previous chain was retired. It cannot be called on a vault that has
200// been sealed, and it cannot be called twice.
201func OpenRestore(cur realm, days int64) {
202	assertNoSend(cur)
203	assertIsAdmin(cur)
204	emitAdmin(cur, "OpenRestore", "days", fmtInt(days))
205
206	state, _, _ := RestoreStatus()
207	if state != "never" {
208		panic("nslogic: restore may be opened exactly once per vault; this " +
209			"one is already " + state + ". A door that reopens is a door.")
210	}
211	if days < 1 || days > maxRestoreDays {
212		panic("nslogic: a restore window must be between 1 and " +
213			strconv.Itoa(maxRestoreDays) + " days")
214	}
215	/* THE PRECONDITION THE COMMENT ABOVE ALREADY CLAIMS.
216	   "Only ever valid on a chain where the registry is EMPTY" was a
217	   statement of intent that nothing enforced, so the window was
218	   openable on a populated vault. The record restorers all refuse an
219	   existing record, but RestoreNameField does not — it writes any
220	   extension key on any EXISTING name, which includes the payment
221	   addresses the site renders on a name's pay page. That is a way to
222	   redirect somebody else's incoming money using only the admin key,
223	   without the timelock and public event that granting a fresh logic
224	   realm would cost. Now it is what it said it was. */
225	/* NAMES ONLY, not domains. A registry is now born with its domains
226	   (genesis), so "no domains" can never be true again and the guard
227	   would have made disaster recovery impossible on every new chain.
228	   The hazard it exists for is RestoreNameField writing onto EXISTING
229	   NAMES — somebody's pay address — and only names carry that. */
230	if _, n, _, _ := nsdata.Sizes(); n != 0 {
231		panic("nslogic: restore is for a registry holding no names yet; " +
232			"this one already holds records")
233	}
234	// TIMELOCKED (timelock.gno). This is the door that mints any name to
235	// anybody with any date; on a fresh registry, before the first name and
236	// before SealRestore, a stolen admin key could otherwise open it at once.
237	requireAction(cur, actionID("OpenRestore", strconv.FormatInt(days, 10)))
238	deadline := time.Now().Unix() + days*86400
239	setRestoreKey(cur, keyRestore, restoreOpen+strconv.FormatInt(deadline, 10))
240}
241
242// SealRestore closes the door permanently. Valid from any state,
243// including "never" — which is how it should be called on a chain that
244// has nothing to restore, so that the capability is provably dead rather
245// than merely unused.
246//
247// There is no matching unseal, and adding one later would defeat every
248// word written at the top of this file.
249func SealRestore(cur realm) {
250	assertNoSend(cur)
251	assertIsAdmin(cur)
252	emitAdmin(cur, "SealRestore")
253	setRestoreKey(cur, keyRestore, restoreSealed)
254	// Read back, not assumed: this call used to report success while the
255	// state stayed "never".
256	if st, _, _ := RestoreStatus(); st != "sealed" {
257		panic("nslogic: the seal did not land; restore reads " + st)
258	}
259}
260
261/*
262RestoreDomain re-creates one domain with its original dates.
263
264ORDER MATTERS: domains before their names. The vault refuses to register
265a name under a domain that does not exist or had lapsed at the moment
266the name claims to have been registered, which is correct behaviour and
267also exactly the order a snapshot should be replayed in.
268
269The vault's own registration path sets `registered` to whatever instant
270it is handed and derives the expiry from the term length, so the
271registration date is restored by passing the ORIGINAL one, and the
272expiry is then corrected to the recorded value rather than a computed
273one. Anything else would quietly hand everybody a fresh term.
274*/
275func RestoreDomain(cur realm, label string, owner address, registered, expires, ttl int64, frozen bool) {
276	assertRestoring(cur)
277	emitAdmin(cur, "RestoreDomain", "label", label, "owner", owner.String())
278	if nsdata.DomainExists(label) {
279		panic("nslogic: " + label + " already exists; restore fills an empty " +
280			"registry and never overwrites a live record")
281	}
282	if registered <= 0 || expires <= 0 {
283		panic("nslogic: a restored record needs its original dates")
284	}
285	if err := nsdata.RegisterDomainRecord(cross(cur), label, owner, registered); err != nil {
286		panic("nslogic: " + err.Error())
287	}
288	nsdata.SetDomainExpiryRecord(cross(cur), label, expires)
289	ttl = clampTTL(ttl)
290	if ttl != 0 {
291		if err := nsdata.SetDomainTTLRecord(cross(cur), label, ttl); err != nil {
292			panic("nslogic: " + err.Error())
293		}
294	}
295	if frozen {
296		nsdata.SetDomainFrozen(cross(cur), label, true)
297	}
298	bumpRestored(cur, 1)
299}
300
301// RestoreName re-creates one name. See RestoreDomain on ordering and on
302// why the dates are passed rather than computed.
303//
304// isNFT is carried across under its old name so snapshot blobs stay
305// readable, and now means PAID: false restores a free-tier name with the
306// hold on its token, so a restore can never quietly make a free name
307// sellable.
308func RestoreName(cur realm, label, domainLabel string, owner address, registered, expires, ttl int64, frozen, isNFT bool) {
309	assertRestoring(cur)
310	emitAdmin(cur, "RestoreName", "label", label, "domain", domainLabel, "owner", owner.String())
311	if nsdata.NameExists(label, domainLabel) {
312		panic("nslogic: " + label + "*" + domainLabel + " already exists; " +
313			"restore fills an empty registry and never overwrites a live record")
314	}
315	if registered <= 0 || expires <= 0 {
316		panic("nslogic: a restored record needs its original dates")
317	}
318	// RestoreNameRecord, not RegisterNameRecord: it ADOPTS a token the
319	// ledger already holds, which is the whole point of a migration —
320	// records move, tokens do not. If the ledger's holder is not the
321	// snapshot's owner, the record is written for the holder and the
322	// snapshot's profile is not replayed onto it.
323	if err := nsdata.RestoreNameRecord(cross(cur), label, domainLabel, owner, registered, isNFT); err != nil {
324		panic("nslogic: " + err.Error())
325	}
326	nsdata.SetNameExpiryRecord(cross(cur), label, domainLabel, expires)
327	ttl = clampTTL(ttl)
328	if ttl != 0 {
329		if err := nsdata.SetNameTTLRecord(cross(cur), label, domainLabel, ttl); err != nil {
330			panic("nslogic: " + err.Error())
331		}
332	}
333	if frozen {
334		nsdata.SetNameFrozen(cross(cur), label, domainLabel, true)
335	}
336	bumpRestored(cur, 1)
337}
338
339/*
340RestoreNames replays many names in ONE transaction.
341
342WHY A BLOB AND NOT A LOOP OF CALLS. A restore is done in one sitting
343against a clock: the window is bounded and the people whose names are
344missing are watching. One transaction per name is one signature per
345name, and at a few hundred names that is an afternoon of clicking during
346which a single mistimed refresh loses the thread. The compact form here
347is unpleasant to read and it is the difference between a restore that
348gets finished and one that gets abandoned half way.
349
350The format is records separated by "|", fields separated by ",":
351
352	label,domain,owner,registered,expires,ttl,frozen,isNFT
353
354Neither a label nor a domain may contain "," or "|" — the character set
355is enforced at registration — and an address contains neither, so the
356split is unambiguous. tools/snapshot.js writes the JSON this is built
357from; see tools/restore-blob.js for the conversion.
358
359It stops at the first bad record and panics, taking the whole
360transaction with it. That is deliberate: a partial batch that silently
361skipped four names would leave four people without theirs and nothing
362anywhere saying which four.
363*/
364func RestoreNames(cur realm, blob string) int {
365	assertRestoring(cur)
366	emitAdmin(cur, "RestoreNames", "bytes", fmtI(len(blob)))
367	if blob == "" {
368		panic("nslogic: nothing to restore")
369	}
370	recs := strings.Split(blob, "|")
371	if len(recs) > 50 {
372		panic("nslogic: at most 50 records per call, or the transaction " +
373			"runs out of gas half way and it is unclear what landed")
374	}
375	done := 0
376	for i, rec := range recs {
377		if rec == "" {
378			continue
379		}
380		r := parseRestoreRecord(rec, i)
381		if nsdata.NameExists(r.label, r.domain) {
382			panic("nslogic: record " + strconv.Itoa(i) + " (" + r.label + "*" + r.domain +
383				") already exists; restore never overwrites a live record")
384		}
385		if err := nsdata.RestoreNameRecord(cross(cur), r.label, r.domain, r.owner, r.registered, r.isNFT); err != nil {
386			panic("nslogic: record " + strconv.Itoa(i) + ": " + err.Error())
387		}
388		nsdata.SetNameExpiryRecord(cross(cur), r.label, r.domain, r.expires)
389		if r.ttl != 0 {
390			if err := nsdata.SetNameTTLRecord(cross(cur), r.label, r.domain, r.ttl); err != nil {
391				panic("nslogic: record " + strconv.Itoa(i) + ": " + err.Error())
392			}
393		}
394		if r.frozen {
395			nsdata.SetNameFrozen(cross(cur), r.label, r.domain, true)
396		}
397		done++
398	}
399	bumpRestored(cur, int64(done))
400	return done
401}
402
403// restoreRec is one parsed line of the blob.
404type restoreRec struct {
405	label, domain string
406	owner         address
407	registered    int64
408	expires, ttl  int64
409	frozen, isNFT bool
410}
411
412/*
413parseRestoreRecord reads one record and refuses anything it is not sure
414about.
415
416EVERY CHECK HERE IS A PANIC AND NOT A SKIP. The blob is machine-written
417from a snapshot, so a malformed record does not mean "this one name is
418odd", it means the file is not what it was believed to be — and the
419worst outcome available is a restore that appears to succeed while
420quietly dropping the records it could not read. Somebody would find out
421months later when one person asked where their name went.
422*/
423func parseRestoreRecord(rec string, i int) restoreRec {
424	f := strings.Split(rec, ",")
425	if len(f) != 8 {
426		panic("nslogic: record " + strconv.Itoa(i) + " has " +
427			strconv.Itoa(len(f)) + " fields, expected 8 " +
428			"(label,domain,owner,registered,expires,ttl,frozen,isNFT)")
429	}
430	if f[0] == "" || f[1] == "" {
431		panic("nslogic: record " + strconv.Itoa(i) + " has no name")
432	}
433	out := restoreRec{
434		label:      f[0],
435		domain:     f[1],
436		owner:      address(f[2]),
437		registered: mustSecs(f[3], i, "registered"),
438		expires:    mustSecs(f[4], i, "expires"),
439		ttl:        ttlSecs(f[5], i),
440		frozen:     f[6] == "1",
441		isNFT:      f[7] == "1",
442	}
443	if !out.owner.IsValid() {
444		panic("nslogic: record " + strconv.Itoa(i) + " has an invalid owner address")
445	}
446	if out.registered <= 0 || out.expires <= 0 {
447		panic("nslogic: record " + strconv.Itoa(i) + " is missing its original dates")
448	}
449	// An expiry before the registration is not a record, it is a
450	// corruption, and restoring it would create a name that was already
451	// reclaimable the day it was bought.
452	if out.expires < out.registered {
453		panic("nslogic: record " + strconv.Itoa(i) + " expires before it was registered")
454	}
455	return out
456}
457
458func mustSecs(s string, i int, what string) int64 {
459	v, err := strconv.ParseInt(s, 10, 64)
460	if err != nil || v < 0 {
461		panic("nslogic: record " + strconv.Itoa(i) + " has a bad " + what)
462	}
463	return v
464}
465
466/*
467ttlSecs reads a TTL and caps it at maxTTL (name.gno). A holder may have set
468any non-negative int64 before the cap existed, and a snapshot written by
469JavaScript rounded such a value above 2^53 — MaxInt64 came back as
4709223372036854776000, which no int64 holds — so the whole batch of fifty
471reverted (re-audit 2026-10-16, M-3). A run of digits too large to parse is
472read as the longest a TTL may be; anything else malformed still refuses
473the record.
474*/
475func ttlSecs(s string, i int) int64 {
476	if v, err := strconv.ParseInt(s, 10, 64); err == nil && v >= 0 {
477		return clampTTL(v)
478	}
479	if s != "" && len(s) <= 40 && strings.Trim(s, "0123456789") == "" {
480		return maxTTL
481	}
482	panic("nslogic: record " + strconv.Itoa(i) + " has a bad ttl")
483}
484
485func clampTTL(v int64) int64 {
486	if v > maxTTL {
487		return maxTTL
488	}
489	return v
490}
491
492// RestoreNameField and RestoreDomainField put back a record's extension
493// keys — profiles, cross-chain addresses, tier markers, and a domain's
494// pricing rules. Separate calls because the extension slot has no fixed
495// shape by design, which is the same reason the snapshot captures it by
496// enumeration rather than by schema.
497func RestoreNameField(cur realm, label, domainLabel, key, value string) {
498	assertRestoring(cur)
499	emitAdmin(cur, "RestoreNameField", "label", label, "domain", domainLabel, "key", key)
500	/* ONLY ONTO A RECORD A RESTORE WROTE, AND ONLY FOR ITS HOLDER.
501
502	   Profile fields include pay addresses, so replaying one is replaying
503	   where money goes. Two audit findings meet here: a name sold between
504	   the snapshot and the restore would have had the seller's addresses
505	   written onto the buyer's name, and a name a user registered DURING
506	   the restore window could have had its pay fields rewritten with the
507	   admin key alone. RestoredFor is set only by RestoreNameRecord, and
508	   only when the ledger's holder matched the snapshot.
509
510	   Skipped rather than panicked, deliberately: a blob of fields rides in
511	   one transaction, and one sold name must not abort everybody else's
512	   restore. tools/restore-blob.js reads the fields back afterwards. */
513	rf := nsdata.RestoredFor(label, domainLabel)
514	if rf == "" || nsdata.GetNameOwner(label, domainLabel) != rf {
515		return
516	}
517	if err := nsdata.SetNameExtraRecord(cross(cur), label, domainLabel, key, value); err != nil {
518		panic("nslogic: " + err.Error())
519	}
520}
521
522func RestoreDomainField(cur realm, label, key, value string) {
523	assertRestoring(cur)
524	emitAdmin(cur, "RestoreDomainField", "label", label, "key", key, "value", value)
525	if err := nsdata.SetDomainExtraRecord(cross(cur), label, key, value); err != nil {
526		panic("nslogic: " + err.Error())
527	}
528}
529
530// RestoreGlobal puts back the vault's global slot — where the default
531// price, the referral tiers and the seasonal card effects live.
532//
533// It refuses to write the restore keys themselves. Without that, a
534// snapshot taken while a window was open would, on being replayed,
535// cheerfully restore the open window along with everything else — and a
536// seal that a backup file can undo is not a seal.
537func RestoreGlobal(cur realm, key, value string) {
538	assertRestoring(cur)
539	emitAdmin(cur, "RestoreGlobal", "key", key, "value", value)
540	if key == keyRestore || key == keyRestoredCount {
541		panic("nslogic: the restore mechanism does not restore itself")
542	}
543	// NOR A ROLE, the rule genesis keeps (nsdata refusedInGenesis): the
544	// granter, the keeper, the lab nomination or anything address-valued
545	// is a power, set deliberately on the chain it belongs to, never by a
546	// backup file round the timelock (re-audit 2026-10-16, L-7). SKIPPED,
547	// not refused, like a stale reverse pointer: globals travel fifty to a
548	// transaction, and one sandbox's lab nomination must not stop the whole
549	// restore (review follow-up). The AdminAction above says it was asked.
550	if key == keyGranter || key == keyKeeper || key == "labdomain" || looksLikeAddress(value) {
551		chain.Emit("RestoreSkipped", "key", key, "why", "a role or an address; name it again through its own setter")
552		return
553	}
554	if err := nsdata.SetGlobalRecord(cross(cur), key, value); err != nil {
555		panic("nslogic: " + err.Error())
556	}
557}
558
559// RestoreAccountField puts back one field of the per-address store —
560// credits and their redemptions, referral counts. Without it a rebuilt
561// registry would forget who had already spent their free name, and the
562// migration grant could be claimed twice.
563func RestoreAccountField(cur realm, owner address, key, value string) {
564	assertRestoring(cur)
565	emitAdmin(cur, "RestoreAccountField", "owner", owner.String(), "key", key, "value", value)
566	// A credit's grant time is never in the future: one written through
567	// this door would escape every void recovery could ever declare.
568	if key == keyCredit && value != "" {
569		if c := parseCredit(value); c.granted > 0 || c.used > 0 {
570			if now := time.Now().Unix(); c.at > now {
571				c.at = now
572			}
573			value = c.encode()
574		}
575	}
576	if err := nsdata.SetAccountRecord(cross(cur), owner, key, value); err != nil {
577		panic("nslogic: " + err.Error())
578	}
579}
580
581// RestorePrimary puts back reverse resolution — which name an address
582// answers to. Registration already sets this for an owner who had none,
583// so this exists for the owners who had chosen a different one.
584//
585// SKIPPED, NOT REFUSED, when the vault would refuse it — the name is not
586// there, or the ledger says somebody else holds it (a sale leaves the
587// seller's pointer behind): pointers travel fifty to a transaction, and one
588// stale pointer must not revert everybody else's (re-audit 2026-10-16, M-3).
589func RestorePrimary(cur realm, owner address, label, domainLabel string) {
590	assertRestoring(cur)
591	emitAdmin(cur, "RestorePrimary", "owner", owner.String(), "label", label, "domain", domainLabel)
592	if !nsdata.NameExists(label, domainLabel) || nsdata.GetNameOwner(label, domainLabel) != owner {
593		return
594	}
595	if err := nsdata.SetPrimaryRecord(cross(cur), owner, label, domainLabel); err != nil {
596		panic("nslogic: " + err.Error())
597	}
598}