Schnorr Signature Scheme and NIP/01 Implementation
This (very) huge commit is containing the whole implementation of the Schnorr signature scheme and the NIP/01 standard in full Erlang. It includes documentation, test suites with eunit and commont_test, partial specification, and articles/notes on the implementation. This commit is also probably one of the most important, it defines the structure of the nostrlib module and all the low level record used to encode and decode events. 99% coverages on nostrlib_schnorr. 85% on nostrlib.
1
.gitignore
vendored
@@ -18,3 +18,4 @@ _build
|
||||
rebar3.crashdump
|
||||
doc
|
||||
*~
|
||||
**.trace
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
erlang 25.1.2
|
||||
rebar 3.20.0
|
||||
pandoc 3.1.1
|
||||
|
||||
41
Makefile
@@ -4,45 +4,76 @@
|
||||
######################################################################
|
||||
BUILD_DIR ?= _build/notes
|
||||
NOTES_DIR ?= notes
|
||||
NOTES = $(shell ls $(NOTES_DIR) | grep -E "^[0-9]+")
|
||||
NOTES = $(shell ls $(NOTES_DIR) | grep -E "^[0-9]+-")
|
||||
PANDOC_OPTS = -C
|
||||
PANDOC = pandoc $(PANDOC_OPTS)
|
||||
|
||||
######################################################################
|
||||
# template to generate the targets
|
||||
######################################################################
|
||||
define pandoc_template =
|
||||
NOTES_TARGETS += $$(BUILD_DIR)/$(1).pdf
|
||||
$$(BUILD_DIR)/$(1).pdf:
|
||||
pandoc -f markdown -t pdf -o $$@ \
|
||||
$(PANDOC) -f markdown -t pdf -o $$@ \
|
||||
--resource-path="$$(NOTES_DIR)/$(1)" \
|
||||
"$$(NOTES_DIR)/$(1)/README.md"
|
||||
|
||||
NOTES_TARGETS += $$(BUILD_DIR)/$(1).epub
|
||||
$$(BUILD_DIR)/$(1).epub:
|
||||
pandoc -f markdown -t epub -o $$@ \
|
||||
$(PANDOC) -f markdown -t epub -o $$@ \
|
||||
--resource-path="$$(NOTES_DIR)/$(1)" \
|
||||
"$$(NOTES_DIR)/$(1)/README.md"
|
||||
|
||||
NOTES_TARGETS += $$(BUILD_DIR)/$(1).txt
|
||||
$$(BUILD_DIR)/$(1).txt:
|
||||
pandoc -f markdown -t plain -o $$@ \
|
||||
$(PANDOC) -f markdown -t plain -o $$@ \
|
||||
--resource-path="$$(NOTES_DIR)/$(1)" \
|
||||
"$$(NOTES_DIR)/$(1)/README.md"
|
||||
|
||||
NOTES_TARGETS += $$(BUILD_DIR)/$(1).html
|
||||
$$(BUILD_DIR)/$(1).html:
|
||||
pandoc -f markdown -t html -o $$@ \
|
||||
$(PANDOC) -f markdown -t html -o $$@ \
|
||||
--resource-path="$$(NOTES_DIR)/$(1)" \
|
||||
"$$(NOTES_DIR)/$(1)/README.md"
|
||||
endef
|
||||
|
||||
######################################################################
|
||||
# default target, used to build automatically the notes
|
||||
######################################################################
|
||||
.PHONY += all
|
||||
all: notes
|
||||
|
||||
######################################################################
|
||||
# create the build directory
|
||||
######################################################################
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $@
|
||||
|
||||
######################################################################
|
||||
# generate all templates based on notes directory name
|
||||
######################################################################
|
||||
$(foreach note,$(NOTES),$(eval $(call pandoc_template,$(note))))
|
||||
|
||||
######################################################################
|
||||
# generate all notes
|
||||
######################################################################
|
||||
.PHONY += notes
|
||||
notes: $(BUILD_DIR) $(NOTES_TARGETS)
|
||||
|
||||
######################################################################
|
||||
# remove all generated articles and notes
|
||||
######################################################################
|
||||
.PHONY += clean
|
||||
clean:
|
||||
rm $(NOTES_TARGETS)
|
||||
|
||||
######################################################################
|
||||
# usage
|
||||
######################################################################
|
||||
help:
|
||||
@echo "Usage: make [help|all|notes|clean]"
|
||||
|
||||
######################################################################
|
||||
# .PHONY target
|
||||
######################################################################
|
||||
.PHONY: $(.PHONY)
|
||||
|
||||
@@ -31,7 +31,7 @@ moment.
|
||||
Here the list of currently supported
|
||||
[nips](https://github.com/nostr-protocol/nips):
|
||||
|
||||
- [ ] [nip/01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [x] [nip/01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [ ] [nip/02: Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)
|
||||
- [ ] [nip/03: OpenTimestamps Attestations for Events](https://github.com/nostr-protocol/nips/blob/master/03.md)
|
||||
- [ ] [nip/04: Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
|
||||
@@ -64,6 +64,11 @@ Here the list of currently supported
|
||||
- [ ] [nip/56: Reporting](https://github.com/nostr-protocol/nips/blob/master/56.md)
|
||||
- [ ] [nip/57: Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md)
|
||||
- [ ] [nip/65: Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
|
||||
- [ ] [nip/78: Arbitrary custom app data](https://github.com/nostr-protocol/nips/blob/master/78.md)
|
||||
|
||||
## Other Implementation (required by nostr)
|
||||
|
||||
- [x] [BIP-0340: Schnorr Signatures for secp256k1](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
16
extra/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Extra Scripts, Tests and Benchmarks
|
||||
|
||||
## modular pow function check
|
||||
|
||||
Scripts used to check and validate the modular pow function
|
||||
implementation in Erlang.
|
||||
|
||||
- [python script (`check_pow.py`)](check_pow.py)
|
||||
- [erlang code (`pow.erl`)](pow.erl)
|
||||
|
||||
## floored modulo operator check
|
||||
|
||||
Scripts used to validated the floored modulo implementation in Erlang.
|
||||
|
||||
- [python script (`check_mod.py`)](check_mod.py)
|
||||
- [erlang code (`mod.erl`)](mod.erl)
|
||||
24
extra/check_mod.py
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
# check_mod.py
|
||||
"""
|
||||
This script is used to check custom modulo operator created
|
||||
in Erlang for the nostr project.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import string
|
||||
import secrets
|
||||
|
||||
limit = 256
|
||||
if len(sys.argv) > 1:
|
||||
limit = int(sys.argv[1])
|
||||
|
||||
generator = secrets.SystemRandom()
|
||||
start = -(2**256)
|
||||
end = 2**256
|
||||
for i in range(limit):
|
||||
a = generator.randrange(start, end)
|
||||
m = generator.randrange(0,end)
|
||||
r = a % m
|
||||
l = ",".join([str(i),str(a),str(m),str(r)])
|
||||
print(l)
|
||||
25
extra/check_pow.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# check_pow.py
|
||||
"""
|
||||
This script is used to check custom pow function created
|
||||
in Erlang for the nostr project.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import string
|
||||
import secrets
|
||||
|
||||
limit = 256
|
||||
if len(sys.argv) > 1:
|
||||
limit = int(sys.argv[1])
|
||||
|
||||
generator = secrets.SystemRandom()
|
||||
start = -(2**256)
|
||||
end = 2**256
|
||||
for i in range(limit):
|
||||
a = generator.randrange(start, end)
|
||||
b = generator.randrange(0,end)
|
||||
m = generator.randrange(0,end)
|
||||
p = pow(a, b, m)
|
||||
l = ",".join([str(i),str(a),str(b),str(m),str(p)])
|
||||
print(l)
|
||||
56
extra/mod.erl
Normal file
@@ -0,0 +1,56 @@
|
||||
%%%===================================================================
|
||||
%%% @doc This script parse the output of the `check_mod.py' module
|
||||
%%% present in `extra' directory. The `nostrlib:mod/2' function must
|
||||
%%% be exported and this script must be executed at the root of the
|
||||
%%% project.
|
||||
%%%
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-module(mod).
|
||||
-export([check/0, check/1]).
|
||||
-define(PYTHON, "python3").
|
||||
-define(CHECK_SCRIPT, "check_mod.py").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `check/0' executes the script using python and check its
|
||||
%% output.
|
||||
%%
|
||||
%% @end
|
||||
%% --------------------------------------------------------------------
|
||||
check() ->
|
||||
Path = filename:join("extra", ?CHECK_SCRIPT),
|
||||
Command = string:join([?PYTHON, Path], " "),
|
||||
Return = os:cmd(Command),
|
||||
Content = list_to_bitstring(Return),
|
||||
check_content(Content).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `check/1' read a file containing the output of check_mod.py.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
check(File) ->
|
||||
{ok, Content} = file:read_file(File),
|
||||
check_content(Content).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `check_content/1' returns true if the results are valid,
|
||||
%% return false if something was wrong
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
check_content(Content) ->
|
||||
Lines = re:split(Content, "\n"),
|
||||
Splitted = lists:map(fun(X) -> re:split(X, ",") end, Lines),
|
||||
Result = [ { binary_to_integer(I)
|
||||
, binary_to_integer(R) =:=
|
||||
nostrlib_schnorr:mod(binary_to_integer(A)
|
||||
,binary_to_integer(M))}
|
||||
|| [I,A,M,R] <- Splitted
|
||||
],
|
||||
Filter = lists:filter(fun({_,false}) -> true;
|
||||
(_) -> false
|
||||
end, Result),
|
||||
case Filter of
|
||||
_ when Filter =:= [] -> {ok, length(Result)};
|
||||
Elsewise -> {error, Elsewise}
|
||||
end.
|
||||
57
extra/pow.erl
Normal file
@@ -0,0 +1,57 @@
|
||||
%%%===================================================================
|
||||
%%% @doc This script parse the output of the `check_pow.py' module
|
||||
%%% present in `extra' directory. The `nostrlib:pow/2' function must
|
||||
%%% be exported and this script must be executed at the root of the
|
||||
%%% project.
|
||||
%%%
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-module(pow).
|
||||
-export([check/0, check/1]).
|
||||
-define(PYTHON, "python3").
|
||||
-define(CHECK_SCRIPT, "check_pow.py").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `check/0' executes the script using python and check its
|
||||
%% output.
|
||||
%%
|
||||
%% @end
|
||||
%% --------------------------------------------------------------------
|
||||
check() ->
|
||||
Path = filename:join("extra", ?CHECK_SCRIPT),
|
||||
Command = string:join([?PYTHON, Path], " "),
|
||||
Return = os:cmd(Command),
|
||||
Content = list_to_bitstring(Return),
|
||||
check_content(Content).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `check/1' read a file containing the output of check_pow.py.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
check(File) ->
|
||||
{ok, Content} = file:read_file(File),
|
||||
check_content(Content).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `check_content/1' returns true if the results are valid,
|
||||
%% return false if something was wrong
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
check_content(Content) ->
|
||||
Lines = re:split(Content, "\n"),
|
||||
Splitted = lists:map(fun(X) -> re:split(X, ",") end, Lines),
|
||||
Result = [ { binary_to_integer(I)
|
||||
, binary_to_integer(P) =:=
|
||||
nostrlib_schnorr:pow(binary_to_integer(A)
|
||||
,binary_to_integer(B)
|
||||
,binary_to_integer(M))}
|
||||
|| [I,A,B,M,P] <- Splitted
|
||||
],
|
||||
Filter = lists:filter(fun({_,false}) -> true;
|
||||
(_) -> false
|
||||
end, Result),
|
||||
case Filter of
|
||||
_ when Filter =:= [] -> {ok, length(Result)};
|
||||
Elsewise -> {error, Elsewise}
|
||||
end.
|
||||
@@ -1,9 +1,181 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%%===================================================================
|
||||
%%% @doc type and data-structure used in nostrlib_decoder.erl
|
||||
%%% module. Can be easily imported in other modules as needed.
|
||||
%%%
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-type to_be_defined() :: any().
|
||||
%% A type created to point out a type to define.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A macro used to translate a kind as integer or atom.
|
||||
%%--------------------------------------------------------------------
|
||||
-define(KIND(K_INTEGER, K_ATOM),
|
||||
kind(K_INTEGER) -> K_ATOM;
|
||||
kind(K_ATOM) -> K_INTEGER
|
||||
).
|
||||
|
||||
%%%===================================================================
|
||||
%%% @doc encoded JSON messages
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-type encoded_event() :: binary() | bitstring() | iodata().
|
||||
|
||||
%%%===================================================================
|
||||
%%% @doc decoded types and data-structures
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-type decoded_event_id() :: undefined | <<_:256>>.
|
||||
%% event_id() is an event id as defined in NIP/01.
|
||||
|
||||
-type decoded_private_key() :: <<_:256>>.
|
||||
%% A private key as a bitstring.
|
||||
|
||||
-type decoded_public_key() :: <<_:256>>.
|
||||
%% A public key as a bistring.
|
||||
|
||||
-type decoded_created_at() :: pos_integer().
|
||||
%% An Unix timestamp as integer.
|
||||
|
||||
-type decoded_kind() :: atom() | 0 | 1 | 2 | 7 | pos_integer().
|
||||
%% A kind represented as positive integer
|
||||
|
||||
-type decoded_kinds() :: [decoded_kind(), ...].
|
||||
%% A list of kinds
|
||||
|
||||
-type decoded_content() :: bitstring() | iodata().
|
||||
%% The main payload of the message as raw string.
|
||||
|
||||
-type decoded_signature() :: <<_:512>>.
|
||||
%% A signature as an hexadecimal string.
|
||||
|
||||
-type decoded_event_ids() :: [decoded_event_id(), ...].
|
||||
%% A list of event id.
|
||||
|
||||
-type decoded_prefix() :: bitstring().
|
||||
%% A prefix as an hexadecimal string.
|
||||
|
||||
-type decoded_author() :: decoded_public_key() | decoded_prefix().
|
||||
%% An author as defined in NIP/01, can be a public key or a prefix.
|
||||
|
||||
-type decoded_authors() :: [decoded_author(), ...].
|
||||
%% A list of authors.
|
||||
|
||||
-type decoded_tag_event_ids() :: decoded_event_ids().
|
||||
%% A tag event_ids, an alias for event id used in tags.
|
||||
|
||||
-type decoded_tag_event_public_keys() :: [decoded_public_key(), ...].
|
||||
%% A tag containing a list of public keys.
|
||||
|
||||
-type decoded_since() :: pos_integer().
|
||||
%% An Unix timestamp as positive integer.
|
||||
|
||||
-type decoded_until() :: pos_integer().
|
||||
%% An Unix timestamp as positive integer.
|
||||
|
||||
-type decoded_limit() :: pos_integer().
|
||||
%% A limit of event as positive integer
|
||||
|
||||
-type decoded_message() :: bitstring().
|
||||
%% A raw message, used in notice.
|
||||
|
||||
-type decoded_subscription_id() :: bitstring().
|
||||
%% A subscription id as a random string.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A tag record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(tag, { name = undefined :: public_key | event_id
|
||||
, value = undefined :: undefined | bitstring()
|
||||
, params = [] :: list()
|
||||
}).
|
||||
|
||||
-type decoded_tag() :: #tag{ params :: undefined }.
|
||||
%% A tag as represented in NIP/01. It can describe an event id or a
|
||||
%% public key.
|
||||
|
||||
-type decoded_tags() :: [decoded_tag(), ...].
|
||||
%% A list of tag, used in events.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full event record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(event, { id = undefined :: decoded_event_id()
|
||||
, public_key = undefined :: decoded_public_key()
|
||||
, created_at = undefined :: decoded_created_at()
|
||||
, kind = undefined :: decoded_kind()
|
||||
, tags = [] :: decoded_tags()
|
||||
, content = undefined :: decoded_content()
|
||||
, signature = undefined :: decoded_signature()
|
||||
}).
|
||||
-type decoded_event() :: #event{signature :: decoded_signature()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full filter record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(filter, { event_ids = [] :: decoded_event_ids()
|
||||
, authors = [] :: decoded_authors()
|
||||
, kinds = [] :: decoded_kinds()
|
||||
, tag_event_ids = [] :: decoded_tag_event_ids()
|
||||
, tag_public_keys = [] :: decoded_tag_event_public_keys()
|
||||
, since = undefined :: decoded_since()
|
||||
, until = undefined :: decoded_until()
|
||||
, limit = undefined :: decoded_limit()
|
||||
}).
|
||||
-type decoded_filter() :: #filter{limit :: decoded_limit()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full request record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(request, { subscription_id = undefined :: decoded_subscription_id()
|
||||
, filter = #filter{} :: [decoded_filter(), ...]
|
||||
}).
|
||||
-type decoded_request() :: #request{filter :: [decoded_filter(), ...]}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full close record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(close, { subscription_id = undefined :: decoded_subscription_id()
|
||||
}).
|
||||
-type decoded_close() :: #close{subscription_id :: decoded_subscription_id()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full notice record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(notice, { message = undefined :: decoded_message() }).
|
||||
-type decoded_notice() :: #notice{ message :: decoded_message() }.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full eose record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(eose, { id = undefined :: decoded_subscription_id() }).
|
||||
-type decoded_eose() :: #eose{ id :: decoded_subscription_id() }.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full subscription record.
|
||||
%%--------------------------------------------------------------------
|
||||
-type decoded_subscription_content() :: decoded_event()
|
||||
| decoded_request().
|
||||
-record(subscription, { id = undefined :: decoded_subscription_id()
|
||||
, content = undefined :: decoded_subscription_content()
|
||||
}).
|
||||
-type decoded_subscription() :: #subscription{ content :: decoded_subscription_content() }.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A type representing all decoded messages available.
|
||||
%%--------------------------------------------------------------------
|
||||
-type decoded_messages() :: decoded_event()
|
||||
| decoded_request()
|
||||
| decoded_close()
|
||||
| decoded_notice()
|
||||
| decoded_subscription()
|
||||
| decoded_eose().
|
||||
|
||||
%%%===================================================================
|
||||
%%% @doc encoded types and data-structure.
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-type message() :: list().
|
||||
%% A nostr message.
|
||||
|
||||
@@ -21,11 +193,3 @@
|
||||
|
||||
-type event() :: map().
|
||||
%% A nostr event.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A macro used to translate a kind as integer or atom.
|
||||
%%--------------------------------------------------------------------
|
||||
-define(KIND(K_INTEGER, K_ATOM),
|
||||
kind(K_INTEGER) -> K_ATOM;
|
||||
kind(K_ATOM) -> K_INTEGER
|
||||
).
|
||||
|
||||
1
include/nostrlib_decode.hrl
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
%%%===================================================================
|
||||
%%% @doc type and data-structure used in nostrlib_decoder.erl
|
||||
%%% module. Can be easily imported in other modules as needed.
|
||||
%%%
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-type event_id() :: undefined | bitstring().
|
||||
%% event_id() is an event id as defined in NIP/01.
|
||||
|
||||
-type public_key() :: bitstring().
|
||||
%% A public key as an hexdecimal string.
|
||||
|
||||
-type created_at() :: pos_integer().
|
||||
%% An Unix timestamp as integer.
|
||||
|
||||
-type kind() :: 0 | 1 | 2 | 7 | pos_integer().
|
||||
%% A kind represented as positive integer
|
||||
|
||||
-type kinds() :: [kind(), ...].
|
||||
%% A list of kinds
|
||||
|
||||
-type tag() :: [bitstring(), ...].
|
||||
%% A tag as represented in NIP/01. It can describe an event id or a
|
||||
%% public key.
|
||||
|
||||
-type tags() :: [tag(), ...].
|
||||
%% A list of tag, used in events.
|
||||
|
||||
-type content() :: bitstring().
|
||||
%% The main payload of the message as raw string.
|
||||
|
||||
-type signature() :: bitstring().
|
||||
%% A signature as an hexadecimal string.
|
||||
|
||||
-type event_ids() :: [event_id(), ...].
|
||||
%% A list of event id.
|
||||
|
||||
-type prefix() :: bitstring().
|
||||
%% A prefix as an hexadecimal string.
|
||||
|
||||
-type author() :: public_key() | prefix().
|
||||
%% An author as defined in NIP/01, can be a public key or a prefix.
|
||||
|
||||
-type authors() :: [author(), ...].
|
||||
%% A list of authors.
|
||||
|
||||
-type tag_event_ids() :: event_ids().
|
||||
%% A tag event_ids, an alias for event id used in tags.
|
||||
|
||||
-type tag_event_public_keys() :: [public_key(), ...].
|
||||
%% A tag containing a list of public keys.
|
||||
|
||||
-type since() :: pos_integer().
|
||||
%% An Unix timestamp as positive integer.
|
||||
|
||||
-type until() :: pos_integer().
|
||||
%% An Unix timestamp as positive integer.
|
||||
|
||||
-type limit() :: pos_integer().
|
||||
%% A limit of event as positive integer
|
||||
|
||||
-type message() :: bitstring().
|
||||
%% A raw message, used in notice.
|
||||
|
||||
-type subscription_id() :: bitstring().
|
||||
%% A subscription id as a random string.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full event record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(event, { id :: event_id()
|
||||
, public_key :: public_key()
|
||||
, created_at :: created_at()
|
||||
, kind :: kind()
|
||||
, tags :: tags()
|
||||
, content :: content()
|
||||
, signature :: signature()
|
||||
}).
|
||||
-type event() :: #event{signature :: signature()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full filter record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(filter, { event_ids :: event_ids()
|
||||
, authors :: authors()
|
||||
, kinds :: kinds()
|
||||
, tag_event_ids :: tag_event_ids()
|
||||
, tag_public_keys :: tag_event_public_keys()
|
||||
, since :: since()
|
||||
, until :: until()
|
||||
, limit :: limit()
|
||||
}).
|
||||
-type filter() :: #filter{limit :: limit()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full request record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(request, { subscription_id :: subscription_id()
|
||||
, filter :: filter()
|
||||
}).
|
||||
-type request() :: #request{filter :: filter()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full close record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(close, { subscription_id :: subscription_id() }).
|
||||
-type close() :: #close{subscription_id :: subscription_id()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full notice record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(notice, { message :: message() }).
|
||||
-type notice() :: #notice{}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full eose record.
|
||||
%%--------------------------------------------------------------------
|
||||
-record(eose, { id :: subscription_id() }).
|
||||
-type eose() :: #eose{}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A full subscription record.
|
||||
%%--------------------------------------------------------------------
|
||||
-type subscription_content() :: event()
|
||||
| request().
|
||||
-record(subscription, { id :: subscription_id()
|
||||
, content :: subscription_content()
|
||||
}).
|
||||
-type subscription() :: #subscription{}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% A type representing all decoded messages available.
|
||||
%%--------------------------------------------------------------------
|
||||
-type decoded_messages() :: event()
|
||||
| request()
|
||||
| close()
|
||||
| notice()
|
||||
| subscription()
|
||||
| eose().
|
||||
552
notes/0004-schnorr-signature-scheme-in-erlang/README.md
Normal file
@@ -0,0 +1,552 @@
|
||||
---
|
||||
date: 2023-03-10
|
||||
title: Schnorr Signature Scheme in Erlang
|
||||
subtitle: |
|
||||
Implementing the Schnorr signature scheme in Erlang with
|
||||
minimal dependencies
|
||||
author: Mathieu Kerjouan
|
||||
keywords: erlang,otp,nostr,schnorr,design,interfaces
|
||||
license: CC BY-NC-ND
|
||||
abstract: |
|
||||
Nostr protocol was mainly designed by Bitcoin developers, or at
|
||||
least, people attracted by the Bitcoin ecosystem. Unfortunately,
|
||||
Erlang/OTP do not have all the cryptographic primitives to build a
|
||||
fully compliant nostr application from scratch without using
|
||||
external dependencies. Indeed, Erlang/OTP is already delivered with
|
||||
`crypto` and `public_key` modules to deal with classical
|
||||
cryptographic functionalities, principally used by SSL/TLS
|
||||
protocol stack. Bitcoin is using, on its side, lot of "unconventional"
|
||||
features -- or not generally offered by default libraries.
|
||||
|
||||
This article is the first one implementing cryptographic functions
|
||||
in Erlang. `nostr` protocol relies on elliptic curve cryptography, in
|
||||
particular `secp256k1` curve. Instead of using Elliptic Curve
|
||||
Digital Signature Algorithm (ECDSA) or Edwards-curve Digital
|
||||
Signature Algorithm (EdDSA), nostr is using Schnorr signature scheme
|
||||
like the Bitcoin project and defined in BIP-0340.
|
||||
toc: true
|
||||
hyperrefoptions:
|
||||
- linktoc=all
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
This article has been redacted in February and March 2023. It
|
||||
describes the methodologies applied and describe the methods used to
|
||||
implement the first NIP from `nostr` protocol in Erlang with a minimal
|
||||
amount of dependencies. The following code has been tested using
|
||||
[Erlang/OTP R25](https://www.erlang.org/news/157) running on
|
||||
[OPENBSD-72-CURRENT](openbsd.org/) and [Parrot
|
||||
Linux](https://parrotsec.org/) (a Debian like distribution).
|
||||
|
||||
|
||||
# Schnorr Signature in Pure Erlang
|
||||
|
||||
Erlang is a multi-purpose language using its own virtual machine
|
||||
called the BEAM [^wikipedia-beam] -- a short name for Bogdan's Erlang
|
||||
Abstract Machine -- and greatly inspired by the Warren Abstract
|
||||
Machine [^wikipedia-wam] (WAM). It was created to help creating
|
||||
distributed enviroment. Usually, low level languages are used to
|
||||
implement cryptographic functions, this article will show its possible
|
||||
to have decent performances with great features when using high level
|
||||
languages.
|
||||
|
||||
The first Schnorr signature scheme designed for Bitcoin
|
||||
[^wikipedia-bitcoin] was defined in BIP-0340
|
||||
[^bip-0340-specification]. This scheme can also be found in BIP-0341
|
||||
[^bip-0341-specification] and BIP-0342 [^bip-0342-specification]
|
||||
specifications. The implementation reference was made in Python
|
||||
[^bip-0340-implementation] [^bip-0340-test-vectors].
|
||||
|
||||
Schnorr signature [^wikipedia-schnorr-signature] -- likes any other
|
||||
cryptographic scheme -- is hard to implement. Fortunately, because
|
||||
Bitcoin is used by lot of people around the world, this signature
|
||||
protocol has been explained many times and anyone can find interesting
|
||||
resources on this topic [^youtube-christof-paar-ecc]
|
||||
[^youtube-pieter-wuille-schnorr]
|
||||
[^youtube-cihangir-tezcan-schnorr-multi-signature]
|
||||
[^youtube-bill-buchanan-schnorr] [^youtube-theoretically] without
|
||||
buying a single book.
|
||||
|
||||
## Collecting Information
|
||||
|
||||
Starting with a small overview [^weboftrust-schnorr-signature], Schnorr
|
||||
signature scheme seems to have a strong security proofs, to be simple,
|
||||
to be fast and to have the property to create secrets without
|
||||
additional exchange.
|
||||
|
||||
Many implementation of this scheme can be found out there, in C
|
||||
[^c-libecc] [^c-cschnorr], C++ [^cpp-schnor], Elixir [^elixir-k256]
|
||||
[^elixir-bitcoinex] [^elixir-bitcoinex-schnorr], Go
|
||||
[^go-schnorr-signature], Python [^python-schnorr-example]
|
||||
[^python-taproot] [^python-solcrypto], Java [^java-samourai-schorr] or
|
||||
Javascript [^javascript-schnorr] [^nodejs-schnorr-signature].
|
||||
|
||||
## Implementing Schnorr Signature Scheme
|
||||
|
||||
Like previously said, the reference implementation was made in
|
||||
Python. This language is not using the same paradigm than Erlang,
|
||||
Python is a script-like language using oriented object programming,
|
||||
while Erlang is a distributed and functional programming language. The
|
||||
way the code will be created will be quite different, and will require
|
||||
small adaptation. Modulo operator and `pow` functions are, for
|
||||
example, not the same in these two universes. Functions with same
|
||||
behaviors than these two will need to be created on the Erlang side, a
|
||||
dedicated section for each of them will be available in this part of
|
||||
the article.
|
||||
|
||||
Dealing with asymetric cryptography -- even more elliptic curves --
|
||||
can be quite complex and a well designed interface should avoid even
|
||||
smallest frictions with the developers. Functions to create standard
|
||||
and secure keys should be available. In this implementation, the
|
||||
`nostrlib_schnorr:new_privatekey/0` will be a wrapper of the
|
||||
[`crypto:generate_key/2`](https://www.erlang.org/doc/man/crypto.html#generate_key-2)
|
||||
function. A secp256k1 private key a 32bytes (256bits) random number
|
||||
and could also have been generated using
|
||||
[`crypto:strong_rand_bytes/1`](https://www.erlang.org/doc/man/crypto.html#strong_rand_bytes-1)
|
||||
function as well but we are assuming the functions provided by the
|
||||
Erlang team in [`crypto`](https://www.erlang.org/doc/man/crypto.html)
|
||||
module are already doing all the validation and are configuring the
|
||||
recommended parameterss [^secp256k1-recommended-parameters].
|
||||
|
||||
```erlang
|
||||
% generate a private key using crypto:generate_key/2
|
||||
{_PublicKey, <<PrivateKey:256/bitstring>>}
|
||||
= crypto:generate_key(ecdh, secp256k1).
|
||||
|
||||
% generate a private key using crypto:strong_rand_bytes/1
|
||||
<<PrivateKey:256/bitstring>>
|
||||
= crypto:strong_rand_bytes(32).
|
||||
|
||||
% generate a private key using nostrlib_schnorr:new_private_key/0.
|
||||
{ok, <<PrivateKey:256/bitstring>>}
|
||||
= nostrlib_schnorr:new_private_key().
|
||||
```
|
||||
|
||||
A public key, derived from the private key, is also required. The one
|
||||
provided by
|
||||
[`crypto:generate_key/2`](https://www.erlang.org/doc/man/crypto.html#generate_key-2)
|
||||
is not suitable for our needs. A specific point on the curve is
|
||||
required and defined in BIP-0340, an operation on this same point is
|
||||
also required. The function to generate a public key with `nostrlib`
|
||||
is `nostrlib_schnorr:new_publickey/1`, where the first and only
|
||||
argument to pass is a valid private key (32bytes length bitstring).
|
||||
|
||||
```erlang
|
||||
{ok, <<PublicKey:256>>}
|
||||
= nostrlib_schnorr:new_public_key(PrivateKey).
|
||||
```
|
||||
|
||||
The Schnorr signature scheme can only sign 32bytes (256bits) messages,
|
||||
the BIP-0340 specification uses SHA256 as main hashing function to
|
||||
produce a 256bits fixed length hash as message. The `crypto` module
|
||||
offers the function
|
||||
[`crypto:hash/2`](https://www.erlang.org/doc/man/crypto.html#hash-2)
|
||||
to generate this payload, where the first argument will be the atom
|
||||
`sha256` and the second argument will be the content to hash. This
|
||||
value can be signed by the functions `nostrlib_schnorr:sign/2` or
|
||||
`nostrlib_schnorr:sign/3` where the first argument is the hash
|
||||
previously generated and the second argument is the private key. The
|
||||
signature returned is a 64bytes (512bits) fixed length bitstring.
|
||||
|
||||
```erlang
|
||||
% create a hash from data, in this example, a raw bitstring.
|
||||
<<Message:256/bitstring>>
|
||||
= crypto:hash(sha256, <<"my data">>).
|
||||
|
||||
% create a signature with default aux_data (set to 0).
|
||||
{ok, <<Signature:512/bitstring>>}
|
||||
= nostrlib_schnorr:sign(Message, PrivateKey).
|
||||
|
||||
% create a signature with aux_data set to 0 (manually).
|
||||
{ok, <<Signature:512bitstring>>}
|
||||
= nostrlib_schnorr:sign(Message, PrivateKey, <<0:256>>).
|
||||
```
|
||||
|
||||

|
||||
|
||||
To be sure this scheme is working, the signature must be verified with
|
||||
the public key. This feature can be done by using
|
||||
`nostrlib_schnorr:verify/3` function, where the first argument is the
|
||||
hash produced with the raw message, the second argument is the public
|
||||
key and the last one is the signature. If the signature is valid, this
|
||||
function returns `true` otherwise it will return `false`.
|
||||
|
||||
```erlang
|
||||
true = nostrlib_schnorr:verify(Message, PublicKey, Signature).
|
||||
```
|
||||
|
||||

|
||||
|
||||
Those interfaces are similar to the one offered by the reference
|
||||
implementation in Python and the ones present in other open-source
|
||||
implementations available on the web.
|
||||
|
||||
### Requirement: Floored Modulo
|
||||
|
||||
Different version of the modulo operator [^wikipedia-modulo] exists
|
||||
with Erlang,
|
||||
[`rem/2`](https://www.erlang.org/doc/reference_manual/expressions.html#arithmetic-expressions),
|
||||
[`div/2`](https://www.erlang.org/doc/reference_manual/expressions.html#arithmetic-expressions),
|
||||
[`math:fmod/2`](https://www.erlang.org/doc/man/math.html#fmod-2) or
|
||||
[`crypto:mod_pow/3`](https://www.erlang.org/doc/man/crypto.html#mod_pow-3). The
|
||||
3 firsts previously quoted operators and/or functions are using
|
||||
truncated modulo. The
|
||||
[`crypto:mod_pow/3`](https://www.erlang.org/doc/man/crypto.html#mod_pow-3)
|
||||
in other hand, could have been a good and powerful function if it was
|
||||
compatible with negative integers...
|
||||
|
||||
[`rem/2`](https://www.erlang.org/doc/reference_manual/expressions.html#arithmetic-expressions)
|
||||
or
|
||||
[`div/2`](https://www.erlang.org/doc/reference_manual/expressions.html#arithmetic-expressions)
|
||||
operators are using truncated modulo, Schnorr signature scheme uses
|
||||
floored modulo. By change, a similar problem has already be solved on
|
||||
[stack overflow](https://stackoverflow.com/a/2386387/6782635). Why
|
||||
reusing the wheel if a decent solution exists into the wild?
|
||||
|
||||
```erlang
|
||||
mod(0,_) -> 0;
|
||||
mod(X,Y) when X > 0 -> X rem Y;
|
||||
mod(X,Y) when X < 0 ->
|
||||
K = (-X div Y)+1,
|
||||
PositiveX = X+K*Y,
|
||||
PositiveX rem Y.
|
||||
```
|
||||
|
||||
This new function must be tested though. The [`%`
|
||||
operator](https://docs.python.org/3.3/reference/expressions.html#binary-arithmetic-operations)
|
||||
in Python is already doing the job, then, it can be used to generate a
|
||||
list of valid input and output. The following script -- present in
|
||||
`extra` directry -- generates 256 lines of CSV, with the inputs and
|
||||
the output.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# check_mod.py
|
||||
# Usage: python check_mod.py > mod.csv
|
||||
|
||||
import sys
|
||||
import string
|
||||
import secrets
|
||||
|
||||
limit = 256
|
||||
generator = secrets.SystemRandom()
|
||||
start = -(2**256)
|
||||
end = 2**256
|
||||
for i in range(limit):
|
||||
a = generator.randrange(start, end)
|
||||
m = generator.randrange(0,end)
|
||||
r = a % m
|
||||
l = ",".join([str(i),str(a),str(m),str(r)])
|
||||
print(l)
|
||||
```
|
||||
|
||||
The CSV file is then opened and parsed on the Erlang side and another
|
||||
function is executed to check if the inputs and outputs are the same.
|
||||
|
||||
```erlang
|
||||
% same result can be found by executing the python
|
||||
% script directly from the BEAM, instead of opening
|
||||
% a file and read its content
|
||||
% os:cmd("python check_mod.py").
|
||||
CheckPow = fun (File) ->
|
||||
{ok, Content} = file:read_file(File),
|
||||
Lines = re:split(Content, "\n"),
|
||||
Splitted = lists:map(fun(X) -> re:split(X, ",") end, Lines),
|
||||
Result = [ { binary_to_integer(I)
|
||||
, binary_to_integer(R) =:=
|
||||
nostrlib_schnorr:mod(binary_to_integer(A)
|
||||
,binary_to_integer(M))}
|
||||
|| [I,A,M,R] <- Splitted
|
||||
],
|
||||
[] =:= lists:filter(fun({_,false}) -> true; (_) -> false end, Result)
|
||||
end.
|
||||
true =:= CheckPow("mod.csv").
|
||||
```
|
||||
|
||||
This function was not the most difficult to implement but was
|
||||
critical. Modulo operator is a standard operation in cryptography and
|
||||
must be compatible with other implementations.
|
||||
|
||||
### Requirement: Modular Pow
|
||||
|
||||
|
||||
In Erlang, the `pow` function can be done using
|
||||
[`math:pow/2`](https://www.erlang.org/doc/man/math.html#pow-2) or by
|
||||
using
|
||||
[`crypto:mod_pow/3`](https://www.erlang.org/doc/man/crypto.html#mod_pow-3),
|
||||
unfortunately, both of them are not natively compatible with the
|
||||
[`pow()`](https://docs.python.org/3/library/functions.html#pow)
|
||||
function present in the Python built-in functions. It is the very same
|
||||
issue than the one with the `modulo` operator. In Erlang,
|
||||
[`math:pow/2`](https://www.erlang.org/doc/man/math.html#pow-2) is
|
||||
returning a float and is limited in size. First problem: float, no one
|
||||
wants to deal with float, even more in cryptography. Second problem:
|
||||
With elliptic curves, the size matters and its generatelly more than
|
||||
256bits long. In other hand,
|
||||
[`crypto:mod_pow/3`](https://www.erlang.org/doc/man/crypto.html#mod_pow-3)
|
||||
is not allowing negative numbers in input, and it will be a problem
|
||||
because the reference implementation is using signed numbers. Then,
|
||||
based on the requirement,
|
||||
[`nostrlib_schnorr`](https://github.com/erlang-punch/nostr) module
|
||||
requires a "modular pow" implementation
|
||||
[^wikipedia-modular-exponentiation].
|
||||
|
||||
The naive implementation of this function can easily be constructed in
|
||||
Erlang using only standard operators and the BIFs, unfortunately, this
|
||||
is quite slow, but it works.
|
||||
|
||||
|
||||
```erlang
|
||||
% first implementation
|
||||
modular_pow(1,0,_) -> 1;
|
||||
modular_pow(0,1,_) -> 0;
|
||||
modular_pow(_,_,1) -> 0;
|
||||
modular_pow(Base, Exponent, Modulus) ->
|
||||
NewBase = mod2(Base, Modulus),
|
||||
modular_pow(NewBase, Exponent, Modulus, 1).
|
||||
|
||||
modular_pow(_Base, 0, _Modulus, Return) -> Return;
|
||||
modular_pow(Base, Exponent, Modulus, Return) ->
|
||||
case mod2(Exponent, 2) =:= 1 of
|
||||
true ->
|
||||
R2 = mod2(Return * Base, Modulus),
|
||||
E2 = Exponent bsr 1,
|
||||
B2 = mod2(Base*Base, Modulus),
|
||||
modular_pow(B2, E2, Modulus, R2);
|
||||
false ->
|
||||
E2 = Exponent bsr 1,
|
||||
B2 = mod2(Base*Base, Modulus),
|
||||
modular_pow(B2, E2, Modulus, Return)
|
||||
end.
|
||||
```
|
||||
|
||||
A second implementation, still using default operators and BIFs, has
|
||||
been implemented. The speed was still pretty slow, but the returned
|
||||
values were valid.
|
||||
|
||||
```erlang
|
||||
% second implementation
|
||||
pow(1,0,_) -> 1;
|
||||
pow(0,1,_) -> 0;
|
||||
pow(_,_,1) -> 0;
|
||||
pow(Base, Exponent, Modulus) ->
|
||||
case 1 band Exponent of
|
||||
1 ->
|
||||
modular_pow(Base, Exponent, Modulus, Base);
|
||||
0 ->
|
||||
modular_pow(Base, Exponent, Modulus, 1)
|
||||
end.
|
||||
|
||||
modular_pow(_Base, 0, _Modulus, Return) -> Return;
|
||||
modular_pow(Base, Exponent, Modulus, Return) ->
|
||||
E2 = Exponent bsr 1,
|
||||
B2 = mod(Base*Base, Modulus),
|
||||
case E2 band 1 of
|
||||
1 ->
|
||||
modular_pow(B2, E2, Modulus, mod(Return*B2, Modulus));
|
||||
_ ->
|
||||
modular_pow(B2, E2, Modulus, Return)
|
||||
end.
|
||||
```
|
||||
|
||||
The main problem of the
|
||||
[`crypto:mod_pow/3`](https://www.erlang.org/doc/man/crypto.html#mod_pow-3)
|
||||
function was directly related to the first argument. When a negative
|
||||
value was given, the returned values were wrong. Well, why not then
|
||||
apply a part of the custom modular pow when the value is negative, and
|
||||
then,
|
||||
[`crypto:mod_pow/3`](https://www.erlang.org/doc/man/crypto.html#mod_pow-3)
|
||||
when the first argument is positive? That's the third implementation
|
||||
and the one currently used.
|
||||
|
||||
```erlang
|
||||
% third implementation
|
||||
pow(1,0,_) -> 1;
|
||||
pow(0,1,_) -> 0;
|
||||
pow(_,_,1) -> 0;
|
||||
pow(Base, Exponent, Modulus) when Base > 0 ->
|
||||
bitstring_to_integer(crypto:mod_pow(Base, Exponent, Modulus));
|
||||
pow(Base, Exponent, Modulus) when Base < 0 ->
|
||||
case 1 band Exponent of
|
||||
1 ->
|
||||
modular_pow(Base, Exponent, Modulus, Base);
|
||||
0 ->
|
||||
modular_pow(Base, Exponent, Modulus, 1)
|
||||
end.
|
||||
|
||||
modular_pow(_Base, 0, _Modulus, Return) -> Return;
|
||||
modular_pow(Base, Exponent, Modulus, Return) ->
|
||||
E2 = Exponent bsr 1,
|
||||
B2 = mod(Base*Base, Modulus),
|
||||
case E2 band 1 of
|
||||
1 ->
|
||||
modular_pow(B2, E2, Modulus, mod(Return*B2, Modulus));
|
||||
_ ->
|
||||
modular_pow(B2, E2, Modulus, Return)
|
||||
end.
|
||||
```
|
||||
|
||||
This "hack" is working when using the test suite, but, the numbers of
|
||||
tests are limited. To be sure the implement is correct, correct values
|
||||
generated from Python can be reused in Erlang. The following script
|
||||
export (available in `extra` directory in the project) exports the
|
||||
input and the output of the `pow` function in CSV format.
|
||||
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# check_pow.py
|
||||
# Usage: python3 check_pow.py > pow.csv
|
||||
|
||||
import sys
|
||||
import secrets
|
||||
limit = 256
|
||||
generator = secrets.SystemRandom()
|
||||
start = -(2**256)
|
||||
end = 2**256
|
||||
for i in range(limit):
|
||||
a = generator.randrange(start, end)
|
||||
b = generator.randrange(0,end)
|
||||
m = generator.randrange(0,end)
|
||||
p = pow(a, b, m)
|
||||
l = ",".join([str(i),str(a),str(b),str(m),str(p)])
|
||||
print(l)
|
||||
```
|
||||
|
||||
When executed, the resulting CSV file can be read again by a small
|
||||
function in Erlang, converting the CSV columns to integer values and
|
||||
applying them a check function (this script can also be found in
|
||||
`extra` directory).
|
||||
|
||||
```erlang
|
||||
% same result can be found by executing the python
|
||||
% script directly from the BEAM, instead of opening
|
||||
% a file and read its content
|
||||
% os:cmd("python check_pow.py").
|
||||
CheckPow = fun (File) ->
|
||||
{ok, Content} = file:read_file(File),
|
||||
Lines = re:split(Content, "\n"),
|
||||
Splitted = lists:map(fun(X) -> re:split(X, ",") end, Lines),
|
||||
Result = [ { binary_to_integer(I)
|
||||
, binary_to_integer(P) =:=
|
||||
nostrlib_schnorr:pow(binary_to_integer(A)
|
||||
,binary_to_integer(B)
|
||||
,binary_to_integer(M))}
|
||||
|| [I,A,B,M,P] <- Splitted
|
||||
],
|
||||
[] =:= lists:filter(fun({_,false}) -> true; (_) -> false end, Result)
|
||||
end.
|
||||
true =:= CheckPow("pow.csv").
|
||||
```
|
||||
|
||||
|
||||
More than 100k values have been tested to check if the implementation
|
||||
was working, and at this time, no crash was reported. This
|
||||
implementation is working pretty well, and have decent performance
|
||||
result. It could be used in test and development environment without
|
||||
problem, but will probably require small optimization to be used in
|
||||
production. It will be a good reason to work on exponentiation
|
||||
[^wikipedia-exponentiation] [^wikipedia-exponentiation-by-squaring],
|
||||
fast exponentiation [^blog-fast-modular-exponentiation]
|
||||
[^blog-efficient-modular-exponentiation]
|
||||
[^a-survey-of-fast-exponentiation-methods] [^blog-fast-exponentiation]
|
||||
and its derivates optimizations
|
||||
[^iacr-outsoursing-modular-exponentiation]
|
||||
[^springer-efficient-elliptic-curve]
|
||||
[^wiley-efficient-modular-exponential-algorithms]
|
||||
[^iacr-faster-point-multiplication]
|
||||
[^efficient-montgomery-exponentiation]...
|
||||
|
||||
## Benchmarking the Solution
|
||||
|
||||
Both implementations have been tested with
|
||||
[`fprof`](https://www.erlang.org/doc/man/fprof.html). This application
|
||||
is available by default on all Erlang/OTP releases and can be really
|
||||
helpful to diagnose an application. Here the quick and dirty way to
|
||||
execute it, these functions needs to be executed in an Erlang shell.
|
||||
|
||||
```erlang
|
||||
fprof:start().
|
||||
fprof:apply(nostrlib_schnorr, test, []).
|
||||
fprof:profile().
|
||||
fprof:analyse().
|
||||
```
|
||||
|
||||
The execution time of the first implementations were quite similar,
|
||||
around 7 seconds. In other hand, the last implementation using
|
||||
[`crypto:mod_pow/3`](https://www.erlang.org/doc/man/crypto.html#mod_pow-3)
|
||||
improved significantly the speed of execution, lowering it down to
|
||||
1.794 seconds. There is no perfect solution, but this one offer a good
|
||||
compromise between speed and safety, at least for an application in
|
||||
development phase.
|
||||
|
||||
# Conclusion
|
||||
|
||||
This article showed how to implement a cryptographic algorithm with
|
||||
Erlang/OTP, based on a reference implementation in Python, without
|
||||
using any external dependencies. It is a mandatory requirement to
|
||||
implement NIP/01[^nip-01-specification] and needed by its
|
||||
implementation in Erlang. The code is still very slow, but no
|
||||
optimizations have been done. In less than a week, the Schnorr
|
||||
signature scheme has been fully implemented, tested and
|
||||
documented. This code can already be reused by other Erlang/OTP
|
||||
project just by importing `nostrlib_schnorr.erl` file. The code
|
||||
produced was only tested by one developer, and was never audited by
|
||||
professional cryptographer though. Using it in production is risky but
|
||||
will be the subject for another article.
|
||||
|
||||
If the readers are a bit curious and interested by the Schnorr
|
||||
signature scheme, the official implementation used by the Bitcoin
|
||||
project can be seen on Github [^bitcoin-implementation-check]
|
||||
[^bitcoin-implementation-schnorr-1]
|
||||
[^bitcoin-implementation-schnorr-2].
|
||||
|
||||
---
|
||||
|
||||
[^a-survey-of-fast-exponentiation-methods]: [A survey of fast exponentiation methods](https://www.dmgordon.org/papers/jalg.pdf)
|
||||
[^bip-0340-implementation]: https://github.com/bitcoin/bips/tree/master/bip-0340/reference.py
|
||||
[^bip-0340-specification]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
|
||||
[^bip-0340-test-vectors]: https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv
|
||||
[^bip-0341-specification]: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
|
||||
[^bip-0342-specification]: https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki
|
||||
[^bitcoin-implementation-check]: https://github.com/bitcoin/bitcoin/pull/22934
|
||||
[^bitcoin-implementation-schnorr-1]: https://github.com/bitcoin/bitcoin/pull/17977
|
||||
[^bitcoin-implementation-schnorr-2]: https://github.com/bitcoin/bitcoin/pull/19953
|
||||
[^blog-efficient-modular-exponentiation]: [Efficient modular exponentiation algorithms](https://eli.thegreenplace.net/2009/03/28/efficient-modular-exponentiation-algorithms) by Eli Bendersky
|
||||
[^blog-fast-exponentiation]: [Fast exponentiation](https://www.johndcook.com/blog/2008/12/10/fast-exponentiation/) by John D. Cook
|
||||
[^blog-fast-modular-exponentiation]: [Fast Modular Exponentiation](https://dev-notes.eu/2019/12/Fast-Modular-Exponentiation/) by David Egan
|
||||
[^c-cschnorr]: https://github.com/metalicjames/cschnorr
|
||||
[^c-libecc]: https://github.com/ANSSI-FR/libecc
|
||||
[^cpp-schnor]: https://github.com/spff/schnorr-signature
|
||||
[^efficient-montgomery-exponentiation]: [An Efficient Montgomery Exponentiation Algorithm for Cryptographic Applications](https://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.99.1460&rep=rep1&type=pdf)
|
||||
[^elixir-bitcoinex-schnorr]: https://github.com/RiverFinancial/bitcoinex/blob/master/lib/secp256k1/schnorr.ex
|
||||
[^elixir-bitcoinex]: https://github.com/RiverFinancial/bitcoinex
|
||||
[^elixir-k256]: https://github.com/davidarmstronglewis/k256
|
||||
[^go-schnorr-signature]: https://github.com/calvinrzachman/schnorr
|
||||
[^iacr-faster-point-multiplication]: [Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms](https://www.iacr.org/archive/crypto2001/21390189.pdf)
|
||||
[^iacr-outsoursing-modular-exponentiation]: IACR: [Outsoursing Modular Exponentiation in Cryptographic Web Applications](https://eprint.iacr.org/2018/300.pdf)
|
||||
[^java-samourai-schorr]: https://code.samourai.io/samouraidev/BIP340_Schnorr
|
||||
[^javascript-schnorr]: https://github.com/guggero/bip-schnorr
|
||||
[^nip-01-specification]: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
[^nodejs-schnorr-signature]: [Node.js for Schnorr signature](https://asecuritysite.com/signatures/js_sch)
|
||||
[^python-schnorr-example]: https://github.com/yuntai/schnorr-examples
|
||||
[^python-solcrypto]: https://github.com/HarryR/solcrypto
|
||||
[^python-taproot]: https://github.com/bitcoinops/taproot-workshop
|
||||
[^secp256k1-recommended-parameters]: [SEC 2: Recommended Elliptic Curve Domain Parameters](https://www.secg.org/SEC2-Ver-1.0.pdf), section 2.4.1
|
||||
[^springer-efficient-elliptic-curve]: [Efficient Elliptic Curve Exponentiation Using Mixed Coordinates](https://link.springer.com/content/pdf/10.1007/3-540-49649-1_6.pdf)
|
||||
[^weboftrust-schnorr-signature]: https://github.com/WebOfTrustInfo/rwot1-sf/blob/master/topics-and-advance-readings/Schnorr-Signatures--An-Overview.md
|
||||
[^wikipedia-beam]: https://en.wikipedia.org/wiki/BEAM_(Erlang_virtual_machine)
|
||||
[^wikipedia-bitcoin]: https://en.wikipedia.org/wiki/Bitcoin
|
||||
[^wikipedia-exponentiation-by-squaring]: https://en.wikipedia.org/wiki/Exponentiation_by_squaring
|
||||
[^wikipedia-exponentiation]: https://en.wikipedia.org/wiki/Exponentiation
|
||||
[^wikipedia-modular-exponentiation]: https://en.wikipedia.org/wiki/Modular_exponentiation
|
||||
[^wikipedia-modulo]: https://en.wikipedia.org/wiki/Modulo
|
||||
[^wikipedia-schnorr-signature]: https://en.wikipedia.org/wiki/Schnorr_signature
|
||||
[^wikipedia-wam]: https://en.wikipedia.org/wiki/Warren_Abstract_Machine
|
||||
[^wiley-efficient-modular-exponential-algorithms]: [Efficient modular exponential algorithms compatiblewith hardware implementation of public-key cryptography](https://onlinelibrary.wiley.com/doi/epdf/10.1002/sec.1511)
|
||||
[^youtube-bill-buchanan-schnorr]: Youtube: [Digital Signatures - ECDSA, EdDSA and Schnorr](https://www.youtube.com/watch?v=S77ES52AGVg)
|
||||
[^youtube-christof-paar-ecc]: Youtube: [Lecture 17: Elliptic Curve Cryptography (ECC) by Christof Paar](https://www.youtube.com/watch?v=zTt4gvuQ6sY)
|
||||
[^youtube-cihangir-tezcan-schnorr-multi-signature]: Youtube: [Schnorr Signatures and Key Aggregation (MuSig)](https://www.youtube.com/watch?v=7YWtg5KUKy4)
|
||||
[^youtube-pieter-wuille-schnorr]: Youtube: [Schnorr signatures for Bitcoin: challenges and opportunities - BPASE '18](https://www.youtube.com/watch?v=oTsjMz3DaLs)
|
||||
[^youtube-theoretically]: Youtube: [Schnorr Digital Signature](https://www.youtube.com/watch?v=mV9hXEFUB6A)
|
||||
184
notes/0004-schnorr-signature-scheme-in-erlang/README_ANNEXE1.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# ANNEXE 1 - BIP-0340 Implementation in Python
|
||||
|
||||
Here the steps to extract and execute
|
||||
[BIP-0340](https://github.com/bitcoin/bips/tree/master/bip-0340)
|
||||
implementation in Python with debug mode activated.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/bitcoin/bips
|
||||
cd bips/bip-0340
|
||||
sed -i "s/^DEBUG = False/DEBUG = True/" reference.py
|
||||
python reference.py
|
||||
```
|
||||
|
||||
Here the output:
|
||||
|
||||
```
|
||||
Test vector #0:
|
||||
Variables in function schnorr_sign at line 118:
|
||||
msg == 0x0000000000000000000000000000000000000000000000000000000000000000
|
||||
seckey == 0x0000000000000000000000000000000000000000000000000000000000000003
|
||||
aux_rand == 0x0000000000000000000000000000000000000000000000000000000000000000
|
||||
d0 == 0x0000000000000000000000000000000000000000000000000000000000000003
|
||||
P == ('0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9',
|
||||
'0x388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672')
|
||||
|
||||
d == 0x0000000000000000000000000000000000000000000000000000000000000003
|
||||
t == 0x54f169cfc9e2e5727480441f90ba25c488f461c70b5ea5dcaaf7af69270aa517
|
||||
k0 == 0x1d2dc1652fee3ad08434469f9ad30536a5787feccfa308e8fb396c8030dd1c69
|
||||
R == ('0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215',
|
||||
'0x849b08486a5b16ea5fd009a3ade472b48a2dc817aeebc33ab4fa25ebbd599f27')
|
||||
|
||||
k == 0xe2d23e9ad011c52f7bcbb960652cfac815365cf9dfa59752c498f20c9f5924d8
|
||||
e == 0x6bb6b93a91f2ecc0cd924f4f9baabb5e6eb21745bb00f2cebdaac908bb5d86ce
|
||||
sig == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
|
||||
Variables in function schnorr_verify at line 141:
|
||||
msg == 0x0000000000000000000000000000000000000000000000000000000000000000
|
||||
pubkey == 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
|
||||
sig == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
|
||||
P == ('0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9',
|
||||
'0x388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672')
|
||||
|
||||
r == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215
|
||||
s == 0x25f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
|
||||
e == 0x6bb6b93a91f2ecc0cd924f4f9baabb5e6eb21745bb00f2cebdaac908bb5d86ce
|
||||
R == ('0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215',
|
||||
'0x7b64f7b795a4e915a02ff65c521b8d4b75d237e851143cc54b05da1342a65d08')
|
||||
|
||||
* Passed signing test.
|
||||
Variables in function schnorr_verify at line 141:
|
||||
msg == 0x0000000000000000000000000000000000000000000000000000000000000000
|
||||
pubkey == 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
|
||||
sig == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
|
||||
P == ('0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9',
|
||||
'0x388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672')
|
||||
|
||||
r == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215
|
||||
s == 0x25f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
|
||||
e == 0x6bb6b93a91f2ecc0cd924f4f9baabb5e6eb21745bb00f2cebdaac908bb5d86ce
|
||||
R == ('0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215',
|
||||
'0x7b64f7b795a4e915a02ff65c521b8d4b75d237e851143cc54b05da1342a65d08')
|
||||
|
||||
* Passed verification test.
|
||||
|
||||
Test vector #1:
|
||||
Variables in function schnorr_sign at line 118:
|
||||
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
|
||||
seckey == 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef
|
||||
aux_rand == 0x0000000000000000000000000000000000000000000000000000000000000001
|
||||
d0 == 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef
|
||||
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
|
||||
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
|
||||
|
||||
d == 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef
|
||||
t == 0x5966c1816cb27e627c8543d63478bdce03c1b115838e469a416b0899fe5723dd
|
||||
k0 == 0xf7becdac22c3d61a97ff4e84a004e1c4919c0e0c51f50dd5bee15c9cbd27318e
|
||||
R == ('0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341',
|
||||
'0x20bc7663da14be43c22eb2ccb49cd746573b2766b277273fcb20a2f6c1a60c6c')
|
||||
|
||||
k == 0xf7becdac22c3d61a97ff4e84a004e1c4919c0e0c51f50dd5bee15c9cbd27318e
|
||||
e == 0xcfb58e748d9648b71fdc909fb7432fc0c954da5bd75cdc9d4804d32648f9839a
|
||||
sig == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
|
||||
Variables in function schnorr_verify at line 141:
|
||||
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
|
||||
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
|
||||
sig == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
|
||||
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
|
||||
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
|
||||
|
||||
r == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341
|
||||
s == 0x8906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
|
||||
e == 0xcfb58e748d9648b71fdc909fb7432fc0c954da5bd75cdc9d4804d32648f9839a
|
||||
R == ('0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341',
|
||||
'0x20bc7663da14be43c22eb2ccb49cd746573b2766b277273fcb20a2f6c1a60c6c')
|
||||
|
||||
* Passed signing test.
|
||||
Variables in function schnorr_verify at line 141:
|
||||
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
|
||||
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
|
||||
sig == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
|
||||
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
|
||||
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
|
||||
r == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341
|
||||
s == 0x8906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
|
||||
e == 0xcfb58e748d9648b71fdc909fb7432fc0c954da5bd75cdc9d4804d32648f9839a
|
||||
R == ('0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341', '0x20bc7663da14be43c22eb2ccb49cd746573b2766b277273fcb20a2f6c1a60c6c')
|
||||
* Passed verification test.
|
||||
|
||||
Test vector #2:
|
||||
Variables in function schnorr_sign at line 118:
|
||||
msg == 0x7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c
|
||||
seckey == 0xc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9
|
||||
aux_rand == 0xc87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906
|
||||
d0 == 0xc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9
|
||||
P == ('0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8',
|
||||
'0xf594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394')
|
||||
d == 0xc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9
|
||||
t == 0xbad44f6e1f50e3c2ad9d2ba5768ad6ab84dc4b8f9b893444234a9c39e8fc58e1
|
||||
k0 == 0xf5878384ed63c5ec428e7ab31bdb446b6884dfad76b7e0599af3f5e838409aab
|
||||
R == ('0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b',
|
||||
'0x5f193d22f6f1d925a7f8c4ceff20cc2a53ba1c3310ca843cf83156c1514bb284')
|
||||
k == 0xf5878384ed63c5ec428e7ab31bdb446b6884dfad76b7e0599af3f5e838409aab
|
||||
e == 0x9bc1ba4a0abbc0792066b2ca0ef771d88af676b322a83dd7517f7c1fd149215a
|
||||
sig == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
|
||||
Variables in function schnorr_verify at line 141:
|
||||
msg == 0x7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c
|
||||
pubkey == 0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8
|
||||
sig == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
|
||||
P == ('0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8',
|
||||
'0xf594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394')
|
||||
r == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b
|
||||
s == 0xab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
|
||||
e == 0x9bc1ba4a0abbc0792066b2ca0ef771d88af676b322a83dd7517f7c1fd149215a
|
||||
R == ('0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b',
|
||||
'0x5f193d22f6f1d925a7f8c4ceff20cc2a53ba1c3310ca843cf83156c1514bb284')
|
||||
* Passed signing test.
|
||||
Variables in function schnorr_verify at line 141:
|
||||
msg == 0x7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c
|
||||
pubkey == 0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8
|
||||
sig == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
|
||||
P == ('0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8',
|
||||
'0xf594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394')
|
||||
r == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b
|
||||
s == 0xab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
|
||||
e == 0x9bc1ba4a0abbc0792066b2ca0ef771d88af676b322a83dd7517f7c1fd149215a
|
||||
R == ('0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b',
|
||||
'0x5f193d22f6f1d925a7f8c4ceff20cc2a53ba1c3310ca843cf83156c1514bb284')
|
||||
* Passed verification test.
|
||||
|
||||
...
|
||||
|
||||
Test vector #12:
|
||||
Variables in function schnorr_verify at line 134:
|
||||
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
|
||||
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
|
||||
sig == 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f69e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
|
||||
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
|
||||
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
|
||||
r == 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
|
||||
s == 0x69e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
|
||||
* Passed verification test.
|
||||
|
||||
Test vector #13:
|
||||
Variables in function schnorr_verify at line 134:
|
||||
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
|
||||
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
|
||||
sig == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e177769fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
|
||||
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
|
||||
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
|
||||
r == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e177769
|
||||
s == 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
|
||||
* Passed verification test.
|
||||
|
||||
Test vector #14:
|
||||
Variables in function schnorr_verify at line 134:
|
||||
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
|
||||
pubkey == 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc30
|
||||
sig == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e17776969e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
|
||||
P == None
|
||||
r == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e177769
|
||||
s == 0x69e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
|
||||
* Passed verification test.
|
||||
|
||||
All test vectors passed.
|
||||
```
|
||||
BIN
notes/0004-schnorr-signature-scheme-in-erlang/schnorr_sign.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
notes/0004-schnorr-signature-scheme-in-erlang/schnorr_verify.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
650
notes/0005-implementing-nip-01-standard-in-pure-erlang/README.md
Normal file
@@ -0,0 +1,650 @@
|
||||
---
|
||||
date: 2023-03-10
|
||||
title: Nostr NIP/01 in Pure Erlang
|
||||
subtitle: |
|
||||
Implementing the first Nostr Implementation Possibility in pure
|
||||
Erlang with minimal dependencies
|
||||
author: Mathieu Kerjouan
|
||||
keywords: erlang,otp,nostr,nip/01,schnorr,design,interfaces
|
||||
license: CC BY-NC-ND
|
||||
abstract: |
|
||||
Designing and creating the first brick of a library is an
|
||||
exhausting process, even in high level language like Erlang.
|
||||
The goal of this article is to explain and detail NIP/01
|
||||
specification then implement it.
|
||||
toc: true
|
||||
hyperrefoptions:
|
||||
- linktoc=all
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
This article has been redacted in February and March 2023. It
|
||||
describes the methodologies applied and describe the methods used to
|
||||
implement the first NIP from nostr protocol in Erlang with a minimal
|
||||
amount of dependencies. The following code has been tested using
|
||||
[Erlang/OTP R25](https://www.erlang.org/news/157) running on
|
||||
[OPENBSD-72-CURRENT](openbsd.org/) and [Parrot
|
||||
Linux](https://parrotsec.org/) (a Debian like distribution).
|
||||
|
||||
# Nostr NIP/01 Library Design and Implementation
|
||||
|
||||
NIP/01[^nip-01-specification] is the first NIP describing the nostr
|
||||
protocol. This is a mandatory specification defining the basic
|
||||
data-structures and how relays and clients communicate together.
|
||||
|
||||

|
||||
|
||||
## HTTP and Websocket
|
||||
|
||||
nostr protocol is mainly using Websocket[^wikipedia-websocket] over
|
||||
HTTPS[^wikipedia-http] connection. The messages, called events, are
|
||||
using JSON[^wikipedia-json] objects and directly pushed/pulled to/from
|
||||
the active Websocket connection. A relay is acting as a classical HTTP
|
||||
server, accepting only allowing Websocket connection. When using a
|
||||
relay, the simplified procedure looks like the following one:
|
||||
|
||||
1. Listen to a defined port usually TCP/80 (HTTP) or TCP/443 (HTTPS);
|
||||
|
||||
2. When a client connects to the socket, initialize a Websocket
|
||||
end-point;
|
||||
|
||||
3. Wait until the client is sending an Event;
|
||||
|
||||
4. Forwards messages if requests.
|
||||
|
||||
In other hand, the client is acting like any standard HTTP client, but
|
||||
supporting only HTTP with Websocket connections. The connection
|
||||
procedure is described below.
|
||||
|
||||
1. Connect to a remote host on TCP/80 (HTTP) or TCP/443 (HTTPS);
|
||||
|
||||
2. When the HTTP connection is ready, ask for a Websocket connection;
|
||||
|
||||
3. When the websocket is ready, subscribes to events or sends events.
|
||||
|
||||
The client authentication is done with cryptographic functions, in
|
||||
particular the Schnorr signature
|
||||
scheme[^schnorr-signature-scheme]. Each messages are delivered with a
|
||||
public key and a signature, this last element is generated by the
|
||||
client with its own private key.
|
||||
|
||||
When the message is forwarded to other clients, the public key is used
|
||||
to ensure the message was correctly created by the correct
|
||||
client. This feature can also be used to send encrypted direct
|
||||
message, a feature defined in NIP/04[^nip-04-specification].
|
||||
|
||||
## Client JSON Payloads
|
||||
|
||||
Clients and Relays are using JSON data-structures to communicate
|
||||
together. In this part of the article, a non randomized private key
|
||||
will be used:
|
||||
`0000000000000000000000000000000000000000000000000000000000000001` and
|
||||
its public key is
|
||||
`79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798`. Few
|
||||
keys should not be used in the wild, this one is probably a good
|
||||
example. **This key pair should be used only for testing and
|
||||
development purpose**.
|
||||
|
||||
### Event Creation and Diffusion
|
||||
|
||||
The goal of nostr is to offer a simple way to diffuse textual content,
|
||||
like Twitter or Mastodon. Those messages are called events and are
|
||||
usually generated by the client and sent to the server using an
|
||||
`Event` object.
|
||||
|
||||

|
||||
|
||||
The procedure is extremely easy, after a successful connection to the
|
||||
relay, the client can directly send a message to the relay. The relay
|
||||
can store it and/or relay the event to other clients if they have a
|
||||
subscription.
|
||||
|
||||
The event sent is a JSON array, where the first element is a string
|
||||
`"EVENT"`. The second element is a JSON object containing 7
|
||||
attributes.
|
||||
|
||||
The `content` attribute contains the message sent by the client and
|
||||
must be a valid UTF8 string.
|
||||
|
||||
The `created_at` attribute
|
||||
defines the event creation date as POSIX timestamp, in other word, a
|
||||
positive integer.
|
||||
|
||||
The `id` attribute is an identifier generated using a SHA256 checkums
|
||||
of a serialized version of the event, it should be unique, is
|
||||
represented as a lowercase hexadecimal string and has a fixed size,
|
||||
256bits or 32bytes for the raw unencoded content, 64bytes or 512bits
|
||||
for the hexadecimal string.
|
||||
|
||||
The `kind` attribute defines the kind of the event, and is a positive
|
||||
integer. NIP/01 defines 3 kinds: `set_metadata` using `0`, `text_note`
|
||||
using `1` and `recommend_server` using `2`.
|
||||
|
||||
The `pubkey` attributes shares the public key of the client. This is a
|
||||
lowercase hexadecimal string representing a secp256k1 public key, its
|
||||
size is fixed to 64bytes or 512bits for its hexadecimal representation
|
||||
and 256bits or 32bytes for its raw unencoded version.
|
||||
|
||||
The `sig` attribute represents the signature of the event. This
|
||||
element is generated by signing the SHA256 hash of the serialized form
|
||||
of the event with the private key on the client side, in other word,
|
||||
it signs the event identifier of the event. This is a lowercase
|
||||
hexadecimal string with a fixed size of 128bytes or 1024bits in its
|
||||
hexadecimal encoded form and 64bytes or 512bits for its raw unencoded
|
||||
format.
|
||||
|
||||
The `tags` attribute can store tags. A tag is an array of string used
|
||||
to extend the event data-structure. NIP/01 defines 2 tags: `e` tag is
|
||||
representing another event, used to "quote", `p` tag is representing
|
||||
someone else public key. A `tags` attribute can be set as an empty
|
||||
array.
|
||||
|
||||
Here an example of a raw object generated by a client:
|
||||
|
||||
```json
|
||||
["EVENT", {
|
||||
"content": "test",
|
||||
"created_at": 1678325509
|
||||
"id": "f986c724a5085ffe093e8145ef953ed5c9d3d20f1c0e7fa3b88bfa5eb96427c6",
|
||||
"kind": 0,
|
||||
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"sig": "140d53496710b06dbc8ce239a454bf48defd31c1067927e236271f5565b72f6888c
|
||||
1098f947e7b5a099c909c046d705e9031e990b5df705b15640858a94f528e",
|
||||
"tags": []
|
||||
}]
|
||||
```
|
||||
|
||||
`nostrlib` has been created to encode and decode these events using
|
||||
records `#event{}` defined in `nostrlib.hrl`:
|
||||
|
||||
```erlang
|
||||
-record(event, { id = undefined :: decoded_event_id()
|
||||
, public_key = undefined :: decoded_public_key()
|
||||
, created_at = undefined :: decoded_created_at()
|
||||
, kind = undefined :: decoded_kind()
|
||||
, tags = [] :: decoded_tags()
|
||||
, content = undefined :: decoded_content()
|
||||
, signature = undefined :: decoded_signature()
|
||||
}).
|
||||
```
|
||||
|
||||
All hexadecimal string are decoded and are stored as bitstring. The
|
||||
created_at field is using the `universaltime` representation. The kind
|
||||
field is using atom to represent the kind of the event instead of an
|
||||
integer. Finally, the tags are represented by the `#tag{}` record,
|
||||
also defined in `nostrlib.hrl` file:
|
||||
|
||||
```erlang
|
||||
-record(tag, { name = undefined :: public_key | event_id
|
||||
, value = undefined :: undefined | bitstring()
|
||||
, params = [] :: list()
|
||||
}).
|
||||
```
|
||||
|
||||
Two functions can be used to encode events, `nostrlib:encode/1` and
|
||||
`nostrlib:encode/2`. These functions are returning a tuple containing
|
||||
the JSON data encoded as bitstring.
|
||||
|
||||
```erlang
|
||||
rr(nostrlib).
|
||||
{ok, EncodedEvent} = nostrlib:encode(#event{}).
|
||||
```
|
||||
|
||||
To decode a message, two functions exist as well: `nostrlib:decode/1`
|
||||
and `nostrlib:decode/2`. These two functions are returning a tuple
|
||||
containing the event as record and a label. This last element gives an
|
||||
idea of the "quality" of the event by alerting the developer/user if
|
||||
the event has been correctly signed or if the identifier is using its
|
||||
strict form (or not).
|
||||
|
||||
```erlang
|
||||
{ok, DecodedEvent, Labels} = nostrlib:decode(EncodedEvent).
|
||||
```
|
||||
|
||||
Now the low level interfaces have been created, high level interfaces
|
||||
can be created. Only the client must be able to send those kind of
|
||||
event, then, the functions should be defined in `nostr_client` module.
|
||||
|
||||
```erlang
|
||||
{ok, Info} = nostr_client:event(Connection, Kind, Content).
|
||||
{ok, Info} = nostr_client:event(Connection, set_metadata, #{ <<"about">> => <<"data">> }).
|
||||
{ok, Info} = nostr_client:event(Connection, text_note, <<"my message to the world">>).
|
||||
{ok, Info} = nostr_client:event(Connection, recommend_server, <<"wss://relay.com/">>).
|
||||
```
|
||||
|
||||
#### Event Identifier Creation
|
||||
|
||||
As seen in the previous section, the event identifier is created by
|
||||
applying the SHA256 hash function on a serialized version of the
|
||||
event. This data-structure is created by creating a new JSON array
|
||||
with the different attributes:
|
||||
|
||||
```json
|
||||
[0,$public_key,$created_at,$kind,$tags,$content]
|
||||
```
|
||||
|
||||
Where
|
||||
|
||||
- `$public_key` is the public key derived from the private key;
|
||||
|
||||
- `$created_at` is the date of the creation of the event, in POSIX
|
||||
format (integer);
|
||||
|
||||
- `$kind` is the kind of the event as integer;
|
||||
|
||||
- `$tags` is a list of tags, usually strings;
|
||||
|
||||
- `$content` is the content of the message, as string.
|
||||
|
||||
The event example seen in the previous section will have the following
|
||||
serialized format (without spaces and carriage returns):
|
||||
|
||||
```json
|
||||
[0
|
||||
,"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
,1678325509
|
||||
,0
|
||||
,[]
|
||||
,"test"
|
||||
]
|
||||
```
|
||||
|
||||
This implementation is creating automatically the event identifier
|
||||
when all the attributes are correctly set but if the readers are
|
||||
curious, the functions used to serialize an event are
|
||||
`nostrlib:serialize/1` and `nostrlib:serialize/5`. The function used
|
||||
to create an event identifier is `nostrlib:create_id/1`. Both of them
|
||||
are private and can only be view in the `nostrlib.erl` file.
|
||||
|
||||
#### Signature Creation and Validation
|
||||
|
||||
A signature is created using the Schnorr signature scheme. The message
|
||||
to sign the event identifier or the serialized version of the
|
||||
event. Any events can be signed using `nostrlib:sign/1` function, this
|
||||
one is based on both `nostrlib_schnorr:sign/2` and
|
||||
`nostrlib_schnorr:sign/3` functions.
|
||||
|
||||
```erlang
|
||||
nostrlib:sign(Event).
|
||||
nostrlib_schnorr:sign(Message, PrivateKey).
|
||||
```
|
||||
|
||||
To verify if a signature is valid, the function `nostrlib:verify/1`
|
||||
can be used on any decoded event. It will return if the message has
|
||||
been correctly signed. Its a wrapper around
|
||||
`nostrlib_schnorr:verify/3` function.
|
||||
|
||||
```erlang
|
||||
nostrlib:verify(Event).
|
||||
```
|
||||
|
||||
### Requesting a New Subscription
|
||||
|
||||
By default, a relay does not send any event. A client must ask for
|
||||
different kind of events with a request. The request is a special
|
||||
message defining a subscription id, a random string created by the
|
||||
client, and a list of filters. The filter is used by the relay to look
|
||||
on its database or from the new event and forward them to the client.
|
||||
|
||||

|
||||
|
||||
The filter is defined has a JSON objects containing 8 optional
|
||||
fields. The `ids` field is containing a list of event ids or prefixes
|
||||
represented as lowercase hexadecimal strings. These values are used by
|
||||
the relay to filter the events by their event identifier.
|
||||
|
||||
The `authors` field is containing a list of public keys or prefixes
|
||||
represented as lower case hexadecimal strings. Those values are used
|
||||
by the relay to filter the events by the public key of the authors.
|
||||
|
||||
The `#e` attribute is containing a list of event identifiers, in their
|
||||
strict format. The behavior is similar than the one provided by the
|
||||
`ids` attribute.
|
||||
|
||||
The `#p` attribute is containing a list of public keys, in their
|
||||
strict format. The behavior is similar than the one provided by the
|
||||
`authors` attribute.
|
||||
|
||||
The `since` and `until` attributes are used to define a date interval,
|
||||
when `since` define the beginning of the interval and `until` defines
|
||||
its end. Both are represented as positive integer and using the POSIX
|
||||
date/time format.
|
||||
|
||||
The `limit` attibute defines how many event the relay is allowed to
|
||||
send during the first request.
|
||||
|
||||
```json
|
||||
["REQ", "client1_subscription_kxFgDz4y", {
|
||||
"ids": [
|
||||
"f986c724a5085ffe093e8145ef953ed5c9d3d20f1c0e7fa3b88bfa5eb96427c6",
|
||||
"f986c724a5085ffe093e8145"
|
||||
],
|
||||
"authors": [
|
||||
"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"79be667ef9dcbbac55a06295ce870b"
|
||||
],
|
||||
"#e": [
|
||||
"f986c724a5085ffe093e8145ef953ed5c9d3d20f1c0e7fa3b88bfa5eb96427c6"
|
||||
],
|
||||
"#p": [
|
||||
"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
],
|
||||
"since": 1678325509,
|
||||
"until": 1678325509,
|
||||
"limit": 10
|
||||
}]
|
||||
```
|
||||
|
||||
`nostrlib` represents these elements with 2 records, `#request{}` and
|
||||
`#filter{}` both defined in `nostrlib.hrl` file.
|
||||
|
||||
```erlang
|
||||
-record(filter, { event_ids = [] :: decoded_event_ids()
|
||||
, authors = [] :: decoded_authors()
|
||||
, kinds = [] :: decoded_kinds()
|
||||
, tag_event_ids = [] :: decoded_tag_event_ids()
|
||||
, tag_public_keys = [] :: decoded_tag_event_public_keys()
|
||||
, since = undefined :: decoded_since()
|
||||
, until = undefined :: decoded_until()
|
||||
, limit = undefined :: decoded_limit()
|
||||
}).
|
||||
-record(request, { subscription_id = undefined :: decoded_subscription_id()
|
||||
, filter = #filter{} :: [decoded_filter(), ...]
|
||||
}).
|
||||
```
|
||||
|
||||
`nostrlib:encode/1` or `nostrlib:encode/2` functions are being used to
|
||||
encode a request.
|
||||
|
||||
```erlang
|
||||
rr(nostrlib).
|
||||
Filter = #filter{}.
|
||||
SubscriptionId = nostrlib:new_subscription_id().
|
||||
{ok, Request} = nostrlib:encode(#request{ subscription_id = SubscriptionId
|
||||
, filter = Filter }).
|
||||
```
|
||||
|
||||
`nostrlib:decode/1` or `nostrlib:decode/2` functions are used to
|
||||
decode the encoded JSON.
|
||||
|
||||
```erlang
|
||||
{ok, Decoded, Labels} = nostrlib:decode(Request).
|
||||
```
|
||||
|
||||
Only a client is allowed to send a request to a
|
||||
relay. `nostr_client:request/2` is then created as a high level
|
||||
interface.
|
||||
|
||||
```
|
||||
{ok, SubscriptionId} = nostr_client:request(Connection, Filter).
|
||||
```
|
||||
|
||||
### Close an Active Subscription
|
||||
|
||||
An active subscription can be closed on demand by the client. The
|
||||
relay will remove the subscription on its side and stop relaying the
|
||||
event to the subscriber.
|
||||
|
||||

|
||||
|
||||
```json
|
||||
["CLOSE", "client1_subscription_kxFgDz4y"]
|
||||
```
|
||||
|
||||
Close event is represented with `#close{}` record defined in
|
||||
`nostrlib.hrl` file.
|
||||
|
||||
```erlang
|
||||
-record(close, { subscription_id = undefined :: decoded_subscription_id()
|
||||
}).
|
||||
```
|
||||
|
||||
As usual, the `nostrlib:encode/1` or `nostrlib:encode/2` functions can
|
||||
be used to generated the JSON payload.
|
||||
|
||||
```erlang
|
||||
rr(nostrlib).
|
||||
{ok, EncodedClose} = nostrlib:encode(#close{ subscription_id = SubscriptionId }).
|
||||
```
|
||||
|
||||
Close events can also be decoded using `nostrlib:decode/1` and
|
||||
`nostrlib:decode/2` functions.
|
||||
|
||||
```erlang
|
||||
{ok, Decoded, Labels} = nostrlib:decode(EncodedClose).
|
||||
```
|
||||
|
||||
This event can only be sent by a client, then the function
|
||||
`nostrlib_client:close/2` can be used to terminate a subcription.
|
||||
|
||||
```erlang
|
||||
nostr_client:close(Connection, SubscriptionId).
|
||||
```
|
||||
|
||||
## Relay JSON Payloads
|
||||
|
||||
This part of the code should be treated as a draft. Indeed, at this
|
||||
time of writing, the nostr relay is not implemented and the present
|
||||
data-structures and algorithms could change in the future.
|
||||
|
||||
The client is sending its messages to a relay, but the relay can also
|
||||
sent back its own message, in particular the events created by other
|
||||
clients. Relays, like defined in NIP/01, are really simple.
|
||||
|
||||
### Event Forwarding
|
||||
|
||||
When a client is sending a message to a relay, the relay will check
|
||||
the subscriptions, if a subscription match the event, then it is
|
||||
forwarded to the client asking for this kind of events. Relays can
|
||||
also store all events in its own database.
|
||||
|
||||

|
||||
|
||||
The example event generated on the client part will look like the
|
||||
following JSON object on other client, after being forwarded by the
|
||||
relay. The only added element is the `"subscription_id"` string, and
|
||||
is the subscription id generated a client.
|
||||
|
||||
```json
|
||||
["EVENT", "client1_subscription_kxFgDz4y", {
|
||||
"content": "test",
|
||||
"created_at": 1678325509
|
||||
"id": "f986c724a5085ffe093e8145ef953ed5c9d3d20f1c0e7fa3b88bfa5eb96427c6",
|
||||
"kind": 0,
|
||||
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"sig": "140d53496710b06dbc8ce239a454bf48defd31c1067927e236271f5565b72f6888c
|
||||
1098f947e7b5a099c909c046d705e9031e990b5df705b15640858a94f528e",
|
||||
"tags": []
|
||||
}]
|
||||
```
|
||||
|
||||
A forwarded event is called a subscription in this
|
||||
implementation. This name will probably be changed in a near future
|
||||
because it could be confused with a request and/or other events. The
|
||||
record used is called `#subscription{}`.
|
||||
|
||||
|
||||
```erlang
|
||||
-record(subscription, { id = undefined :: decoded_subscription_id()
|
||||
, content = undefined :: decoded_subscription_content()
|
||||
}).
|
||||
```
|
||||
|
||||
The subscription event can only be used by a relay, and should be
|
||||
defined in `nostr_relay` module. The function will be called
|
||||
`nostr_relay:event/2`, where the first argument will be a subcription
|
||||
id (connected to a client) and the second argument will be the event
|
||||
to forward.
|
||||
|
||||
```erlang
|
||||
ok = nostr_relay:event(Subscription, Event).
|
||||
```
|
||||
|
||||
### Notice Message
|
||||
|
||||
Relays can send direct messages to one or more client. This feature is
|
||||
used to inform a client of something happening on the server.
|
||||
|
||||

|
||||
|
||||
The JSON object used is a simple array, with the first element set to
|
||||
a string `"NOTICE"` and the second element is a valid unicode string.
|
||||
|
||||
```json
|
||||
["NOTICE", "message from the relay"]
|
||||
```
|
||||
|
||||
The record used internally is called `#notice{}`.
|
||||
|
||||
```erlang
|
||||
-record(notice, { message = undefined :: decoded_message() }).
|
||||
```
|
||||
|
||||
A relay should have the possibility to send a message to one or many
|
||||
users connected to the service. In this case, the function
|
||||
`nostr_relay:notice/2` should created. The first argument will be a
|
||||
connection, a list of connections or a special value like the atom
|
||||
`all` used to contact, one, many or all users. The second argument
|
||||
will be the message to send as a bitstring.
|
||||
|
||||
```erlang
|
||||
ok = nostr_relay:notice(all, <<"my message">>).
|
||||
ok = nostr_relay:notice([C1, C2, C3], <<"my message">>).
|
||||
ok = nostr_relay:notice(Connection, <<"my message">>).
|
||||
```
|
||||
|
||||
## Event Kinds Concept
|
||||
|
||||
An event sent by a client can be set with a kind. At this time, in
|
||||
NIP/01, only 3 kinds have been defined. This feature extend the
|
||||
concept of event by specifying what kind of data will be contained in
|
||||
the `content` field.
|
||||
|
||||
Events using the kind `0` or `set_metadata` will be behave like a
|
||||
profile page on classical social networks. The content will be
|
||||
interpreted as a JSON object containing, by default, 3 attributes are
|
||||
available:
|
||||
|
||||
- `name` attribute defines the name of the user as a string;
|
||||
|
||||
- `about` attribute allows to set a description about the user;
|
||||
|
||||
- `picture` attribute should be set with an URL pointing to an image.
|
||||
|
||||
Events using the kind `1` or `text_notes` will behave like a plaintext
|
||||
messages. The content field will be interpret as a simple string.
|
||||
|
||||
Finally, events using the kind `2` or `recommend_server` will have the
|
||||
content attribute set to a valid URL pointing to a prefered server.
|
||||
|
||||
| kind id | kind name | description | example |
|
||||
|---------|--------------------|---------------------|-------------------------------|
|
||||
| `0` | `set_metadata` | a stringified json | `"{\"about\":\"hello\",\"name\":\"myname\",...}"`
|
||||
| `1` | `text_note` | a plaintext message | `"this a text message"` |
|
||||
| `2` | `recommend_server` | an URL | `"wss://my.cool.server.com/"` |
|
||||
|
||||
## Subscription Concept
|
||||
|
||||
A client can ask a relay to have events. The client is in charge of
|
||||
generating a subscription id (a random string) and a filter to receive
|
||||
events. At this time, this feature is not implemented, but an instance
|
||||
of the subscription should be available on the server side, identified
|
||||
by an unique id.
|
||||
|
||||
# Connecting the Dots
|
||||
|
||||
The functions `nostrlib:encode/1` and `nostrlib:encode/2` are used to
|
||||
encode a message using a record and convert them in a JSON payload.
|
||||
|
||||
```erlang
|
||||
{ok, Encoded} = nostrlib:encode(Event).
|
||||
{ok, Encoded} = nostrlib:encode(Event, Opts).
|
||||
```
|
||||
|
||||
The functions `nostrlib:decode/1` and `nostrlib:decode/2` are used to
|
||||
decode a JSON object and convert them into an Erlang term, like a
|
||||
record.
|
||||
|
||||
```erlang
|
||||
{ok, Decoded, Labels} = nostrlib:decode(JSON_Message)
|
||||
{ok, Decoded, Labels} = nostrlib:decode(JSON_Message, Opts)
|
||||
```
|
||||
|
||||
The functions `nostrlib:check/1` and `nostrlib:check/2` are checking
|
||||
an event without decoding it. The raw message, if valid, is then
|
||||
returned.
|
||||
|
||||
```erlang
|
||||
{ok, JSON_Message, Labels} = nostrlib:check(JSON_Message).
|
||||
{ok, JSON_Message, Labels} = nostrlib:check(JSON_Message, Opts).
|
||||
```
|
||||
|
||||
The function `nostrlib:sign/2` is used to sign an event with a private
|
||||
key. `nostrlib:verify/1` is used to verify the signature in an event
|
||||
and be sure the message is valid.
|
||||
|
||||
```erlang
|
||||
{ok, Signature} = nostrlib:create_signature(Event, PrivateKey).
|
||||
{ok, Event} = nostrlib:sign(Event, PrivateKey).
|
||||
true = nostrlib:verify(Event).
|
||||
true = nostrlib:verify(EventId, PublicKey, Signature).
|
||||
```
|
||||
|
||||
`nostrlib:new_subscription_id/0` is used to generate a new
|
||||
subscription id, mainly used by the client.
|
||||
|
||||
```erlang
|
||||
SubscriptionId = nostrlib:new_subscription_id().
|
||||
```
|
||||
|
||||
`nostrlib:check_hex/1` and `nostrlib:is_hex/1` are used to ensure an
|
||||
element is correctly using an hexadecimal format.
|
||||
|
||||
```erlang
|
||||
{ok, <<"abcd">>} = nostrlib:check_hex(<<"abcd">>).
|
||||
{error, _} = nostrlib:check_hex(<<"zabcd">>).
|
||||
true = nostrlib:is_hex(<<"abcd">>).
|
||||
false = nostrlib:check_hex(<<"zabcd">>).
|
||||
```
|
||||
|
||||
`nostrlib:integer_to_hex/1` and `nostrlib:hex_to_integer/1` are used
|
||||
to convert integers to hexadecimal strings.
|
||||
|
||||
```erlang
|
||||
<<"7b">> = nostrlib:integer_to_hex(123).
|
||||
123 = nostrlib:hex_to_integer(<<"7b">>).
|
||||
```
|
||||
|
||||
`nostrlib:binary_to_hex/1` and `nostrlib:hex_to_binary/1` are used to
|
||||
convert binary/bitstring to hexadecimal strings.
|
||||
|
||||
```erlang
|
||||
<<"7b">> = nostrlib:binary_to_hex(<<123>>).
|
||||
<<"{">> = nostrlib:hex_to_binary(<<"7b">>).
|
||||
```
|
||||
|
||||
Functions present in `nostr_client` and `nostr_relay` modules are not
|
||||
implemented and will not be presented here.
|
||||
|
||||
# Conclusion
|
||||
|
||||
In a bit more than 2 weeks, huge improvement were made. The
|
||||
foundations have been created to welcome the next features. Like in
|
||||
any other project, the beginning is probably one of the most complex
|
||||
part. This project could have been started from different angles,
|
||||
instead of starting by the client, the first step would have been to
|
||||
start working on the relay, but it was not the case for some reasons:
|
||||
the JSON objects are shared by the clients and the relay. That means
|
||||
if the parser is working correctly as client, it will work correctly
|
||||
as relay. It is less dangerous to create a client and break it locally
|
||||
than deploying a relay into the wild.
|
||||
|
||||
---
|
||||
|
||||
[^wikipedia-websocket]: https://en.wikipedia.org/wiki/WebSocket
|
||||
[^wikipedia-http]: https://en.wikipedia.org/wiki/HTTP
|
||||
[^wikipedia-json]: https://en.wikipedia.org/wiki/JSON
|
||||
[^nip-01-specification]: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
[^nip-04-specification]: https://github.com/nostr-protocol/nips/blob/master/04.md
|
||||
[^schnorr-signature-scheme]: https://en.wikipedia.org/wiki/Schnorr_signature
|
||||
@@ -0,0 +1,97 @@
|
||||
# ANNEXES2 - Identity Draft
|
||||
|
||||
The identity will alter the behavior of the client by configuring
|
||||
default values. An identity is encrypted and stored in a vault.
|
||||
|
||||
0. An identity MUST BE defined by an ID (randomly chosen or not)
|
||||
1. An identity MUST CONTAIN the user private key
|
||||
2. An identity CAN BE encrypted
|
||||
3. An identity CAN HAVE a creation date
|
||||
4. An identity CAN HAVE a recommended server
|
||||
5. An identity CAN EMBED metadata
|
||||
6. An identity CAN HAVE custom configuration
|
||||
7. An identity CAN HAVE custom labels
|
||||
|
||||
```erlang
|
||||
% create a new identity
|
||||
% nostr_identity:new/0 create a fully random identity with
|
||||
% high security level by default
|
||||
Identity = nostr_identity:new().
|
||||
|
||||
% nostr_identity:new/1 creates with a new name with high
|
||||
% security level by default
|
||||
Identity = nostr_identity:new(Name).
|
||||
|
||||
% nostr_identity:new/2 creates a new custom account, it can be
|
||||
% used to import identity.
|
||||
{ok, Identity} = nostr_identity:new(Name, Opts).
|
||||
{ok, Identity} = nostr_identity:new(<<"my name">>, [
|
||||
% password used to encrypt the content of the data-structure
|
||||
% and stored in the vault. It is also used to encrypt the
|
||||
% private_key by default
|
||||
{password, Password},
|
||||
|
||||
% private key defined by the user or automatically created
|
||||
% when an identity is created
|
||||
{private_key, PrivateKey},
|
||||
|
||||
% generated using the private key during the creation of
|
||||
% the identity
|
||||
{public_key, PublicKey},
|
||||
|
||||
% random seed created when the identity is created
|
||||
{seed, crypto:strong_rand_bytes(64)}
|
||||
|
||||
% created when the identity is created
|
||||
{created_at, erlang:system_time()},
|
||||
|
||||
% recommend_server event sent to the relay
|
||||
{recommend_server, <<"wss://myrelay.local">>},
|
||||
|
||||
% metadata event sent to the relay
|
||||
{metadata, #{
|
||||
name => <<"my name">>,
|
||||
about => <<"about">>,
|
||||
picture => <<"url">>
|
||||
}
|
||||
},
|
||||
|
||||
% a custom configuration to alter the behavior or the
|
||||
% connection when identity is used.
|
||||
{configuration, #{
|
||||
relays => #{
|
||||
<<"wss://...">> => <<"wss://...">>,
|
||||
<<"wss://...">> => <<"wss://...">>
|
||||
},
|
||||
subscriptions => #{
|
||||
<<"wss://...">> => [],
|
||||
<<"wss://...">> => []
|
||||
},
|
||||
actions => #{}
|
||||
}
|
||||
},
|
||||
|
||||
% labels are used to check if the client or the relay
|
||||
% is supporting a list of features.
|
||||
{labels, #{
|
||||
<<"connection/tor">> => true,
|
||||
<<"connection/i2p">> => true,
|
||||
<<"connection/proxy">> => true,
|
||||
<<"vault">> => true,
|
||||
<<"nip/01">> => true,
|
||||
}
|
||||
}
|
||||
]).
|
||||
|
||||
% list available identity (hashed form)
|
||||
{ok, List} = nostr_identity:list().
|
||||
|
||||
% get an identity
|
||||
{ok, Identity} = nostr_identity:get(Name).
|
||||
{ok, Identity} = nostr_identity:get(Name, Opts).
|
||||
{ok, Result} = nostr_identity:save(Name).
|
||||
{ok, Identity} = nostr_identity:export(Name).
|
||||
|
||||
% create a new connection using one identity
|
||||
{ok, Connection} = nostr_client:start("wss://relay.local", [{identity, Identity}]).
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# ANNEXE2 - Types, Data-Structures and Definition Summary
|
||||
|
||||
Here the common types/values you can find in nostr protocol.
|
||||
|
||||
| type name | type | value |
|
||||
|---------------|----------------|------------------------------------------------------|
|
||||
| `#id` | `string` | a 32 bytes (256bits) lowercase hexadecimal string |
|
||||
| `#public_key` | `string` | a 32bytes (256bits) lowercase hexadecimal string |
|
||||
| `#prefix` | `string` | a 0 to 32bytes (256bits) hexadecimal string |
|
||||
| `#kind` | `integer` | a positive integer |
|
||||
| `#signature` | `string` | a 64bytes (512bits) lowercase hexadecimal string |
|
||||
| `#timestamp` | `integer` | a UNIX/POSIX timestamp |
|
||||
| `#tag` | `[string,...]` | a list of string or integer |
|
||||
|
||||
|
||||
| Object | attribute | type | example |
|
||||
|----------|--------------|---------------------|---------------------------------------|
|
||||
| `Event` | `id` | `#id` | `"f986c724a5085ffe093e8145ef953e..."` |
|
||||
| `Event` | `pubkey` | `#public_key` | `"79be667ef9dcbbac55a06295ce870b..."` |
|
||||
| `Event` | `created_at` | `#timestamp` | `1678325509` |
|
||||
| `Event` | `kind` | `[#kind,...] ` | `0` |
|
||||
| `Event` | `tags` | `[#tag,...]` | `[["p","79be667ef9dcbbac55a062..."]` |
|
||||
| `Event` | `content` | `string` | `"test` |
|
||||
| `Event` | `sig` | `#signature` | `"140d53496710b06dbc8ce239a454bf..."` |
|
||||
| `Filter` | `ids` | `[#prefix,...]` | `["f986c724a5085ffe093e8145"]` |
|
||||
| `Filter` | `authors` | `[#prefix,...]` | `["79be667ef9dcbbac55a06295ce87..."]` |
|
||||
| `Filter` | `kinds` | `[#kind,...]` | `[0,1,2]` |
|
||||
| `Filter` | `#e` | `[#id,...]` | `["f986c724a5085ffe093e8145ef95..."]` |
|
||||
| `Filter` | `#p` | `[#public_key,...]` | `["79be667ef9dcbbac55a06295ce87..."]` |
|
||||
| `Filter` | `since` | `#timestamp` | `1678325509` |
|
||||
| `Filter` | `until` | `#timestamp` | `1678325509` |
|
||||
| `Filter` | `limit` | `integer` | `10` |
|
||||
| `Close` | `subscription_id` | `string` | `"randomstring"` |
|
||||
| `Notice` | `message | `string` | `"a valid message"` |
|
||||
|
||||
| Object | Attribute | nostr record | nostr record field |
|
||||
|---------|--------------|--------------|-----------------------|
|
||||
| `Event` | `id` | `#event{}` | `E#event.id` |
|
||||
| `Event` | `pubkey` | `#event{}` | `E#event.public_key` |
|
||||
| `Event` | `created_at` | `#event{}` | `E#event.created_at` |
|
||||
| `Event` | `kind` | `#event{}` | `E#event.kind` |
|
||||
|
||||
`nostrlib` Erlang library will be divided in many modules, some of
|
||||
them will be available for common developers, and others will be used
|
||||
as internal functions to deal with different kind of data or
|
||||
implementing new features.
|
||||
|
||||
## Regular Expression Definition
|
||||
|
||||
Note: those regexes have not been tested. They are put it as reminder.
|
||||
|
||||
```erlang
|
||||
{ok, Regex_PrivateKey} = re:compile(<<"^[0-9a-f]{64}$">>, [extended,anchored]).
|
||||
Regex_PrivateKey = Regex_PublicKey = Regex_EventId.
|
||||
|
||||
{ok, Regex_Signature} = re:compile( <<"^[0-9a-f]{128}$">>, [extended,anchored]).
|
||||
|
||||
{ok, Regex_Prefix} = re:compile(<<"^[0-9a-f]{48,64}$">>, [extended,anchored]).
|
||||
|
||||
{ok, Regex_Content} = re:compile(<<"^$">>, [unicode,anchored,noteol])
|
||||
```
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -9,6 +9,17 @@ what was their ideas.
|
||||
**All articles MUST be licensed under [CC
|
||||
BY-NC-ND](https://creativecommons.org/licenses/by-nc-nd/4.0/)**.
|
||||
|
||||
- [Implementing nostr Client in Erlang](0003-implementing-nostr-client-in-erlang)
|
||||
- [From Erlang to nostr](0002-from-erlang-to-nostr)
|
||||
- [Create Github Actions Workflow](0001-create-github-actions-workflow)
|
||||
| Date | Title | Author | Notes |
|
||||
|------------|-----------------------------|--------|-------|
|
||||
| 2023-03-09 | [Nostr NIP/01 in Pure Erlang](0005-implementing-nip-01-standard-in-pure-erlang) | Mathieu Kerjouan | R25
|
||||
| 2023-03-09 | [Schnorr signature scheme in Erlang](0004-schnorr-signature-scheme-in-erlang) | Mathieu Kerjouan | R25
|
||||
| 2023-02-25 | [Implementing nostr Client in Erlang](0003-implementing-nostr-client-in-erlang) | Mathieu Kerjouan | R25
|
||||
| 2023-02-10 | [From Erlang to nostr](0002-from-erlang-to-nostr) | Mathieu Kerjouan | R25
|
||||
| 2023-02-10 | [Create Github Actions Workflow](0001-create-github-actions-workflow) | Mathieu Kerjouan | R25
|
||||
|
||||
The codes presented in these articles are usually tested under OpenBSD
|
||||
and ParrotLinux (Debian-like distribution) with the latest major
|
||||
release of Erlang (R25).
|
||||
|
||||
These articles are also available in EPUB, PDF and HTML files. A
|
||||
template is available in [`_template`](_template) directory.
|
||||
|
||||
11
notes/_template/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
date: 2023-02-25
|
||||
title:
|
||||
subtitle:
|
||||
author:
|
||||
keywords:
|
||||
license: CC BY-NC-ND
|
||||
abstract:
|
||||
---
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
,stdlib
|
||||
,cowboy
|
||||
,gun
|
||||
,crypto
|
||||
,public_key
|
||||
]}
|
||||
,{optional_applications, []}
|
||||
,{env, []}
|
||||
@@ -29,10 +31,10 @@
|
||||
,nostr_manager_relay_sup
|
||||
,nostr_manager_client_sup
|
||||
]}
|
||||
,{licenses, ["MIT"]}
|
||||
,{links
|
||||
,["https://erlang-punch.com"
|
||||
,{licenses, ["MIT"]}
|
||||
,{links
|
||||
,["https://erlang-punch.com"
|
||||
,"https://github.com/erlang-punch/nostr"
|
||||
]}
|
||||
]
|
||||
]
|
||||
}.
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
%%%===================================================================
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%% @doc nostr_client module is the main interface to communicate with
|
||||
%%% all the nostr Erlang client application. It will offer all
|
||||
%%% important function to be used by anyone.
|
||||
%%%
|
||||
%%% == Examples ==
|
||||
%%%
|
||||
%%% ```
|
||||
%%% % load records
|
||||
%%% rr(nostrlib).
|
||||
%%%
|
||||
%%% % set some variables
|
||||
%%% Host = "relay.nostrich.de".
|
||||
%%% Filter = #filter{ limit = 1 }.
|
||||
%%%
|
||||
%%% % create a new connection
|
||||
%%% {ok, Connection} = nostr_client:connect(Host).
|
||||
%%%
|
||||
%%% % create a new subscription
|
||||
%%% {ok, Subscription} = nostr_client:request(Host, Filter).
|
||||
%%%
|
||||
%%% % close the current active connection
|
||||
%%% ok = nostrlib_client:close(Host, Subscription).
|
||||
%%%
|
||||
%%% % send and event
|
||||
%%% Opts = [{private_key, PrivateKey}].
|
||||
%%% ok = nostrlib_client:event(Host, text_note, <<"hello">>, Opts).
|
||||
%%% '''
|
||||
%%%
|
||||
%%% @todo replace host by another ID.
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-module(nostr_client).
|
||||
-export([connect/1, connect/2]).
|
||||
-export([event/4]).
|
||||
-export([request/2, request/3]).
|
||||
-export([close/2, close/3]).
|
||||
-include_lib("kernel/include/logger.hrl").
|
||||
-include("nostrlib.hrl").
|
||||
|
||||
@@ -23,6 +51,7 @@
|
||||
-spec connect(Host) -> Return when
|
||||
Host :: host(),
|
||||
Return :: any(). % TODO: check the return function of gun module.
|
||||
|
||||
connect(Host) ->
|
||||
DefaultOptions = [],
|
||||
connect(Host, DefaultOptions).
|
||||
@@ -36,5 +65,131 @@ connect(Host) ->
|
||||
Host :: host(),
|
||||
Options :: options(),
|
||||
Return :: any(). % TODO: check the return function of gun module.
|
||||
|
||||
connect(Host, Options) ->
|
||||
nostr_client_connection:start([{host, Host}, {options, Options}]).
|
||||
nostr_manager_client_sup:start_client_sup([{host, Host}, {options, Options}]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `event/4' function send an event to an active connection.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec event(Host, Kind, Content, Opts) -> Return when
|
||||
Host :: string(),
|
||||
Kind :: atom(),
|
||||
Content :: bitstring(),
|
||||
Opts :: proplists:proplists(),
|
||||
Return :: ok.
|
||||
|
||||
event(Host, set_metadata, Content, Opts)
|
||||
when is_map(Content) ->
|
||||
case get_connection(Host) of
|
||||
{ok, Connection} ->
|
||||
Metadata = thoas:encode(Content),
|
||||
Event = #event{ kind = set_metadata, content = Metadata },
|
||||
{ok, Payload} = nostrlib:encode(Event, Opts),
|
||||
nostr_client_connection:send_raw(Connection, Payload);
|
||||
Elsewise -> Elsewise
|
||||
end;
|
||||
event(Host, text_note, Content, Opts)
|
||||
when is_binary(Content) ->
|
||||
case get_connection(Host) of
|
||||
{ok, Connection} ->
|
||||
Event = #event{ kind = text_note, content = Content},
|
||||
{ok, Payload} = nostrlib:encode(Event, Opts),
|
||||
nostr_client_connection:send_raw(Connection, Payload);
|
||||
Elsewise -> Elsewise
|
||||
end;
|
||||
event(Host, recommend_server, Content, Opts) ->
|
||||
case nostrlib_url:check(Content, Opts) of
|
||||
{ok, Url} ->
|
||||
case get_connection(Host) of
|
||||
{ok, Connection} ->
|
||||
Event = #event{ kind = recommend_server, content = Url },
|
||||
{ok, Payload} = nostrlib:encode(Event, Opts),
|
||||
nostr_client_connection:send_raw(Connection, Payload);
|
||||
Elsewise -> Elsewise
|
||||
end;
|
||||
Elsewise -> Elsewise
|
||||
end;
|
||||
event(_,Kind,_,_) ->
|
||||
{error, [{kind, Kind},{message, unsupported}]}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `request/2'
|
||||
%%
|
||||
%% @see request/3
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec request(Host, Filter) -> Return when
|
||||
Host :: string(),
|
||||
Filter :: decoded_filter(),
|
||||
Return :: {ok, bitstring()}.
|
||||
|
||||
request(Host, Filter) ->
|
||||
request(Host, Filter, []).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `request/3' function send a request to an active connection.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec request(Host, Filter, Opts) -> Return when
|
||||
Host :: string(),
|
||||
Filter :: decoded_filter(),
|
||||
Opts :: proplists:proplists(),
|
||||
Return :: {ok, bitstring()}.
|
||||
|
||||
request(Host, Filter, Opts) ->
|
||||
case get_connection(Host) of
|
||||
{ok, Connection} ->
|
||||
SubscriptionId = nostrlib:new_subscription_id(),
|
||||
Request = #request{ subscription_id = SubscriptionId
|
||||
, filter = Filter },
|
||||
{ok, Payload} = nostrlib:encode(Request, Opts),
|
||||
ok = nostr_client_connection:send_raw(Connection, Payload),
|
||||
{ok, SubscriptionId};
|
||||
Elsewise -> Elsewise
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `close/2'
|
||||
%%
|
||||
%% @see close/3
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec close(Host, SubscriptionId) -> Return when
|
||||
Host :: string(),
|
||||
SubscriptionId :: binary(),
|
||||
Return :: ok.
|
||||
|
||||
close(Host, SubscriptionId) ->
|
||||
close(Host, SubscriptionId, []).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `close/3' function closes an active subscription.
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec close(Host, SubscriptionId, Opts) -> Return when
|
||||
Host :: string(),
|
||||
SubscriptionId :: iodata(),
|
||||
Opts :: proplists:proplists(),
|
||||
Return :: ok.
|
||||
|
||||
close(Host, SubscriptionId, Opts) ->
|
||||
case get_connection(Host) of
|
||||
{ok, Connection} ->
|
||||
Close = #close{ subscription_id = SubscriptionId },
|
||||
{ok, Payload} = nostrlib:encode(Close, Opts),
|
||||
nostr_client_connection:send_raw(Connection, Payload);
|
||||
Elsewise -> Elsewise
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @hidden
|
||||
%%--------------------------------------------------------------------
|
||||
get_connection(Host) ->
|
||||
case pg:get_members(client, {Host, connection}) of
|
||||
[] -> {error, [{host, Host}, {connection, not_connected}]};
|
||||
[Connection] -> {ok, Connection}
|
||||
end.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
%%% ```
|
||||
%%% % create a new connection to relay.nostrich.de
|
||||
%%% {ok, Pid} = nostr_client_connection:start([{host, "relay.nostrich.de"}]).
|
||||
%%%
|
||||
%%%
|
||||
%%% % craft a request
|
||||
%%% Subscription = nostrlib_client:create_subscription_id().
|
||||
%%% Req = [<<"REQ">>, Subscription, #{kinds => [0,1],limit => 10}].
|
||||
@@ -370,4 +370,3 @@ handle_info(Message, State) ->
|
||||
websocket_message_router(Message, State) ->
|
||||
Host = proplists:get_value(host, State#state.arguments, undefined),
|
||||
nostr_client_router:raw_pool(Host, Message).
|
||||
|
||||
|
||||
@@ -81,4 +81,3 @@ spec_controller(Args) ->
|
||||
Return :: supervisor:startchild_ret().
|
||||
start_controller(Pid, Args) ->
|
||||
supervisor:start_child(Pid, spec_controller(Args)).
|
||||
|
||||
|
||||
@@ -100,8 +100,12 @@ terminate(_Reason, _State) ->
|
||||
% receive a raw message and do all the parser/router magic
|
||||
handle_cast({raw, Data} = Message, State) ->
|
||||
?LOG_DEBUG("~p", [{?MODULE, self(), cast, Message, State}]),
|
||||
Parsed = nostrlib_decoder:decode(Data),
|
||||
?LOG_DEBUG("~p", [{?MODULE, self(), parsed, Parsed, State}]),
|
||||
case nostrlib:decode(Data) of
|
||||
{ok, Parsed, Labels} ->
|
||||
?LOG_DEBUG("~p", [{?MODULE, self(), parsed, {Parsed, Labels}, State}]);
|
||||
Elsewise ->
|
||||
?LOG_DEBUG("~p", [{?MODULE, self(), parsed, Elsewise, State}])
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
handle_cast(Message, State) ->
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
%%% == Examples ==
|
||||
%%%
|
||||
%%% ```
|
||||
%%% nostr_client_router_sup:start_link([{host, "relay.nostrich.de"}]).
|
||||
%%% pg:get_members(client, {"relay.nostrich.de", router}).
|
||||
%%% Host = "relay.nostrich.de".
|
||||
%%% nostr_client_router_sup:start_link([{host, Host}]).
|
||||
%%% pg:get_members(client, {Host, router}).
|
||||
%%% '''
|
||||
%%%
|
||||
%%% @end
|
||||
@@ -51,7 +52,7 @@ supervisor() ->
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
children(Args) ->
|
||||
[ spec_router(Args) || _ <- lists:seq(0, 9)].
|
||||
[ spec_router(Args) || _ <- lists:seq(0, 20)].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
|
||||
1712
src/nostrlib.erl
@@ -10,8 +10,6 @@
|
||||
-export([event/1]).
|
||||
-export([request/2]).
|
||||
-export([close/1]).
|
||||
-export([create_subscription_id/0]).
|
||||
-export([create_event_id/1]).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("nostrlib.hrl").
|
||||
|
||||
@@ -37,36 +35,6 @@ request(SubscriptionId, Filters)
|
||||
when is_list(Filters) ->
|
||||
[<<"REQ">>, SubscriptionId, filters(Filters)].
|
||||
|
||||
-spec request_test() -> any().
|
||||
request_test() ->
|
||||
In_001 = request(<<"721983390570381">>, #{
|
||||
kinds => [0,1,2,7],
|
||||
since => 1676057052,
|
||||
limit => 450
|
||||
}),
|
||||
Out_001 = [
|
||||
<<"REQ">>,
|
||||
<<"721983390570381">>,
|
||||
#{ kinds => [0,1,2,7], since => 1676057052, limit => 450}
|
||||
],
|
||||
?assertEqual(Out_001, In_001),
|
||||
|
||||
% Check if the conversion from atom to integer is working
|
||||
In_002 = request(
|
||||
<<"721983390570381">>,
|
||||
#{ kinds => [nostrlib:kind(0)
|
||||
,nostrlib:kind(1)
|
||||
,nostrlib:kind(2)
|
||||
,nostrlib:kind(7)],
|
||||
since => 1676057052, limit => 450
|
||||
}),
|
||||
Out_002 = [
|
||||
<<"REQ">>,
|
||||
<<"721983390570381">>,
|
||||
#{ kinds => [0,1,2,7], since => 1676057052, limit => 450
|
||||
}],
|
||||
?assertEqual(Out_002, In_002).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @TODO this function is used internally and should not be exported
|
||||
%%--------------------------------------------------------------------
|
||||
@@ -133,61 +101,3 @@ close(SubscriptionId)
|
||||
when is_bitstring(SubscriptionId) ->
|
||||
[<<"CLOSE">>, SubscriptionId].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc create_subscription_id/0 generate a new subscription id based
|
||||
%% on crypto:strong_rand_bytes/1 function and returning a
|
||||
%% random integer as string.
|
||||
%% @end
|
||||
%% @TODO review the way to generate subscription id.
|
||||
%%--------------------------------------------------------------------
|
||||
-spec create_subscription_id() -> Return when
|
||||
Return :: pos_integer().
|
||||
create_subscription_id() ->
|
||||
<<SubscriptionIdRaw:64/integer>> = crypto:strong_rand_bytes(8),
|
||||
erlang:integer_to_binary(SubscriptionIdRaw).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc create_event_id/1 generates a compatible checksum based on
|
||||
%% the given event.
|
||||
%%
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec create_event_id(Event) -> Return when
|
||||
Event :: event(),
|
||||
Return :: iodata().
|
||||
create_event_id(Event) ->
|
||||
SerializedEvent = serialize_event(Event),
|
||||
SerializedJson = thoas:encode(SerializedEvent),
|
||||
BinaryHash = crypto:hash(sha256, SerializedJson),
|
||||
UppercaseHash = << <<(erlang:integer_to_binary(X, 16))/bitstring>>
|
||||
|| <<X:4>> <= BinaryHash >>,
|
||||
string:lowercase(UppercaseHash).
|
||||
|
||||
-spec create_event_id_test() -> any().
|
||||
create_event_id_test() ->
|
||||
% a random event from an open relay
|
||||
Event = #{ <<"content">> => <<240,159,164,153>>
|
||||
, <<"created_at">> => 1676749221
|
||||
, <<"id">> => <<"5b5479e7adc2a7902572c2ee5325c2db6c31097fa7f4b86bb7e586d3ee7249ea">>
|
||||
, <<"kind">> => 7
|
||||
, <<"pubkey">> => <<"52e98835d909f73315eb391faa203506aa30bc533290a937a0c84db3eba16573">>
|
||||
, <<"sig">> => <<"1e7e9802c604482e0d9b076bc6d89d37135ac00bbffce4a0bd9162f8d52569ed71db43c6a0c435005b2087fec8af260b86d30437a867b1984bc8cab99e607b30">>
|
||||
, <<"tags">> =>[[<<"p">>,<<"7b3f7803750746f455413a221f80965eecb69ef308f2ead1da89cc2c8912e968">>,<<"wss://relay.damus.io">>]
|
||||
,[<<"e">>,<<"b221a746d78058a7a3403bba4b0b123d36c827635e3eba0dcf8f564e9fc013d4">>,<<"wss://nostr-pub.wellorder.net">>,<<"root">>]
|
||||
,[<<"e">>,<<"d8ca85941e1aab6a475736f20ef6c7b62c77477899594b5d2ac011eba5282954">>]
|
||||
,[<<"p">>,<<"7ecd3fe6353ec4c53672793e81445c2a319ccf0a298a91d77adcfa386b52f30d">>]]
|
||||
},
|
||||
#{ <<"id">> := Id } = Event,
|
||||
?assertEqual(Id, create_event_id(Event)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
serialize_event(#{ <<"pubkey">> := PublicKey
|
||||
, <<"created_at">> := CreatedAt
|
||||
, <<"kind">> := Kind
|
||||
, <<"tags">> := Tags
|
||||
, <<"content">> := Content
|
||||
} = _Event) ->
|
||||
[0 ,PublicKey,CreatedAt,Kind,Tags,Content].
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
%%%===================================================================
|
||||
%%% @doc DRAFT: `nostrlib_decoder' module offers a way to decode the message
|
||||
%%% coming from the connection and used in the router. The generated
|
||||
%%% Erlang term is a record (or a tuple if used in other language).
|
||||
%%%
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-module(nostrlib_decoder).
|
||||
-export([decode/1]).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("nostrlib_decoder.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% spec used for eunit
|
||||
%%--------------------------------------------------------------------
|
||||
-spec test() -> any().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc `decode/1' decodes a raw message coming from a connection,
|
||||
%% parse it with thoas and then convert it to a record defined in
|
||||
%% `nostrlib_decoder.hrl' file.
|
||||
%%
|
||||
%% @todo creates more test
|
||||
%% @todo creates examples
|
||||
%% @see thoas:decode/1
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode(Bitstring) -> Return when
|
||||
Bitstring :: bitstring(),
|
||||
Return :: decoded_messages().
|
||||
decode(Bitstring) ->
|
||||
case thoas:decode(Bitstring) of
|
||||
{ok, Json} ->
|
||||
decode_message(Json);
|
||||
Elsewise -> Elsewise
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Random messages from open relay
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode_test() -> any().
|
||||
decode_test() ->
|
||||
% decode a valid json message of kind 1
|
||||
IN_001 = <<"[\"EVENT\",\"8034879165223001\",{\"id\":\"2f0e96269",
|
||||
"b7ece13f63c39a26fd0b3ee1e6a41afd4f8aab0cb8afdcb9ab3",
|
||||
"a64e\",\"pubkey\":\"a3eb29554bd27fca7f53f66272e4bb5",
|
||||
"9d066f2f31708cf341540cb4729fbd841\",\"created_at\":",
|
||||
"1677096834,\"kind\":1,\"tags\":[[\"e\",\"5aec5f6b41",
|
||||
"844cd9fd7635f98fcd5ac814ac1618fc8a896d78011973ebc17",
|
||||
"6ec\"],[\"p\",\"e623bb2e90351b30818de33debd506aa9ea",
|
||||
"e04d8268be65ceb2dcc1ef6881765\"],[\"p\",\"472f440f2",
|
||||
"9ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669",
|
||||
"301e\"]],\"content\":\"#[2] has convinced me\",\"si",
|
||||
"g\":\"430506421243e3cf0e737efd22101d38765ddd5235a9d",
|
||||
"47602adf0c5a4dbe63a9c9d6e267375bfcfb4d95d1558135bd9",
|
||||
"2ad26b1b86437ff27c63ff713d2825e3\"}]">>,
|
||||
OUT_001 = {ok, #subscription{ id = <<"8034879165223001">>
|
||||
, content = #event{ id = <<"2f0e96269b7ece13f63c39a26fd0b3ee1e6a41afd4f8aab0cb8afdcb9ab3a64e">>
|
||||
, public_key = <<"a3eb29554bd27fca7f53f66272e4bb59d066f2f31708cf341540cb4729fbd841">>
|
||||
, created_at = 1677096834
|
||||
, kind = 1
|
||||
, tags = [[<<"e">>,<<"5aec5f6b41844cd9fd7635f98fcd5ac814ac1618fc8a896d78011973ebc176ec">>]
|
||||
,[<<"p">>,<<"e623bb2e90351b30818de33debd506aa9eae04d8268be65ceb2dcc1ef6881765">>]
|
||||
,[<<"p">>,<<"472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e">>]
|
||||
]
|
||||
, content = <<"#[2] has convinced me">>
|
||||
, signature = <<"430506421243e3cf0e737e"
|
||||
,"fd22101d38765ddd5235a9"
|
||||
,"d47602adf0c5a4dbe63a9c"
|
||||
,"9d6e267375bfcfb4d95d15"
|
||||
,"58135bd92ad26b1b86437f"
|
||||
,"f27c63ff713d2825e3">>}
|
||||
}
|
||||
},
|
||||
?assertEqual(OUT_001, decode(IN_001)),
|
||||
|
||||
% decode an unsupported but valid json message
|
||||
IN_002 = <<"[]">>,
|
||||
OUT_002 = {error, {unsupported, []}},
|
||||
?assertEqual(OUT_002, decode(IN_002)),
|
||||
|
||||
% decode another valid json with utf8 symbol.
|
||||
IN_003 = <<91,34,69,86,69,78,84,34,44,34,49,53,52,56,48,50,
|
||||
48,48,48,57,54,57,53,48,56,51,49,51,53,34,44,123,
|
||||
34,105,100,34,58,34,48,52,56,53,98,56,48,97,100,
|
||||
98,54,97,102,49,48,100,51,53,56,48,99,52,52,57,97,
|
||||
57,98,49,101,50,102,54,57,54,55,55,102,49,52,101,
|
||||
54,49,98,49,54,100,102,97,53,100,101,51,51,98,49,
|
||||
99,54,54,56,101,102,53,102,97,34,44,34,112,117,98,
|
||||
107,101,121,34,58,34,56,97,57,56,49,102,49,97,101,
|
||||
51,102,97,98,51,51,48,48,98,53,52,56,99,52,102,50,
|
||||
48,54,53,52,99,98,48,102,49,100,51,53,48,52,57,56,
|
||||
99,52,98,54,54,56,52,57,98,55,51,101,56,53,52,54,
|
||||
48,48,49,100,99,97,48,34,44,34,99,114,101,97,116,
|
||||
101,100,95,97,116,34,58,49,54,55,55,51,48,53,56,
|
||||
54,49,44,34,107,105,110,100,34,58,49,44,34,116,97,
|
||||
103,115,34,58,91,91,34,101,34,44,34,102,57,55,97,
|
||||
100,56,54,48,49,51,53,102,102,52,49,50,53,49,51,
|
||||
52,98,57,48,52,100,57,49,102,57,53,52,98,55,49,54,
|
||||
100,54,50,52,50,51,49,57,54,57,101,50,48,49,97,97,
|
||||
49,54,99,99,50,55,54,55,100,55,49,97,57,34,93,44,
|
||||
91,34,112,34,44,34,100,101,57,48,99,53,100,98,51,
|
||||
54,97,52,48,49,49,102,57,100,53,56,52,100,102,99,
|
||||
49,56,100,101,49,97,53,55,50,52,54,56,54,56,54,55,
|
||||
57,56,52,55,57,51,101,102,53,50,54,51,51,49,98,53,
|
||||
49,102,56,98,52,51,101,57,34,93,93,44,34,99,111,
|
||||
110,116,101,110,116,34,58,34,68,97,109,110,32,119,
|
||||
101,108,108,44,32,73,32,99,97,110,226,128,153,116,
|
||||
32,101,118,101,110,32,99,111,117,110,116,32,116,
|
||||
104,97,116,33,34,44,34,115,105,103,34,58,34,52,54,
|
||||
52,49,50,102,54,49,99,100,100,49,48,54,102,102,98,
|
||||
51,52,101,57,101,55,49,52,99,57,56,99,48,54,100,
|
||||
97,55,55,51,51,98,100,51,100,53,55,98,101,100,53,
|
||||
98,53,55,100,101,49,97,55,57,97,53,55,50,98,102,
|
||||
99,99,101,53,50,98,55,97,102,52,50,97,97,50,54,
|
||||
100,53,101,48,50,56,48,52,97,97,53,54,56,102,53,
|
||||
55,54,99,49,97,99,56,54,53,100,55,98,53,57,102,97,
|
||||
54,51,48,52,100,55,97,53,55,99,101,99,99,57,98,99,
|
||||
53,57,49,99,34,125,93>>,
|
||||
OUT_003 = {ok,#subscription{ id = <<"1548020009695083135">>
|
||||
, content = #event{ id = <<"0485b80adb6af10d3580c449a9b1e2f69677f14e61b16dfa5de33b1c668ef5fa">>
|
||||
, public_key = <<"8a981f1ae3fab3300b548c4f20654cb0f1d350498c4b66849b73e8546001dca0">>
|
||||
, created_at = 1677305861
|
||||
, kind = 1
|
||||
, tags = [[<<"e">>,<<"f97ad860135ff4125134b904d91f954b716d624231969e201aa16cc2767d71a9">>]
|
||||
,[<<"p">>,<<"de90c5db36a4011f9d584dfc18de1a5724686867984793ef526331b51f8b43e9">>]]
|
||||
, content = <<68,97,109,110,32,119,101,108,108,44,32,73,32,99,97
|
||||
,110,226,128,153,116,32,101,118,101,110,32,99,111
|
||||
,117,110,116,32,116,104,97,116,33>>
|
||||
, signature = <<"46412f61cdd106ffb34e9e714c98c06da"
|
||||
,"7733bd3d57bed5b57de1a79a572bfcce5"
|
||||
,"2b7af42aa26d5e02804aa568f576c1ac8"
|
||||
,"65d7b59fa6304d7a57cecc9bc591c">>
|
||||
}
|
||||
}
|
||||
},
|
||||
?assertEqual(OUT_003, decode(IN_003)).
|
||||
|
||||
% kind 0
|
||||
% <<"[\"EVENT\",\"17771556064953075220123\",{\"id\":\"433e78562c17101284e130026c0b4ac82a41d442576f92fae929b6fbe441cd3b\",\"pubkey\":\"02748827a1016a393c780aec1d96191a3b8df1c397d09351029cbb25b2d83443\",\"created_at\":1676898304,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"berean\\\",\\\"nip05\\\":\\\"berean@nostrplebs.com\\\",\\\"picture\\\":\\\"https://nostr.build/i/p/nostr.build_b764966a4970638de60956883c63fc4e0a8d8bf1d7c54e0da5562f402e716c2c.jpg\\\",\\\"banner\\\":\\\"\\\",\\\"about\\\":\\\"Don't Trust. Verify.\\\\n#Bitcoin #Plebchain\\\",\\\"lud06\\\":\\\"\\\",\\\"lud16\\\":\\\"lastingbead65@walletofsatoshi.com\\\",\\\"username\\\":\\\"berean\\\",\\\"display_name\\\":\\\"berean\\\",\\\"displayName\\\":\\\"\\\",\\\"website\\\":\\\"\\\",\\\"Tags\\\":\\\"#bitcoin #nostr #plebchain\\\",\\\"nip05valid\\\":true,\\\"followingCount\\\":157,\\\"followersCount\\\":153}\",\"sig\":\"1252bfa38c4b93a9551ec04e141e57bbaf1b3696446790bb8c8af50d50f3e3c5f903926e9f4114abc9a3e7f44221d504864599447ffa6fb244e00f1929f951cc\"}]">>
|
||||
% <<"[\"EVENT\",\"17771556064953075220123\",{\"id\":\"5a18e9add8877b0df36bf1500a16bcacc0e446c7b330b52cda08ecb8f3021656\",\"pubkey\":\"9d96589eee0e57d07a5f1877285f42e8618e40ab2b94546a04dcad5eb8cbd0e8\",\"created_at\":1676898679,\"kind\":0,\"tags\":[],\"content\":\"{\\\"display_name\\\":\\\"\\\",\\\"website\\\":\\\"\\\",\\\"name\\\":\\\"machasm\\\",\\\"about\\\":\\\"Total Nostr Noob\\\",\\\"lud06\\\":\\\"lnbc10n1p3lyhprpp5fnftakvwgxp9vsgn2xdv47n276vksc564juld7umgn79fuzs9xgqdqqcqzpgxqyz5vqsp5c2vzcfgvax0tgzc5smg835g665k3px72yt44qtjfsafjcclkgzvq9qyyssqejmspclfnd8val39mnmy8grjn5hkpxemue4tw4kd94uky8cmk9j9re9fu8vtnv0mjeclatyvsdp7hkzu7vl8uml9rg6m4k866xtkawspx4nc93\\\",\\\"banner\\\":\\\"\\\",\\\"picture\\\":\\\"https://imgur.com/gallery/r6m07Dk\\\",\\\"nip05\\\":\\\"machasm@nostrplebs.com\\\",\\\"nip05_updated_at\\\":1676845714}\",\"sig\":\"8524a862fd059a6dc9d0199888f77b531487c9962e99e3753f9640da0c4680b4361db60d07aa44e03ed23ce71478366137c9b0c40748ed710c3929168637f66a\"}]">>
|
||||
|
||||
% kind 2
|
||||
% <<"[\"EVENT\",\"17771556064953075220111\",{\"id\":\"379345c8843c72bbe3c4165df7cb4cf7d88e88964d7b2440a9024ce12ca7140d\",\"pubkey\":\"d0872ed8cd4ef83ab9fc56841dedaee15866aa80eb811959b294e627757a6819\",\"created_at\":1672028822,\"kind\":2,\"tags\":[],\"content\":\"wss://nostr.orangepill.dev\",\"sig\":\"d5b8e7fc9928ec6d40452f9f1be05aca6303f9da85ee128052ce77fc2f27b66629ce4ee5c93231b4f0946d1709f71d3d25c7236dfcbef0a5eb90e86db8683e9a\"}]">>
|
||||
% <<"[\"EVENT\",\"17771556064953075220111\",{\"id\":\"3cd9aae254c99e4e31ca4ddaa8cc85cb030dcebcd64088bd490d2118eb19aad7\",\"pubkey\":\"b8060b54a86d9a8fab04328ce134f0f2f20d2e2c67c128932d0bfb3732abf1f6\",\"created_at\":1672044455,\"kind\":2,\"tags\":[],\"content\":\"wss://nostr-pub.wellorder.net\",\"sig\":\"4900c0eef7040c63960a99d75c31d3473fe7afc687e6ad337483c7bc8dcbc1793867da316dfa94bd69fffe3c8ee3f27422f739af9383026ef6c8fa3bbe15c55b\"}]">>
|
||||
|
||||
% kind 7
|
||||
% <<"[\"EVENT\",\"17771556064953075220\",{\"id\":\"6bef2129264acfe7fb43d33b418e40f69d511922c7203a1739fff933a80073fc\",\"pubkey\":\"a2d9f796461e3926e82d6ff02661be5fc57d3d6b3b31b6aaf76344db8280e331\",\"created_at\":1677314334,\"kind\":7,\"tags\":[[\"e\",\"7ed274a08644586b6bb369276d58197908dcdec0381d3dfac6328d336a62d69e\"],[\"p\",\"69074169ed68fa74c37d3926359f4100635c37eea5cfece064ed022ed06f792b\"]],\"content\":\"+\",\"sig\":\"bb9aa9df016baf7a8b7c7e302af3f4a9d716aeb734aee22d51e6cfb7df92d1ac2542aceb254533c958169e3f0622ce12b9d6257dac9dd42673b9ab0f21cc3706\"}]">>
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc internal function used to decode every element of a JSON
|
||||
%% encoded message and convert it to record.
|
||||
%%
|
||||
%% @todo creates more test.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
% decode an event message from client to relay
|
||||
decode_message([<<"EVENT">>, Event]) ->
|
||||
decode_message_event(Event);
|
||||
|
||||
% decode an event message from relay to client
|
||||
decode_message([<<"EVENT">>, SubscriptionId, Event]) ->
|
||||
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
|
||||
{ok, ParsedEvent} = decode_message_event(Event),
|
||||
Return = #subscription{ id = ParsedSubscriptionId
|
||||
, content = ParsedEvent
|
||||
},
|
||||
{ok, Return};
|
||||
|
||||
% decode a subscription request
|
||||
decode_message([<<"REQ">>, SubscriptionId, Filter]) ->
|
||||
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
|
||||
{ok, ParsedFilter} = decode_message_filter(Filter),
|
||||
Return = #subscription{ id = ParsedSubscriptionId
|
||||
, content = ParsedFilter
|
||||
},
|
||||
{ok, Return};
|
||||
|
||||
% decode a end of subscription request
|
||||
decode_message([<<"CLOSE">>, SubscriptionId]) ->
|
||||
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
|
||||
Return = #close{ subscription_id = ParsedSubscriptionId },
|
||||
{ok, Return};
|
||||
|
||||
% decode a notice message
|
||||
decode_message([<<"NOTICE">>, Notice]) ->
|
||||
{ok, ParsedNotice} = decode_message_notice(Notice),
|
||||
Return = #notice{ message = ParsedNotice },
|
||||
{ok, Return};
|
||||
|
||||
% decode an end of subscription message
|
||||
decode_message([<<"EOSE">>, SubscriptionId]) ->
|
||||
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
|
||||
Return = #eose{ id = ParsedSubscriptionId },
|
||||
{ok, Return};
|
||||
|
||||
decode_message(Message) ->
|
||||
{error, {unsupported, Message}}.
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
decode_message_event(#{ <<"id">> := EventId
|
||||
, <<"pubkey">> := PublicKey
|
||||
, <<"created_at">> := CreatedAt
|
||||
, <<"kind">> := Kind
|
||||
, <<"tags">> := Tags
|
||||
, <<"content">> := Content
|
||||
, <<"sig">> := Signature
|
||||
}) ->
|
||||
Event = #event{ id = EventId
|
||||
, created_at = CreatedAt
|
||||
, public_key = PublicKey
|
||||
, kind = Kind
|
||||
, tags = Tags
|
||||
, content = Content
|
||||
, signature = Signature
|
||||
},
|
||||
{ok, Event}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
decode_message_subscription_id(SubscriptionId)
|
||||
when is_bitstring(SubscriptionId) ->
|
||||
{ok, SubscriptionId}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
decode_message_notice(Notice)
|
||||
when is_bitstring(Notice) ->
|
||||
Notice = #notice{ message = Notice },
|
||||
{ok, Notice}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
decode_message_filter(#{ <<"ids">> := EventIds
|
||||
, <<"authors">> := Authors
|
||||
, <<"kinds">> := Kinds
|
||||
, <<"#e">> := TagEventIds
|
||||
, <<"#p">> := TagPublicKey
|
||||
, <<"since">> := Since
|
||||
, <<"until">> := Until
|
||||
, <<"limit">> := Limit
|
||||
}) ->
|
||||
Filter = #filter{ event_ids = EventIds
|
||||
, authors = Authors
|
||||
, kinds = Kinds
|
||||
, tag_event_ids = TagEventIds
|
||||
, tag_public_keys = TagPublicKey
|
||||
, since = Since
|
||||
, until = Until
|
||||
, limit = Limit
|
||||
},
|
||||
{ok, Filter}.
|
||||
39
src/nostrlib_event.erl
Normal file
@@ -0,0 +1,39 @@
|
||||
%%%===================================================================
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% == Examples ==
|
||||
%%%
|
||||
%%% ```
|
||||
%%% nostrlib_event:create(metadata, #{}
|
||||
%%% '''
|
||||
%%%
|
||||
%%% Here the manual steps to check a message.
|
||||
%%%
|
||||
%%% ```
|
||||
%%% % 1. read the file (or take it from the wild)
|
||||
%%% {ok, M} = file:read_file("test/nostrlib_SUITE_data/valid_event_kind1.json").
|
||||
%%%
|
||||
%%% % 2. decode the json
|
||||
%%% {ok, J} = thoas:decode(M).
|
||||
%%%
|
||||
%%% % 3. decode the message and convert it to record
|
||||
%%% {ok,{_,_,E}} = nostrlib_decode:message(J).
|
||||
%%%
|
||||
%%% % 4. serialize the message
|
||||
%%% X = nostrlib_event:serialize(E).
|
||||
%%%
|
||||
%%% % 5. create the hash and convert others values from hex to binary
|
||||
%%% HashMessage = crypto:hash(sha256, X).
|
||||
%%% PublicKey = nostrlib:hex_to_binary(E#event.public_key).
|
||||
%%% Signature = nostrlib:hex_to_binary(E#event.signature).
|
||||
%%%
|
||||
%%% % 6. verify the message with the public key and the signature.
|
||||
%%% true = nostrlib_schnorr:verify(HashMessage, PublicKey, Signature).
|
||||
%%% '''
|
||||
%%%
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-module(nostrlib_event).
|
||||
|
||||
|
||||
|
||||
5
src/nostrlib_identity.erl
Normal file
@@ -0,0 +1,5 @@
|
||||
%%%===================================================================
|
||||
%%% @doc DRAFT
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-module(nostrlib_identity).
|
||||
@@ -1,66 +0,0 @@
|
||||
%%%===================================================================
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-module(nostrlib_kind).
|
||||
-export([metadata/3, text_note/1]).
|
||||
-export([recommend_server/1, recommend_server/2]).
|
||||
-include("nostrlib.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec metadata(Username, About, Picture) -> Return when
|
||||
Username :: to_be_defined(),
|
||||
About :: to_be_defined(),
|
||||
Picture :: to_be_defined(),
|
||||
Return :: map().
|
||||
metadata(Username, About, Picture) ->
|
||||
#{ name => Username
|
||||
, about => About
|
||||
, picture => Picture
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec text_note(Content) -> Return when
|
||||
Content :: to_be_defined(),
|
||||
Return :: map().
|
||||
text_note(Content) ->
|
||||
text_note(#{}, Content).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec text_note(Map, Content) -> Return when
|
||||
Map :: map(),
|
||||
Content :: to_be_defined(),
|
||||
Return :: map().
|
||||
text_note(Map, Content) ->
|
||||
maps:put(content, Content, Map).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec recommend_server(ServerUrl) -> Return when
|
||||
ServerUrl :: to_be_defined(),
|
||||
Return :: map().
|
||||
recommend_server(ServerUrl) ->
|
||||
recommend_server(#{}, ServerUrl).
|
||||
|
||||
-spec recommend_server(Map, ServerUrl) -> Return when
|
||||
Map :: map(),
|
||||
ServerUrl :: to_be_defined(),
|
||||
Return :: map().
|
||||
recommend_server(Map, ServerUrl) ->
|
||||
text_note(Map, ServerUrl).
|
||||
@@ -1,22 +0,0 @@
|
||||
%%%===================================================================
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-module(nostrlib_relay).
|
||||
-export([notice/1]).
|
||||
-include("nostrlib.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc notice/1 function returns a notification message to the
|
||||
%% client.
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec notice(Message) -> Return when
|
||||
Message :: iodata(),
|
||||
Return :: any().
|
||||
|
||||
notice(Message)
|
||||
when is_bitstring(Message) ->
|
||||
[<<"NOTICE">>, Message].
|
||||
1035
src/nostrlib_schnorr.erl
Normal file
@@ -1,19 +0,0 @@
|
||||
%%%===================================================================
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-module(nostrlib_secp256k1).
|
||||
-export([create_keys/0]).
|
||||
-include("nostrlib.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec create_keys() -> Return when
|
||||
Return :: {PublicKey, PrivateKey},
|
||||
PublicKey :: iodata(),
|
||||
PrivateKey :: iodata().
|
||||
create_keys() ->
|
||||
crypto:generate_key(ecdh, secp256k1).
|
||||
@@ -1,67 +0,0 @@
|
||||
%%%===================================================================
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
|
||||
%%%===================================================================
|
||||
-module(nostrlib_tags).
|
||||
-export([e/2, event/2]).
|
||||
-export([p/2, public_key/2]).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("nostrlib.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% extra-specification for eunit.
|
||||
%%--------------------------------------------------------------------
|
||||
-spec test() -> any().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc an alias for event.
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec e(EventId, RecommendedRelayUrl) -> Return when
|
||||
EventId :: iodata(),
|
||||
RecommendedRelayUrl :: iodata(),
|
||||
Return :: any().
|
||||
|
||||
e(EventId, RecommendedRelayUrl) ->
|
||||
event(EventId, RecommendedRelayUrl).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec event(EventId, RecommendedRelayUrl) -> Return when
|
||||
EventId :: iodata(),
|
||||
RecommendedRelayUrl :: iodata(),
|
||||
Return :: any().
|
||||
|
||||
event(EventId, RecommendedRelayUrl) ->
|
||||
["e", EventId, RecommendedRelayUrl].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc An alias for publickey
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec p(PublicKey, RecommendedRelayUrl) -> Return when
|
||||
PublicKey :: iodata(),
|
||||
RecommendedRelayUrl :: iodata(),
|
||||
Return :: any().
|
||||
|
||||
p(PublicKey, RecommendedRelayUrl) ->
|
||||
public_key(PublicKey, RecommendedRelayUrl).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec public_key(PublicKey, RecommendedRelayUrl) -> Return when
|
||||
PublicKey :: iodata(),
|
||||
RecommendedRelayUrl :: iodata(),
|
||||
Return :: any().
|
||||
|
||||
public_key(PublicKey, RecommendedRelayUrl) ->
|
||||
["p", PublicKey, RecommendedRelayUrl].
|
||||
87
src/nostrlib_url.erl
Normal file
@@ -0,0 +1,87 @@
|
||||
%%%===================================================================
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%%===================================================================
|
||||
-module(nostrlib_url).
|
||||
-export([check/1]).
|
||||
-export([check_hostname/1]).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
-spec test() -> any().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
-spec check(Url) -> Return when
|
||||
Url :: iodata(),
|
||||
Return :: {ok, Url}
|
||||
| {error, proplists:proplists()}.
|
||||
|
||||
check(Url) ->
|
||||
CheckHost = fun(Hostname, VUrl) ->
|
||||
case check_hostname(Hostname) of
|
||||
{ok, Hostname} ->
|
||||
{ok, VUrl};
|
||||
Elsewise ->
|
||||
Elsewise
|
||||
end
|
||||
end,
|
||||
case uri_string:parse(Url) of
|
||||
#{host := Host, path := <<>>, scheme := <<"ws">>} ->
|
||||
CheckHost(Host, Url);
|
||||
#{host := Host, path := <<>>, scheme := <<"wss">>} ->
|
||||
CheckHost(Host, Url);
|
||||
#{scheme := Scheme, path := <<>>} ->
|
||||
{error, [{scheme, Scheme}]};
|
||||
#{path := Path} ->
|
||||
{error, [{path, Path}]}
|
||||
end.
|
||||
|
||||
|
||||
-spec check_test() -> any().
|
||||
check_test() ->
|
||||
[?assertEqual({ok, <<"wss://rsslay.fiatjaf.com">>}
|
||||
, check(<<"wss://rsslay.fiatjaf.com">>))
|
||||
,?assertEqual({ok, <<"wss://somerelay.com">>}
|
||||
, check(<<"wss://somerelay.com">>))
|
||||
,?assertEqual({ok, <<"ws://somerelay.com">>}
|
||||
, check(<<"ws://somerelay.com">>))
|
||||
,?assertEqual({ok, <<"wss://d.a.c.are.somerelay.com">>}
|
||||
, check(<<"wss://d.a.c.are.somerelay.com">>))
|
||||
,?assertEqual({error, [{path, <<"/test">>}]}
|
||||
, check(<<"wss://somerelay.com/test">>))
|
||||
,?assertEqual({error, [{scheme, <<"https">>}]}
|
||||
,check(<<"https://httprelay.com">>))
|
||||
,?assertEqual({error, [{hostname, <<"httprelay_!.com">>}]}
|
||||
,check(<<"wss://httprelay_!.com">>))
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
-spec check_hostname() -> iodata().
|
||||
check_hostname() ->
|
||||
Pattern = <<"(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)">>,
|
||||
{ok, Regex} = re:compile(Pattern),
|
||||
Regex.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
-spec check_hostname(Hostname) -> Return when
|
||||
Hostname :: bitstring(),
|
||||
Return :: {ok, Hostname}
|
||||
| {error, proplists:proplists()}.
|
||||
check_hostname(Hostname) ->
|
||||
Regex = check_hostname(),
|
||||
case re:run(Hostname, Regex) of
|
||||
{match, _} ->
|
||||
{ok, Hostname};
|
||||
_ ->
|
||||
{error, [{hostname, Hostname}]}
|
||||
end.
|
||||
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%%====================================================================
|
||||
%%% @doc
|
||||
%%%
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @end
|
||||
%%%====================================================================
|
||||
-module(nostrlib_SUITE).
|
||||
-export([suite/0]).
|
||||
-export([init_per_suite/1, end_per_suite/1]).
|
||||
-export([init_per_group/2, end_per_group/2]).
|
||||
-export([init_per_testcase/2, end_per_testcase/2]).
|
||||
-export([groups/0, all/0]).
|
||||
-export([common/0, common/1]).
|
||||
-export([valid_json/0, valid_json/1]).
|
||||
-export([encode_event/0, encode_event/1]).
|
||||
-export([encode_request/0, encode_request/1]).
|
||||
-export([encode_close/0, encode_close/1]).
|
||||
-export([encode_eose/0, encode_eose/1]).
|
||||
-export([encode_subscription/0, encode_subscription/1]).
|
||||
-export([encode_notice/0, encode_notice/1]).
|
||||
-export([decode_event/0, decode_event/1]).
|
||||
-export([decode_close/0, decode_close/1]).
|
||||
-export([decode_notice/0, decode_notice/1]).
|
||||
-export([decode_request/0, decode_request/1]).
|
||||
-export([decode_subscription/0, decode_subscription/1]).
|
||||
-export([decode_eose/0, decode_eose/1]).
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include("nostrlib.hrl").
|
||||
|
||||
-spec suite() -> any().
|
||||
-spec init_per_suite(any()) -> any().
|
||||
-spec end_per_suite(any()) -> any().
|
||||
@@ -18,20 +36,514 @@
|
||||
-spec groups() -> any().
|
||||
-spec all() -> any().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
suite() -> [{timetrap,{minutes,10}}].
|
||||
|
||||
init_per_suite(Config) -> Config.
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
init_per_suite(_Config) -> [].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
end_per_suite(_Config) -> ok.
|
||||
|
||||
init_per_group(_GroupName, Config) -> Config.
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
init_per_group(_GroupName, Config) -> Config.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
end_per_group(_GroupName, _Config) -> ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
init_per_testcase(_TestCase, Config) -> Config.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
end_per_testcase(_TestCase, _Config) -> ok.
|
||||
|
||||
groups() -> [].
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
groups() -> [{encode, [parallel], [encode_event
|
||||
,encode_request
|
||||
,encode_close
|
||||
,encode_eose
|
||||
,encode_subscription
|
||||
,encode_notice
|
||||
]}
|
||||
,{decode, [parallel], [decode_event
|
||||
,decode_close
|
||||
,decode_notice
|
||||
,decode_request
|
||||
,decode_subscription
|
||||
,decode_eose
|
||||
]}
|
||||
,{common, [parallel], [common
|
||||
,valid_json]}
|
||||
].
|
||||
|
||||
all() -> [].
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
all() -> [{group, encode, [parallel]}
|
||||
,{group, decode, [parallel]}
|
||||
,{group, common, [parallel]}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec common() -> Return when
|
||||
Return :: any().
|
||||
common() -> [].
|
||||
|
||||
-spec common(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
common(_Config) ->
|
||||
<<_:256/bitstring>> = nostrlib:new_subscription_id(),
|
||||
|
||||
{error, [{encode, unsupported}]}
|
||||
= nostrlib:encode(#{}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec valid_json() -> Return when
|
||||
Return :: any().
|
||||
valid_json() -> [].
|
||||
|
||||
-spec valid_json(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
valid_json(Config) ->
|
||||
DataDir = proplists:get_value(data_dir, Config),
|
||||
Files = [ "valid_event_request.json"
|
||||
, "valid_event_kind0.json"
|
||||
, "valid_event_kind1.json"
|
||||
, "valid_event_kind2.json"
|
||||
, "valid_event_kind7.json"
|
||||
],
|
||||
Test = fun Test([]) -> ok;
|
||||
Test([File|Rest]) ->
|
||||
FullPath = filename:join(DataDir, File),
|
||||
{ok, Json} = file:read_file(FullPath),
|
||||
{ok, Decoded, Labels} = nostrlib:decode(Json),
|
||||
ct:pal(info, "decoded ~p (~p): ~n~p", [File, Labels, Decoded]),
|
||||
{ok, Encoded} = nostrlib:encode(Decoded),
|
||||
ct:pal(info, "encoded ~p: ~n~p", [File, Encoded]),
|
||||
Test(Rest)
|
||||
end,
|
||||
Test(Files),
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode_request() -> Return when
|
||||
Return :: any().
|
||||
encode_request() -> [].
|
||||
|
||||
-spec encode_request(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
encode_request(_Config) ->
|
||||
PrivateKey = <<1:256>>,
|
||||
Opts = [{private_key, PrivateKey}],
|
||||
|
||||
% a subcription_id must be defined
|
||||
{error, [{subscription_id, undefined}]}
|
||||
= nostrlib:encode(#request{},Opts),
|
||||
|
||||
% a subscription id can't be an atom
|
||||
{error, [{subscription_id, test}]}
|
||||
= nostrlib:encode(#request{subscription_id = test}, Opts),
|
||||
|
||||
% a filter can't be null
|
||||
{error, [{filter, []}]}
|
||||
= nostrlib:encode(#request{subscription_id = <<"1234">>, filter = []}, Opts),
|
||||
|
||||
% a filter can't be null
|
||||
{error, [{filter, #{}}]}
|
||||
= nostrlib:encode(#request{subscription_id = <<"1234">>}, Opts),
|
||||
|
||||
% limit can't be negative
|
||||
{error,[{limit,-10}]}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ limit = -10 }}),
|
||||
|
||||
% a limit is a positive integer
|
||||
{ok,<<"[\"REQ\",\"1234\",{\"limit\":10}]">>}
|
||||
= nostrlib:encode(#request{subscription_id = <<"1234">>, filter = #filter{ limit = 10 }}, Opts),
|
||||
|
||||
% since can't be an integer
|
||||
{error,[{since,123}]}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ since = 123 }}),
|
||||
|
||||
% since is an universal date
|
||||
{ok,<<"[\"REQ\",\"test\",{\"since\":1577836800}]">>}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ since = {{2020,1,1},{0,0,0} }}}),
|
||||
|
||||
% until can't be an integer
|
||||
{error,[{until,123}]}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ until = 123 }}),
|
||||
|
||||
% until is an universal date
|
||||
{ok,<<"[\"REQ\",\"test\",{\"until\":1577836800}]">>}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ until = {{2020,1,1},{0,0,0} }}}),
|
||||
|
||||
% an author can be a full id
|
||||
{ok,<<"[\"REQ\",\"test\",{\"authors\":[\"0000000000000000000000000000000000000000000000000000000000000001\"]}]">>}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ authors = [<<1:256>>] }}),
|
||||
|
||||
% an author can be a prefix
|
||||
{ok,<<"[\"REQ\",\"test\",{\"authors\":[\"0000000000000001\"]}]">>}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ authors = [<<1:64>>] }}),
|
||||
|
||||
% a prefix can't be smaller than 32bits
|
||||
{error,[{prefix,<<0,0,1>>}]}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ authors = [<<1:24>>] }}),
|
||||
|
||||
% an event id can be a prefix
|
||||
{ok,<<"[\"REQ\",\"test\",{\"ids\":[\"0000000000000001\"]}]">>}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ event_ids = [<<1:64>>] }}),
|
||||
|
||||
% an event id in a tag can't be a prefix
|
||||
{error,[{content,[<<0,0,0,0,0,0,0,1>>]}]}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ tag_event_ids = [<<1:64>>] }}),
|
||||
|
||||
% an event id in a tag is a full address
|
||||
{ok,<<"[\"REQ\",\"test\",{\"#e\":[\"0000000000000000000000000000000000000000000000000000000000000001\"]}]">>}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ tag_event_ids = [<<1:256>>] }}),
|
||||
|
||||
% a public key in a tag can't be a prefix
|
||||
{error,[{content,[<<0,0,0,0,0,0,0,1>>]}]}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ tag_public_keys = [<<1:64>>] }}),
|
||||
|
||||
% a public key in a tag must be complete
|
||||
{ok,<<"[\"REQ\",\"test\",{\"#p\":[\"0000000000000000000000000000000000000000000000000000000000000001\"]}]">>}
|
||||
= nostrlib:encode(#request{ subscription_id = <<"test">>, filter = #filter{ tag_public_keys = [<<1:256>>] }}).
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode_subscription() -> Return when
|
||||
Return :: any().
|
||||
encode_subscription() -> [].
|
||||
|
||||
-spec encode_subscription(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
encode_subscription(_Config) ->
|
||||
PrivateKey = <<1:256>>,
|
||||
Opts = [{private_key, PrivateKey}],
|
||||
|
||||
{error,[{id,undefined}]}
|
||||
= nostrlib:encode(#subscription{}),
|
||||
|
||||
{error,[{content,undefined}]}
|
||||
= nostrlib:encode(#subscription{ id = <<"test">> }),
|
||||
|
||||
{error,[{content,undefined}]}
|
||||
= nostrlib:encode(#subscription{ id = <<"test">>, content = <<>> }),
|
||||
|
||||
{error,[{content,[]}]}
|
||||
= nostrlib:encode(#subscription{ id = <<"test">>, content = [] }),
|
||||
|
||||
|
||||
Event = <<"{\"content\":\"hello\",\"created_at\":1577836800,\"id\":\"1c00f47c449d3d1da178a8287aa06ecf5664af22d1c2dad1f1ecdaac664a7b27\",\"kind\":0,\"pubkey\":\"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"sig\":\"47ae0058e481c23aa34bf36dde38cf012f42df75febcd66aa30716b8e1e8fbdf7bd731cfd55680898b82f9ea5c4334b66590fb566db70f8c498daf5c9fc16b30\",\"tags\":[]}">>,
|
||||
Subscription = <<"[\"EVENT\",\"test\"," , Event/bitstring , "]">>,
|
||||
|
||||
{ok, Subscription}
|
||||
= nostrlib:encode(#subscription{ id = <<"test">>
|
||||
, content = #event{ content = <<"hello">>
|
||||
, kind = set_metadata
|
||||
, created_at = {{2020,1,1},{0,0,0}}
|
||||
}
|
||||
}, Opts),
|
||||
|
||||
{ok, Subscription}
|
||||
= nostrlib:encode(#subscription{ id = <<"test">>
|
||||
, content = Event }, Opts).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode_notice() -> Return when
|
||||
Return :: any().
|
||||
encode_notice() -> [].
|
||||
|
||||
-spec encode_notice(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
encode_notice(_Config) ->
|
||||
% a message must be defined
|
||||
{error, [{message, undefined}]}
|
||||
= nostrlib:encode(#notice{}),
|
||||
|
||||
% a message can't be an atom
|
||||
{error,[{message,test}]}
|
||||
= nostrlib:encode(#notice{ message = test }),
|
||||
|
||||
% a message must be a bitstring
|
||||
{ok,<<"[\"NOTICE\",\"test\"]">>}
|
||||
= nostrlib:encode(#notice{ message = <<"test">> }).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode_event() -> Return when
|
||||
Return :: any().
|
||||
encode_event() -> [].
|
||||
|
||||
-spec encode_event(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
encode_event(_Config) ->
|
||||
PrivateKey = <<1:256>>,
|
||||
Opts = [{private_key, PrivateKey}],
|
||||
|
||||
{error,[{content,undefined}]}
|
||||
= nostrlib:encode(#event{}),
|
||||
|
||||
{error,[{kind,undefined}]}
|
||||
= nostrlib:encode(#event{content = <<>>}),
|
||||
|
||||
{error,[{kind,[]}]}
|
||||
= nostrlib:encode(#event{kind = [], content = <<>>}),
|
||||
|
||||
{error,[{public_key,undefined},{private_key,undefined}]}
|
||||
= nostrlib:encode(#event{kind = set_metadata, content = <<>>}),
|
||||
|
||||
{error,[{public_key,<<>>}]}
|
||||
= nostrlib:encode(#event{kind = set_metadata, content = <<>>, public_key = <<>> }),
|
||||
|
||||
{error,[{private_key, <<1:255>>}]}
|
||||
= nostrlib:encode(#event{kind = set_metadata, content = <<>>}
|
||||
,[{private_key, <<1:255>>}]),
|
||||
|
||||
{ok, <<_/bitstring>>}
|
||||
= nostrlib:encode(#event{kind = set_metadata, content = <<>>}, Opts),
|
||||
|
||||
{error, [{created_at,{}}]}
|
||||
= nostrlib:encode(#event{kind = set_metadata, content = <<>>, public_key = <<1:256>>
|
||||
, created_at = {}}
|
||||
,Opts),
|
||||
|
||||
{error,[{signature,<<>>}]}
|
||||
= nostrlib:encode(#event{kind = set_metadata, content = <<>>, public_key = <<1:256>>
|
||||
, signature = <<>> }
|
||||
,Opts),
|
||||
|
||||
{error,[{event_id,[]}]}
|
||||
= nostrlib:encode(#event{kind = set_metadata, content = <<>>, public_key = <<1:256>>
|
||||
, id = []}
|
||||
,Opts),
|
||||
|
||||
% encore a metadata event
|
||||
Event0 = #event{ kind = set_metadata
|
||||
, content = thoas:encode(#{ about => <<"test">> })
|
||||
},
|
||||
{ok, R0} = nostrlib:encode(Event0, Opts),
|
||||
{ok, [<<"EVENT">>, #{<<"kind">> := 0}]} = thoas:decode(R0),
|
||||
|
||||
% encode a test note event
|
||||
Event1 = #event{ kind = text_note
|
||||
, content = <<"this is a note">>
|
||||
},
|
||||
{ok, R1} = nostrlib:encode(Event1, Opts),
|
||||
{ok, [<<"EVENT">>, #{<<"kind">> := 1}]} = thoas:decode(R1),
|
||||
|
||||
% encode a recommend server event
|
||||
Event2 = #event{ kind = recommend_server
|
||||
, content = <<"wss://myserver.local">>
|
||||
},
|
||||
{ok, R2} = nostrlib:encode(Event2, Opts),
|
||||
{ok, [<<"EVENT">>, #{<<"kind">> := 2}]} = thoas:decode(R2).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode_close() -> Return when
|
||||
Return :: any().
|
||||
encode_close() -> [].
|
||||
|
||||
-spec encode_close(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
encode_close(_Config) ->
|
||||
Close0 = #close{},
|
||||
{error, [{subscription_id, undefined}]} = nostrlib:encode(Close0),
|
||||
|
||||
Close1 = #close{ subscription_id = <<"test">> },
|
||||
{ok, C1} = nostrlib:encode(Close1),
|
||||
{ok, [<<"CLOSE">>, <<"test">>]} = thoas:decode(C1).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec encode_eose() -> Return when
|
||||
Return :: any().
|
||||
encode_eose() -> [].
|
||||
|
||||
-spec encode_eose(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
encode_eose(_Config) ->
|
||||
Eose0 = #eose{},
|
||||
{error, [{id, undefined}]} = nostrlib:encode(Eose0),
|
||||
Eose1 = #eose{ id = <<"test">> },
|
||||
{ok, E1} = nostrlib:encode(Eose1),
|
||||
{ok, [<<"EOSE">>, <<"test">>]} = thoas:decode(E1).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode_event() -> Return when
|
||||
Return :: any().
|
||||
decode_event() -> [].
|
||||
|
||||
-spec decode_event(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
decode_event(_Config) ->
|
||||
{error,[{event,{missing,id}}]}
|
||||
= nostrlib:decode(thoas:encode([<<"EVENT">>, #{}])),
|
||||
|
||||
{error,[{event,{bad,id}},{labels,[]}]}
|
||||
= nostrlib:decode(thoas:encode([<<"EVENT">>, #{ id => <<"test">> }])).
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode_close() -> Return when
|
||||
Return :: any().
|
||||
decode_close() -> [].
|
||||
|
||||
-spec decode_close(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
decode_close(_Config) ->
|
||||
{error,[{subscription_id,<<>>}]}
|
||||
= nostrlib:decode(thoas:encode([<<"CLOSE">>, <<>> ])),
|
||||
|
||||
{error,[{subscription_id,1}]}
|
||||
= nostrlib:decode(thoas:encode([<<"CLOSE">>, 1])),
|
||||
|
||||
{ok,#close{subscription_id = <<"test">>},[]}
|
||||
= nostrlib:decode(thoas:encode([<<"CLOSE">>, <<"test">>])).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode_notice() -> Return when
|
||||
Return :: any().
|
||||
decode_notice() -> [].
|
||||
|
||||
-spec decode_notice(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
decode_notice(_Config) ->
|
||||
{ok,#notice{message = <<>>},[]}
|
||||
= nostrlib:decode(thoas:encode([<<"NOTICE">>, <<>>])),
|
||||
|
||||
{ok,#notice{message = <<"test">>},[]}
|
||||
= nostrlib:decode(thoas:encode([<<"NOTICE">>, <<"test">>])).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode_request() -> Return when
|
||||
Return :: any().
|
||||
decode_request() -> [].
|
||||
|
||||
-spec decode_request(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
decode_request(_Config) ->
|
||||
{ok, #request{filter = #filter{kinds = [set_metadata]}}, _}
|
||||
= nostrlib:decode(thoas:encode([<<"REQ">>, <<"test">>, #{ kinds => [0] } ])),
|
||||
|
||||
{ok, #request{filter = #filter{tag_public_keys = [<<1:256>>]}}, _}
|
||||
= nostrlib:decode(thoas:encode([<<"REQ">>, <<"test">>, #{ <<"#p">> => [nostrlib:binary_to_hex(<<1:256>>)] } ])),
|
||||
|
||||
{ok, #request{filter = #filter{tag_event_ids = [<<1:256>>]}}, _}
|
||||
= nostrlib:decode(thoas:encode([<<"REQ">>, <<"test">>, #{ <<"#e">> => [nostrlib:binary_to_hex(<<1:256>>)] } ])),
|
||||
|
||||
{ok, #request{filter = #filter{event_ids = [<<1:256>>]}}, _}
|
||||
= nostrlib:decode(thoas:encode([<<"REQ">>, <<"test">>, #{ <<"ids">> => [nostrlib:binary_to_hex(<<1:256>>)] } ])),
|
||||
|
||||
{ok, #request{filter = #filter{authors = [<<1:256>>]}}, _}
|
||||
= nostrlib:decode(thoas:encode([<<"REQ">>, <<"test">>, #{ <<"authors">> => [nostrlib:binary_to_hex(<<1:256>>)] } ])),
|
||||
|
||||
{ok,#request{subscription_id = <<"test">>,
|
||||
filter = #filter{event_ids = [],authors = [],kinds = [],
|
||||
tag_event_ids = [],tag_public_keys = [],since = undefined,
|
||||
until = undefined,limit = undefined}}, []}
|
||||
= nostrlib:decode(thoas:encode([<<"REQ">>, <<"test">>, #{}])).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode_subscription() -> Return when
|
||||
Return :: any().
|
||||
decode_subscription() -> [].
|
||||
|
||||
-spec decode_subscription(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
decode_subscription(_Config) ->
|
||||
Event = #event{ kind = set_metadata, content = <<"test">>},
|
||||
Subscription = #subscription{ id = <<"test">>, content = Event },
|
||||
{ok, EncodedEvent} = nostrlib:encode(Subscription, [{private_key, <<1:256>>}]),
|
||||
|
||||
{error,{unsupported,[<<"EVENT">>,<<"123123123123">>,<<>>]}}
|
||||
= nostrlib:decode(thoas:encode([<<"EVENT">>, <<"123123123123">>, <<>>])),
|
||||
|
||||
{error,[{event,{missing,id}}]}
|
||||
= nostrlib:decode(thoas:encode([<<"EVENT">>, <<"123123123123">>, #{}])),
|
||||
|
||||
{ok, _Message, _Labels} = nostrlib:decode(EncodedEvent).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec decode_eose() -> Return when
|
||||
Return :: any().
|
||||
decode_eose() -> [].
|
||||
|
||||
-spec decode_eose(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
decode_eose(_Config) ->
|
||||
{error,{unsupported,[<<"EOSE">>]}}
|
||||
= nostrlib:decode(thoas:encode([<<"EOSE">>])),
|
||||
|
||||
{ok,#eose{id = <<"test">>},[]}
|
||||
= nostrlib:decode(thoas:encode([<<"EOSE">>, <<"test">>])).
|
||||
|
||||
1
test/nostrlib_SUITE_data/valid_eose.json
Normal file
@@ -0,0 +1 @@
|
||||
["EOSE","9635033750818420944"]
|
||||
1
test/nostrlib_SUITE_data/valid_event_kind0.json
Normal file
@@ -0,0 +1 @@
|
||||
["EVENT","5452643154455862",{"id":"5da65160faa8f1e0232f5e9f9f36267188581446ca955a71c756591ef0fed9f2","pubkey":"a96ed24d83fb80552b13734babd96562743ede16cb22c306189380c67ab0eb4f","created_at":1677417436,"kind":0,"tags":[],"content":"{\"banner\":\"https://cdn.cdnparenting.com/articles/2021/07/31125225/1817265176.webp\",\"website\":\"\",\"nip05\":\"coldpotato@nostrplebs.com\",\"picture\":\"https://void.cat/d/67H6AKeprGmKq7mp2b9n9r\",\"lud16\":\"coldpotato@stacker.news\",\"display_name\":\"Cold Potato ðâ¡ï¸\",\"about\":\"\",\"name\":\"coldpotato\",\"nip05valid\":false,\"username\":\"coldpotato\",\"displayName\":\"Cold Potato ðâ¡ï¸\",\"lud06\":\"\"}","sig":"266858908030fd70dfb4af8aae3aa7fb3339ba12215222c8a4877056c8d4dee1a520b90cc9a35f9ece38be6f2e4eb1998fe4b0a8864ad5c64e68507f7685fba0"}]
|
||||
1
test/nostrlib_SUITE_data/valid_event_kind1.json
Normal file
@@ -0,0 +1 @@
|
||||
["EVENT","5452643154455862",{"pubkey":"e623bb2e90351b30818de33debd506aa9eae04d8268be65ceb2dcc1ef6881765","content":"Rest isn't stagnation.","id":"1e76eecdcd101063df6c56afd58d90a1d9e81265ea255148c6fc9789d168420e","created_at":1677417294,"sig":"1b9096d0c8f48aed807f148a4b35b97cc9b28cc8ea698e4ea6d8fda405c463569eb8bed01a77596a4ffa2daab89b7601d08d4646d2f8a82e7cd63388bb1f88e1","kind":1,"tags":[]}]
|
||||
@@ -0,0 +1 @@
|
||||
["EVENT","5533953073697082",{"id":"bdcd9602ef0b2445aed07a6898ff2321684731c964f7f6df800424f5ecc38abd","pubkey":"307aac7d72b5d568f46bab349c1c2d45fda709d3d2d7a87b2973448f700310e2","created_at":1678194769,"kind":1,"tags":[["e","9bbc2195083dddd99aa633706cff7ed45248047bab03108a10b7336c79f9485d"],["p","8ebf24a6d1a0bb69f7bce5863fec286ff3a0ebafdff0e89e2ed219d83f219238"],["p","8ebf24a6d1a0bb69f7bce5863fec286ff3a0ebafdff0e89e2ed219d83f219238"]],"content":"boater","sig":"d9047f3c96c1dcbd2b28d03101d7cd7c20d1cdbf19373f11919bd06e394cf2a13eec3f2b2fc82e06bb0bec7c6310105f4eb8c9a514b45d0a602e08200b13f794"}]
|
||||
1
test/nostrlib_SUITE_data/valid_event_kind2.json
Normal file
@@ -0,0 +1 @@
|
||||
["EVENT","14983797176934273925",{"id":"e7f5850dd535feba822e35747d022ea7c29d7c0f226f75f08af433abb20357f6","pubkey":"5132e8bf2ac08dc03016dda748ab8c9c1207d595afb35b87539413b99932ad72","created_at":1675315873,"kind":2,"tags":[],"content":"wss://rsslay.fiatjaf.com","sig":"c0d56d5f45c0bebc97c980e42b906ea336d9a21806b3813bf819982e3ba1816d1673c3ae4c23366afac61e1bfb5cdc880318c5ef5808e28252cb14189ef2faad"}]
|
||||
1
test/nostrlib_SUITE_data/valid_event_kind7.json
Normal file
@@ -0,0 +1 @@
|
||||
["EVENT","5452643154455862",{"id":"fb9af93e73551efd48e381fe320bb543e7bc119a83b702628b76b1a7ac5e75ca","pubkey":"0e1314f29c0a64ec3679671c59dabaf655594e1ca6bbd8712366e3ac175a964f","created_at":1677417344,"kind":7,"tags":[["e","76918fd820d4165419796e699693123e1c47955c5d89d1fc91100f3b4235608d"],["p","c49f8402ef410fce5a1e5b2e6da06f351804988dd2e0bad18ae17a50fc76c221"]],"content":"+","sig":"0815450293a7c4e3079ae3d62baeba5e441ca026eb4a0fa8c145e8ad969673ad155cb2db7916393db6db0b562b06f86acc7dc38b5e1bad238aaa534a27f91161"}]
|
||||
1
test/nostrlib_SUITE_data/valid_event_request.json
Normal file
@@ -0,0 +1 @@
|
||||
["REQ","5452643154455862",{"kinds":[0,1,2,7],"since":1677330792,"limit":450}]
|
||||
174
test/nostrlib_schnorr_SUITE.erl
Normal file
@@ -0,0 +1,174 @@
|
||||
%%%====================================================================
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%%====================================================================
|
||||
-module(nostrlib_schnorr_SUITE).
|
||||
-export([suite/0]).
|
||||
-export([init_per_suite/1, end_per_suite/1]).
|
||||
-export([init_per_group/2, end_per_group/2]).
|
||||
-export([init_per_testcase/2, end_per_testcase/2]).
|
||||
-export([groups/0, all/0]).
|
||||
-export([verification_vectors/0, verification_vectors/1]).
|
||||
-export([signature_vectors/0, signature_vectors/1]).
|
||||
-export([common/0, common/1]).
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-spec suite() -> any().
|
||||
-spec init_per_suite(any()) -> any().
|
||||
-spec end_per_suite(any()) -> any().
|
||||
-spec init_per_group(any(), any()) -> any().
|
||||
-spec end_per_group(any(), any()) -> any().
|
||||
-spec init_per_testcase(any(), any()) -> any().
|
||||
-spec end_per_testcase(any(), any()) -> any().
|
||||
-spec groups() -> any().
|
||||
-spec all() -> any().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
suite() -> [{timetrap,{minutes,10}}].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
init_per_suite(_Config) -> [].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
end_per_suite(_Config) -> ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
init_per_group(_GroupName, Config) -> Config.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
end_per_group(_GroupName, _Config) -> ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
init_per_testcase(_TestCase, Config) -> Config.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
end_per_testcase(_TestCase, _Config) -> ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%--------------------------------------------------------------------
|
||||
groups() -> [{vectors, [parallel], [verification_vectors
|
||||
,signature_vectors
|
||||
,common]}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
all() -> [{group, vectors, [parallel]}].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec common() -> any().
|
||||
common() -> [].
|
||||
|
||||
-spec common(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
common(_Config) ->
|
||||
{ok, <<PrivateKey:256>>} = nostrlib_schnorr:new_privatekey(),
|
||||
{ok, <<_:256>>} = nostrlib_schnorr:new_publickey(PrivateKey).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec verification_vectors() -> any().
|
||||
verification_vectors() -> [].
|
||||
|
||||
-spec verification_vectors(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
verification_vectors(Config) ->
|
||||
DataDir = proplists:get_value(data_dir, Config),
|
||||
VerificationTest = fun (#{ <<"index">> := I
|
||||
, <<"message">> := M
|
||||
, <<"public key">> := P
|
||||
, <<"signature">> := S
|
||||
, <<"verification result">> := V }) ->
|
||||
Info = [{index, I}
|
||||
,{message, M}
|
||||
,{public_key, P}
|
||||
,{signature, S}
|
||||
,{result, V}
|
||||
],
|
||||
V = nostrlib_schnorr:verify(M, P, S),
|
||||
ct:pal(info, "verify (ok): ~p", [Info])
|
||||
end,
|
||||
lists:map(VerificationTest, test_vectors(DataDir)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec signature_vectors() -> any().
|
||||
signature_vectors() -> [].
|
||||
|
||||
-spec signature_vectors(Config) -> Return when
|
||||
Config :: proplists:proplists(),
|
||||
Return :: any().
|
||||
signature_vectors(Config) ->
|
||||
DataDir = proplists:get_value(data_dir, Config),
|
||||
SignatureTest = fun (#{ <<"secret key">> := <<>> }) ->
|
||||
ignored;
|
||||
(#{ <<"index">> := I
|
||||
, <<"message">> := M
|
||||
, <<"aux_rand">> := A
|
||||
, <<"signature">> := S
|
||||
, <<"secret key">> := K }) ->
|
||||
Info = [{index, I}
|
||||
,{message, M}
|
||||
,{secret_key, K}
|
||||
,{aux_rand, A}
|
||||
,{signature, S}
|
||||
],
|
||||
{ok, S} = nostrlib_schnorr:sign(M, K, A),
|
||||
ct:pal(info, "signature (ok): ~p", [Info])
|
||||
end,
|
||||
lists:map(SignatureTest, test_vectors(DataDir)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc internal function. read the test-vectors.csv file from
|
||||
%% BIP-0340 and converts it in an Erlang like format.
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
test_vectors(Directory) ->
|
||||
{ok, File} = file:read_file(filename:join(Directory, "test-vectors.csv")),
|
||||
Lines = re:split(File, "\r\n"),
|
||||
Fields = lists:map(fun(X) ->
|
||||
re:split(X, ",")
|
||||
end, Lines),
|
||||
Cleaned = lists:filter(fun _F([<<>>]) -> false; _F(_) -> true end, Fields),
|
||||
[Header|Content] = Cleaned,
|
||||
Zip = fun _Zip ([<<>>]) -> undefined;
|
||||
_Zip (C) -> Z = lists:zip(Header, C),
|
||||
M = maps:from_list(Z),
|
||||
maps:map(fun _F(<<"aux_rand">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
|
||||
_F(<<"secret key">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
|
||||
_F(<<"signature">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):512>>;
|
||||
_F(<<"public key">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
|
||||
_F(<<"message">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
|
||||
_F(<<"verification result">>, <<"FALSE">>) -> false;
|
||||
_F(<<"verification result">>, <<"TRUE">>) -> true;
|
||||
_F(_X, Y) -> Y
|
||||
end, M)
|
||||
end,
|
||||
lists:map(Zip, Content).
|
||||
16
test/nostrlib_schnorr_SUITE_data/test-vectors.csv
Normal file
@@ -0,0 +1,16 @@
|
||||
index,secret key,public key,aux_rand,message,signature,verification result,comment
|
||||
0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE,
|
||||
1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE,
|
||||
2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE,
|
||||
3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n
|
||||
4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE,
|
||||
5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve
|
||||
6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false
|
||||
7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message
|
||||
8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value
|
||||
9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0
|
||||
10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1
|
||||
11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve
|
||||
12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size
|
||||
13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order
|
||||
14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size
|
||||
|