Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions golpe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ tablesRaw:
EventPayload:
flags: 'MDB_INTEGERKEY'

## NIP-62: Tracks pubkeys that have requested vanishing
## keys are 32-byte binary pubkeys
## vals are uint64_t vanish timestamp (max created_at from vanish requests)
VanishPubkey:
flags: ''

config:
- name: db
desc: "Directory that contains the strfry LMDB database"
Expand Down
99 changes: 99 additions & 0 deletions src/apps/relay/RelayCron.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,105 @@ void RelayServer::runCron() {



// NIP-62: Delete events for vanished pubkeys

cron.repeat(30 * 1'000'000UL, [&]{
if (!cfg().relay__nip62__enabled) return;

struct VanishEntry {
std::string pubkey;
uint64_t vanishTs;
};

std::vector<VanishEntry> vanishEntries;
std::vector<uint64_t> vanishLevIds;
const uint64_t batchLimit = 10000;

{
auto txn = env.txn_ro();

// Collect all vanish entries
{
auto cursor = lmdb::cursor::open(txn, env.dbi_VanishPubkey);
std::string_view k, v;
if (cursor.get(k, v, MDB_FIRST)) {
do {
if (k.size() == 32 && v.size() == sizeof(uint64_t)) {
vanishEntries.push_back({std::string(k), lmdb::from_sv<uint64_t>(v)});
}
} while (cursor.get(k, v, MDB_NEXT));
}
}

for (auto &entry : vanishEntries) {
// Scan pubkey index for events authored by this pubkey
auto searchPrefix = std::string(entry.pubkey);
auto startKey = makeKey_StringUint64(searchPrefix, 0);

env.generic_foreachFull(txn, env.dbi_Event__pubkey, startKey, lmdb::to_sv<uint64_t>(0), [&](auto k, auto v) {
if (!k.starts_with(searchPrefix)) return false;

auto levId = lmdb::from_sv<uint64_t>(v);
auto ev = env.lookup_Event(txn, levId);
if (!ev) return true;

PackedEventView packed(ev->buf);

// Skip kind 62 events (preserve bookkeeping)
if (packed.kind() == 62) return true;

if (packed.created_at() <= entry.vanishTs) {
vanishLevIds.push_back(levId);
if (vanishLevIds.size() >= batchLimit) {
return false;
}
}

return true;
});

// Scan tag index for gift wraps (kind 1059) addressed to this pubkey
if (vanishLevIds.size() < batchLimit) {
auto tagPrefix = std::string("p") + entry.pubkey;
auto tagStartKey = makeKey_StringUint64(tagPrefix, 0);

env.generic_foreachFull(txn, env.dbi_Event__tag, tagStartKey, lmdb::to_sv<uint64_t>(0), [&](auto k, auto v) {
if (k.size() != tagPrefix.size() + 8 || !k.starts_with(tagPrefix)) return false;

auto levId = lmdb::from_sv<uint64_t>(v);
auto ev = env.lookup_Event(txn, levId);
if (!ev) return true;

PackedEventView packed(ev->buf);

// Delete all gift wraps (kind 1059) addressed to this pubkey (spec says ALL, no timestamp qualifier)
if (packed.kind() == 1059) {
vanishLevIds.push_back(levId);
if (vanishLevIds.size() >= batchLimit) {
return false;
}
}

return true;
});
}

}
}

if (vanishLevIds.size() > 0) {
auto txn = env.txn_rw();
NegentropyFilterCache neFilterCache;

uint64_t numDeleted = deleteEvents(txn, neFilterCache, vanishLevIds);

txn.commit();

if (numDeleted) LI << "NIP-62 vanish: deleted " << numDeleted << " events";
}
});


cron.run();

while (1) std::this_thread::sleep_for(std::chrono::seconds(1'000'000));
Expand Down
31 changes: 31 additions & 0 deletions src/apps/relay/RelayIngester.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,37 @@ void RelayServer::ingesterProcessEvent(lmdb::txn &txn, RelayServerCtx &rsctx, ui
}
}

// NIP-62: Validate kind 62 (Request to Vanish) relay tags
if (packed.kind() == 62) {
auto idHex = to_hex(packed.id());

if (!cfg().relay__nip62__enabled) {
sendOKResponse(connId, idHex, false, "blocked: NIP-62 not enabled on this relay");
return;
}

bool foundMatchingRelay = false;
std::string serviceUrl = cfg().relay__auth__serviceUrl;

for (const auto &tagj : origJson.at("tags").get_array()) {
const auto &tag = tagj.get_array();
if (tag.size() < 2) continue;
auto tagName = tag[0].as<std::string_view>();
if (tagName != "relay") continue;
auto tagVal = tag[1].as<std::string_view>();

if (tagVal == "ALL_RELAYS" || (!serviceUrl.empty() && tagVal == serviceUrl)) {
foundMatchingRelay = true;
break;
}
}

if (!foundMatchingRelay) {
sendOKResponse(connId, idHex, false, "blocked: vanish request not targeting this relay");
return;
}
}

{
auto existing = lookupEventById(txn, packed.id());
if (existing) {
Expand Down
1 change: 1 addition & 0 deletions src/apps/relay/RelayWebsocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ void RelayServer::runWebsocket(ThreadPool<MsgWebsocket>::Thread &thr) {
if (cfg().relay__auth__enabled && cfg().relay__auth__serviceUrl.size() > 0) output.push_back(42);
if (cfg().relay__maxFilterLimitCount > 0) output.push_back(45);
if (cfg().relay__negentropy__enabled) output.push_back(77);
if (cfg().relay__nip62__enabled) output.push_back(62);

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

Expand Down
4 changes: 4 additions & 0 deletions src/apps/relay/golpe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ config:
desc: "Maximum records that sync will process before returning an error"
default: 1000000

- name: relay__nip62__enabled
desc: "Enable NIP-62 Request to Vanish support"
default: true

- name: relay__filterValidation__enabled
desc: "Enable strict filter validation for REQ messages"
default: false
Expand Down
56 changes: 52 additions & 4 deletions src/events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,42 @@ void writeEvents(lmdb::txn &txn, NegentropyFilterCache &neFilterCache, std::vect
continue;
}

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

// NIP-62: Reject events from vanished pubkeys (except kind 62 itself)
{
std::string_view vanishVal;
if (packed.kind() != 62 && env.dbi_VanishPubkey.get(txn, packed.pubkey(), vanishVal)) {
uint64_t vanishTs = lmdb::from_sv<uint64_t>(vanishVal);
if (packed.created_at() <= vanishTs) {
ev.status = EventWriteStatus::Deleted;
continue;
}
}
}

// NIP-62: Reject gift wraps addressed to vanished pubkeys
if (packed.kind() == 1059) {
bool vanished = false;
packed.foreachTag([&](char tagName, std::string_view tagVal){
if (tagName == 'p') {
std::string_view vanishVal;
if (env.dbi_VanishPubkey.get(txn, tagVal, vanishVal)) {
vanished = true;
return false;
}
}
return true;
});
if (vanished) {
ev.status = EventWriteStatus::Deleted;
continue;
}
}

if (isReplaceableKind(packed.kind()) || isParamReplaceableKind(packed.kind())) {
std::optional<std::string> replace;

Expand Down Expand Up @@ -332,9 +363,12 @@ void writeEvents(lmdb::txn &txn, NegentropyFilterCache &neFilterCache, std::vect
packed.foreachTag([&](char tagName, std::string_view tagVal){
if (tagName == 'e') {
auto otherEv = lookupEventById(txn, tagVal);
if (otherEv && PackedEventView(otherEv->buf).pubkey() == packed.pubkey()) {
if (logDeletions) LI << "Deleting event (kind 5, e-tag). id=" << to_hex(tagVal);
levIdsToDelete.push_back(otherEv->primaryKeyId);
if (otherEv) {
PackedEventView otherPacked(otherEv->buf);
if (otherPacked.pubkey() == packed.pubkey() && otherPacked.kind() != 62) {
if (logDeletions) LI << "Deleting event (kind 5, e-tag). id=" << to_hex(tagVal);
levIdsToDelete.push_back(otherEv->primaryKeyId);
}
}
} else if (tagName == 'a') {
try { // parsing a-tag can fail
Expand Down Expand Up @@ -365,6 +399,20 @@ void writeEvents(lmdb::txn &txn, NegentropyFilterCache &neFilterCache, std::vect
});
}

// NIP-62: Set vanish marker for kind 62 events
if (packed.kind() == 62 && cfg().relay__nip62__enabled) {
std::string_view existingVal;
uint64_t existingTs = 0;
if (env.dbi_VanishPubkey.get(txn, packed.pubkey(), existingVal)) {
existingTs = lmdb::from_sv<uint64_t>(existingVal);
}
if (packed.created_at() > existingTs) {
uint64_t newTs = packed.created_at();
env.dbi_VanishPubkey.put(txn, packed.pubkey(), lmdb::to_sv<uint64_t>(newTs));
LI << "NIP-62 vanish marker set for pubkey=" << to_hex(packed.pubkey()) << " ts=" << newTs;
}
}

if (ev.status == EventWriteStatus::Pending) {
ev.levId = env.insert_Event(txn, ev.packedStr);

Expand Down
5 changes: 5 additions & 0 deletions strfry.conf
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ relay {
maxSyncEvents = 1000000
}

nip62 {
# Enable NIP-62 Request to Vanish support
enabled = true
}

filterValidation {
# Enable strict filter validation for REQ messages
enabled = false
Expand Down
13 changes: 13 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,16 @@ These commands test the query engine, with and without `limit`:
These commands test the monitor engine:

perl test/filterFuzzTest.pl monitor

## NIP-62 (Request to Vanish) E2E tests

These tests start a live strfry relay and exercise the full NIP-62 lifecycle
over websocket: vanish requests, cron-based event deletion, re-broadcast
prevention, gift wrap cleanup, relay tag validation, and more.

Requires Python 3.8+ with `secp256k1` and `websockets`:

pip install secp256k1 websockets
python3 test/nip62_e2e_test.py

The suite takes ~3 minutes (cron sweep polling at 30s intervals for 5 tests).
17 changes: 17 additions & 0 deletions test/cfgs/nip62Test.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
db = "./strfry-db-nip62-test/"

events {
rejectEventsOlderThanSeconds = 9999999999
}

relay {
port = 40562

auth {
serviceUrl = "ws://127.0.0.1:40562"
}

nip62 {
enabled = true
}
}
13 changes: 13 additions & 0 deletions test/cfgs/nip62TestDisabled.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
db = "./strfry-db-nip62-test/"

events {
rejectEventsOlderThanSeconds = 9999999999
}

relay {
port = 40562

nip62 {
enabled = false
}
}
Loading
Loading