Technical Report · November 2024

Nostr Protocol for Decentralized Heartbeats

Using Nostr's censorship-resistant event system for proof-of-life signaling in dead man's switch applications. How cryptographic signatures and relay redundancy eliminate trusted intermediaries.

1. Why Nostr

A dead man's switch requires a signaling mechanism where:

Nostr (Notes and Other Stuff Transmitted by Relays) satisfies all requirements. It's a simple protocol where users sign messages with their private key and publish to multiple independent relays. Anyone can run a relay; anyone can read from any relay.

2. Nostr Fundamentals

2.1 Identity

Nostr identity is a secp256k1 keypair:

There is no username registration, no account creation, no email verification. Your keypair is your identity. If you control the private key, you control the identity.

2.2 Events

Everything in Nostr is an "event" — a JSON object with a specific structure:

{
  "id": "<sha256 hash of serialized event>",
  "pubkey": "<hex public key>",
  "created_at": 1704067200,
  "kind": 1,
  "tags": [],
  "content": "Hello, world",
  "sig": "<BIP-340 Schnorr signature>"
}

The kind field determines event type. Kind 1 is a text note. Higher kinds are reserved for specific applications.

2.3 Relays

Relays are dumb servers that store and forward events. They don't verify identity, don't require accounts, and can be run by anyone. Users publish to multiple relays for redundancy.

3. EchoLock Event Types

EchoLock defines six custom event kinds in the 30000+ range (replaceable events):

Kind Name Publisher Purpose
30078 Heartbeat User Proof-of-life signal
30079 Share Storage User Encrypted share for guardian
30080 Share Release Guardian Released share for recipients
30081 Message Storage User Encrypted message payload
30082 Guardian Registration Guardian Announces availability
30083 Guardian Acknowledgment Guardian Confirms share receipt

4. Heartbeat Protocol

4.1 Event Structure

{
  "kind": 30078,
  "pubkey": "<user_npub_hex>",
  "created_at": 1704067200,
  "tags": [
    ["d", "<switch_id>"],
    ["expiry", "1704672000"],
    ["threshold_hours", "72"],
    ["guardian", "<guardian1_npub>"],
    ["guardian", "<guardian2_npub>"],
    ["guardian", "<guardian3_npub>"],
    ["guardian", "<guardian4_npub>"],
    ["guardian", "<guardian5_npub>"]
  ],
  "content": "",
  "sig": "<schnorr_signature>"
}

4.2 Key Fields

4.3 Publishing Flow

async function publishHeartbeat(
  switchId: string,
  thresholdHours: number,
  guardians: string[]
): Promise<void> {
  const expiry = Math.floor(Date.now() / 1000) + (thresholdHours * 3600);
  
  const event = {
    kind: 30078,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ['d', switchId],
      ['expiry', expiry.toString()],
      ['threshold_hours', thresholdHours.toString()],
      ...guardians.map(g => ['guardian', g])
    ],
    content: ''
  };
  
  const signedEvent = await signEvent(event, userNsec);
  
  // Publish to multiple relays for redundancy
  await Promise.allSettled(
    RELAY_URLS.map(url => publishToRelay(url, signedEvent))
  );
}

5. Guardian Monitoring

5.1 Subscription

Guardians subscribe to heartbeat events from users they protect:

// Guardian subscribes to user's heartbeats
const subscription = {
  kinds: [30078],
  authors: [userPubkey],
  '#d': [switchId]
};

relay.subscribe(subscription, (event) => {
  updateLastHeartbeat(event.pubkey, event.tags);
});

5.2 Release Trigger

async function monitorHeartbeats(
  userPubkey: string,
  switchId: string
): Promise<void> {
  while (true) {
    const latestHeartbeat = await queryRelays({
      kinds: [30078],
      authors: [userPubkey],
      '#d': [switchId],
      limit: 1
    });
    
    if (!latestHeartbeat) {
      await sleep(CHECK_INTERVAL);
      continue;
    }
    
    const expiryTag = latestHeartbeat.tags.find(t => t[0] === 'expiry');
    const expiry = parseInt(expiryTag[1]);
    
    if (Date.now() / 1000 > expiry) {
      // Heartbeat expired — release share
      await releaseShare(switchId);
      break;
    }
    
    await sleep(CHECK_INTERVAL);
  }
}

6. Censorship Resistance

6.1 Relay Redundancy

EchoLock publishes to 7+ relays by default. Users can add custom relays. For a heartbeat to be censored, all relays would need to collude or be compromised simultaneously.

6.2 Signature Verification

Every event is signed with BIP-340 Schnorr signatures. Relays cannot:

Guardians verify signatures locally — they don't trust relays to authenticate events.

6.3 No Account Dependency

There is no "Nostr account" that can be banned or suspended. The user's identity is their keypair. As long as they control their nsec, they can publish to any relay that accepts connections.

7. Encrypted Communication

7.1 NIP-44

Private messages between users (and between users and guardians) use NIP-44 encryption — XChaCha20-Poly1305 with a shared secret derived from ECDH.

// Encrypt share to guardian's pubkey
const sharedSecret = secp256k1.getSharedSecret(
  userPrivkey,
  guardianPubkey
);
const key = hkdf(sharedSecret, 'nip44-v2');
const ciphertext = xchacha20poly1305.encrypt(key, nonce, plaintext);

7.2 Share Release Events

When a guardian releases their share, they encrypt it to each designated recipient's public key:

{
  "kind": 30080,
  "pubkey": "<guardian_npub_hex>",
  "created_at": 1704672000,
  "tags": [
    ["d", "<switch_id>:<guardian_index>"],
    ["e", "<original_switch_event_id>"],
    ["p", "<recipient1_npub>"],
    ["p", "<recipient2_npub>"]
  ],
  "content": "<nip44_encrypted_share>",
  "sig": "<schnorr_signature>"
}

8. Recovery Process

Recipients recover the encrypted message by:

  1. Querying relays for kind:30080 events tagged with their pubkey
  2. Decrypting each share using their nsec
  3. Collecting at least 3 shares (threshold)
  4. Reconstructing AES key via Lagrange interpolation
  5. Fetching kind:30081 (encrypted message) from relays
  6. Decrypting message with reconstructed AES key

This entire process requires only Nostr relay queries and local cryptographic operations. No server, no API, no service dependency.

9. Relay Recommendations

EchoLock defaults to relays with:

Self-Hosting
Users can run their own relay and configure guardians to connect to it. This provides maximum control but requires operational commitment.

10. Conclusion

Nostr provides the ideal substrate for dead man's switch heartbeats: cryptographically signed events, censorship-resistant distribution, and zero dependency on any single service provider. The protocol's simplicity — JSON events, secp256k1 signatures, dumb relays — makes it robust against the failure modes that plague centralized alternatives.

Combined with Shamir secret sharing for key distribution and Bitcoin for optional timestamp proofs, Nostr enables dead man's switches that operate autonomously on public infrastructure, controlled entirely by cryptographic keys.