Skip to content

fix(recovery): make the restore-anyway override work for dead relay lists - #759

Merged
spe1020 merged 6 commits into
zapcooking:mainfrom
dmnyc:fix/lazarus-override-followups
Sep 27, 2026
Merged

spe1020 merged 6 commits into
zapcooking:mainfrom
dmnyc:fix/lazarus-override-followups

Conversation

@dmnyc

@dmnyc dmnyc commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #753, bringing data recovery up to the Lazarus spec's 0.6.2-draft (https://github.com/dmnyc/lazarus/blob/main/SPEC.md).

  • Dead relays no longer count as answered. The scan asked relays through nostr-tools' subscribeMany, which reports a relay that refused the connection or sent CLOSED as an EOSE, and invents one for a relay that stays silent past 4.4 seconds. A dead write relay could confirm the current version, and the check before signing could pass with no write relay reachable. This affects the recovery that shipped in Lazarus data recovery replaces NIP-78 account backup #753. Each relay is now asked directly, and only a real EOSE is an answer.
  • Restore anyway can now fix a dead relay list. A relay list restore is judged on the write relays the restored version names, as the spec allows. Before, the override published to the same dead write relays that had just failed the check, so it couldn't fix the case it exists for. The current relays still get the restore as a best effort.
  • After a relay list restore, the app uses the restored relays. zap's relay-list cache takes the restored list, so later scans and edits in the same session don't go back to the dead relays. A restored list that names no write relays falls back to zap's own relays instead of the dead current ones.
  • After any restore, the local event cache takes the recovered version instead of only being invalidated, since an invalidated copy can be refetched from a relay still serving the clobbered one.
  • The check before signing treats a version from the same second with a lower id (the one relays keep) as a change.
  • Items are counted once each, a tag without a value isn't one, a profile's items are its fields, and relay URLs compare normalized. Relay lists now show added and removed counts in the review too.
  • Private items. The review says which version's private items couldn't be decrypted. When it's the current version's, the restore replaces items nobody counted, so it takes the same confirmation step as a restore that removes items.
  • Encryption key lists (kind 10044) count the n tags NIP-4e puts keys in; the registry counted p tags, so every key list scanned as empty. The review's description of the two versions had the directions reversed; it now states what both mean, in the direction of the restore.
  • The override's confirmation resets after every attempt, so a box ticked for a failed attempt can't carry into the next one.
  • The retry button says "Retry the restore", since it signs and publishes once the check passes.
  • A stale comment no longer says the spec is silent on unreachable relays.

…ists

A relay list restore is now judged on the write relays the restored version names, as the spec allows, so the override can fix a list whose current relays are dead; the current relays still get it as a best effort. The override's confirmation re-arms after every attempt instead of staying ticked, the retry button says it retries the restore, and a stale comment no longer claims the spec is silent on unreachable relays.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Relay-list cache updates and fallback handling remain unresolved, with a test also needing correction.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Updates Lazarus recovery so relay-list restores can bypass dead write relays and resets restore confirmation between attempts.

Changes:

  • Select restored relay-list write relays for publishing.
  • Reset override confirmation after each attempt.
  • Update retry wording and package version.
File Summary
src/​lib/​lazarus/​source.ts Selects publish relays for restored events.
src/​lib/​lazarus/​source.test.ts Tests restored relay selection.
src/​lib/​lazarus/​lazarusPublish.ts Passes the chosen event into relay selection.
src/​lib/​lazarus/​lazarusPublish.test.ts Verifies relay-selection integration.
src/​components/​LazarusDeltaPanel.svelte Resets confirmation and updates retry text.
package.json Bumps the application version.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

}

const { write, extra } = await getLazarusPublishRelays(pubkey, respondingRelays);
const { write, extra } = await getLazarusPublishRelays(pubkey, respondingRelays, chosen);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6275e4e. relayListCache gained seedFromEvent(pubkey, event) — it writes the recovered kind-10002 into the memory cache and IndexedDB exactly like a network answer (same freshness/persistence, in-flight fetch for the pubkey dropped) — and refreshLocalCopy now branches on kind 10002 to seed it with the published event. Seeding rather than invalidating on purpose: an invalidate would refetch over relays that may still serve the clobbered copy, while the seed answers every subsequent get() this session with the restored list. Covered by a new test asserting the seed happens with the pubkey and the recovered event (and does not fire for other kinds).

Comment thread src/lib/lazarus/source.ts Outdated
Comment on lines +202 to +203
const restored = restoring?.kind === 10002 ? parseRelayList(restoring).write : [];
const write = restored.length > 0 ? restored : current;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6275e4e. A restored kind-10002 that names no write relays now falls back to getCurrentRelays() instead of current — consistent with getUserRelays's own semantics, where a list naming no write relays is 'missing' and the app's relays stand in as the write set. Falling back to current judged the restore on the dead write relays the override exists to replace. The current relays still receive the recovery as a best-effort extra. New test pins the read-only-restored-list case (write = app defaults, dead relay kept as extra).

dmnyc and others added 5 commits September 26, 2026 15:20
…estore, app relays for read-only restores

Copilot review on zapcooking#759:

- refreshLocalCopy had no kind-10002 branch, so a successful relay-list
  restore left relayListCache serving the dead cached list — the next
  scan or edit in the session kept targeting the dead relays and could
  clobber the restore. relayListCache gains seedFromEvent(pubkey, event),
  written exactly like a network answer, and the publish path seeds it
  with the recovered event (the spec's update-the-local-copy duty).
- getLazarusPublishRelays fell back to the CURRENT write relays when the
  restored kind-10002 named no write relays — the dead relays the
  override exists to replace. A restored list with no write relays is
  'missing' the same way getUserRelays treats one, so the app's relays
  stand in as the write set; the current relays still get the event as a
  best effort.
The kind 10044 registry entry counted p tags, but NIP-4e lists encryption pubkeys in n tags, so every key list scanned as empty.
The scan asked relays through nostr-tools' subscribeMany, which reports a relay that refused the connection or sent CLOSED as an EOSE before the close, and invents an EOSE for a silent relay after 4.4 seconds. Dead relays counted as answered with nothing, so a refused write relay could confirm current and let the re-read before a restore pass. Each relay is now asked directly: only EOSE is an answer, a refused connection or a CLOSED is a failure, and silence times out.

Items are counted once each, a tag without a value isn't one, a profile's items are its fields, and relay URLs compare normalized. Versions from the same second order as NIP-01 keeps them, including in the re-read. The review says which side's private items couldn't be decrypted, and uncounted ones on current take the removal confirmation. Relay lists get added and removed counts too, and the encryption key list states what both versions mean in the restore's direction. After a restore, the local event cache takes the recovered version instead of only being invalidated.
@spe1020
spe1020 merged commit 6925237 into zapcooking:main Sep 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants