Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ write path, since there is no fsync to amortize.
| `max_content_length` | — | u32 | `102400` | Maximum event `content` length, in bytes. |
| `query_limit_default` | — | u32 | `500` | Events returned per REQ when the client sets no `limit`. |
| `query_limit_max` | — | u32 | `5000` | Hard cap on events returned per REQ. |
| `query_scan_multiplier` | `WISP_QUERY_SCAN_MULTIPLIER` | u32 | `20` | Caps entries scanned per query at `limit × multiplier` before stopping, so a selective filter cannot page-fault the whole event DB. `0` disables the cap. |
| `max_event_age` | — | i64 (seconds) | `94608000` | Reject events whose `created_at` is older than this (default 3 years). |
| `max_future_seconds` | — | i64 (seconds) | `900` | Reject events dated more than this far in the future (default 15 minutes). |
| `min_pow_difficulty` | `WISP_MIN_POW_DIFFICULTY` | u8 | `0` | Required NIP-13 proof-of-work leading-zero bits. `0` disables. |
Expand Down
10 changes: 10 additions & 0 deletions src/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub const Config = struct {
max_content_length: u32,
query_limit_default: u32,
query_limit_max: u32,
// Bounds the entries a single query may scan to `limit * query_scan_multiplier`
// before stopping, so a selective filter (few matches) cannot page-fault the
// entire event DB looking for `limit` results. 0 disables the cap.
query_scan_multiplier: u32,
max_event_age: i64,
max_future_seconds: i64,
storage_path: []const u8,
Expand Down Expand Up @@ -90,6 +94,7 @@ pub const Config = struct {
.max_content_length = 102400,
.query_limit_default = 500,
.query_limit_max = 5000,
.query_scan_multiplier = 20,
.max_event_age = 94608000,
.max_future_seconds = 900,
.storage_path = "./data",
Expand Down Expand Up @@ -201,6 +206,8 @@ pub const Config = struct {
self.query_limit_default = try std.fmt.parseInt(u32, value, 10);
} else if (std.mem.eql(u8, key, "query_limit_max")) {
self.query_limit_max = try std.fmt.parseInt(u32, value, 10);
} else if (std.mem.eql(u8, key, "query_scan_multiplier")) {
self.query_scan_multiplier = try std.fmt.parseInt(u32, value, 10);
} else if (std.mem.eql(u8, key, "max_event_age")) {
self.max_event_age = try std.fmt.parseInt(i64, value, 10);
} else if (std.mem.eql(u8, key, "max_future_seconds")) {
Expand Down Expand Up @@ -316,6 +323,9 @@ pub const Config = struct {
if (getenv("WISP_QUERIES_PER_MINUTE")) |v| {
self.queries_per_minute = std.fmt.parseInt(u32, v, 10) catch self.queries_per_minute;
}
if (getenv("WISP_QUERY_SCAN_MULTIPLIER")) |v| {
self.query_scan_multiplier = std.fmt.parseInt(u32, v, 10) catch self.query_scan_multiplier;
}
if (getenv("WISP_IDLE_SECONDS")) |v| {
self.idle_seconds = std.fmt.parseInt(u32, v, 10) catch self.idle_seconds;
}
Expand Down
17 changes: 17 additions & 0 deletions src/handler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,9 @@ pub const Handler = struct {

var total_count: u64 = 0;
for (filters) |filter| {
// Uses the capped query() intentionally: for a selective filter this
// count is approximate (bounded by the scan cap) rather than exact,
// which is the accepted DoS tradeoff for network-reachable COUNT.
var iter = self.store.query(&[_]nostr.Filter{filter}, self.config.query_limit_max) catch {
self.sendClosed(conn, sub_id, "error: query failed");
return;
Expand Down Expand Up @@ -708,6 +711,10 @@ pub const Handler = struct {
};

if (self.shutdown.load(.acquire)) return;
// Serving-side enumeration uses the capped query() by design: this path is
// network-reachable, so reconciliation stays DoS-safe. If the scan cap
// truncates enumeration it is detected below and surfaced as NEG-ERR
// rather than silently under-enumerating.
var iter = self.store.query(&[_]nostr.Filter{filter}, self.config.negentropy_max_sync_events) catch {
Comment thread
wksantiago marked this conversation as resolved.
conn.removeNegSession(sub_id);
self.sendNegErr(conn, sub_id, "error: query failed");
Expand All @@ -728,6 +735,16 @@ pub const Handler = struct {
}
}

// The scan cap may stop enumeration before all matching stored events are
// seen. Sealing a partial set would make reconciliation report events as
// missing that the relay actually holds, so surface truncation as an error
// rather than silently sealing an incomplete set.
if (iter.truncated) {
conn.removeNegSession(sub_id);
self.sendNegErr(conn, sub_id, "error: result set too large to reconcile");
return;
}

session.storage.seal();
session.sealed = true;

Expand Down
4 changes: 3 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ pub fn main(init: std.process.Init) !void {
defer lmdb.deinit();

var store = try Store.init(allocator, &lmdb);
store.query_scan_multiplier = config.query_scan_multiplier;
defer store.deinit();

var mgmt_store = try ManagementStore.init(allocator, &lmdb);
Expand Down Expand Up @@ -405,7 +406,7 @@ fn runExport(allocator: std.mem.Allocator, db_path: []const u8) !void {
const stderr_file = stdFile(std.posix.STDERR_FILENO);

const empty_filters = [_]nostr.Filter{};
var iter = try store.query(&empty_filters, std.math.maxInt(u32));
var iter = try store.queryFull(&empty_filters, std.math.maxInt(u32));
defer iter.deinit();

var exported: u64 = 0;
Expand Down Expand Up @@ -433,4 +434,5 @@ test {
_ = @import("server.zig");
_ = @import("relay_metrics.zig");
_ = @import("subscriptions.zig");
_ = @import("store.zig");
}
4 changes: 2 additions & 2 deletions src/spider.zig
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ pub const Spider = struct {
.limit_val = 1,
}};

var iter = self.store.query(&filters, 1) catch return false;
var iter = self.store.queryFull(&filters, 1) catch return false;
defer iter.deinit();

const json = (iter.next() catch return false) orelse return false;
Expand Down Expand Up @@ -557,7 +557,7 @@ pub const Spider = struct {
.{ .authors_bytes = pubkeys },
};

var iter = self.store.query(&filters, 100000) catch {
var iter = self.store.queryFull(&filters, 100000) catch {
log.err("{s}: Failed to query local events for negentropy", .{relay_url});
return true;
};
Expand Down
Loading
Loading