# NIP-51 Kind 10000 Encrypted Block List

## Problem

Corrupt/unwanted events (e.g., malformed kind:7375 cashu token events) persist on relays even after NIP-09 kind:5 deletion requests. Relays accept the deletion event but continue serving the originals. The agent needs a way to permanently ignore specific events, pubkeys, and hashtags.

## Solution

Implement a **kind 10000 mute/block list** per NIP-51, with all entries **encrypted by default** (NIP-44 encrypt-to-self in the `content` field). Provide an in-memory cache that filters events at the `nostr_handler` layer so blocked items are dropped system-wide.

## Event Format (NIP-51 Kind 10000)

```json
{
  "kind": 10000,
  "tags": [],
  "content": "<NIP-44 encrypted to self: JSON array of tag tuples>"
}
```

When decrypted, `content` yields:
```json
[
  ["p", "<blocked-pubkey-hex>"],
  ["e", "<blocked-event-id-hex>"],
  ["t", "spam"]
]
```

- Kind 10000 is **replaceable** (10000-19999 range) — each publish replaces the previous
- Public entries go in `tags`, private entries go in encrypted `content`
- **Default: all entries are private/encrypted**

## Data Flow Analysis

Events enter the didactyl system through two paths:

### Path 1: Subscriptions (streaming)
```
nostr_relay_pool_subscribe() → on_event callback → various handlers
```
Used by: DM subscriptions, admin context, agent context, self-skills, custom subscriptions

### Path 2: Synchronous queries
```
nostr_relay_pool_query_sync() → nostr_handler_query_json() → caller parses JSON array
```
Used by: cashu_wallet_load_from_relays(), tool queries, startup checks, skill loading

### Where to Filter

There is **no single upstream chokepoint** that catches both paths. The two best integration points are:

1. **`nostr_handler_query_json()`** — wraps all sync queries. Add filtering in the event loop at line 2862 before adding events to the result array.

2. **Managed subscription dispatch** — the internal function at line 606 that activates subscriptions. Wrap each subscription's `on_event` callback with a filtering wrapper.

Both are in `nostr_handler.c` and can call the same `nostr_block_list_is_event_filtered()` function.

```mermaid
graph TD
    subgraph "nostr_core_lib - Pool Layer"
        POOL[nostr_relay_pool]
    end

    subgraph "nostr_handler.c - FILTERING LAYER"
        QJ[nostr_handler_query_json<br/>line 2842]
        SWF[nostr_handler_subscribe_with_filter<br/>line 2880]
        MS[managed subscription dispatch<br/>line 606]
        
        QJ --> |"filter here: skip blocked events<br/>before adding to result array"| QJ_OUT[Return filtered JSON]
        MS --> |"wrap on_event: check block list<br/>before dispatching to callback"| CB[Original on_event callback]
        SWF --> |"wrap on_event: check block list"| SWF_CB[Original on_event callback]
    end

    subgraph "nostr_block_list module"
        BL[nostr_block_list_is_event_filtered]
        BL --> PS[Blocked pubkeys set]
        BL --> ES[Blocked event IDs set]
        BL --> TS[Blocked hashtags set]
    end

    POOL --> QJ
    POOL --> MS
    POOL --> SWF
    QJ --> BL
    MS --> BL
    SWF --> BL
```

## Module Design

### New Files

#### `src/nostr_block_list.h`

```c
#ifndef DIDACTYL_NOSTR_BLOCK_LIST_H
#define DIDACTYL_NOSTR_BLOCK_LIST_H

#include "config.h"
#include "cjson/cJSON.h"

// Lifecycle
int  nostr_block_list_init(didactyl_config_t* cfg);
int  nostr_block_list_load_from_relays(void);
void nostr_block_list_cleanup(void);

// Query — O(1) lookups, thread-safe
int  nostr_block_list_is_pubkey_blocked(const char* pubkey_hex);
int  nostr_block_list_is_event_blocked(const char* event_id_hex);
int  nostr_block_list_is_hashtag_blocked(const char* hashtag);

// Generic filter — checks event's pubkey, id, and t-tags
// Returns 1 if event should be blocked, 0 if allowed
int  nostr_block_list_is_event_filtered(cJSON* event);

// Edit — modifies private (encrypted) entries by default
int  nostr_block_list_add(const char* tag_key, const char* tag_value, int is_public);
int  nostr_block_list_remove(const char* tag_key, const char* tag_value);

// Refresh in-memory cache from relays
int  nostr_block_list_refresh(void);

// Introspection — returns JSON with all blocked items
char* nostr_block_list_json(void);

#endif
```

#### `src/nostr_block_list.c`

Internal state:
```c
typedef struct {
    didactyl_config_t* cfg;
    int initialized;
    int loaded;
    pthread_mutex_t mutex;

    // Public entries (from tags)
    char** pub_pubkeys;    int pub_pubkey_count;
    char** pub_event_ids;  int pub_event_id_count;
    char** pub_hashtags;   int pub_hashtag_count;

    // Private entries (from decrypted content)
    char** priv_pubkeys;   int priv_pubkey_count;
    char** priv_event_ids; int priv_event_id_count;
    char** priv_hashtags;  int priv_hashtag_count;
} block_list_state_t;
```

Key implementation details:
- `load_from_relays()`: Query kind:10000 authored by agent pubkey, parse public tags, NIP-44 decrypt content for private tags
- `is_event_filtered(event)`: Extract `id`, `pubkey`, and `t` tags from event JSON, check against both public and private sets
- `add()/remove()`: Fetch existing kind:10000, decrypt content, modify private tag list, re-encrypt, republish
- All lookups are O(n) linear scan initially (simple); can upgrade to hash set if list grows large

### New Tool Files

#### `src/tools/tool_nostr_block.c`

Two tool functions:

**`execute_nostr_block_list()`** — View current block list
```json
// Input
{ "type": "all" }  // or "p", "e", "t"

// Output
{
  "success": true,
  "blocked_pubkeys": ["abc123..."],
  "blocked_events": ["def456..."],
  "blocked_hashtags": ["spam"],
  "total_entries": 3
}
```

**`execute_nostr_block_edit()`** — Add/remove block entries
```json
// Input
{
  "action": "add",
  "items": [["e", "<event-id>"], ["p", "<pubkey>"]],
  "public": false
}

// Output
{
  "success": true,
  "action": "add",
  "items_affected": 2,
  "event_id": "<new-kind-10000-event-id>"
}
```

## Integration Points

### 1. `nostr_handler_query_json()` — Sync query filtering

At line 2862 in `nostr_handler.c`, before adding each event to the result array:

```c
for (int i = 0; i < event_count; i++) {
    if (!events[i]) continue;

    // NEW: block list filter
    if (nostr_block_list_is_event_filtered(events[i])) {
        cJSON_Delete(events[i]);
        continue;
    }

    cJSON* dup = cJSON_Duplicate(events[i], 1);
    if (dup) cJSON_AddItemToArray(arr, dup);
    cJSON_Delete(events[i]);
}
```

This catches: cashu wallet loading, tool queries, skill loading, startup checks.

### 2. Subscription event dispatch — Streaming filter

For managed subscriptions (line ~606) and `nostr_handler_subscribe_with_filter()` (line 2880), wrap the `on_event` callback:

```c
// Wrapper that checks block list before dispatching
static void filtered_on_event(cJSON* event, const char* relay_url, void* user_data) {
    if (nostr_block_list_is_event_filtered(event)) return;
    // dispatch to original callback
    ...
}
```

This catches: DMs, admin context events, agent context events, self-skill events, custom subscriptions.

### 3. Startup sequence in `main.c`

```c
nostr_handler_init(cfg);
nostr_block_list_init(cfg);
nostr_block_list_load_from_relays();  // must load before cashu wallet
cashu_wallet_init(cfg);
```

### 4. After block list edits

When `execute_nostr_block_edit()` publishes an updated kind:10000, call `nostr_block_list_refresh()` to reload the in-memory cache.

## Encryption Details

### Encrypt to self (for private entries)

```c
// Encrypt: sender=agent privkey, recipient=agent pubkey
nostr_nip44_encrypt(
    cfg->keys.private_key,      // sender private key (32 bytes)
    cfg->keys.public_key,       // recipient public key (agent's own, 32 bytes)
    private_tags_json_string,   // plaintext
    output_buffer,
    output_buffer_size
);

// Decrypt: recipient=agent privkey, sender=agent pubkey
nostr_nip44_decrypt(
    cfg->keys.private_key,      // recipient private key
    cfg->keys.public_key,       // sender public key (agent's own)
    encrypted_content,          // ciphertext from event content
    output_buffer,
    output_buffer_size
);
```

## Implementation Checklist

- [ ] Create `src/nostr_block_list.h` with API declarations
- [ ] Create `src/nostr_block_list.c` with:
  - [ ] State struct and lifecycle (init/cleanup)
  - [ ] `load_from_relays()` — fetch kind:10000, parse public tags, NIP-44 decrypt private tags
  - [ ] `is_pubkey_blocked()`, `is_event_blocked()`, `is_hashtag_blocked()` — lookup functions
  - [ ] `is_event_filtered()` — generic event check combining all three
  - [ ] `add()` / `remove()` — fetch existing, decrypt, modify, re-encrypt, republish
  - [ ] `refresh()` — reload from relays
  - [ ] `nostr_block_list_json()` — introspection
- [ ] Create `src/tools/tool_nostr_block.c` with:
  - [ ] `execute_nostr_block_list()` — view tool
  - [ ] `execute_nostr_block_edit()` — edit tool
- [ ] Register tools in `tools_schema.c` and `tools_dispatch.c`
- [ ] Add declarations to `tools_internal.h`
- [ ] Integrate filtering in `nostr_handler.c`:
  - [ ] Filter in `nostr_handler_query_json()` event loop
  - [ ] Wrap subscription `on_event` callbacks with block list check
- [ ] Add `nostr_block_list_init()` and `nostr_block_list_load_from_relays()` to startup in `main.c`
- [ ] Update `Makefile` to compile new source files
- [ ] Test: add corrupt cashu token event IDs to block list, verify wallet skips them on reload
