From 3bf1b22103289920224d95aa986f2bfffefc3cce Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Fri, 13 Mar 2026 22:04:49 +0100 Subject: [PATCH] Complete MIP-02 recipient-gated welcome conformance tests --- PROGRESS_MARMOT.md | 4 +- test/parrhesia/web/conformance_test.exs | 125 +++++++++++++++++++++++- 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/PROGRESS_MARMOT.md b/PROGRESS_MARMOT.md index 51bf5f3..1331896 100644 --- a/PROGRESS_MARMOT.md +++ b/PROGRESS_MARMOT.md @@ -28,9 +28,9 @@ Spec source: `~/marmot/README.md` + MIP-00..05. ## M3 — MIP-02 (welcome events) -- [ ] Support wrapped Welcome delivery via NIP-59 (`1059`) recipient-gated reads +- [x] Support wrapped Welcome delivery via NIP-59 (`1059`) recipient-gated reads - [x] Validate relay behavior for unsigned inner Welcome semantics (kind `444` envelope expectations) -- [ ] Ensure durability/ack semantics support Commit-then-Welcome sequencing requirements +- [x] Ensure durability/ack semantics support Commit-then-Welcome sequencing requirements - [x] Add negative tests for malformed wrapped Welcome payloads ## M4 — MIP-03 (group events) diff --git a/test/parrhesia/web/conformance_test.exs b/test/parrhesia/web/conformance_test.exs index 70630f9..d80c6a0 100644 --- a/test/parrhesia/web/conformance_test.exs +++ b/test/parrhesia/web/conformance_test.exs @@ -4,6 +4,7 @@ defmodule Parrhesia.Web.ConformanceTest do alias Ecto.Adapters.SQL.Sandbox alias Parrhesia.Protocol.EventValidator alias Parrhesia.Repo + alias Parrhesia.Storage alias Parrhesia.Web.Connection setup do @@ -42,7 +43,113 @@ defmodule Parrhesia.Web.ConformanceTest do assert Jason.decode!(frame) == ["OK", event["id"], true, "ok: event stored"] end - defp valid_event do + test "wrapped kind 1059 welcome delivery is recipient-gated" do + {:ok, state} = Connection.init(subscription_index: nil) + recipient = String.duplicate("9", 64) + + wrapped_welcome = + valid_event(%{ + "kind" => 1059, + "tags" => [["p", recipient], ["e", String.duplicate("a", 64)]], + "content" => "encrypted-welcome-payload" + }) + + assert {:push, {:text, ok_frame}, ^state} = + Connection.handle_in( + {Jason.encode!(["EVENT", wrapped_welcome]), [opcode: :text]}, + state + ) + + assert Jason.decode!(ok_frame) == ["OK", wrapped_welcome["id"], true, "ok: event stored"] + + req_payload = Jason.encode!(["REQ", "sub-welcome", %{"kinds" => [1059], "#p" => [recipient]}]) + + assert {:push, restricted_frames, ^state} = + Connection.handle_in({req_payload, [opcode: :text]}, state) + + decoded_restricted = + Enum.map(restricted_frames, fn {:text, frame} -> Jason.decode!(frame) end) + + assert [ + "CLOSED", + "sub-welcome", + "restricted: giftwrap access requires recipient authentication" + ] = + Enum.find(decoded_restricted, fn frame -> List.first(frame) == "CLOSED" end) + + auth_event = valid_auth_event(state.auth_challenge, recipient) + + assert {:push, {:text, auth_frame}, authed_state} = + Connection.handle_in({Jason.encode!(["AUTH", auth_event]), [opcode: :text]}, state) + + assert Jason.decode!(auth_frame) == ["OK", auth_event["id"], true, "ok: auth accepted"] + + assert {:push, frames, _next_state} = + Connection.handle_in({req_payload, [opcode: :text]}, authed_state) + + decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end) + + assert ["EVENT", "sub-welcome", result_event] = + Enum.find(decoded, fn frame -> List.first(frame) == "EVENT" end) + + assert result_event["id"] == wrapped_welcome["id"] + assert List.last(decoded) == ["EOSE", "sub-welcome"] + end + + test "kind 445 commit ACK implies durable visibility before wrapped welcome ACK" do + previous_features = Application.get_env(:parrhesia, :features, []) + + Application.put_env(:parrhesia, :features, Keyword.put(previous_features, :nip_ee_mls, true)) + + on_exit(fn -> + Application.put_env(:parrhesia, :features, previous_features) + end) + + {:ok, state} = Connection.init(subscription_index: nil) + + commit_event = + valid_event(%{ + "kind" => 445, + "tags" => [["h", String.duplicate("b", 64)]], + "content" => "commit-envelope" + }) + + assert {:push, {:text, commit_ok_frame}, ^state} = + Connection.handle_in( + {Jason.encode!(["EVENT", commit_event]), [opcode: :text]}, + state + ) + + assert Jason.decode!(commit_ok_frame) == ["OK", commit_event["id"], true, "ok: event stored"] + + assert {:ok, persisted_commit} = Storage.events().get_event(%{}, commit_event["id"]) + assert persisted_commit["id"] == commit_event["id"] + + wrapped_welcome = + valid_event(%{ + "kind" => 1059, + "tags" => [["p", String.duplicate("c", 64)], ["e", String.duplicate("d", 64)]], + "content" => "encrypted-welcome-payload" + }) + + assert {:push, {:text, welcome_ok_frame}, ^state} = + Connection.handle_in( + {Jason.encode!(["EVENT", wrapped_welcome]), [opcode: :text]}, + state + ) + + assert Jason.decode!(welcome_ok_frame) == [ + "OK", + wrapped_welcome["id"], + true, + "ok: event stored" + ] + + assert {:ok, persisted_welcome} = Storage.events().get_event(%{}, wrapped_welcome["id"]) + assert persisted_welcome["id"] == wrapped_welcome["id"] + end + + defp valid_event(overrides \\ %{}) do now = System.system_time(:second) base = %{ @@ -54,6 +161,20 @@ defmodule Parrhesia.Web.ConformanceTest do "sig" => String.duplicate("2", 128) } - Map.put(base, "id", EventValidator.compute_id(base)) + event = Map.merge(base, overrides) + Map.put(event, "id", EventValidator.compute_id(event)) + end + + defp valid_auth_event(challenge, pubkey) do + event = %{ + "pubkey" => pubkey, + "created_at" => System.system_time(:second), + "kind" => 22_242, + "tags" => [["challenge", challenge]], + "content" => "", + "sig" => String.duplicate("8", 128) + } + + Map.put(event, "id", EventValidator.compute_id(event)) end end