|
| 1 | +# Recovery Package |
| 2 | + |
| 3 | +This package implements phase-1 local recovery for Loop static addresses and |
| 4 | +L402 authentication state. |
| 5 | + |
| 6 | +## Goal |
| 7 | + |
| 8 | +The recovery flow is meant to let a fresh or repaired Loop instance continue |
| 9 | +using the same static address and the same L402-backed server identity after a |
| 10 | +local crash, disk loss, or Loop data-directory replacement. |
| 11 | + |
| 12 | +The package also keeps older generations of recoverable state around. That |
| 13 | +matters when a user first creates one paid-L402/static-address pair, later |
| 14 | +loses the local Loop state, and then creates a different paid-L402/static- |
| 15 | +address pair. In that case the older deposits are still recoverable as long as |
| 16 | +the older timestamped backup file is kept. |
| 17 | + |
| 18 | +The recovery data is intentionally local: |
| 19 | + |
| 20 | +- It is written into the active Loop network data directory. |
| 21 | +- It contains only the minimum local state needed to rebuild client-side state. |
| 22 | +- It is encrypted with a key derived from the same lnd seed material that backs |
| 23 | + static-address keys. |
| 24 | + |
| 25 | +## What Is Backed Up |
| 26 | + |
| 27 | +Each encrypted backup stores: |
| 28 | + |
| 29 | +- A backup format version. |
| 30 | +- The active Loop network. |
| 31 | +- Static-address protocol version. |
| 32 | +- Static-address server pubkey. |
| 33 | +- Static-address expiry. |
| 34 | +- The exact client key locator when available. |
| 35 | +- The static-address `pkScript`. |
| 36 | +- The derived taproot address string as an additional recovery hint. |
| 37 | +- The address initiation height. |
| 38 | +- The paid local `l402.token` file. |
| 39 | + |
| 40 | +The paid token file is preserved as a raw blob instead of decomposed token |
| 41 | +fields so the restore path remains compatible with Aperture's current file-store |
| 42 | +format without requiring a public token constructor. |
| 43 | + |
| 44 | +A given backup can contain: |
| 45 | + |
| 46 | +- Only a paid L402 token. |
| 47 | +- A paid L402 token together with a static address. |
| 48 | + |
| 49 | +Deposit state itself is not serialized into the backup file. Deposits are |
| 50 | +rediscovered on restore through the normal reconciliation path. |
| 51 | + |
| 52 | +## File Location |
| 53 | + |
| 54 | +This package keeps two backup forms in the active Loop network data directory: |
| 55 | + |
| 56 | +- A canonical latest snapshot: |
| 57 | + `<loop-data-dir>/L402_backup.enc` |
| 58 | +- Immutable timestamped snapshots: |
| 59 | + `<loop-data-dir>/L402_backup_<timestamp>.enc` |
| 60 | + or `<loop-data-dir>/L402_backup_<timestamp>_<n>.enc` |
| 61 | + |
| 62 | +In the normal `loopd` layout this resolves inside the active network-specific |
| 63 | +directory, for example: |
| 64 | + |
| 65 | +`~/.loop/mainnet/L402_backup.enc` |
| 66 | + |
| 67 | +and |
| 68 | + |
| 69 | +`~/.loop/mainnet/L402_backup_20260414T093001.000000123Z.enc` |
| 70 | + |
| 71 | +The canonical `L402_backup.enc` file is always updated to the latest snapshot |
| 72 | +for compatibility and convenience. The timestamped files are the archival |
| 73 | +history that lets the user recover older generations. |
| 74 | + |
| 75 | +## Encryption Model |
| 76 | + |
| 77 | +The file is encrypted with `secretbox` using a symmetric key derived from lnd |
| 78 | +via `Signer.DeriveSharedKey`. |
| 79 | + |
| 80 | +The derivation uses: |
| 81 | + |
| 82 | +- A fixed NUMS public key. |
| 83 | +- The static-address key family. |
| 84 | +- Key index `0`. |
| 85 | + |
| 86 | +This gives the backup a deterministic local encryption key tied to the lnd seed |
| 87 | +without requiring an interactive password in phase 1. |
| 88 | + |
| 89 | +## Backup Creation Flow |
| 90 | + |
| 91 | +On startup, `loopd` asks this package to backfill the latest snapshot if any |
| 92 | +recoverable state exists. |
| 93 | + |
| 94 | +The backup write is a no-op when: |
| 95 | + |
| 96 | +- No static address exists locally. |
| 97 | +- No paid `l402.token` file exists locally. |
| 98 | + |
| 99 | +If either state source exists, the package: |
| 100 | + |
| 101 | +1. Collects the local static-address parameters. |
| 102 | +2. Reads the paid `l402.token` file from the Loop data dir. |
| 103 | +3. Serializes the payload as JSON. |
| 104 | +4. Encrypts it. |
| 105 | +5. If the recoverable state changed, atomically writes a new timestamped backup. |
| 106 | +6. Updates `L402_backup.enc` to the same latest snapshot. |
| 107 | + |
| 108 | +If the recoverable state did not change, no new timestamped archive is created. |
| 109 | +This avoids producing duplicate backups on every startup. |
| 110 | + |
| 111 | +The daemon also triggers backup refreshes when: |
| 112 | + |
| 113 | +- A usable paid L402 token has just been fetched and stored locally. |
| 114 | +- A new static address has just been created, stored, and imported into lnd. |
| 115 | + |
| 116 | +Pending or otherwise intermediate L402 token state is intentionally not backed |
| 117 | +up. |
| 118 | + |
| 119 | +## Restore Flow |
| 120 | + |
| 121 | +`loop recover --backup_file <path>` eventually calls into this package from |
| 122 | +inside `loopd`. |
| 123 | + |
| 124 | +If the backup path is omitted, the package uses the timestamped backup with the |
| 125 | +latest filename timestamp, falling back to `L402_backup.enc` for legacy or |
| 126 | +backfill-only states. |
| 127 | + |
| 128 | +Restore performs the following steps: |
| 129 | + |
| 130 | +1. Read and decrypt the backup file. |
| 131 | +2. Validate backup version and network. |
| 132 | +3. If static-address metadata is present, reconstruct the static-address client |
| 133 | + pubkey: |
| 134 | + - First try the exact backed-up key locator. |
| 135 | + - If the locator is missing or unusable, scan backward with a gap of 20 in |
| 136 | + the static-address key family. |
| 137 | +4. If static-address metadata is present, re-create the local static-address |
| 138 | + record. |
| 139 | +5. If static-address metadata is present, re-import the static-address |
| 140 | + tapscript into lnd. |
| 141 | +6. If a paid token is present, restore the paid `l402.token` file into the |
| 142 | + local token store directory. |
| 143 | +7. If static-address metadata is present, trigger best-effort deposit |
| 144 | + reconciliation. |
| 145 | + |
| 146 | +Restoring an older timestamped backup is expected to be done into a fresh Loop |
| 147 | +data directory because Loop still only supports a single local static address at |
| 148 | +a time. The same recommendation applies when the local directory already |
| 149 | +contains a different paid L402 token, because recovery is intended to recreate a |
| 150 | +specific prior generation of local state rather than merge multiple ones. |
| 151 | + |
| 152 | +## Deposit Reconciliation |
| 153 | + |
| 154 | +This package does not attempt to fully rebuild the complete historical deposit |
| 155 | +state from chain data alone. |
| 156 | + |
| 157 | +Instead, after static-address restore it calls the deposit manager's normal |
| 158 | +reconciliation path and lets Loop discover currently visible deposits from lnd's |
| 159 | +wallet view. |
| 160 | + |
| 161 | +This is intentionally best effort: |
| 162 | + |
| 163 | +- It is enough to recover practical local usability. |
| 164 | +- It avoids duplicating the full deposit FSM logic in the recovery layer. |
| 165 | +- It keeps the restore path aligned with normal manager behavior. |
| 166 | + |
| 167 | +## Package Boundaries |
| 168 | + |
| 169 | +This package owns: |
| 170 | + |
| 171 | +- Backup payload definition. |
| 172 | +- Encryption/decryption. |
| 173 | +- Reading/writing backup files. |
| 174 | +- Reading/writing paid L402 token files. |
| 175 | +- Static-address key re-derivation and restore orchestration. |
| 176 | +- Post-restore deposit reconciliation orchestration. |
| 177 | + |
| 178 | +This package does not own: |
| 179 | + |
| 180 | +- gRPC transport. |
| 181 | +- CLI command handling. |
| 182 | +- `loopd` startup wiring. |
| 183 | + |
| 184 | +Those remain in `loopd/` and `cmd/loop/`. |
0 commit comments