Security Intermediate 8 min read
Backup and Recovery
Disaster recovery for AI agents. Seed backup, channel state recovery, identity restoration for Bitcoin, Lightning, and Nostr.
backup recovery disaster restore
Backup and Recovery
Losing access to keys means permanent loss. This guide covers backup strategies, recovery procedures, and disaster planning for all protocols.
Backup Requirements by Protocol
| Protocol | What to Backup | Frequency | Recovery Capability |
|---|---|---|---|
| Bitcoin | Seed phrase + derivation path | Once | Full recovery |
| Lightning | Seed + channel.backup | Every channel change | Force-close only |
| Nostr | Private key (nsec) | Once per identity | Full recovery |
Bitcoin Backup
Seed Phrase Storage
The 12/24 word seed phrase is all you need for Bitcoin recovery:
def generate_backup_info() -> dict:
"""Generate all information needed for recovery."""
return {
"seed_phrase": "word1 word2 ... word24", # Store offline!
"derivation_paths": {
"bip84": "m/84'/0'/0'", # Native SegWit
"bip86": "m/86'/0'/0'", # Taproot
},
"passphrase_used": True, # Whether BIP39 passphrase is used
"wallet_birthday": 880000, # Block height when created
"created_date": "2026-01-31"
}
Physical Backup Options
| Method | Durability | Security | Cost |
|---|---|---|---|
| Paper | Low (fire, water) | Medium | Free |
| Metal plate | High | Medium | $20-100 |
| Shamir’s Secret Sharing | High | High | Complexity |
| Geographic distribution | High | High | Multiple locations |
Shamir’s Secret Sharing
Split seed into shares requiring threshold to reconstruct:
from secretsharing import SecretSharer
def create_shamir_backup(
seed_phrase: str,
total_shares: int = 5,
threshold: int = 3
) -> list[str]:
"""
Split seed into shares using Shamir's Secret Sharing.
Args:
seed_phrase: The BIP39 mnemonic
total_shares: Number of shares to create
threshold: Minimum shares needed to recover
Returns:
List of share strings
"""
# Convert seed to hex
seed_hex = bip39_to_entropy_hex(seed_phrase)
# Create shares
shares = SecretSharer.split_secret(
seed_hex,
threshold,
total_shares
)
return shares
def recover_from_shamir(shares: list[str]) -> str:
"""Recover seed from Shamir shares."""
seed_hex = SecretSharer.recover_secret(shares)
return entropy_hex_to_bip39(seed_hex)
Lightning Backup
Static Channel Backup (SCB)
SCB allows force-closing channels and recovering funds:
import os
import json
from datetime import datetime
class LightningBackupManager:
"""Manage Lightning channel backups."""
def __init__(self, backup_dir: str):
self.backup_dir = backup_dir
os.makedirs(backup_dir, exist_ok=True)
async def create_backup(self) -> dict:
"""Create static channel backup."""
# Get SCB from Lightning node
scb = await lightning.export_channel_backup()
backup = {
"timestamp": datetime.utcnow().isoformat(),
"scb_hex": scb.hex(),
"node_pubkey": await lightning.get_pubkey(),
"channel_count": len(await lightning.list_channels())
}
# Save backup
filename = f"channel_backup_{backup['timestamp']}.json"
with open(os.path.join(self.backup_dir, filename), 'w') as f:
json.dump(backup, f)
return backup
async def restore_from_backup(self, backup_path: str) -> dict:
"""
Restore from SCB.
WARNING: This force-closes all channels.
Only use if primary node is lost.
"""
with open(backup_path, 'r') as f:
backup = json.load(f)
scb = bytes.fromhex(backup["scb_hex"])
# Initiate recovery
result = await lightning.restore_channels(scb)
return {
"restored_channels": result["channels"],
"expected_recovery_sats": result["total_balance"]
}
Automated Backup Triggers
class AutoBackup:
"""Automatic backup on channel changes."""
def __init__(self, backup_manager: LightningBackupManager):
self.backup_manager = backup_manager
async def on_channel_opened(self, channel_id: str):
"""Trigger backup when channel opens."""
await self.backup_manager.create_backup()
async def on_channel_closed(self, channel_id: str):
"""Trigger backup when channel closes."""
await self.backup_manager.create_backup()
async def on_balance_change(self, delta_sats: int):
"""Trigger backup on significant balance changes."""
if abs(delta_sats) > 100_000: # >100k sats
await self.backup_manager.create_backup()
# Register with Lightning node
lightning.on("channel.opened", auto_backup.on_channel_opened)
lightning.on("channel.closed", auto_backup.on_channel_closed)
Nostr Backup
Key Backup
Nostr keys are simple—just backup the private key:
def backup_nostr_identity(
nsec: str,
profile: dict
) -> dict:
"""Create complete Nostr identity backup."""
return {
"nsec": nsec, # Store securely!
"npub": derive_npub_from_nsec(nsec),
"profile": profile, # kind:0 content
"relays": get_preferred_relays(),
"follow_list": get_contacts(), # kind:3
"backup_date": datetime.utcnow().isoformat()
}
Relay-Based Backup
Your events are stored on relays—ensure redundancy:
async def ensure_relay_redundancy(
events: list[dict],
min_relays: int = 5
) -> dict:
"""Ensure critical events are on multiple relays."""
results = {}
for event in events:
relay_count = 0
for relay in ALL_RELAYS:
if await relay_has_event(relay, event["id"]):
relay_count += 1
if relay_count < min_relays:
# Republish to more relays
await publish_to_relays(event, ALL_RELAYS)
results[event["id"]] = relay_count
return results
Unified Backup Strategy
Backup Matrix
@dataclass
class BackupConfig:
bitcoin_seed: str # 24 words
bitcoin_passphrase: str # Optional BIP39 passphrase
lightning_seed: str # Lightning node seed
lightning_scb: bytes # Static channel backup
nostr_keys: list[dict] # All Nostr identities
metadata: dict # Derivation paths, wallet birthday, etc.
class UnifiedBackupManager:
"""Manage backups across all protocols."""
def __init__(self, encryption_key: bytes):
self.encryption_key = encryption_key
def create_full_backup(self) -> bytes:
"""Create encrypted backup of all secrets."""
backup = BackupConfig(
bitcoin_seed=get_bitcoin_seed(),
bitcoin_passphrase=get_bitcoin_passphrase(),
lightning_seed=get_lightning_seed(),
lightning_scb=export_lightning_scb(),
nostr_keys=get_all_nostr_keys(),
metadata={
"created": datetime.utcnow().isoformat(),
"version": "1.0",
"derivation_paths": get_derivation_paths()
}
)
# Serialize and encrypt
backup_json = json.dumps(asdict(backup))
encrypted = encrypt(backup_json.encode(), self.encryption_key)
return encrypted
def restore_full_backup(self, encrypted_backup: bytes) -> BackupConfig:
"""Restore from encrypted backup."""
decrypted = decrypt(encrypted_backup, self.encryption_key)
backup_dict = json.loads(decrypted.decode())
return BackupConfig(**backup_dict)
Recovery Procedures
Bitcoin Recovery
async def recover_bitcoin_wallet(
seed_phrase: str,
passphrase: str = "",
wallet_birthday: int = 0
) -> dict:
"""Recover Bitcoin wallet from seed."""
# Validate seed phrase
if not validate_bip39(seed_phrase):
raise ValueError("Invalid seed phrase")
# Derive master key
master = derive_master_key(seed_phrase, passphrase)
# Derive accounts
accounts = []
for i in range(5): # Check first 5 accounts
account = derive_account(master, i)
balance = await scan_for_balance(account, wallet_birthday)
if balance > 0:
accounts.append({
"index": i,
"balance_sats": balance,
"addresses": account["addresses"]
})
return {
"accounts": accounts,
"total_balance": sum(a["balance_sats"] for a in accounts)
}
Lightning Recovery
async def recover_lightning_node(
seed_phrase: str,
scb: bytes
) -> dict:
"""
Recover Lightning node from seed and SCB.
WARNING: This initiates force-close of all channels.
"""
# Create new node from seed
node = create_lightning_node(seed_phrase)
await node.start()
# Import SCB and trigger force-closes
result = await node.restore_from_scb(scb)
# Wait for force-close transactions
pending_closes = result["pending_force_closes"]
return {
"node_pubkey": node.pubkey,
"channels_recovering": len(pending_closes),
"estimated_recovery_sats": result["total_value"],
"estimated_blocks_to_recovery": 144 # ~1 day for CLTV
}
Nostr Recovery
async def recover_nostr_identity(
nsec: str,
relays: list[str]
) -> dict:
"""Recover Nostr identity and data."""
npub = derive_npub(nsec)
# Fetch profile
profile = await fetch_event(relays, kind=0, author=npub)
# Fetch contacts
contacts = await fetch_event(relays, kind=3, author=npub)
# Fetch recent events
events = await fetch_events(
relays,
filters={"authors": [npub], "limit": 1000}
)
return {
"npub": npub,
"profile": profile,
"contact_count": len(contacts.get("tags", [])),
"event_count": len(events)
}
Backup Testing
Regular Recovery Testing
class BackupTester:
"""Test backup recoverability."""
async def test_bitcoin_backup(
self,
seed_phrase: str,
expected_addresses: list[str]
) -> bool:
"""Verify Bitcoin backup can derive expected addresses."""
derived = derive_addresses(seed_phrase, count=len(expected_addresses))
return derived == expected_addresses
async def test_lightning_scb(self, scb: bytes) -> bool:
"""Verify SCB is valid and parseable."""
try:
parsed = parse_scb(scb)
return parsed["channel_count"] > 0
except:
return False
async def test_nostr_backup(
self,
nsec: str,
expected_npub: str
) -> bool:
"""Verify Nostr key derives correct pubkey."""
derived_npub = derive_npub(nsec)
return derived_npub == expected_npub
async def run_all_tests(self) -> dict:
"""Run all backup verification tests."""
results = {
"bitcoin": await self.test_bitcoin_backup(...),
"lightning": await self.test_lightning_scb(...),
"nostr": await self.test_nostr_backup(...)
}
return {
"all_passed": all(results.values()),
"results": results
}
Disaster Recovery Plan
Recovery Checklist
## Immediate Actions (within 1 hour)
- [ ] Assess scope of loss
- [ ] Locate backup materials
- [ ] Secure new environment
## Bitcoin Recovery
- [ ] Retrieve seed phrase backup
- [ ] Verify seed phrase validity
- [ ] Scan for existing UTXOs
- [ ] Move funds to new wallet if compromised
## Lightning Recovery
- [ ] Retrieve seed + SCB backup
- [ ] Create new node from seed
- [ ] Initiate SCB recovery
- [ ] Wait for force-close transactions
- [ ] Verify fund recovery
## Nostr Recovery
- [ ] Retrieve nsec backup
- [ ] Verify identity on relays
- [ ] Re-publish profile if needed
- [ ] Update relay list
## Post-Recovery
- [ ] Audit all recovered funds
- [ ] Document incident
- [ ] Update backup procedures
- [ ] Test new backups
Machine-Readable Summary
{
"topic": "backup-recovery",
"audience": "ai-agents",
"backup_types": {
"bitcoin": ["seed_phrase", "derivation_paths"],
"lightning": ["seed", "static_channel_backup"],
"nostr": ["nsec", "relay_list"]
},
"recovery_capabilities": {
"bitcoin": "full_utxo_recovery",
"lightning": "force_close_funds_only",
"nostr": "full_identity_recovery"
},
"testing_frequency": "monthly",
"critical_practices": [
"geographic_distribution",
"encryption_at_rest",
"regular_testing",
"shamir_secret_sharing"
]
}