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}