Add signature verification and lossless event tag storage

This commit is contained in:
2026-03-14 04:20:42 +01:00
parent 18e429e05a
commit e12085af2f
8 changed files with 152 additions and 46 deletions

View File

@@ -21,6 +21,7 @@ defmodule Parrhesia.Protocol.EventValidator do
| :invalid_content
| :invalid_sig
| :invalid_id_hash
| :invalid_signature
| :invalid_marmot_keypackage_content
| :missing_marmot_encoding_tag
| :invalid_marmot_encoding_tag
@@ -54,7 +55,8 @@ defmodule Parrhesia.Protocol.EventValidator do
:ok <- validate_tags(event["tags"]),
:ok <- validate_content(event["content"]),
:ok <- validate_sig(event["sig"]),
:ok <- validate_id_hash(event) do
:ok <- validate_id_hash(event),
:ok <- validate_signature(event) do
validate_kind_specific(event)
end
end
@@ -89,6 +91,7 @@ defmodule Parrhesia.Protocol.EventValidator do
invalid_content: "invalid: content must be a string",
invalid_sig: "invalid: sig must be 64-byte lowercase hex",
invalid_id_hash: "invalid: event id does not match serialized event",
invalid_signature: "invalid: event signature is invalid",
invalid_marmot_keypackage_content: "invalid: kind 443 content must be non-empty base64",
missing_marmot_encoding_tag: "invalid: kind 443 must include [\"encoding\", \"base64\"]",
invalid_marmot_encoding_tag: "invalid: kind 443 must include [\"encoding\", \"base64\"]",
@@ -193,6 +196,29 @@ defmodule Parrhesia.Protocol.EventValidator do
end
end
defp validate_signature(event) do
if verify_event_signatures?() do
verify_signature(event)
else
:ok
end
end
defp verify_signature(%{"id" => id, "pubkey" => pubkey, "sig" => sig}) do
with {:ok, id_bin} <- Base.decode16(id, case: :lower),
{:ok, pubkey_bin} <- Base.decode16(pubkey, case: :lower),
{:ok, sig_bin} <- Base.decode16(sig, case: :lower),
true <- Secp256k1.schnorr_valid?(sig_bin, id_bin, pubkey_bin) do
:ok
else
_other -> {:error, :invalid_signature}
end
rescue
_error -> {:error, :invalid_signature}
end
defp verify_signature(_event), do: {:error, :invalid_signature}
defp valid_tag?(tag) when is_list(tag) do
tag != [] and Enum.all?(tag, &is_binary/1)
end
@@ -473,6 +499,12 @@ defmodule Parrhesia.Protocol.EventValidator do
match?({:ok, _decoded}, Base.decode16(value, case: :lower))
end
defp verify_event_signatures? do
:parrhesia
|> Application.get_env(:features, [])
|> Keyword.get(:verify_event_signatures, true)
end
defp max_event_future_skew_seconds do
:parrhesia
|> Application.get_env(:limits, [])

View File

@@ -56,6 +56,7 @@ defmodule Parrhesia.Storage.Adapters.Postgres.Events do
pubkey: event.pubkey,
created_at: event.created_at,
kind: event.kind,
tags: event.tags,
content: event.content,
sig: event.sig
}
@@ -66,13 +67,7 @@ defmodule Parrhesia.Storage.Adapters.Postgres.Events do
{:ok, nil}
persisted_event ->
tags = load_tags([{persisted_event.created_at, persisted_event.id}])
{:ok,
to_nostr_event(
persisted_event,
Map.get(tags, {persisted_event.created_at, persisted_event.id}, [])
)}
{:ok, to_nostr_event(persisted_event)}
end
end
end
@@ -93,15 +88,7 @@ defmodule Parrhesia.Storage.Adapters.Postgres.Events do
|> sort_persisted_events()
|> maybe_apply_query_limit(opts)
event_keys = Enum.map(persisted_events, fn event -> {event.created_at, event.id} end)
tags_by_event = load_tags(event_keys)
nostr_events =
Enum.map(persisted_events, fn event ->
to_nostr_event(event, Map.get(tags_by_event, {event.created_at, event.id}, []))
end)
{:ok, nostr_events}
{:ok, Enum.map(persisted_events, &to_nostr_event/1)}
end
end
@@ -609,6 +596,7 @@ defmodule Parrhesia.Storage.Adapters.Postgres.Events do
pubkey: normalized_event.pubkey,
created_at: normalized_event.created_at,
kind: normalized_event.kind,
tags: normalized_event.tags,
content: normalized_event.content,
sig: normalized_event.sig,
d_tag: normalized_event.d_tag,
@@ -628,6 +616,7 @@ defmodule Parrhesia.Storage.Adapters.Postgres.Events do
pubkey: event.pubkey,
created_at: event.created_at,
kind: event.kind,
tags: event.tags,
content: event.content,
sig: event.sig
}
@@ -820,44 +809,21 @@ defmodule Parrhesia.Storage.Adapters.Postgres.Events do
end
end
defp load_tags([]), do: %{}
defp load_tags(event_keys) when is_list(event_keys) do
created_at_values = Enum.map(event_keys, fn {created_at, _event_id} -> created_at end)
event_id_values = Enum.map(event_keys, fn {_created_at, event_id} -> event_id end)
query =
from(tag in "event_tags",
where: tag.event_created_at in ^created_at_values and tag.event_id in ^event_id_values,
order_by: [asc: tag.idx],
select: %{
event_created_at: tag.event_created_at,
event_id: tag.event_id,
name: tag.name,
value: tag.value
}
)
query
|> Repo.all()
|> Enum.group_by(
fn tag -> {tag.event_created_at, tag.event_id} end,
fn tag -> [tag.name, tag.value] end
)
end
defp to_nostr_event(persisted_event, tags) do
defp to_nostr_event(persisted_event) do
%{
"id" => Base.encode16(persisted_event.id, case: :lower),
"pubkey" => Base.encode16(persisted_event.pubkey, case: :lower),
"created_at" => persisted_event.created_at,
"kind" => persisted_event.kind,
"tags" => tags,
"tags" => normalize_persisted_tags(persisted_event.tags),
"content" => persisted_event.content,
"sig" => Base.encode16(persisted_event.sig, case: :lower)
}
end
defp normalize_persisted_tags(tags) when is_list(tags), do: tags
defp normalize_persisted_tags(_tags), do: []
defp decode_hex(value, bytes, reason) when is_binary(value) do
if byte_size(value) == bytes * 2 do
case Base.decode16(value, case: :mixed) do