Skip to content

Commit 86b052e

Browse files
alltheseasclaude
andcommitted
Implement NIP-62 Request to Vanish support
Two-phase approach: writer thread sets a VanishPubkey marker instantly (blocking re-storage), cron thread sweeps and deletes events in batches every 30s to avoid long write locks. Deletes all authored events up to the vanish timestamp plus kind 1059 gift wraps addressed via p-tag. Kind 62 events are preserved for bookkeeping and protected from kind 5 deletion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f31a1b9 commit 86b052e

File tree

7 files changed

+198
-4
lines changed

7 files changed

+198
-4
lines changed

golpe.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ tablesRaw:
110110
EventPayload:
111111
flags: 'MDB_INTEGERKEY'
112112

113+
## NIP-62: Tracks pubkeys that have requested vanishing
114+
## keys are 32-byte binary pubkeys
115+
## vals are uint64_t vanish timestamp (max created_at from vanish requests)
116+
VanishPubkey:
117+
flags: ''
118+
113119
config:
114120
- name: db
115121
desc: "Directory that contains the strfry LMDB database"

src/apps/relay/RelayCron.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,105 @@ void RelayServer::runCron() {
6262

6363

6464

65+
// NIP-62: Delete events for vanished pubkeys
66+
67+
cron.repeat(30 * 1'000'000UL, [&]{
68+
if (!cfg().relay__nip62__enabled) return;
69+
70+
struct VanishEntry {
71+
std::string pubkey;
72+
uint64_t vanishTs;
73+
};
74+
75+
std::vector<VanishEntry> vanishEntries;
76+
std::vector<uint64_t> vanishLevIds;
77+
const uint64_t batchLimit = 10000;
78+
79+
{
80+
auto txn = env.txn_ro();
81+
82+
// Collect all vanish entries
83+
{
84+
auto cursor = lmdb::cursor::open(txn, env.dbi_VanishPubkey);
85+
std::string_view k, v;
86+
if (cursor.get(k, v, MDB_FIRST)) {
87+
do {
88+
if (k.size() == 32 && v.size() == sizeof(uint64_t)) {
89+
vanishEntries.push_back({std::string(k), lmdb::from_sv<uint64_t>(v)});
90+
}
91+
} while (cursor.get(k, v, MDB_NEXT));
92+
}
93+
}
94+
95+
for (auto &entry : vanishEntries) {
96+
// Scan pubkey index for events authored by this pubkey
97+
auto searchPrefix = std::string(entry.pubkey);
98+
auto startKey = makeKey_StringUint64(searchPrefix, 0);
99+
100+
env.generic_foreachFull(txn, env.dbi_Event__pubkey, startKey, lmdb::to_sv<uint64_t>(0), [&](auto k, auto v) {
101+
if (!k.starts_with(searchPrefix)) return false;
102+
103+
auto levId = lmdb::from_sv<uint64_t>(v);
104+
auto ev = env.lookup_Event(txn, levId);
105+
if (!ev) return true;
106+
107+
PackedEventView packed(ev->buf);
108+
109+
// Skip kind 62 events (preserve bookkeeping)
110+
if (packed.kind() == 62) return true;
111+
112+
if (packed.created_at() <= entry.vanishTs) {
113+
vanishLevIds.push_back(levId);
114+
if (vanishLevIds.size() >= batchLimit) {
115+
return false;
116+
}
117+
}
118+
119+
return true;
120+
});
121+
122+
// Scan tag index for gift wraps (kind 1059) addressed to this pubkey
123+
if (vanishLevIds.size() < batchLimit) {
124+
auto tagPrefix = std::string("p") + entry.pubkey;
125+
auto tagStartKey = makeKey_StringUint64(tagPrefix, 0);
126+
127+
env.generic_foreachFull(txn, env.dbi_Event__tag, tagStartKey, lmdb::to_sv<uint64_t>(0), [&](auto k, auto v) {
128+
if (k.size() != tagPrefix.size() + 8 || !k.starts_with(tagPrefix)) return false;
129+
130+
auto levId = lmdb::from_sv<uint64_t>(v);
131+
auto ev = env.lookup_Event(txn, levId);
132+
if (!ev) return true;
133+
134+
PackedEventView packed(ev->buf);
135+
136+
// Delete all gift wraps (kind 1059) addressed to this pubkey (spec says ALL, no timestamp qualifier)
137+
if (packed.kind() == 1059) {
138+
vanishLevIds.push_back(levId);
139+
if (vanishLevIds.size() >= batchLimit) {
140+
return false;
141+
}
142+
}
143+
144+
return true;
145+
});
146+
}
147+
148+
}
149+
}
150+
151+
if (vanishLevIds.size() > 0) {
152+
auto txn = env.txn_rw();
153+
NegentropyFilterCache neFilterCache;
154+
155+
uint64_t numDeleted = deleteEvents(txn, neFilterCache, vanishLevIds);
156+
157+
txn.commit();
158+
159+
if (numDeleted) LI << "NIP-62 vanish: deleted " << numDeleted << " events";
160+
}
161+
});
162+
163+
65164
cron.run();
66165

67166
while (1) std::this_thread::sleep_for(std::chrono::seconds(1'000'000));

src/apps/relay/RelayIngester.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,37 @@ void RelayServer::ingesterProcessEvent(lmdb::txn &txn, RelayServerCtx &rsctx, ui
185185
}
186186
}
187187

188+
// NIP-62: Validate kind 62 (Request to Vanish) relay tags
189+
if (packed.kind() == 62) {
190+
auto idHex = to_hex(packed.id());
191+
192+
if (!cfg().relay__nip62__enabled) {
193+
sendOKResponse(connId, idHex, false, "blocked: NIP-62 not enabled on this relay");
194+
return;
195+
}
196+
197+
bool foundMatchingRelay = false;
198+
std::string serviceUrl = cfg().relay__auth__serviceUrl;
199+
200+
for (const auto &tagj : origJson.at("tags").get_array()) {
201+
const auto &tag = tagj.get_array();
202+
if (tag.size() < 2) continue;
203+
auto tagName = tag[0].as<std::string_view>();
204+
if (tagName != "relay") continue;
205+
auto tagVal = tag[1].as<std::string_view>();
206+
207+
if (tagVal == "ALL_RELAYS" || (!serviceUrl.empty() && tagVal == serviceUrl)) {
208+
foundMatchingRelay = true;
209+
break;
210+
}
211+
}
212+
213+
if (!foundMatchingRelay) {
214+
sendOKResponse(connId, idHex, false, "blocked: vanish request not targeting this relay");
215+
return;
216+
}
217+
}
218+
188219
{
189220
auto existing = lookupEventById(txn, packed.id());
190221
if (existing) {

src/apps/relay/RelayWebsocket.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ void RelayServer::runWebsocket(ThreadPool<MsgWebsocket>::Thread &thr) {
5858
if (cfg().relay__auth__enabled && cfg().relay__auth__serviceUrl.size() > 0) output.push_back(42);
5959
if (cfg().relay__maxFilterLimitCount > 0) output.push_back(45);
6060
if (cfg().relay__negentropy__enabled) output.push_back(77);
61+
if (cfg().relay__nip62__enabled) output.push_back(62);
6162

6263
std::sort(output.get_array().begin(), output.get_array().end());
6364

src/apps/relay/golpe.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ config:
136136
desc: "Maximum records that sync will process before returning an error"
137137
default: 1000000
138138

139+
- name: relay__nip62__enabled
140+
desc: "Enable NIP-62 Request to Vanish support"
141+
default: true
142+
139143
- name: relay__filterValidation__enabled
140144
desc: "Enable strict filter validation for REQ messages"
141145
default: false

src/events.cpp

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,42 @@ void writeEvents(lmdb::txn &txn, NegentropyFilterCache &neFilterCache, std::vect
271271
continue;
272272
}
273273

274-
if (env.lookup_Event__deletion(txn, std::string(packed.id()) + std::string(packed.pubkey()))) {
274+
if (packed.kind() != 62 && env.lookup_Event__deletion(txn, std::string(packed.id()) + std::string(packed.pubkey()))) {
275275
ev.status = EventWriteStatus::Deleted;
276276
continue;
277277
}
278278

279+
// NIP-62: Reject events from vanished pubkeys (except kind 62 itself)
280+
{
281+
std::string_view vanishVal;
282+
if (packed.kind() != 62 && env.dbi_VanishPubkey.get(txn, packed.pubkey(), vanishVal)) {
283+
uint64_t vanishTs = lmdb::from_sv<uint64_t>(vanishVal);
284+
if (packed.created_at() <= vanishTs) {
285+
ev.status = EventWriteStatus::Deleted;
286+
continue;
287+
}
288+
}
289+
}
290+
291+
// NIP-62: Reject gift wraps addressed to vanished pubkeys
292+
if (packed.kind() == 1059) {
293+
bool vanished = false;
294+
packed.foreachTag([&](char tagName, std::string_view tagVal){
295+
if (tagName == 'p') {
296+
std::string_view vanishVal;
297+
if (env.dbi_VanishPubkey.get(txn, tagVal, vanishVal)) {
298+
vanished = true;
299+
return false;
300+
}
301+
}
302+
return true;
303+
});
304+
if (vanished) {
305+
ev.status = EventWriteStatus::Deleted;
306+
continue;
307+
}
308+
}
309+
279310
if (isReplaceableKind(packed.kind()) || isParamReplaceableKind(packed.kind())) {
280311
std::optional<std::string> replace;
281312

@@ -332,9 +363,12 @@ void writeEvents(lmdb::txn &txn, NegentropyFilterCache &neFilterCache, std::vect
332363
packed.foreachTag([&](char tagName, std::string_view tagVal){
333364
if (tagName == 'e') {
334365
auto otherEv = lookupEventById(txn, tagVal);
335-
if (otherEv && PackedEventView(otherEv->buf).pubkey() == packed.pubkey()) {
336-
if (logDeletions) LI << "Deleting event (kind 5, e-tag). id=" << to_hex(tagVal);
337-
levIdsToDelete.push_back(otherEv->primaryKeyId);
366+
if (otherEv) {
367+
PackedEventView otherPacked(otherEv->buf);
368+
if (otherPacked.pubkey() == packed.pubkey() && otherPacked.kind() != 62) {
369+
if (logDeletions) LI << "Deleting event (kind 5, e-tag). id=" << to_hex(tagVal);
370+
levIdsToDelete.push_back(otherEv->primaryKeyId);
371+
}
338372
}
339373
} else if (tagName == 'a') {
340374
try { // parsing a-tag can fail
@@ -365,6 +399,20 @@ void writeEvents(lmdb::txn &txn, NegentropyFilterCache &neFilterCache, std::vect
365399
});
366400
}
367401

402+
// NIP-62: Set vanish marker for kind 62 events
403+
if (packed.kind() == 62 && cfg().relay__nip62__enabled) {
404+
std::string_view existingVal;
405+
uint64_t existingTs = 0;
406+
if (env.dbi_VanishPubkey.get(txn, packed.pubkey(), existingVal)) {
407+
existingTs = lmdb::from_sv<uint64_t>(existingVal);
408+
}
409+
if (packed.created_at() > existingTs) {
410+
uint64_t newTs = packed.created_at();
411+
env.dbi_VanishPubkey.put(txn, packed.pubkey(), lmdb::to_sv<uint64_t>(newTs));
412+
LI << "NIP-62 vanish marker set for pubkey=" << to_hex(packed.pubkey()) << " ts=" << newTs;
413+
}
414+
}
415+
368416
if (ev.status == EventWriteStatus::Pending) {
369417
ev.levId = env.insert_Event(txn, ev.packedStr);
370418

strfry.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ relay {
171171
maxSyncEvents = 1000000
172172
}
173173

174+
nip62 {
175+
# Enable NIP-62 Request to Vanish support
176+
enabled = true
177+
}
178+
174179
filterValidation {
175180
# Enable strict filter validation for REQ messages
176181
enabled = false

0 commit comments

Comments
 (0)