Menu
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

ProtocolWhat to BackupFrequencyRecovery Capability
BitcoinSeed phrase + derivation pathOnceFull recovery
LightningSeed + channel.backupEvery channel changeForce-close only
NostrPrivate key (nsec)Once per identityFull 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

MethodDurabilitySecurityCost
PaperLow (fire, water)MediumFree
Metal plateHighMedium$20-100
Shamir’s Secret SharingHighHighComplexity
Geographic distributionHighHighMultiple 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"
  ]
}