Implement NIP-66 relay discovery publishing

This commit is contained in:
2026-03-18 14:50:25 +01:00
parent dc5f0c1e5d
commit f2856d000e
12 changed files with 1174 additions and 6 deletions

View File

@@ -0,0 +1,114 @@
defmodule Parrhesia.Nip66Test do
use Parrhesia.IntegrationCase, async: false, sandbox: true
alias Parrhesia.API.Events
alias Parrhesia.API.RequestContext
alias Parrhesia.NIP66
alias Parrhesia.Protocol.EventValidator
alias Parrhesia.Web.Listener
alias Parrhesia.Web.RelayInfo
setup do
previous_nip66 = Application.get_env(:parrhesia, :nip66)
previous_relay_url = Application.get_env(:parrhesia, :relay_url)
on_exit(fn ->
Application.put_env(:parrhesia, :nip66, previous_nip66)
Application.put_env(:parrhesia, :relay_url, previous_relay_url)
end)
:ok
end
test "publish_snapshot stores monitor and discovery events for the configured relay" do
identity_path = unique_identity_path()
relay_url = "ws://127.0.0.1:4413/relay"
Application.put_env(:parrhesia, :relay_url, relay_url)
Application.put_env(:parrhesia, :nip66,
enabled: true,
publish_interval_seconds: 600,
publish_monitor_announcement?: true,
timeout_ms: 2_500,
checks: [:open, :read, :nip11],
geohash: "u33dc1",
targets: [
%{
listener: :public,
relay_url: relay_url,
topics: ["marmot"],
relay_type: "PublicInbox"
}
]
)
probe_fun = fn _target, _probe_opts, _publish_opts ->
{:ok,
%{
checks: [:open, :read, :nip11],
metrics: %{rtt_open_ms: 12, rtt_read_ms: 34},
relay_info: nil,
relay_info_body: nil
}}
end
assert {:ok, [monitor_event, discovery_event]} =
NIP66.publish_snapshot(
path: identity_path,
now: 1_700_000_000,
context: %RequestContext{},
probe_fun: probe_fun
)
assert monitor_event["kind"] == 10_166
assert discovery_event["kind"] == 30_166
assert :ok = EventValidator.validate(monitor_event)
assert :ok = EventValidator.validate(discovery_event)
assert {:ok, stored_events} =
Events.query(
[%{"ids" => [monitor_event["id"], discovery_event["id"]]}],
context: %RequestContext{}
)
assert Enum.sort(Enum.map(stored_events, & &1["kind"])) == [10_166, 30_166]
assert Enum.any?(discovery_event["tags"], &(&1 == ["d", relay_url]))
assert Enum.any?(discovery_event["tags"], &(&1 == ["n", "clearnet"]))
assert Enum.any?(discovery_event["tags"], &(&1 == ["T", "PublicInbox"]))
assert Enum.any?(discovery_event["tags"], &(&1 == ["t", "marmot"]))
assert Enum.any?(discovery_event["tags"], &(&1 == ["rtt-open", "12"]))
assert Enum.any?(discovery_event["tags"], &(&1 == ["rtt-read", "34"]))
assert Enum.any?(discovery_event["tags"], &(&1 == ["N", "66"]))
assert Enum.any?(discovery_event["tags"], &(&1 == ["R", "!payment"]))
relay_info = JSON.decode!(discovery_event["content"])
assert relay_info["self"] == discovery_event["pubkey"]
assert 66 in relay_info["supported_nips"]
end
test "relay info only advertises NIP-66 when the publisher is enabled" do
listener = Listener.from_opts(listener: %{id: :public, bind: %{port: 4413}})
Application.put_env(:parrhesia, :nip66, enabled: false)
refute 66 in RelayInfo.document(listener)["supported_nips"]
Application.put_env(:parrhesia, :nip66, enabled: true)
assert 66 in RelayInfo.document(listener)["supported_nips"]
end
defp unique_identity_path do
path =
Path.join(
System.tmp_dir!(),
"parrhesia_nip66_identity_#{System.unique_integer([:positive, :monotonic])}.json"
)
on_exit(fn ->
_ = File.rm(path)
end)
path
end
end

View File

@@ -0,0 +1,70 @@
defmodule Parrhesia.Protocol.EventValidatorNip66Test do
use ExUnit.Case, async: true
alias Parrhesia.Protocol.EventValidator
test "accepts valid kind 30166 relay discovery events" do
event = valid_discovery_event()
assert :ok = EventValidator.validate(event)
end
test "rejects kind 30166 discovery events without d tags" do
event = valid_discovery_event(%{"tags" => [["N", "11"]]})
assert {:error, :missing_nip66_d_tag} = EventValidator.validate(event)
end
test "accepts valid kind 10166 monitor announcements" do
event = valid_monitor_announcement()
assert :ok = EventValidator.validate(event)
end
test "rejects kind 10166 monitor announcements without frequency tags" do
event = valid_monitor_announcement(%{"tags" => [["c", "open"]]})
assert {:error, :missing_nip66_frequency_tag} = EventValidator.validate(event)
end
defp valid_discovery_event(overrides \\ %{}) do
base_event = %{
"pubkey" => String.duplicate("1", 64),
"created_at" => System.system_time(:second),
"kind" => 30_166,
"tags" => [
["d", "wss://relay.example.com/relay"],
["n", "clearnet"],
["N", "11"],
["R", "!payment"],
["R", "auth"],
["t", "marmot"],
["rtt-open", "12"]
],
"content" => "{}",
"sig" => String.duplicate("2", 128)
}
event = Map.merge(base_event, overrides)
Map.put(event, "id", EventValidator.compute_id(event))
end
defp valid_monitor_announcement(overrides \\ %{}) do
base_event = %{
"pubkey" => String.duplicate("3", 64),
"created_at" => System.system_time(:second),
"kind" => 10_166,
"tags" => [
["frequency", "900"],
["timeout", "open", "5000"],
["c", "open"],
["c", "nip11"]
],
"content" => "",
"sig" => String.duplicate("4", 128)
}
event = Map.merge(base_event, overrides)
Map.put(event, "id", EventValidator.compute_id(event))
end
end

View File

@@ -0,0 +1,48 @@
defmodule Parrhesia.Tasks.Nip66PublisherTest do
use Parrhesia.IntegrationCase, async: false
alias Parrhesia.API.Events
alias Parrhesia.API.Identity
alias Parrhesia.API.RequestContext
alias Parrhesia.Tasks.Nip66Publisher
test "publishes a NIP-66 snapshot when ticked" do
path =
Path.join(
System.tmp_dir!(),
"parrhesia_nip66_worker_#{System.unique_integer([:positive, :monotonic])}.json"
)
on_exit(fn ->
_ = File.rm(path)
end)
probe_fun = fn _target, _probe_opts, _publish_opts ->
{:ok, %{checks: [:open], metrics: %{rtt_open_ms: 8}, relay_info: nil, relay_info_body: nil}}
end
worker =
start_supervised!(
{Nip66Publisher,
name: nil,
interval_ms: 60_000,
path: path,
now: 1_700_000_123,
probe_fun: probe_fun,
config: [enabled: true, publish_interval_seconds: 600, targets: [%{listener: :public}]]}
)
send(worker, :tick)
_ = :sys.get_state(worker)
{:ok, %{pubkey: pubkey}} = Identity.get(path: path)
assert {:ok, events} =
Events.query(
[%{"authors" => [pubkey], "kinds" => [10_166, 30_166]}],
context: %RequestContext{}
)
assert Enum.sort(Enum.map(events, & &1["kind"])) == [10_166, 30_166]
end
end