From 1984e2054987a2f9a3aedb7cc68fef46f2caa731 Mon Sep 17 00:00:00 2001 From: niamtokik Date: Sun, 26 Feb 2023 14:33:56 +0000 Subject: [PATCH] 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. --- .gitignore | 1 + .tool-versions | 1 + Makefile | 41 +- README.md | 7 +- extra/README.md | 16 + extra/check_mod.py | 24 + extra/check_pow.py | 25 + extra/mod.erl | 56 + extra/pow.erl | 57 + include/nostrlib.hrl | 184 +- include/nostrlib_decode.hrl | 1 + include/nostrlib_decoder.hrl | 140 -- .../README.md | 552 ++++++ .../README_ANNEXE1.md | 184 ++ .../schnorr_sign.png | Bin 0 -> 54480 bytes .../schnorr_verify.png | Bin 0 -> 42563 bytes .../README.md | 650 +++++++ .../README_ANNEXE1.md | 97 + .../README_ANNEXE2.md | 61 + .../client_close.png | Bin 0 -> 18336 bytes .../client_event.png | Bin 0 -> 25217 bytes .../client_request.png | Bin 0 -> 31632 bytes .../nostr_diagram.png | Bin 0 -> 70840 bytes .../relay_notice.png | Bin 0 -> 12221 bytes notes/README.md | 17 +- notes/_template/README.md | 11 + src/nostr.app.src | 10 +- src/nostr_client.erl | 159 +- src/nostr_client_connection.erl | 3 +- src/nostr_client_controller_sup.erl | 1 - src/nostr_client_router.erl | 8 +- src/nostr_client_router_sup.erl | 7 +- src/nostrlib.erl | 1712 ++++++++++++++++- src/nostrlib_client.erl | 90 - src/nostrlib_decoder.erl | 263 --- src/nostrlib_event.erl | 39 + src/nostrlib_identity.erl | 5 + src/nostrlib_kind.erl | 66 - src/nostrlib_relay.erl | 22 - src/nostrlib_schnorr.erl | 1035 ++++++++++ src/nostrlib_secp256k1.erl | 19 - src/nostrlib_tags.erl | 67 - src/nostrlib_url.erl | 87 + test/nostrlib_SUITE.erl | 524 ++++- test/nostrlib_SUITE_data/valid_eose.json | 1 + .../valid_event_kind0.json | 1 + .../valid_event_kind1.json | 1 + .../valid_event_kind1_with_tags.json | 1 + .../valid_event_kind2.json | 1 + .../valid_event_kind7.json | 1 + .../valid_event_request.json | 1 + test/nostrlib_schnorr_SUITE.erl | 174 ++ .../test-vectors.csv | 16 + 53 files changed, 5700 insertions(+), 739 deletions(-) create mode 100644 extra/README.md create mode 100644 extra/check_mod.py create mode 100644 extra/check_pow.py create mode 100644 extra/mod.erl create mode 100644 extra/pow.erl create mode 100644 include/nostrlib_decode.hrl delete mode 100644 include/nostrlib_decoder.hrl create mode 100644 notes/0004-schnorr-signature-scheme-in-erlang/README.md create mode 100644 notes/0004-schnorr-signature-scheme-in-erlang/README_ANNEXE1.md create mode 100644 notes/0004-schnorr-signature-scheme-in-erlang/schnorr_sign.png create mode 100644 notes/0004-schnorr-signature-scheme-in-erlang/schnorr_verify.png create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/README.md create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE1.md create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE2.md create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/client_close.png create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/client_event.png create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/client_request.png create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/nostr_diagram.png create mode 100644 notes/0005-implementing-nip-01-standard-in-pure-erlang/relay_notice.png create mode 100644 notes/_template/README.md delete mode 100644 src/nostrlib_decoder.erl create mode 100644 src/nostrlib_event.erl create mode 100644 src/nostrlib_identity.erl delete mode 100644 src/nostrlib_kind.erl delete mode 100644 src/nostrlib_relay.erl create mode 100644 src/nostrlib_schnorr.erl delete mode 100644 src/nostrlib_secp256k1.erl delete mode 100644 src/nostrlib_tags.erl create mode 100644 src/nostrlib_url.erl create mode 100644 test/nostrlib_SUITE_data/valid_eose.json create mode 100644 test/nostrlib_SUITE_data/valid_event_kind0.json create mode 100644 test/nostrlib_SUITE_data/valid_event_kind1.json create mode 100644 test/nostrlib_SUITE_data/valid_event_kind1_with_tags.json create mode 100644 test/nostrlib_SUITE_data/valid_event_kind2.json create mode 100644 test/nostrlib_SUITE_data/valid_event_kind7.json create mode 100644 test/nostrlib_SUITE_data/valid_event_request.json create mode 100644 test/nostrlib_schnorr_SUITE.erl create mode 100644 test/nostrlib_schnorr_SUITE_data/test-vectors.csv diff --git a/.gitignore b/.gitignore index 6c4a77f..321bcf3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ _build rebar3.crashdump doc *~ +**.trace diff --git a/.tool-versions b/.tool-versions index 307a310..36ca1c6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ erlang 25.1.2 rebar 3.20.0 +pandoc 3.1.1 diff --git a/Makefile b/Makefile index a7f9047..1ad8ba0 100644 --- a/Makefile +++ b/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) diff --git a/README.md b/README.md index e2bff1b..b8528aa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/extra/README.md b/extra/README.md new file mode 100644 index 0000000..ba4277a --- /dev/null +++ b/extra/README.md @@ -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) diff --git a/extra/check_mod.py b/extra/check_mod.py new file mode 100644 index 0000000..e15dbda --- /dev/null +++ b/extra/check_mod.py @@ -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) diff --git a/extra/check_pow.py b/extra/check_pow.py new file mode 100644 index 0000000..ab46683 --- /dev/null +++ b/extra/check_pow.py @@ -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) diff --git a/extra/mod.erl b/extra/mod.erl new file mode 100644 index 0000000..28fd0dc --- /dev/null +++ b/extra/mod.erl @@ -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. diff --git a/extra/pow.erl b/extra/pow.erl new file mode 100644 index 0000000..5345d68 --- /dev/null +++ b/extra/pow.erl @@ -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. diff --git a/include/nostrlib.hrl b/include/nostrlib.hrl index bff5933..a6c217f 100644 --- a/include/nostrlib.hrl +++ b/include/nostrlib.hrl @@ -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 +%%%=================================================================== -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 -). diff --git a/include/nostrlib_decode.hrl b/include/nostrlib_decode.hrl new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/include/nostrlib_decode.hrl @@ -0,0 +1 @@ + diff --git a/include/nostrlib_decoder.hrl b/include/nostrlib_decoder.hrl deleted file mode 100644 index 6386de3..0000000 --- a/include/nostrlib_decoder.hrl +++ /dev/null @@ -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 -%%%=================================================================== --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(). diff --git a/notes/0004-schnorr-signature-scheme-in-erlang/README.md b/notes/0004-schnorr-signature-scheme-in-erlang/README.md new file mode 100644 index 0000000..66b70ea --- /dev/null +++ b/notes/0004-schnorr-signature-scheme-in-erlang/README.md @@ -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, <>} + = crypto:generate_key(ecdh, secp256k1). + +% generate a private key using crypto:strong_rand_bytes/1 +<> + = crypto:strong_rand_bytes(32). + +% generate a private key using nostrlib_schnorr:new_private_key/0. +{ok, <>} + = 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, <>} + = 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. +<> + = crypto:hash(sha256, <<"my data">>). + +% create a signature with default aux_data (set to 0). +{ok, <>} + = nostrlib_schnorr:sign(Message, PrivateKey). + +% create a signature with aux_data set to 0 (manually). +{ok, <>} + = nostrlib_schnorr:sign(Message, PrivateKey, <<0:256>>). +``` + +![Schnorr signature function diagram](schnorr_sign.png) + +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). +``` + +![Schnorr verify function diagram](schnorr_verify.png) + +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) diff --git a/notes/0004-schnorr-signature-scheme-in-erlang/README_ANNEXE1.md b/notes/0004-schnorr-signature-scheme-in-erlang/README_ANNEXE1.md new file mode 100644 index 0000000..1ba409a --- /dev/null +++ b/notes/0004-schnorr-signature-scheme-in-erlang/README_ANNEXE1.md @@ -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. +``` diff --git a/notes/0004-schnorr-signature-scheme-in-erlang/schnorr_sign.png b/notes/0004-schnorr-signature-scheme-in-erlang/schnorr_sign.png new file mode 100644 index 0000000000000000000000000000000000000000..132a19a34919bc9b0e7b00450cb9d98de5ba3945 GIT binary patch literal 54480 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

8^0&`lptP&d&{&nEg#evhi z*IMU0;~xGEX0n`>8$M~DEUJO;enBSw?Yo*n?ITN`PE&th78KQR=IK}HU@Nt>6YQ4i z>fQ5+<>*aSXn5+ps?sERzuu|VP;0gmT!x`5}+X>Y=(S=-f0=hQUfE(#i)S9qi^ zt7>Eyvu~Q6q8FO4R(-@$9x2alHD!8i?ut*nh>p7Sa9^?zSk`U0ex zA=EKtu-!c#wxrc>)u%Z(;cD zpD6tAd^#WU5YJ?>_P@T_-w81zQew_Nru+o49RGQ=48S6jQcL~xPgef>Trr5WYhO_I z=-;>a`-ce|*f9^;d)DAY-nU!2lK7n$d!$}O(H{dKZ8Nl&-^AM7mQqhU_rgltni#=f z#tVqx2#fnSuJ?tIPHdM}!u3meQJRRo#<6={Nv>U~Gw(^n7g#h~@jCbK*$ruyc?$TB z?p0rUnfZA{587+RyRmNizg$a{IO2UK-wTgO4 z7^H&%2T&?85UkwHH-&uLtj4|%TYtLJwCos%;$NHN%70M!uaM%ltEcL0y~^#HcZypbv)&<`R5No26Q5)X8oE2K?*CQ!)iyfT#quf-U&sXA9QuG zt}T?2|8&p8=1_@e%d3j(!5v8)9H&j1!Z*I;%0;?oXRD-?&Aq83?6*1S$qH-NFE33Me$bWb52N79>XG6WAsGcd!3NGiwS#EZ8s|FUG+b7>VN;AJN17oPO^CGJjIRvGVJeu*7;X7_G>k-AjA!l zq8fx+Fn~u#8T%-u=6Qg<-D?-BXNC}DQ_`0dg|+CZEoOVqGlk$y^Ivi|dmDHvSvJ*t zMbzdc-Q}m-M_&eR>i$d;?h9E~?5zJ*UMeJD`_-EBb3Z*r6vNLI^dmL!i{uqlv9Cvi zGR#17Nv|?8d5DP5SZ7l;S<7G%?@9Z9sa-1}R=d+~mHJ~1#Q>7bEYMyc0<|p2)9**j z87MJr(MyJd7?kyGr`p=_*f?_L_6R_hs>x!{xqoS&?wiD)?)lbLvlmMdEA4Z*|JLty zF95{=>|uTqV=xb^Bsg|B;@DC0ovmssf@Q`mqR|@bafQ861{`s}HP!A*HDu#{=Wqz8 z+O=_4Vb*wPer~R6kzKxs8p2aLt9c%0XL?>6Pq!dMuCW<&s#LZYu~>^K=G!d{oJIvq z_&M$l$D{u;=s3Ux6Pj?u!G%3o!46C?ew!diR=%@(-6uUV30vgx*FVAV<(nXR7MYoy zUw{^!A3G0*a>Wcg?!c+{G^6ODg!hup?)ns4ott&oZTzpnJXzItU*ufBkcS73jCC%G zI%GEK<;-?%@+H(q%6@GExAAMcuOc!bjvBM~;d6tCYv9D-qNuK?H8@_nP17Ea2Is}c z5|+>7_RCG1U!JwUJUrAfB}QUyb@Xp*kp?DpfjHswe_2hipup__%xAIlhRL7yG2;we zGA*E=?jOtdYu#`2AdBAEuqWcbZ7?A4?2zJ|(MbN6-S$K5b~#Vit$&&49rAxl=aLe_nkT$*-HscxV4MF@JgCzjU+yb@gV%ISk#K zY54QC$XbDOkT{$x{^!+K_2C!$bycZQ^8GuY{rSTjcs?zw3}NNJ4BTH=-$(p~sea=6 qe;~WRPW%5p3W%Bg-}g~=Wv=%h`+Sln{M;eQg- ziRgsrzB9S+=UdP7t@Zu={`0Oi%r)1!u5)%f`|PuS`$TG}DUgxeCBebLAyZP6)xyES z8^*!GZ6~@4?yU5r3gO@oMWE&M(T-j=2&6R*E1%3iSFF4|cFt~SRz6u)US10nipvUN zVe4w)=*Hz_jRvEBYaWryXc{Ug z7<$6w?fAU;v7IT}!ax^t_R4OC2DWg1q>Hwt7Rp&c)5%j?Lqyw9M^jHtMO)fM$Jt6t zTUFTr>S$x3sNv>};de&Z$T~sQL_8oKN)8?ZI+k9ZS~@ne&ip*u`k+S#RarSJD{Tx$ zPea%Rj3cMzq=+$8&~{PxM!Ksb(9&|&>QX{FZV1rED_{r5*x5Tf3L@=1A<8HX0}Br; z9eW`_pS&ep%SJ(4+rm*+URp_B9cmz?#3Sb>uLtplBa~p4n%+)o-c~4CYfGdaQc6Hd z(@|fUPtZm|)lJ%4PKF;YB8O1XkdZaCma)~gg;`iY-62@6b%8xeyP(}2rH~>5`bxSM zb_hi~1&D(M5Qemxh?a{i)J_2==Pakeqol86r{pFmuLg**a`E)E7eRq%l!f%9GcFkv1ih@l(C(HZF?Xf34X=?X!kwJn{Llq|i_E*{d#`~na) zn1z}JkC®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 literal 0 HcmV?d00001 diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/README.md b/notes/0005-implementing-nip-01-standard-in-pure-erlang/README.md new file mode 100644 index 0000000..c809cd9 --- /dev/null +++ b/notes/0005-implementing-nip-01-standard-in-pure-erlang/README.md @@ -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. + +![Nostr Infrastructure Diagram](nostr_diagram.png) + +## 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. + +![Diagram of a Client sending an Event](client_event.png) + +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. + +![Diagram of a Client request](client_request.png) + +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. + +![Diagram of a Client closing a subscription](client_close.png) + +```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. + +![Diagram of a relay forwarding events](client_request.png) + +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. + +![Diagram of a relay noticing client](relay_notice.png) + +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 diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE1.md b/notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE1.md new file mode 100644 index 0000000..847ba02 --- /dev/null +++ b/notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE1.md @@ -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}]). +``` diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE2.md b/notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE2.md new file mode 100644 index 0000000..43340ce --- /dev/null +++ b/notes/0005-implementing-nip-01-standard-in-pure-erlang/README_ANNEXE2.md @@ -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]) +``` diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/client_close.png b/notes/0005-implementing-nip-01-standard-in-pure-erlang/client_close.png new file mode 100644 index 0000000000000000000000000000000000000000..482b49be63368189b9c564abf628cf1ea549567f GIT binary patch literal 18336 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 a)n-C~xExSnfxo;%23FToD_6O5_kRHrqhXW) literal 0 HcmV?d00001 diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/client_event.png b/notes/0005-implementing-nip-01-standard-in-pure-erlang/client_event.png new file mode 100644 index 0000000000000000000000000000000000000000..ac7a03bf4cd7a76981fc045e66712334ac88ea23 GIT binary patch literal 25217 zcmeFZWmuHk_XZ3I3VIMk1qCS)mF|}AZiWtN28Lz^kWvIuN>Zg11e6wOh7c5_yI}z7 z96E-2_i)tT`M;mvkMDJP(DTfm{p?tKt$W?;-h^qW$rE3uyo`f`L#(JEqlJTmhsMFd zbtk+Co>aIf7khFuHi=n5Sg_4#M+}1%yUs93>CN1xv$>n6HF6++8CkNNE)l*XEmX_3Y z*H%!pRhM#bgn^fF^K(Jf6g(kZss_AT0&rPb84E2QK^-M^MQwc@J11MPXmWB&imq;I zT%JxU9x!QFH${1OJ6n4XCm|^Vd38xOh?T0Ng3V(%!bJz6Cm^RS1oISvB3yWMbA;)0^U;cvU+eOEe#z*P633Yq7uYT zMbMU4mzUqoM@nBtQV9W3Qn7--_;kD=_ByVDK8{{$lA1i~R(h7U4*Dt@1`2!#Jzej| za&{h2h%^tsuAYv)ldclz-p5B*+W?H-)!LTROAuM%~)U1ejk$HafB}c}G46 zbweE~@KUhmcHln`E?Fs0NvNv_w;Ehk5f0^of`>l3HdY8dSr;cSD{VPBX?r^l0To$S zeQhO#jurTr&sGt1pd=%$3DXkb*VBbrTY718Iv}Ji9i_bWc_Ff#4iEfy!v=dI)$s!yS2j zs(ZTSSeq*eLhE(+4xItY7dPe(&_H)~xzJ4Y9JTU#eVCD2UM+5w`;Yv7>I z$>+?YAfV->1|DiCt07>H`kp>2-WFE;I&M1Fir%W=xtFY$v=3a$;jx2|p$fvsT~}4b z-o{Bw5BoY^b#*0feGeBdLxjFNcwoUL>A+{DBqSv5CFo(TH1EA?2w zM@T?G73!mF;o>RlZ79zVh9#u{hp6dtDmd9lb2@9gDaZoLkg`@6dMpc(g*t2T8G;EY zOLFt-Tj=re@@YX7;Bq!v77A9*0y36-@{cuqz?7wweVnDe6;yas`2-x*1l*iF1T?)5B$)! zR5Mh!arZD3w1c?od&%p$aXUWdbJG!22k)^`0J~;rC8=+AR0d07HXE(0Rc|MtT?vT|2>J#ld8a1>=Ebr7a&6PFM-<}mx4Njnmk z8Sl{tqa-Dk^cOF`sG(0!U-%R_VR`1O({Cxlyod7T(8tx(xF##yD^0{GLAX@2wJusa zjiVLQ-nrhs3NL_iXxOA~dE3NJc8YDQ)QG044wFjY;Nt)OQNM?`ONcI`WuSb4gGWdg zc>W`hUIMWo^WRqVc){?{e{OWh{O_BA8=XS`HvjiK()fhvtG6G%3;SO`IMXTa|N8=7##5G&i{M?|IS?r1pKE&kIJ7p zfL9QrlP~-kJ?KtQCU9dtIw<|^f1Z(|gf9JO<}NRArYGnx$nsqK-#qD%?pJ>=0d`DN zq__>$(idOa-2LA?!NT16-xnn6|Nn*gy~O`tEQ~%|bZRQQfq_9xY%BvKBV$Zj8i%f~ zE*Tlw-EZH%kzK!TiKOInxYyFsq9+i#MFQ+#q2%vo;OWpYFi4*sZ>!Igy3WWg4dy4^ zASb896=h;lR8m&XG_CO=qoH9ZARv%?GbH}(>CuOc?q3`6U?r zhMXFz(RrVV{#abm^ZMh%ZR@?&5xHC9)4|t2+V^GkW~owzy?F6S_sDB?m>z>`KGzyw zY|}x>fTZ=?>EU*nlCbansF>gSc;OupDGECO-Sm$g2F?(Wv+C2O1)Nsytc^j0J(ut0 z8li4 zh6pNwGGrGOzhm@3E~M?(k5|?^i#^peX+5 zyaoB;4;Gqnr1|Z=HTK&bD0A!a>PQk$A1!z0V2}$xFdE}`8nw!M@)bJ7uBfUSKT!>* z${o|u(Ye*<$9tR@AAg%=;$vo}qOoyu33RAazgFvPwfn=fe9X#P6Dg{OP09Q3`lbU4O+%kP zeR@U?-e#4joR*ZtBHZkMw6|6an-I1M14c*mvK<9V(DIxxyPf-` z#J;bIyiID<&vCx}ttG3d_n?98NX_Q}DDCuwtF+v+-%gY+8b5*EgqewnDZNh&oL6wB z1`o8&sxB)xh7dD8dNf|~tAL~I9nq(i!n#vtfypQC9aoE5OoQP`|KNB0J;^x)9z@@l z`m(83QlOke*)e5KV|wWs8IoJ+^`oUw>wz3yZvSHhM}mWsW-uJ;$3*>SvY+AuJB^0; z+C<>%tj{iyBuq_B8B%w3mR|1`+F&y(i=&qg>0g%+UsFZwUw<_4*Q+00UhIFfkm{?Z z_GnY48DFtHd@8>j#T+Gu`LxLvIw8gL~`5~K^L3)`bg$3iq9%RRfB;1n2)!6Pk$nZo}1L_}f=7(t4BXxX0;KnXk|kH#zAtyhP? z6s%>$3h$llPb$~=?9P_-seSeQN`x+>p`i@@y{WL96=$bMb1};7uYna-mcEKy9suOa6Pn<8++Ps7D4hKBa5V(y8*GqnBzd^oIt-&yKgDJ}=Dn7?4NL zQki(rbu^k*FN|5?b~kO2q=bgOfK<BM8>MIdfEXvdu`;LFAyZ#fSk=<~a?g%0CoS#maTKNq$({R;tDS@4 z)lsBt(!a3?gy_c2n=Nrn+Tn>N zL7gJj|J7EVVRa}MqOK-5ji2VS_-?%rx%HCEx=E30dO|Tn*66nt*X!ba{``3h9JZe~ zA;#0&{wE9KXI%5+X5Mr00l7yS(|(wprL770WDD?{mHTguV3o31KQWUzeG)*;INN8wi+xM{RbM^K-@$vCZTpV@B#n@oAa58jsbw~O|p%^P7_gOI9SyAiiAK$NW zDJVq*{_^4y(e?WZ<*V;Y)V`49Isy3_WOcKn&S)`|hoBnO0U-HkDs8 zObCJ3jG9t6O+MLHoq36g5I`N2ZxqD)?~T|t=_G;0rIa*m+niHH0?Yf-!|rFUx7(jq zWsg~Z_3#Yh#F)j!MQPv~ zyfH(@?h#znS$;=*a1N^M%+ARlCSBFu01Mny|51aj$y{hN$K_J3A3&KSAS2jlmx`X0Ezgr*!avt zHX-2bXwtO2x2V9vAQ9$>LRNCs$QDMggoZTcZ;L?(S!V?OUc{;;_3Vz(`Yh60=Cn^2 zAfcCb1Wjt>qWZGmqFkG)<)Vj3{;B%azB^>b(M%hF96B=W!#g{eA(^4V6DbsvMiegD z71RDPfGGytl+gU`FMM<>@l!v4DzrHavP@Bqdv=x=+|rI!Z7dgit_z6^d-aN^06Apc z=eE;|RHN$6WZqRQI7RI=hM5MThYHi`lH9s6!0F#1r+oe=k~4F{*NqLTJU(9=-IY`0 zpS*$uL7+*1Xdrj~pq3!C;PnP9yXQUIZwEx$jVGLK3~}#y@VfXAGF7vW;-R4&NSD2G z#y=Z43c9AeO8M-6U4s~5S-bm&%MvfXUhia3*K=py(2uMjg&)Kzxo2g+Qz!|E%Zo@f zh1nC$)7`=V0;{vN(Io($6%*M_W*0M}%2cm|j&EP4eEPTJjfN{6w{XA6N6`c+nYa0$ z9WJC5+x4VB6MH^rMtmQ6q@3#WH(QGeEZB2H*X&ca-7g+dp8`z<5{mx0dbLb;1_YB^ z7b%1Oc3T9XdC%^TE)YH>7>hjXz%HM)g2PU)sHj+G_h5R0yitPTcPOFLAlR``Ss5x6 zNEK_f5f1|1XZ%fXf4-4`5pU!M?3Km)Yk$`46CQM^ARYi2!~5!~cRt~lt?vUD!L*PT zPv}zTJ)gt%|pIp|hiYn*B|37GfE}CFxDb+{zVD)QPf?A3&0k>@aQ2}jWYinzGqJPrs zw{Mw1+~YsZ%gf^}D9*Jio%W-!@ngK6VAP0qt`CpTI`guVD_7M{Yli2e78s z7L6f_3!Nzd_m+jozrw-{EKb{99&iIgRZ>w=iHP!Cjs!-91n$M4p`jrh;^tPYqpN!x zGO!yT7iYzayYxL`WBt2kM#mpJbHSthwa}I3jK2whuo8eI;i1Cz(L)k6l$6XjZ{AFY zB=3f_A(4*^imk$AknPufKnnvAq1^qL&Fzt#w<1ZwEHp0AxJ^$8r*6a+En49borfQNY zUuW;KEr)<_FZK%jA#bKYP7+`W`P#EPyX#OBTfR0{PDN6V*{?fuYIlMkf!0Taed7OV z4WeLrp9+AojH14K9v^!E_Jie#4t zqNc5vUgKKY+SVh**2V4%YBNp!5>N2hrY9n8?>kBT#+CTTbQ^2sldS*;+CD0=`~Kou znE_1m!cnr2dpILI@M>(LK06!FB1Ln6XRy3uNAPF0%&BmRTZI#i(`0hCr+4R8bd5b6 z!u=WELqd~JR>lSG6eV>NAL;Dx#1qT)=eRY|Z!e+G|OT9k`d#e1OtPJMY zh>80HIGo=1n45 zk%_5=a3F$`k9$6KB^MZUQ>>|HC$hGou`wdaVprGH6n1Q&uWyIH1Gs~kuBj>WLjVlJ zw?Jz4fLg>WuE3<)=cF-6^_Hr@vkln0XuLnm8dQJ1dx^EG39~*?9RZP(3*mZTa#k0z z0}ir1h_##e`ox=*%e(5thm@F^U%$d3Qc_QW^J^vCbd)EjL(-1C5dG5?Sc9VH;2{3T zabx#Duo!3hLPLYy>ArnsAYsJJHxJ47u4v z)uj5_aqu!JDt|ZA<4=?N0X!1%6kIC`%JgdOKP86=A@&&Rk{vC%W9*O8q^kTeM3U`LON=H|hKvw5s`~iS!6P_of z%Ao*s-6k6Sy8?m=63=`b9rJ>pKUehesrugBtSl@0e0FyBR#0jOCEz#t3gTY=j+BHI zdgg<925zT^3vIya=&{G+-o1MYAZ#H4-*N6xj?i@+4^k27&?9tLe8H=%J7E($szzlv~2e^14-&q@x!T~c)|M&R9`Or zIZJ`{^!N#EM)$AZSBcJ4PJBFj0Zfu4`ZM02bK{a*4}dW}7F@QciBIO6l<3w06ZsFq zArw@e+Mf&%NC-ckdPb5w;1XJvu(949lri#ms?_+k2fwa(Z+8gHfvl!n+TD2aVng*x z=$os5o$4SF^5EC2VyhN1@9F0hdAY$c`@p7zZvUMt{xYiz05D3{)|nuc6!m0(b^*#R zfherJ>HD`6kY~CK=t2}haxmM9qg44nQ2ZcM8dabNj$H&9BU;+Ai1s z8p?_94)4Xk6HcfndXP*4NB`qvJ$KWhMJNxSQ^vcuhYE$?7S*)O8St6C3bPqj){$JSAG}Q=)tt8d1z`Rg9bbZOt~dh)OXo5 zP&KmOe{nvfb~rfBhAvP7hbT|~ixd_ry%qKK!r2`?Ghsv?1^i=&ix+v_MH|u8*2Y7_ zDisJHw^slxT74HDHgROtFOo3zSz;6~zgLv=0C{N;(PxB@ygOC4QScBSQJ7451Au1S zdoDe)6fHkrGt2?p{Lg#SqdGw6oJHZXNz}XI!Tv$xBbrk}cyHn2AG5ySCtjjN0}gD* zT^TRer@_}eLZC*ro&^s^44M@t)0}rmS7hBrE(Az!E5JYCb{qM6f;6FU^F1G-pj3Uw zbP5jog?6;u-N3*!0+jax^;<5>5ah|;m_JN$N>?30g5=6>CVWHg;){4tpCl&x5bU@y zyUUN85QWKFzd3+8;ojy8(Cd7I3v5%l$1; z`GoB`MCc%)^&LuFZM+~d6wyYO+V!hogAy-0PE_ftiSFuP(;H=9J2Z^I5rGAzfDUU! zWM*f-3b|IZB$Y;{r=@k*=Thk@Tt+51I5bol#Kz2=93{X@MnC`)!t0D0M`b9lkQOP> zMK_4xqS9kuJieg!o_BJ)y39OjA^yJg^Ra(IW5#tp=dixcc}W%)ZqM|?+vwiIds0uI zHZpRTw|EJg1(hBQIsR= zhu5l)=taM8Uye2JQa;PeVU1M+e~)V$uxI`-(6T|gJ)#(sx+m@)!E4v6OZlC8&3iYx z=%S(?Uz!`q-R^3*bI&DSfLyQ8iW61{BPjruMS5>rogN>862kSc$jF4fwK3(;tJLAq zFt0wA9YFdvv51SCff!&lRUg#E!p+_5xOpck(1`$#3weFY$yd0Slj^cgJcUwcnL$kT z2?Y^xRD5?g;b+>hhHVGNZ}b*fM#cu3%(9h6;SgExu(o4|EU_m;o@yktLmaQ*AGGjIxkL`bGRL<&OMT6VQoEi8Hw%+(kLiZzAalGcgjvjp z$c|jPLB)?hexLLc)`ZJ=-ku_iB(fW=UIoEuYDyWXK?0QBp8rAYn_)c z9)6RIdJ^)kM5NK3FZFXiipGo$9dwB%x}ia;5@gc(4^z3JbW=GS1>3`Q$6LylKCdp? z5_g}DCa@aZ<+1IIv*~zuTfmc39A(BJS))gY?*`6iZM&nQ{V;BfYVYd*!HRMHlD9l^OmiZx_7EA zyRS%B?lY%%DdoN?H>{jvd_HjX<{jy#dJTM49ZMdK2I*xhvB0=F>$R47aopz6{Yn2L zRTRijZ;N>4{d0@6J|IT^lfeQi0MN2-EsTi@Ac1rmQ=<%#Pz4Yxfa%Bg@3(Tzi|9#T z;0cY{v_*=WMcYuzDK_FCnLh6rsnqd;yP`Xo&EhatxMlYaXql>qr=S}PCc7+$^fgAoVW_ZZb1?tQrbvpMx zUZxHMM#VMtEi(qB@%CQa5?N10_Ji+fzvf~lAjzhV#zp2q$emv0^_AoczO!#O#C-Q0 zEs1{isflz1wTC)y0eZ(GPdcE1=s`t+v&YXAfX1YwSa4A(3s30wnSHm*wNlH~ei>Dx z;W|~eG*1v286PdK>_2(<<(KgQVNS`-!U3e`wlMHac&Y);lpxpKMsF2a=5Mhm858#$!t3777xA%NoZAOKVO>DyMzb z1JO(oh7n#)v>hI+&nZkfX+YMq-Ic@?Vp;Rh{Ai;<+Zs41WA7g)^{)La8z9T&esbW# zag|G!e##HG;pnw0(FQg(@V#Q?oz)t*;JlOfkmo0gcvotrSD^EC_i$v>BShH=!T8oT zN_ECzWurhKyTGutRo8ty%3}))lhf5XEZGhi6gSbQH(c6C+}GqyO;fGo@DXixOv8_Z z#!E)D;!cxniT7WV4_T%CDla@zqr7)DW_W)6tVm1S)1=FUr{$w!tb-5X(V;|r*|aDh zpRO2Yk)aZ^oatu-318N{o_zvZ-e%4$;8pGy4Cl->wG(#&_Q6r0fq`Uw53 zN`+wP@J-}*TOOq4L9g8j4a@}lIl4(U1zq@oI=!hcvi*3e+qpO+@mXH(dv=CO|LIq~ zOWy@n41=MORDv$=ADte|l==JAK#QOw*Nj5oer`ExRIa}+ym43x_&EWpNa}!W<%*)) zBex5T&)9$Y&uwm*hKAE}a(4iN@h@FM$AemIG$3+YK51q+#Fuzi}{^W;u8?u_I!7E zxEdi>2Uf4xqVXcOID^!@V~sGV_nJ0YsB{MDMyKG+KOFD@?;n0x3cXn6(f1Z_%Y`oT z6-Y8Df>b{NhgyyYRxy3?a5h#w{ASY537>g=U`>a}$xh!~q{#ZGT_HI{;TYldsg?OA z&Dv`}C-!3Ftk|~3-?!%2`dolo_4%iU3VOr6+x-H>Rme3czT0G_BdhEchl{KqCa(J(twJz22l;9ZESOko_Tg5+<3RN-5YL<9kQ z#S|XaGjBdKE1wKFGchc6uzn;j;rs$6fCR<-2}CbCJI&v4X3JB)!8gBRZ|p6O8C)c? zM9+K|ZkgG(S*~vsCapC2HCt)0)7Mxy%BL%r4Yj}E4uciAe%S&(l7IQUZV2ei1W=s1 zb!IY@)@4JLM|*GQnJgfoeh-it2^jaWrXjhbkSnh+Yi}M0oa{nce*Pr&Yy3*@au2uW z>2+FK=i{k}7cYjL_W@!!V?(Y$zPh@)#v~-@<w-2l5NV(>i#) zfcn90!gFZAUO4luk5HKxUKtS@6JP7!dhg?@9}d>VgL#DCV`LnFd5nlm;gho&TIF3W zdi>*M-R_C`vnx%;D74^ zsCCwNPozRadW>q?qtK&6V=O814@e1L?N<-wvx}##jB2DzngW*O$%T3wXWX%1a1|Y?R%`#|9`S52b5Vc%j$3sXlmQrdPTz3<@Hn?g;e?mUjxT>&V7FX37XGO-(#C(J(}~%CPzb7Kzmx?r3?a@voarJ;o9~o7Za;J_*~eG9 zQWh4n?^SajQK*p^Ut+Pj(wp8zO*Z)c?dMeL)rGGY;Wa`fd3o(`1}o4n%?%Aq{KbaK zUYq$JW-cys_hqT(u6uNj)ZrVBp@sb zDk^d9?J6_OowZ}%8mdo!+4X)5bA|Ca4i_~Q1$R+uQ+vq&9#R=cK-1S=MTRi@(GlP0 zRe2xB!v|91&9^c~G-FkFvHqdTBhS>arL!{uB*YR}@Xb;O><8MXkW!G>75CXRC6B+k zxbO`Bm#Rl`6}bNnqc1f)(Hdd$d3OmiW^K_LEh503Yz|8|hZBrwBm{c$>ut0;?bGe_A?Mx8JC1zO0 zt})2ZkFs zgHb(sV=O-ETjm2MDFSpB; zeU=M*F~gxM`@PF)UGfg^Puqu_&Amg~Uw>RZLi-b=tH?{mR8>qzAntz?{$-A#Vx{_(- zlPG>Fov5|CK@=X3y({FluGOV z5dk2mWCD+zw`Th4x@ICy{}491yWXh2J7t2~kS(Nsz8>B|`LAeVLR1aEmie+h^z7I_ zUgfn%t;`$VnVU3mcXOKs>9c^|b>G9e#FMdG4*Tx{%^Iu4p6;(#qff^y$7=>fD9AsW z`7Zh40Y1Fqim!ZP@?D7oUIDFxx7#yz+j!8rG`h7Tm6)k$&h6&}3PkZY8H+8ZS$a3W z4^j76nxv>fuJ?5p?2{K)%GWzjn=Z3u zYC&X+1Zln$BVxOISE?y|`jWGGc*Tj2A{hJq-($bcM5|a+>=6d_&O0A_c!ztYsmzry zXh4FK&lxyZw?5Q3Pu4amar?MNbxh#-UqK6B$^}`xTRT_t0YLW!CcU3D89+8K9E$w- zQHD|gwz}=rU)lgE3;{5$F?S~rUda-h)Bg$H;K*-G+AP__cIywbq+4o>NhMu z+vL%m?XQcvNyEazw%l4!EbQW+Kmk(M8I4;1S?bNMS8oZRWAuIVY3jA^>Kq8<~>k*n4J!#@UZxNQvP8_A~i?nMP0XWXy;N`hBPesFq(p8I~CM3i>$w{W=6Y`cQI!> zewk65gCxgCu)RoXlB|^Vf!|*HvDMfzi^z_xDhOS)VkDT&X?kt@omCbV5_vYow@V+S5 z73X-%D;v2yss^jtyK7Vmoou>6u^P8(!j!teTX8g_?EhjfpK~b+NP77c|72;e}V6Joextb>X z{-u@1d=FH$|1TTq`pdW`dU+)!I;Y!dXFtzn70h{rX%I8N(rdMV8VzVGBDi#^4RERE zkFKNxU0f5S@d$>&HD2&?a2fJW1sDe7Pb>V6*Zwi9Y_7Xxa}=%fqj6S7#vh2P_b?x1 z!e`pcWsW0{kHt@l-qU^ut?g&(Jk6W~h@GCN-CV%67S^6pKu;2jn=rJPsKst5`1;MP z25*=e#xacz`ZKIu6g@1Dm4?839JTBG{aRXEx&5~Cj4ONf6HU?z3bZYEY{y4z)0~ho zF)=EM>bRXZfoTkxbyHq`{P;0zU!R1i((2sKJIrroj z6?GrT7M^i%acMS1QnOIvc=Qdp?ld+n59G>5t$-Zui(#Z%T~8(zK+-HT>#zylhdQ(i zYgd;;iEA#mn9QOQGLa6rCoYJ!(jhFR4Xm~KAj@Hr)LHhRAEaHwJ4Tfk%CXgQ_Ucvc zo_!Amg%_J$Sh>cvemIb90zBNr0itZRDm!wcp^mx`y~L!QbqhVInIYH0RzCiCRht^6 zE;JWc)4a&hf{oqtohi@}B8%r~))ATJ%|uu;+G?EZ#VlT7nk5vfyw+oT=Uq^2_ArddH3Vp`MExLHkIt5}iysg8e5Sok zpB;6Z)izCRd>GxbFqt@m*^d*eIo($s=i%^w=;sTMJ@tz$_CU5OZ!>xx`u183EBmhW zbo&pPz{5-(yZ841qYqj6A-H3kq!yMU9&n0+0QxcFLHbo%vGIdSYu&7(8{rLX*RXqJ z{vHyUkExO`oU*^wZ8nQh7G+~o1rq3pI$DQO6+j7c!Bq;2so~?U>1jT@%;QGRHP+*F zId#X7Jbij#=c$WHN(*Z%wUo>@?QiHY7YF%nUCaXO+^rk=Dr zxBtAfU9jNh`C@1vrmz2Ia}qVabCNU3ymaaLvTC8v$|JQCQc!=tf~{KYsj2ocaavnj zZ(&vf@+AdO}xlrXDb2FP_s_t@c$2xQYmp1s~6tjrEReA6Z3+?yR3wyY7i+ zO4qfJ-Edr^M2D@(D}DVgSh@7>N~UYZt=nQB({^Y)J|sWsb#)tUd1i$=p4%E{N&DG) zNbBw2B(q9^qF^&{I`%Eh5V`&BEK4Lc?rdDhpi^W;#(U>;`P@E1K=?=9yFvR-_dGF6iJZCR4l_ zh>@L*6^EQLJDCAPpNl}rijUpKA?uSrpIvu-4ostf1*&}gXy^- z?3V_>K6xv55GlS!HA^>zYpSw2h}$t&fMoaV-O64YN8K%V3qFklx6$yy{B1;Olp~=& z(|btF4u#ro7VTqCZ^+4%gDhgmd*R5Rdi<+xSIu^Zr%$+=_aRUtgtP2mI^Ot?W%fCE z^lhX9Eiw>A`Htn%=Ix#sQloYo_uty4GMQ9)aM76l>e_(KRcP|#F}r-)M^m5KG3Qqq z)ET}E*>7I2Gi{2ZcKYXM654OA^72`H`|13TnhU)1$tzaJ-WhXGx8>F-P`U7*ot?3) zT`PSY0nyJ>CC+-1NW2@KM3yZ1SAAqXY$seH5!dRPzwC}VQtKJej`I8$ST8enl7aPQuieWZgJB29hw&SBie ztwFz)>yk<*mrOj59 zt`B<$xsw>GQX=k_HzV5K&f`rLdvuJKAR@9 zJ!kLv*G99WMl|uBsks{%_m<@~b9WTqC7n>jNzn%iJa3%L0fuYCb4P4QqngonzyZ=@ z6?_=Js*9v4aB^yDzn?;AVUoRf&;qXcm~!W{A(AAlSm4rAwL~FfL>DMp7oe6Jv-eI$ zQ7`*m4cO#qWVZVe>^vFDof77QPIj-OHwxSbcWcRNFttE5I3t#@oSy|jc?P7|<{Yiu zf?jE8py2wM&`M5*7~!5Rp*w{w(%H@#U>#o7IpNtnjWxz$v9XCKiA;u)wT&#T7&P;KWdU$T?u~ zLuh#BJrW9(XCROUnn9xaNO1H-IfFb=`92~Vu+qR<=d1fWmbnGU72m~rRdEUEtsd3M zDB+=#gPAA+y?{}8A7_EUTXgkO@*${TuL~^HU?x4;k^U7!-rL|Vl;8QZt0%Ujv*d_U z7`d6h^}=Xxp-PKM#NN`MVd|PADwi?7!d+i6?GRcq_KE7!Ne<*0FZ^|~-!xnA2NPv= ziLO|SXGg7DH<^x}?!FKo%0HcD&AF$(~9Lk?$iE8dml`lLO-LO}`7 z14A7B^rcXMAde7@a!i|OYv@{Mm|fq?WP8i3`DK%ACHq0%GI}_FtXEW^|DvMS*R0TE zY9nLgyY^!|_bR=S3;X+%v@#x(jfFqo#p!tO-<)JaMOQ;J7l?9&QbT!7R@P&KQ6q-f z9V+zF)zSG0ilI)#R?SUIPt0n3O%joo()F}>!RqeI|AYI=HO16qLtBFG1cytDY)0@_ zUt0CV=2ZV?p6t`g8f}*+J;~u6bqX~<6Uc@-F#|qNdT5tjvH~X?j-C=+vD*CgW4vE8 zbX(3@uGiBXPg!n-E+7}ifp2F|AV;V0W}Fod5=)lBbrJt*y@@n zHceDMPZNcLdXL#+-v08=k_x@vaq~u?e0FAgbVVeEGNS(smOvtLv1-*8+t=DYP^vrt zN+fv1Li*82EBU~RVKF8!>$7(DXu2rxT}}eRwD~4#ad-baPNwE-?L@t`Xc?lbheyV4 z+nLp0gIl*Aj}#s7EeZYt*>SX~kX(J?^hxDHDo>XG;^>ki-*tUZb6bRhQXrp5U=3-J zMBs+~)tkS&8%qymEK_6&gY|xUVu7nAbxu^J+;6FYU5%#cQ?0&yz=vG9pyh#SH?Vb` zkylkG^=q$Ym;6lf$wIo`47-$D=S#VbXONVw?A@6b)9v-|DLZ zFW~vAR!{@X71q6R&dOaI-dSOb8MY~sk$d2iEkTi}m0WJpE6^1?*etgA%e0p-oA5vs zxzc~zufOslqO9@CV%%GhdQquoSxeWaVW)~YpDLQr$~UVs&L^w$9=GTGi}%9T_tO?Dsi0_RdZeF%JhoV=E&dA;GHcgY2#FI;m1x?_0p zo7vHoef_aLl{Mw!MoBkASi#Af;*#L8^=FE9^dy79CVCLtBa{LwBDV*$#~t@}uEG7` z4}7eKB?=y+?Xnamv)we^jshTS?TEO4p0)boiJU!=yGz9zH=JdHgnVjKYMm(6dinBl zrony-`^xPJ*PKAMhH6v=22@M?4gVE6zCln|8f&f>Jb zN-kxs$C8%8m&ebIU4Puv)0M0w><0pl|L|G`^<}bAh`}f7dwGYbo@#>X_a(>)=sj`U zY+mtsIf|VS_6pg0{%MmT-!nj<%s#!s?(!;s2(#HKg8s>roqQKaIZat&pOSJi0G=b` z!PE04Es(H3C2vvjR*w;O;h7IAwrAqGS1|JfRR32^Jd~vCg9Du!s`sWrej5+;DMEY0 zmZ}~tCZVT*xrvF7XPjw@aECN6Mr2@5UI+>HsI2(~rLdUAY;=>__vdBrUxC|&8H^<= zcw37qE@IR}_X2IIb?fz}OW`$YoNsZnRm^lK-I$!l<={o5TTiH9ud@srTM?iY24Y}O`OwAZP;7T0Wano&LISmC_7Sl!rD)gqRmKZk8QV>|8vW+uOy0LF z9efo+P@sUC0a8K;T-S=ad*jKH+(Oy+cMuranT|!s5eb zcMee0Kbpd*g{IJB`6sn=3xX01b|wREOD%3Hy55&g)7txsqyBM@$DPfp(w}jWfke1t zA^Sl2@)J-Lbl7A*Y+Y!(8|aFaK%yz7s5ZauWgGea$Ve6R*z~(cr7eC%%*%yL-Tg0v zdRC3S+KF`>VWI`EUo7hJJ5sAk0jDw+0(VO83>aEsptQPR?Yoj+9?m3y>;f@fY?n09{}O-)`fsPkzCtHVBMk z0hb?g=iCrQf>zo3GI`zK-YcV>u2%9h?&~C;RF*Mzwtv$uo%2LR-P7}iiwt6@7L>fy z)733JXUJlFe$?pZ>bV_nHV?Vp68Z;s@-lE0-ZQqHQ5k+L(KXf~adUY)tn^E@EE`Kn z^9qOz8nOJX%vCOMDD$0s)YW96GjcBNJOaGP%jnTFP+6AtKi-tDJl<*pzZs+# zk8JYB=Qhb&P$lGk{GAY{wo5EjiqOOcwd>~#L>q)$b7y4PRkrV$`$wLG0W|)%5-+O! z_#H7XCm=G6EM3D_S66E)u%$giL*|pCeIR8!{2{*B|FT{Z{|*JJl%y(r)-p=;K83MM z5KyR3xL(L5F~e*$lFT(xIa>)SG)K@7>w$Pxxs7xFR`H`Kccj z;=oFpq;3;a@)r4>*_oRY34%05W65yC8Mzsf463Ydt6!Q&y>_q+4*tF}d!-CeV?)Qh zdO7hO0BfBW*85@q8WV2l_O>5{1@o#r8l{O4R$wWHv%cPm5LeRbYDu-{ zesHHa1yuJ^HY5B2SMq4Pod+wnZv{nT$1x5{@(R3267+yHmMMSP$uMNk3uNOZ9WU4o zie>Z)jP0LDbHc_2$4Xv>(3#7D{g#Wem!IDe9EIVDKx!YxGgdQJxl)CNtym?^&spca zK3>%!inL=6n<)6CSq&1{i(V;51(5MW0|*1Z*VdOnXhU_C(`2nlEmjny*n%amxSH?G zSazQ2mIFZ&Rx(%OIHHA>C@Lx`Z5bJTx`pd}W#dXm^277h_Q%U4Faq+ROIq*h4BnO9 zddQ_Ja+LqNvRbcYJ-<#_#?dGYDz4X%5epqp`1b4)mBC#`Mr$+|(hS<(94SWTqKhcx zS5{X~1oUPrz^fmD9~ zhgFDGl(mfSJfC$sQ>DD`At3d{s$`d<@a1-yvRV@}&?zx}32RoI@yb@#KanYv+)Ul2 zWF+vGtdF0r8I?^$U;sXu1=?AP8mO_eL{Ih`uf^_f`fUTNS$!T@(&dR1Hu*EfGRC+U zF23;JZ3+oAA~=w>fmxl$uRV%SKx;Q%l6DV1=14>goiZ@wR#|Q?i@r!~LK#%HR z0Pd*)jl8VKE=3J@YIL;u@ojQ)o8a{qF9>IJwX_~1cccSR^;l*9NGUXNv-iOldjoJa zkKYs&a$+kg1UZPW{Gc;;#`O<-OGSH*CBNdgzUs|1-{)o;2l~5WJ>_R|Yu4(zmBI4- zaYaAxr$Q`=E??b6HaseHVOWId#VP7qe_oh7?EC6$pqUj%ZmVeE8i`NvTuE}!$G-bx z?gG8Cys}PTDjDTcl5?duT6ksL9SXg0A4w}1D4gxo97!!lHdh<0JW3+A=~hM>r{xn;abii~_fs)6dBqW-g4!w$>+GIF$; z^D8Y=Amigl4w1h+xh63cfOovb4d{kplGxmHu z=1YpTX-ZrX!VR&q#)tSeRN%J~{FZbc_j3Pe9_^Vw0H2Q$ZhYEfKj8pfqee>7(;EQr`&`#Bn;nkzE z2>1}9mkR&M-Fl9P?(M((X01OO>&%#mEW3u9;ELSA!%e%q(bn>>Yox@jzOq46enco# z?FW?ZQMCE;^&EJBOpFJ2hYvDg0y@H0KS_b^w~VSv>i=oy%;TZ%;yoUbrAPJ_lzk^9 zQFcYym2D_XnE_4at3LAM zlnb9P=7ndVyfrvwRm6Yw=fw+5m?W3yUrWdvZWRKVq($galN? z)f5CB)?*jB5V5CkVVVnw;57u~_tG@sB9bTkXU#v^J*5&z>cr94=S@vbrOuvp1RQk2 zF68oMajG{22wuTxS0E4e+jr|a&`^s6YX0LCpnr`MNQi8`n|A^?L0J7G!h5pb1f1vd zej6MZ_%;M_O`}RQcLRY^up1m(i|=f$uPGd4!j~;G!FpHD^GYo(BHVMU@hf@fV+n$n(ou zBHnz|>{c~q#<*3ND`k+DyRJXCp()t!=Ti+wX5tjflRz#1sKCXdq4{kx5Ud{=bPJ%0 ztR~>QJF53aPna2SkdUkOCHGq!N~2Rh(q!~4&sgH(;-o+shf4oQi?26#cXgqt7?1d= z+dWCXUptRLrh)qj3MipLsY(LK1IHT@)9P8EihoYp09Hlkrim4eQJ7+9cF*yRb^<5g zegh`5vEcL$>}sUN#s6ksJbU)+>X*kVah7SH2q2+AvK@oAIQ-cO5oW?c20oU>kd`N2 z7tYZ7bjd8z6~1Hl!ZNo$X!9Z$p*}0#C3b|TV)|`}Fi^5Ng)BS=Y1dy@hHrQWF=Ni^ z)rwmD#(j-iQ5_jl0BQjz^4oKF+BiDO*FTL8g5P6-!e43?TDCTv>WsCgVH5fh96VEd z@3nZ2I=8~Oo{ci>7)a6+o6Kmg_~3=GCq1mV%OX1o@7PSeaHKO&XK-@rjJ< zKR8#YQz6IUIco8s^@cV)Fqt%JnWv{^cE8=#bN#*_%$+Wb%pN3=8;Tj2m_A_=Ga0o^f3XX$?lO#GL-IJ!ZVf$_lfe=UCZ3B4HE&1sj4xco+AF+ZInvUAnSUnrA#?y?oZ?1{}}waFLs4*tg^vS`i~7KBkmI4Be?O` z1xgp=0{h61Hl>q+q3-khJsAprQK%N9ggAg^7z)e=v+%R*WbH zc-2)3ahF2;%UPv&%ccsVeY`_!&NOvnH)+28dS1)1(DtvP=<{yDdEf3)%>8P@Ww@@y zBQH#rubG!FESD}Gz05s5$=hZqNvF0VcRA+4q3Oei(h3TuoSd8}u#*CXRzHUV=QSTh zYZ=}o^qnWlU@*L9PN~O1tYKK!VerBCki`RyvVZWO2~!%LsF@Q)5Q=~-kQU5jXkmRDF)AlINx)K9$B(88@FKpY|LEUTIM)B{%@O^Ir{iy=TptVoc-z@n1RG2kRTI~BwNHG7?4P|%gq(QyVI zN(o}{sn3}hbcMNHmRsdf|BcOG!c# z`v;rSUm1~&A2(Tr6kQWWR=H8q_%xsi%MNd92;RMBI9g)BxZ zM8HFv-ail!g>c!`&E{EwS>HmUoIUf>UU60M&qLlXj)*4rZz-c@TGFM| z3IK^CZ|#Dysp)CH(zV})gIIplO_RZG)j8?6!5&lk%x1SB&u1bIIbpHA&CbUyv@DUz z&8`N}AcEZBN%~W^%z)c#B|6GE3gb_QjMovMBkO1U+pyLua)v^+M`S&l7Qoz}WFHi# ze?m;31=OMgHGJMFeX!bf%n}W&@C<1V`;g0*d&aOJn@eL03`B4L059VMB$f>>;`p!c zy-$rja3Qh8kcTX+F9Re{Y|%l2@;wE+-Vn>=3{{&_}oY-3nW8CS2L6 z7Wls9_@!ZD%8`xYfGrgKzGW9!mbI8EbILX=uxTrwAdSI>ANCMqd=@O}2M*!ZGX}O3 zQW_V+0XSh zLYgiQd*liF9Y>CSOO<|Q@K_RQ0o9Y>)nYJv?4x%1FQ&ur8*{`AKpZ2Zs*O z)~pzhN;GT_Kuq=DsR%-YCBxKjB)E0!oV?{$4RUs(+pKDHsoKpq|L$UF~u>Qr?|TQ<3rhp3zgiQ2`Nea(R>0p;|GgT zhapSr=%Ny*l9DcMxV@>JV-2uU1WROIM4rNokw@OHt6R_83LwDTXTb)2(N?|gv_d3g z9vf`<_~;Q?bI|wS5szqQ?TIGu#kztobeFb)3jnh|sVD2ev?09^Go#=qkBrpQ3%)Da}lEO{7H1U>e}@3*xkEuuy#xclLsOZ1?csdD?TZQ=Jiiv1NhgCtt+S(0|gBZ?Rk=J!!dnbmvgbOXR@aiACO#5 z$!;)GJf6yDU=X7pewmT+otf;;7odCMu>Uk`StRNH8i`sRXLA*@xhdlufczoq{~;WL z!$ZG#Z~D#lrR1i{&gE&I`=}Y5@m#Qr@&2_cCQNwgl zCgB_Ioh$DS{Fi06$2jWQ1Z+*}=7H^@pl!$tT-d{$pz&DbpP3}@Bg(9drIy`6`I;M4 zRBIt{Vfb~~#Ck<{f_KkWa8nMHVh3u(Y~Bsll#|N=U-9^#B*oxqg4GPU0$=$G=-L6* z0ti#u=(OBrQfz}&q;?Aby%T@=e7yIqTer~$kq`b$e-48FaBBsnIE8*Z{Px!O;tLwq>fXsu-aSRTLg8}@Ft_%6N@w%Z z_RntbdMov9_1-G)-}cRb#4lxi^#?Ign~V(A@ho>g`+hE7*xsiOR-(PK3Im-n*!v`z z+ZEbBKf9T_YxPA)1K}=OH}pSTrlUhzb(sHsdYO|f*UdsVQiRLu4^PqML%6k z%yil^HXCBsk|R`F8;mR}Y+gGBSMUq7{TnMr4I)V1F_$ZAxU{biU2!#KEd8M}VA>x+ z;}s+@zY_A|7mDn)X`andCtlP>i&#^)BMS(6UCVkDt1BDu zVf5sLD!i(l3u?P4d%(|26m(OgI)MJ|2EI11j;|6?3m|oUTQ2$DG|Y9eL&~hsd!(V< z=kx*(1m{`Mu1il8BHCC2D6qe`$kN4g0x{_A{@%%^@7}S?&}5KPE@kNs)kyvx_osLw zbL{$z7dGAwRIj1=1VomT>~|Y8?b4)EUeC&N_~@Do1!m5VhC>Udfr*%$^*(xxzh)Cz zP)&4Oinozon1@N#kFjUhtoHYIVRg?3QZ_|RbXOFM@u78J$BBe}{J)!JaWnz8 zo0FhmKd{e&(0m~DdZ)ID>ki7W_Uy&14cPnW0*VLU^rYVMXNF6}Pp5n(cl51b@7v0z z*)xv&DNp&*PV}70mFbxyoUx74)OjHm_T&}{BXJq*JTH@lrIcz?y~f>RqIqzH6P(pd z^k_kPrtjzy_w5Bn+OeCW$aOVWF*%{0nuio05?E}r-v-XFZkfEJkNxe9KO*~3zKPkW z>oI0hYAjh#?T<_Ujw4_d6EBw z*+?s#FD%WiBI$wGDd5JMcE)y*P>e`=FU8^6Zw+?(Qc~5=SfVsi@f1IIDDxUkV^^KK`e8Aa3+$BNck TI$*O0{FoS78kV2C_TWDND9o)M literal 0 HcmV?d00001 diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/client_request.png b/notes/0005-implementing-nip-01-standard-in-pure-erlang/client_request.png new file mode 100644 index 0000000000000000000000000000000000000000..3c9d590034b497178e7f0faa3c7566456d6bf8c2 GIT binary patch literal 31632 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 literal 0 HcmV?d00001 diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/nostr_diagram.png b/notes/0005-implementing-nip-01-standard-in-pure-erlang/nostr_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..b3443c506e7d4bf89e911e7cee16c5d36719bb2b GIT binary patch literal 70840 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

5Txvz{3G)&ug9Z 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 Ry#@ZrNGLw55Ho)DzW_5U7kvN# literal 0 HcmV?d00001 diff --git a/notes/0005-implementing-nip-01-standard-in-pure-erlang/relay_notice.png b/notes/0005-implementing-nip-01-standard-in-pure-erlang/relay_notice.png new file mode 100644 index 0000000000000000000000000000000000000000..f65ed9f15f55e5debaf6a83dfb0db8de87a22b7c GIT binary patch literal 12221 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 literal 0 HcmV?d00001 diff --git a/notes/README.md b/notes/README.md index b2319da..8bcff32 100644 --- a/notes/README.md +++ b/notes/README.md @@ -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. diff --git a/notes/_template/README.md b/notes/_template/README.md new file mode 100644 index 0000000..77aba5c --- /dev/null +++ b/notes/_template/README.md @@ -0,0 +1,11 @@ +--- +date: 2023-02-25 +title: +subtitle: +author: +keywords: +license: CC BY-NC-ND +abstract: +--- + + diff --git a/src/nostr.app.src b/src/nostr.app.src index 2947607..58c2e0a 100644 --- a/src/nostr.app.src +++ b/src/nostr.app.src @@ -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" ]} - ] + ] }. diff --git a/src/nostr_client.erl b/src/nostr_client.erl index 2d39c4e..7ac7595 100644 --- a/src/nostr_client.erl +++ b/src/nostr_client.erl @@ -1,13 +1,41 @@ %%%=================================================================== +%%% @author Mathieu Kerjouan %%% @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 %%%=================================================================== -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. + diff --git a/src/nostr_client_connection.erl b/src/nostr_client_connection.erl index b2c0305..640dd4f 100644 --- a/src/nostr_client_connection.erl +++ b/src/nostr_client_connection.erl @@ -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). - diff --git a/src/nostr_client_controller_sup.erl b/src/nostr_client_controller_sup.erl index 42cec24..6a1d778 100644 --- a/src/nostr_client_controller_sup.erl +++ b/src/nostr_client_controller_sup.erl @@ -81,4 +81,3 @@ spec_controller(Args) -> Return :: supervisor:startchild_ret(). start_controller(Pid, Args) -> supervisor:start_child(Pid, spec_controller(Args)). - diff --git a/src/nostr_client_router.erl b/src/nostr_client_router.erl index d467899..b6cfb6e 100644 --- a/src/nostr_client_router.erl +++ b/src/nostr_client_router.erl @@ -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) -> diff --git a/src/nostr_client_router_sup.erl b/src/nostr_client_router_sup.erl index e6d8add..0224fba 100644 --- a/src/nostr_client_router_sup.erl +++ b/src/nostr_client_router_sup.erl @@ -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)]. %%-------------------------------------------------------------------- %% diff --git a/src/nostrlib.erl b/src/nostrlib.erl index afcbb59..e5798fd 100644 --- a/src/nostrlib.erl +++ b/src/nostrlib.erl @@ -1,15 +1,53 @@ %%%=================================================================== +%%% @author Mathieu Kerjouan +%%% %%% @doc `nostrlib' contains all functions commonly used by relays and %%% clients. This is a low level interface and should not be directly %%% used. %%% +%%% This module provides all functions to convert hexadecimal strings +%%% to integer or bitstring. only lowercase hexadecimal strings are +%%% accepted. +%%% +%%% == Examples == +%%% +%%% A raw JSON message can be decoded using `nostrlib:decode/1' +%%% function. By default, every element of the message is checked and +%%% a list of label is created containing the information generated by +%%% the parser. For example, if a message has not a strict id like +%%% defined by NIP/01, a label will be added saying so. +%%% +%%% ``` +%%% {ok, DecodedMessage, Labels} = nostrlib:decode(Message). +%%% {ok, DecodedMessage, Labels} = nostrlib:decode(Message, Opts). +%%% ''' +%%% +%%% The `nostrlib:encode/1' function can be used to encode a nostr +%%% record to its JSON format. +%%% +%%% ``` +%%% {ok, EncodedMessage} = nostrlib:encode(Event). +%%% {ok, EncodedMessage} = nostrlib:encode(Event, Opts). +%%% ''' +%%% +%%% If the event id or the signature is missing, the encoding function +%%% will try to generate it or return an error with the reason. +%%% +%%% @todo create encode/1 function +%%% @todo create decode/1 function %%% @end -%%% @author Mathieu Kerjouan %%%=================================================================== -module(nostrlib). --export([encode/1]). --export([since/0, since/1]). --export([kind/1, kinds/1]). +-export([decode/1, decode/2]). +-export([encode/1, encode/2]). +-export([check/1, check/2]). +-export([verify/1, verify/3]). +-export([sign/2]). +-export([integer_to_hex/1, hex_to_integer/1]). +-export([binary_to_hex/1, hex_to_binary/1]). +-export([create_signature/2]). +-export([check_hex/1, is_hex/1]). +-export([new_subscription_id/0]). -include_lib("eunit/include/eunit.hrl"). -include("nostrlib.hrl"). @@ -19,15 +57,1366 @@ -spec test() -> any(). %%-------------------------------------------------------------------- -%% @doc encode/1 is a wrapper around thoas:encode/1 for the moment. -%% see Thoas +%% @doc `encode/1' converts a nostr record messages into nostr JSON +%% format. +%% +%% @see encode/2 %% @end %%-------------------------------------------------------------------- -spec encode(Message) -> Return when - Message :: message(), - Return :: iodata(). + Message :: decoded_message(), + Return :: encoded_event(). + encode(Message) -> - thoas:encode(Message). + encode(Message, []). + +%%-------------------------------------------------------------------- +%% @doc `encode/2' converts a nostr record message into nostr JSON +%% data. +%% +%% This function is mainly used to low level communication with the +%% relays or the servers but can help developers or users to create +%% customer payload. Some rules are applied though. +%% +%%

  • the record must be valid with raw values in it
  • +%%
  • a private key needs to be passed if the signature is not already present
  • +%%
  • if a value is not defined it will be automatically be created if possible
  • +%% +%% == Examples == +%% +%% ``` +%% % load the records from nostrlib library +%% rr(nostrlib). +%% +%% % encode an event +%% nostrlib:encode(#event{ kind = set_metadata, content = <<>>} +%% ,[{private_key, PrivateKey}]). +%% +%% nostrlib:encode( +%% +%% ''' +%% +%% @todo add examples +%% @end +%%-------------------------------------------------------------------- +-spec encode(Message, Opts) -> Return when + Message :: message(), + Opts :: proplists:proplists(), + Return :: iodata(). + +encode(#event{} = Event, Opts) -> + case encode_event(Event, Opts) of + {ok, Encoded} -> {ok, to_json(Encoded)}; + Elsewise -> Elsewise + end; +encode(#subscription{} = Subscription, Opts) -> + case encode_subscription(Subscription, Opts) of + {ok, Encoded} -> {ok, to_json(Encoded)}; + Elsewise -> Elsewise + end; +encode(#request{} = Request, Opts) -> + case encode_request(Request, Opts) of + {ok, Encoded} -> {ok, to_json(Encoded)}; + Elsewise -> Elsewise + end; +encode(#close{} = Close, Opts) -> + case encode_close(Close, Opts) of + {ok, Encoded} -> {ok, to_json(Encoded)}; + Elsewise -> Elsewise + end; +encode(#notice{} = Notice, Opts) -> + case encode_notice(Notice, Opts) of + {ok, Encoded} -> {ok, to_json(Encoded)}; + Elsewise -> Elsewise + end; +encode(#eose{} = EOSE, Opts) -> + case encode_eose(EOSE, Opts) of + {ok, Encoded} -> {ok, to_json(Encoded)}; + Elsewise -> Elsewise + end; +encode(_, _) -> + {error, [{encode, unsupported}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_request(#request{ subscription_id = undefined }, _Opts) -> + {error, [{subscription_id, undefined}]}; +encode_request(#request{ subscription_id = <> + , filter = Filter } = _Request, _Opts) + when is_list(Filter) orelse is_record(Filter, filter) -> + case encode_filters(Filter) of + {ok, []} -> + {error, [{filter, []}]}; + {ok, F} when F =:= #{} -> + {error, [{filter, #{}}]}; + {ok, F} -> + {ok, [<<"REQ">>, SubscriptionId, F]}; + Elsewise -> Elsewise + end; +encode_request(#request{ subscription_id = SubscriptionId}, _Opts) -> + {error, [{subscription_id, SubscriptionId}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_filters(#filter{} = Filter) -> + encode_filter(Filter); +encode_filters(Filters) + when is_list(Filters) -> + encode_filters(Filters, []). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filters([], Buffer) -> {ok, lists:reverse(Buffer)}; +encode_filters([Filter|Rest], Buffer) -> + case encode_filter(Filter) of + {ok, F} -> encode_filters(Rest, [F|Buffer]); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter(Filter) -> + encode_filter_ids(Filter, #{}). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_ids(#filter{ event_ids = EventIds } = Filter, Buffer) + when EventIds =:= [] orelse EventIds =:= undefined -> + encode_filter_authors(Filter, Buffer); +encode_filter_ids(#filter{ event_ids = EventIds } = Filter, Buffer) -> + case encode_prefixes(EventIds) of + {ok, E} -> + Next = Buffer#{ <<"ids">> => E }, + encode_filter_authors(Filter, Next); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_authors(#filter{ authors = Authors } = Filter, Buffer) + when Authors =:= [] orelse Authors =:= undefined -> + encode_filter_kinds(Filter, Buffer); +encode_filter_authors(#filter{ authors = Authors } = Filter, Buffer) -> + case encode_prefixes(Authors) of + {ok, A} -> + Next = Buffer#{ <<"authors">> => A }, + encode_filter_kinds(Filter, Next); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_prefixes(Prefixes) -> + encode_prefixes(Prefixes, []). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_prefixes([], Buffer) -> {ok, lists:reverse(Buffer)}; +encode_prefixes([<>|Rest], Buffer) -> + case Prefix of + _ when bit_size(Prefix) < 32 -> {error, [{prefix, Prefix}]}; + _ when bit_size(Prefix) > 256 -> {error, [{prefix, Prefix}]}; + _ -> + Encoded = binary_to_hex(Prefix), + encode_prefixes(Rest, [Encoded|Buffer]) + end; +encode_prefixes(Prefixes,_) -> + {error, [{prefixes, Prefixes}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_kinds(#filter{ kinds = Kinds } = Filter, Buffer) + when Kinds =:= [] orelse Kinds =:= undefined -> + encode_filter_tag_event_ids(Filter, Buffer); +encode_filter_kinds(#filter{ kinds = Kinds } = Filter, Buffer) -> + Encoded = kinds(Kinds), + Next = Buffer#{ <<"kinds">> => Encoded }, + encode_filter_tag_event_ids(Filter, Next). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_tag_content(Content) -> + encode_filter_tag_content(Content, []). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_tag_content([], Buffer) -> {ok, lists:reverse(Buffer)}; +encode_filter_tag_content([<>|Rest], Buffer) -> + Encoded = binary_to_hex(Content), + encode_filter_tag_content(Rest, [Encoded|Buffer]); +encode_filter_tag_content(Content, _) -> + {error, [{content, Content}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_tag_event_ids(#filter{ tag_event_ids = TagEventIds } = Filter, Buffer) + when TagEventIds =:= [] orelse TagEventIds =:= undefined -> + encode_filter_tag_public_keys(Filter, Buffer); +encode_filter_tag_event_ids(#filter{ tag_event_ids = EventIds } = Filter, Buffer) + when is_list(EventIds) -> + case encode_filter_tag_content(EventIds) of + {ok, E} -> + Next = Buffer#{ <<"#e">> => E }, + encode_filter_tag_public_keys(Filter, Next); + Elsewise -> Elsewise + end; +encode_filter_tag_event_ids(#filter{ tag_event_ids = T }, _) -> + {error, {tag_event_ids, T}}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_tag_public_keys(#filter{ tag_public_keys = PublicKeys } = Filter, Buffer) + when PublicKeys =:= [] orelse PublicKeys =:= undefined -> + encode_filter_since(Filter, Buffer); +encode_filter_tag_public_keys(#filter{ tag_public_keys = PublicKeys } = Filter, Buffer) + when is_list(PublicKeys) -> + case encode_filter_tag_content(PublicKeys) of + {ok, P} -> + Next = Buffer#{ <<"#p">> => P }, + encode_filter_since(Filter, Next); + Elsewise -> Elsewise + end; +encode_filter_tag_public_keys(#filter{ tag_event_ids = T }, _) -> + {error, {tag_public_keys, T}}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_since(#filter{ since = Since } = Filter, Buffer) + when Since =:= undefined -> + encode_filter_until(Filter, Buffer); +encode_filter_since(#filter{ since = {{_,_,_},{_,_,_}} = Since } = Filter, Buffer) -> + Timestamp = erlang:universaltime_to_posixtime(Since), + Next = Buffer#{ <<"since">> => Timestamp }, + encode_filter_until(Filter, Next); +encode_filter_since(#filter{ since = Since }, _Buffer) -> + {error, [{since, Since}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_until(#filter{ until = Until } = Filter, Buffer) + when Until =:= undefined -> + encode_filter_limit(Filter, Buffer); +encode_filter_until(#filter{ until = {{_,_,_},{_,_,_}} = Until } = Filter, Buffer) -> + Timestamp = erlang:universaltime_to_posixtime(Until), + Next = Buffer#{ <<"until">> => Timestamp }, + encode_filter_limit(Filter, Next); +encode_filter_until(#filter{ until = Until }, _Buffer) -> + {error, [{until, Until}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_filter_limit(#filter{ limit = Limit } = _Filter, Buffer) + when Limit =:= undefined -> + {ok, Buffer}; +encode_filter_limit(#filter{ limit = Limit } = _Filter, Buffer) + when is_integer(Limit) andalso Limit > 0 -> + Next = Buffer#{ <<"limit">> => Limit }, + {ok, Next}; +encode_filter_limit(#filter{ limit = Limit}, _Buffer) -> + {error, [{limit, Limit}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec encode_filters_test() -> any(). +encode_filters_test() -> + [?assertEqual({ok, #{}}, encode_filters(#filter{})) + ,?assertEqual({ok, #{ <<"limit">> => 10 }} + ,encode_filters(#filter{ limit = 10 })) + ,?assertEqual({ok, #{ <<"since">> => 1577840461}} + ,encode_filters(#filter{ since = {{2020,01,01},{01,01,01}} })) + ,?assertEqual({ok, #{ <<"until">> => 1577840461}} + ,encode_filters(#filter{ until = {{2020,01,01},{01,01,01}} })) + ,?assertEqual({ok, #{ <<"kinds">> => [0,1] }} + ,encode_filters(#filter{ kinds = [set_metadata, text_note] })) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_eose(#eose{ id = undefined }, _Opts) -> + {error, [{id, undefined}]}; +encode_eose(#eose{ id = <> }, _Opts) -> + {ok, [<<"EOSE">>, Id]}. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_subscription(#subscription{ id = undefined }, _Opts) -> + {error, [{id, undefined}]}; +encode_subscription(#subscription{ content = undefined }, _Opts) -> + {error, [{content, undefined}]}; +encode_subscription(#subscription{ content = <<>> }, _Opts) -> + {error, [{content, undefined}]}; +encode_subscription(#subscription{ content = Content }, _Opts) + when not is_bitstring(Content) andalso not is_record(Content, event) -> + {error, [{content, Content}]}; +encode_subscription(#subscription{ id = Id + , content = #event{} = Content + }, Opts) -> + case encode_event(Content, Opts) of + {ok, [<<"EVENT">>, Rest]} -> + {ok, [<<"EVENT">>, Id, Rest]}; + Elsewise -> Elsewise + end; +encode_subscription(#subscription{ id = Id + , content = <> + }, Opts) -> + case thoas:decode(Content) of + {ok, Json} -> + case check([<<"EVENT">>, Json], Opts) of + {ok, [<<"EVENT">>, Event]} -> + {ok, [<<"EVENT">>, Id, Event]}; + {ok, Event} when is_map(Event) -> + {ok, [<<"EVENT">>, Id, Event]}; + Elsewise -> Elsewise + end; + Elsewise -> + Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_notice(#notice{ message = undefined }, _Opts) -> + {error, [{message, undefined}]}; +encode_notice(#notice{ message = <> }, _Opts) -> + {ok, [<<"NOTICE">>, Message]}; +encode_notice(#notice{ message = Message }, _Opts) -> + {error, [{message, Message}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_close(#close{ subscription_id = undefined }, _) -> + {error, [{subscription_id, undefined}]}; +encode_close(#close{ subscription_id = <>}, _) + when is_bitstring(SubscriptionId) -> + {ok, [<<"CLOSE">>, SubscriptionId]}; +encode_close(#close{ subscription_id = SubscriptionId }, _) -> + {error, [{subscription_id, SubscriptionId}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_event(Event, Opts) -> + encode_event_content(Event, Opts, #{}). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_event_content(#event{ content = undefined }, _Opts, _Buffer) -> + {error, [{content, undefined}]}; +encode_event_content(#event{ content = Content} = Event, _Opts, Buffer) + when is_bitstring(Content) -> + Next = Buffer#{ <<"content">> => Content }, + encode_event_kind(Event, _Opts, Next). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_event_kind(#event{ kind = undefined }, _Opts, _Buffer) -> + {error, [{kind, undefined}]}; +encode_event_kind(#event{ kind = Kind } = Event, Opts, Buffer) + when is_atom(Kind) -> + Next = Buffer#{ <<"kind">> => kind(Kind) }, + encode_event_tags(Event, Opts, Next); +encode_event_kind(#event{ kind = Kind }, _, _) -> + {error, [{kind, Kind}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_event_tags(#event{ tags = Undef } = Event, Opts, Buffer) + when Undef =:= [] orelse Undef =:= undefined -> + Next = Buffer#{ <<"tags">> => [] }, + encode_event_created_at(Event, Opts, Next); +encode_event_tags(#event{ tags = Tags} = Event, Opts, Buffer) + when is_list(Tags) -> + case encode_tags(Tags) of + {ok, T} -> + Next = Buffer#{ <<"tags">> => T }, + encode_event_created_at(Event, Opts, Next); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +% @todo add support for 64bits unix timestamp +encode_event_created_at(#event{ created_at = undefined } = Event, Opts, Buffer) -> + Next = Buffer#{ <<"created_at">> => erlang:system_time(seconds) }, + encode_event_public_key(Event, Opts, Next); +encode_event_created_at(#event{ created_at = {{_,_,_},{_,_,_}} = CreatedAt } = Event, Opts, Buffer) -> + Next = Buffer#{ <<"created_at">> => erlang:universaltime_to_posixtime(CreatedAt) }, + encode_event_public_key(Event, Opts, Next); +encode_event_created_at(#event{ created_at = CreatedAt } = _Event, _Opts, _Buffer) -> + {error, [{created_at, CreatedAt}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_event_public_key(#event{ public_key = undefined } = Event, Opts, Buffer) -> + case proplists:get_value(private_key, Opts, undefined) of + undefined -> + {error, [{public_key, undefined}, {private_key, undefined}]}; + <> -> + {ok, PublicKey} = nostrlib_schnorr:new_publickey(PrivateKey), + Next = Buffer#{ <<"pubkey">> => binary_to_hex(PublicKey) }, + Event2 = Event#event{ public_key = PublicKey }, + encode_event_id(Event2, Opts, Next); + <> -> + {error, [{private_key, PrivateKey}]}; + _ -> + {error, [{private_key, undefined}]} + end; +encode_event_public_key(#event{ public_key = <> } = Event, Opts, Buffer) -> + Next = Buffer#{ <<"pubkey">> => binary_to_hex(PublicKey) }, + encode_event_id(Event, Opts, Next); +encode_event_public_key(#event{ public_key = <>}, _Opts, _Buffer) -> + {error, [{public_key, PublicKey}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_event_id(#event{ id = undefined } = Event, Opts, Buffer) -> + case create_id(Buffer) of + {ok, Id} -> + Next = Buffer#{ <<"id">> => Id }, + encode_event_signature(Event, Opts, Next); + Elsewise -> Elsewise + end; +encode_event_id(#event{ id = <> } = Event, Opts, Buffer) -> + Next = Buffer#{ <<"id">> => binary_to_hex(Id) }, + + encode_event_signature(Event, Opts, Next); +encode_event_id(#event{ id = Id }, _Opts, _Buffer) -> + {error, [{event_id, Id}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_event_signature(#event{ signature = undefined } = Event, Opts, #{ <<"id">> := Id } = Buffer) -> + case proplists:get_value(private_key, Opts, undefined) of + undefined -> {error, [{private_key, undefined}]}; + <> -> + {ok, Signature} = create_signature(Id, PrivateKey), + Next = Buffer#{ <<"sig">> => binary_to_hex(Signature) }, + encode_event_final(Event, Opts, Next) + end; +encode_event_signature(#event{ signature = <> } = Event, Opts, Buffer) -> + case verify(Event) of + true -> + Next = Buffer#{ <<"sig">> => binary_to_hex(Signature) }, + encode_event_final(Event, Opts, Next); + false -> + {error, [{signature, Signature}]} + end; +encode_event_signature(#event{ signature = Signature }, _Opts, _Buffer) -> + {error, [{signature, Signature}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +encode_event_final(_Event, _Opts, Buffer) -> + {ok, [<<"EVENT">>, Buffer]}. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_tags(Tags) -> + encode_tags(Tags, []). + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_tags(Tags, Opts) -> + encode_tags(Tags, Opts, []). + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +encode_tags([], _, Buffer) -> + {ok, lists:reverse(Buffer)}; +encode_tags([Tag|Rest], Opts, Buffer) -> + case encode_tag(Tag, Opts) of + {ok, T} -> encode_tags(Rest, Opts, [T|Buffer]); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec encode_tags_test() -> any(). +encode_tags_test() -> + BinKey1 = <<1:256>>, + HexKey1 = <<(<< <<"0">> || _ <- lists:seq(0,62) >>)/bitstring, "1">>, + BinKey2 = <<2:256>>, + HexKey2 = <<(<< <<"0">> || _ <- lists:seq(0,62) >>)/bitstring, "2">>, + BinKey3 = <<3:256>>, + HexKey3 = <<(<< <<"0">> || _ <- lists:seq(0,62) >>)/bitstring, "3">>, + [?assertEqual({ok, [[<<"p">>, HexKey1] + ,[<<"p">>, HexKey2] + ,[<<"e">>, HexKey3]]} + ,encode_tags([#tag{ name = public_key, value = BinKey1 } + ,#tag{ name = public_key, value = BinKey2 } + ,#tag{ name = event_id, value = BinKey3 } + ]) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +encode_tag(#tag{ name = public_key, value = PublicKey, params = []}, _Opts) -> + {ok, [<<"p">>, binary_to_hex(PublicKey)]}; +encode_tag(#tag{ name = public_key, value = PublicKey, params = Params}, _Opts) -> + {ok, [<<"p">>, binary_to_hex(PublicKey)] ++ Params}; +encode_tag(#tag{ name = event_id, value = EventId, params = []}, _Opts) -> + {ok, [<<"e">>, binary_to_hex(EventId)]}; +encode_tag(#tag{ name = event_id, value = EventId, params = Params}, _Opts) -> + {ok, [<<"e">>, binary_to_hex(EventId)] ++ Params}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec encode_tag_test() -> any(). +encode_tag_test() -> + BinKey = <<1:256>>, + HexKey = <<(<< <<"0">> || _ <- lists:seq(0,62) >>)/bitstring, "1">>, + [?assertEqual({ok, [<<"p">>, HexKey]} + ,encode_tag(#tag{ name = public_key, value = BinKey}, []) + ) + ,?assertEqual({ok, [<<"p">>, HexKey, <<"wss://relay">>]} + ,encode_tag(#tag{ name = public_key, value = BinKey, params = [<<"wss://relay">>]}, []) + ) + ,?assertEqual({ok, [<<"e">>, HexKey]} + ,encode_tag(#tag{ name = event_id, value = BinKey}, []) + ) + ,?assertEqual({ok, [<<"e">>, HexKey, <<"wss://relay">>]} + ,encode_tag(#tag{ name = event_id, value = BinKey, params= [<<"wss://relay">>]}, []) + ) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc +%% @end +%%-------------------------------------------------------------------- +to_json(Map) -> + thoas:encode(Map). + +%%-------------------------------------------------------------------- +%% @doc `decode/1' +%% +%% @see decode/2 +%% @end +%%-------------------------------------------------------------------- +-spec decode(Message) -> Return when + Message :: encoded_event(), + Return :: decoded_messages(). +decode(Message) -> + decode(Message, []). + +%%-------------------------------------------------------------------- +%% @doc `decode/2' +%% @end +%%-------------------------------------------------------------------- +-spec decode(Message, Opts) -> Return when + Message :: encoded_event(), + Opts :: proplists:proplists(), + Return :: decoded_messages(). + +decode(Json, _Opts) + when is_map(Json) orelse is_list(Json) -> + decode_message(Json); +decode(<>, _Opts) -> + case thoas:decode(Message) of + {ok, Json} -> + decode_message(Json); + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @doc `check/1' +%% +%% @see check/2 +%% @end +%%-------------------------------------------------------------------- +-spec check(Event) -> Return when + Event :: encoded_event(), + Return :: {ok, Event} + | {error, any()}. + +check(Message) -> + check(Message, []). + +%%-------------------------------------------------------------------- +%% @doc `check/2' checks and returns the content based on the +%% specification, without returning the labels and without decoding +%% the input. +%% +%% == Examples == +%% +%% ``` +%% ''' +%% +%% @end +%%-------------------------------------------------------------------- +-spec check(Event, Opts) -> Return when + Event :: encoded_event(), + Opts :: proplists:proplists(), + Return :: {ok, Event} + | {error, any()}. + +check(Message, Opts) -> + case decode(Message, Opts) of + {ok, _, _} -> {ok, Message}; + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @doc internal function used to decode every element of a JSON +%% encoded message and convert it to record. +%% +%% @todo creates more test. +%% @end +%%-------------------------------------------------------------------- +-spec decode_message(Json) -> Return when + Json :: term(), + Return :: {ok, term()} + | {error, proplists:proplists()}. + +% decode an event message from client to relay +decode_message([<<"EVENT">>, Event]) + when is_map(Event) -> + decode_event_id(Event, #event{}, []); + +% decode an event message from relay to client +decode_message([<<"EVENT">>, <>, Event]) + when is_map(Event) -> + decode_subscription(SubscriptionId, Event, []); + +% decode a subscription request +decode_message([<<"REQ">>, <>, Filter]) -> + decode_request(SubscriptionId, Filter, []); + +% decode a close message: end of subscription request +decode_message([<<"CLOSE">>, SubscriptionId]) -> + decode_close(SubscriptionId, []); + +% decode a notice message +decode_message([<<"NOTICE">>, Notice]) -> + decode_notice(Notice, []); + +% decode an end of subscription message +decode_message([<<"EOSE">>, SubscriptionId]) -> + decode_eose(SubscriptionId, []); + +decode_message(Message) -> + {error, {unsupported, Message}}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_event_id(#{ <<"id">> := <> } = Rest, Buffer, Labels) + when byte_size(RawEventId) =:= 64 -> + case is_hex(RawEventId) of + true -> + EventId = hex_to_binary(RawEventId), + Next = Buffer#event{ id = EventId }, + decode_event_pubkey(Rest, Next, Labels); + false -> + {error, [{event, {bad, id}}]} + end; +decode_event_id(#{ <<"id">> := <<_/bitstring>> }, _, Labels) -> + Reason = [{event, {bad, id}} + ,{labels, Labels} + ], + {error, Reason}; +decode_event_id(_, _, _) -> + Reason = [{event, {missing, id}}], + {error, Reason}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_event_pubkey(#{ <<"pubkey">> := <> } = Rest, Buffer, Labels) + when byte_size(RawPublicKey) =:= 64 -> + case is_hex(RawPublicKey) of + true -> + PublicKey = hex_to_binary(RawPublicKey), + Next = Buffer#event{ public_key = PublicKey }, + decode_event_created_at(Rest, Next, Labels); + false -> + {error, [{event, {bad, public_key}}]} + end; +decode_event_pubkey(#{ <<"pubkey">> := <<_/bitstring>> }, _, _) -> + {error, [{event, {bad, public_key}}]}; +decode_event_pubkey(_, _, _) -> + {error, [{event, {missing, pubkey}}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_event_created_at(#{ <<"created_at">> := RawCreatedAt } = Rest, Buffer, Labels) + when is_integer(RawCreatedAt) -> + CreatedAt = erlang:posixtime_to_universaltime(RawCreatedAt), + Next = Buffer#event{ created_at = CreatedAt }, + decode_event_kind(Rest, Next, Labels); +decode_event_created_at(_,_,_) -> + {error, [{event, {missing, created_at}}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_event_kind(#{ <<"kind">> := RawKind } = Rest, Buffer, Labels) + when is_integer(RawKind) -> + case kind(RawKind) of + unsupported -> {error, [{event, {unsupported, kind}}]}; + Kind -> + Next = Buffer#event{ kind = Kind }, + decode_event_tags(Rest, Next, Labels) + end; +decode_event_kind(_,_,_) -> + {error, [{event, {missing, kind}}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_event_tags(#{ <<"tags">> := RawTags } = Rest, Buffer, Labels) + when is_list(RawTags) -> + case decode_tags(RawTags) of + {ok, Tags} -> + Next = Buffer#event{ tags = Tags }, + decode_event_content(Rest, Next, Labels); + Elsewise -> Elsewise + end; +decode_event_tags(_,_,_) -> + {error, [{event, {missing, tags}}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_event_content(#{ <<"content">> := Content } = Rest, Buffer, Labels) + when is_binary(Content) -> + Next = Buffer#event{ content = Content }, + decode_event_signature(Rest, Next, Labels); +decode_event_content(_,_,_) -> + {error, [{event, {missing, content}}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_event_signature(#{ <<"sig">> := <> } = Rest, Buffer, Labels) + when byte_size(RawSignature) =:= 128 -> + case is_hex(RawSignature) of + true -> + Signature = hex_to_binary(RawSignature), + Next = Buffer#event{ signature = Signature }, + decode_check_event_id(Rest, Next, Labels); + false -> + Reason = [{reason, bad_signature} + ,{payload, Rest} + ,{structure, Buffer} + ,{labels, Labels} + ], + {error, Reason} + end; +decode_event_signature(Rest, Buffer, Labels) -> + Reason = [{reason, missing_signature} + ,{payload, Rest} + ,{structure, Buffer} + ,{labels, Labels} + ], + {error, Reason}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_check_event_id(#{ <<"id">> := Id } = Rest, Buffer, Labels) -> + case {ok, Id} =:= create_id(Rest) of + true -> + decode_check_signature(Rest, Buffer, [{has_strict_event_id, true}|Labels]); + false -> + decode_check_signature(Rest, Buffer, [{has_strict_event_id, false}|Labels]) + end. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_check_signature(Rest, Buffer, Labels) -> + case verify(Rest) of + true -> + {ok, Buffer, [{has_valid_signature, true}|Labels]}; + false -> + Reason = [{reason, signature} + ,{payload, Rest} + ,{structure, Buffer} + ,{labels, [{has_valid_signature, false}|Labels]} + ], + {error, Reason} + end. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +%% create_id(#event{} = Event) -> +%% Serialized = serialize(Event), +%% {ok, crypto:hash(sha256, Serialized)}; +create_id(Event) + when is_map(Event) -> + case serialize(Event) of + {ok, Serial} -> + RawId = crypto:hash(sha256, Serial), + {ok, binary_to_hex(RawId)}; + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @doc `create_signature/2' creates a signature based on an event id +%% and a private key. It returns a raw bin. +%% +%% This function accepts both hexadecimal format and binary format. It +%% will output always a binary format. +%% +%% == Examples == +%% +%% ``` +%% Binary = <<1:256>>. +%% Hex = << <<"1">> || _ <- lists:seq(0,63) >>. +%% +%% Signature = create_signature(Binary, Binary). +%% Signature = create_signature(Binary, Hex). +%% Signature = create_signature(Hex, Binary). +%% Signature = create_signature(Hex, Hex). +%% ''' +%% +%% @end +%% -------------------------------------------------------------------- +-spec create_signature(EventId, PrivateKey) -> Return when + EventId :: decoded_event_id(), + PrivateKey :: decoded_private_key(), + Return :: {ok, decoded_signature()} + | {error, proplists:proplists()}. + +create_signature(<>, <>) -> + case {is_hex(Id), is_hex(PrivateKey)} of + {true, true} -> + BinId = hex_to_binary(Id), + BinPrivateKey = hex_to_binary(PrivateKey), + nostrlib_schnorr:sign(BinId, BinPrivateKey); + {false, true} -> + BinPrivateKey = hex_to_binary(PrivateKey), + nostrlib_schnorr:sign(Id, BinPrivateKey); + {true, false} -> + BinId = hex_to_binary(Id), + nostrlib_schnorr:sign(BinId, PrivateKey); + {false, false} -> + nostrlib_schnorr:sign(Id, PrivateKey) + end. + +% @hidden +-spec create_signature_test() -> any(). +create_signature_test() -> + Bin = <<1:256>>, + Head = << <<"0">> || _ <- lists:seq(0,62) >>, + Hex = <>, + Signature = <<106,173,19,98,223,201,254,15,199,209,119,83,34,226 + ,82,65,104,146,115,163,202,32,2,58,203,186,161,191 + ,248,192,30,206,174,71,86,28,235,92,115,214,198,59 + ,87,188,119,104,171,1,101,141,142,41,156,37,57,40 + ,210,191,40,114,92,0,166,1>>, + [?assertEqual({ok, Signature}, create_signature(Bin, Bin)) + ,?assertEqual({ok, Signature}, create_signature(Hex, Bin)) + ,?assertEqual({ok, Signature}, create_signature(Bin, Hex)) + ,?assertEqual({ok, Signature}, create_signature(Hex, Hex)) + ]. + +%%-------------------------------------------------------------------- +%% @doc `verify/1' function verifies a nostr record or a raw parsed +%% JSON. +%% +%% @todo add a way to verify raw event as bitstring (JSON). +%% @end +%%-------------------------------------------------------------------- +-spec verify(Event) -> Return when + Event :: decoded_event(), + Return :: boolean(). + +verify(#event{ id = Id + , public_key = PublicKey + , signature = Signature = _Event }) -> + verify(Id, PublicKey, Signature); +verify(#{ <<"id">> := Id + , <<"pubkey">> := PublicKey + , <<"sig">> := Signature }) -> + BinId = hex_to_binary(Id), + BinPublicKey = hex_to_binary(PublicKey), + BinSignature = hex_to_binary(Signature), + verify(BinId, BinPublicKey, BinSignature). + +%%-------------------------------------------------------------------- +%% @doc `verify/3' +%% @end +%%-------------------------------------------------------------------- +-spec verify(EventId, PublicKey, Signature) -> Return when + EventId :: decoded_event_id(), + PublicKey :: decoded_public_key(), + Signature :: decoded_signature(), + Return :: boolean(). + +verify(BinId, BinPublicKey, BinSignature) -> + nostrlib_schnorr:verify(BinId, BinPublicKey, BinSignature). + +%%-------------------------------------------------------------------- +%% @doc `sign/2' +%% @end +%%-------------------------------------------------------------------- +-spec sign(Event, PrivateKey) -> Event when + PrivateKey :: decoded_private_key(), + Event :: {ok, event()}. + +sign(#event{ id = Id } = Event, PrivateKey) -> + HexId = nostrlib:hex_to_binary(Id), + {ok, RawSignature} = nostrlib_schnorr:sign(HexId, PrivateKey), + HexSignature = nostrlib:binary_to_hex(RawSignature), + Event#event{ signature = HexSignature }. + +%%-------------------------------------------------------------------- +%% @doc `check_tag/1' check if a `tag' is valid, like specified in +%% nip/01. +%% +%% @end +%%-------------------------------------------------------------------- +-spec decode_tag(Tag) -> Return when + Tag :: [iodata(), ...], + Return :: {ok, Tag} + | {error, proplists:proplists()}. + +% public key tag +decode_tag([<<"p">>, <>]) -> + T = #tag{ name = public_key + , value = hex_to_binary(PublicKey) + }, + {ok, T}; +decode_tag([<<"p">>, <>, <>]) -> + T = #tag{ name = public_key + , value = hex_to_binary(PublicKey) + , params = [RecommendedRelayUrl] + }, + {ok, T}; + +% event id tag +decode_tag([<<"e">>, <>]) -> + T = #tag{ name = event_id + , value = hex_to_binary(EventId) + }, + {ok, T}; +decode_tag([<<"e">>, <>, <>]) -> + T = #tag{ name = event_id + , value = hex_to_binary(EventId) + , params = [RecommendedRelayUrl] + }, + {ok, T}; + +decode_tag(Tag) -> + {error, [{tag, Tag}]}. + +%%-------------------------------------------------------------------- +%% @doc `check_tags/1' checks if a list of `tags' are valid. +%% +%% @end +%%-------------------------------------------------------------------- +-spec decode_tags(Tags) -> Return when + Tag :: [iodata(), ...], + Tags :: [Tag, ...], + Return :: {ok, Tags} + | {error, proplists:proplists()}. + +decode_tags(Tags) -> + decode_tags(Tags, []). + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +decode_tags([], Buffer) -> + {ok, lists:reverse(Buffer)}; +decode_tags([Tag|Rest], Buffer) -> + case decode_tag(Tag) of + {ok, T} -> decode_tags(Rest, [T|Buffer]); + Elsewise -> Elsewise + end; +decode_tags(Tags, _) -> + {error, [{tags, Tags}]}. + +%%-------------------------------------------------------------------- +%% @doc +%% @todo add more tests +%% @end +%%-------------------------------------------------------------------- +-spec serialize(event()) -> list(). +serialize(#{ <<"pubkey">> := <> }) + when byte_size(PublicKey) =/= 64 -> + {error, [{public_key, PublicKey}]}; +serialize(#{ <<"created_at">> := CreatedAt }) + when not is_integer(CreatedAt) orelse CreatedAt<0 -> + {error, [{created_at, CreatedAt}]}; +serialize(#{ <<"kind">> := Kind }) + when not is_integer(Kind) -> + {error, [{kind, Kind}]}; +serialize(#{ <<"tags">> := Tags }) + when not is_list(Tags) -> + {error, [{tags, Tags}]}; +serialize(#{ <<"content">> := Content }) + when not is_binary(Content) -> + {error, [{content, Content}]}; +serialize(#{ <<"pubkey">> := <> + , <<"created_at">> := CreatedAt + , <<"kind">> := Kind + , <<"tags">> := Tags + , <<"content">> := <> + } = _Event) -> + Encoded = thoas:encode([0,PublicKey,CreatedAt,Kind,Tags,Content]), + {ok, Encoded}; +serialize(Event) -> + {error, [{serialize, Event}]}. + +% @hidden +-spec serialize_test() -> any(). +serialize_test() -> + [?assertEqual({error,[{created_at, -1}]} + ,serialize(#{ <<"created_at">> => -1 })) + ,?assertEqual({error,[{kind, test}]} + ,serialize(#{ <<"kind">> => test })) + ,?assertEqual({error,[{content, test}]} + ,serialize(#{ <<"content">> => test })) + ,?assertEqual({error,[{tags, <<>>}]} + ,serialize(#{ <<"tags">> => <<>> })) + ,?assertEqual({error,[{public_key, <<>>}]} + ,serialize(#{ <<"pubkey">> => <<>> })) + ,?assertEqual({error,[{serialize, <<>>}]} + ,serialize(<<>>)) + ]. + + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +decode_subscription(<>, RawEvent, Labels) -> + case decode_event_id(RawEvent, #event{}, []) of + {ok, Event, EventLabels} -> + Subscription = #subscription{ id = SubscriptionId + , content = Event + }, + {ok, Subscription, [EventLabels|Labels]}; + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +decode_filters(Filter) + when is_map(Filter) -> + decode_filter(Filter); +decode_filters(Filters) + when is_list(Filters)-> + decode_filters(Filters, []). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_filters([], Buffer) -> + {ok, lists:reverse(Buffer)}; +decode_filters([Filter|Rest], Buffer) -> + case decode_filter(Filter) of + {ok, F} -> decode_filters(Rest, [F|Buffer]); + Elsewise -> Elsewise + end. + +% @hidden +-spec decode_filters_test() -> any(). +decode_filters_test() -> + [?assertEqual({ok, []}, decode_filters([])) + ,?assertEqual({ok, #filter{}}, decode_filters(#{})) + ,?assertEqual({ok, [#filter{}]}, decode_filters([#{}])) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +decode_filter(Filter) -> + decode_filter_check(Filter). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_filter_check(#{ <<"since">> := Since }) + when not is_integer(Since) orelse Since < 0 -> + {error, [{since, Since}]}; +decode_filter_check(#{ <<"until">> := Until }) + when not is_integer(Until) orelse Until < 0 -> + {error, [{until, Until}]}; +decode_filter_check(#{ <<"since">> := Since, <<"until">> := Until }) + when Since > Until -> + {error, [{until, Until}, {since, Since}]}; +decode_filter_check(#{ <<"limit">> := Limit }) + when not is_integer(Limit) orelse Limit < 0 -> + {error, [{limit, Limit}]}; +decode_filter_check(#{ <<"ids">> := EventsIds }) + when not is_list(EventsIds) -> + {error, [{event_ids, EventsIds}]}; +decode_filter_check(#{ <<"authors">> := Authors }) + when not is_list(Authors) -> + {error, [{authors, Authors}]}; +decode_filter_check(#{ <<"#e">> := TagEventIds }) + when not is_list(TagEventIds) -> + {error, [{tag_event_ids, TagEventIds}]}; +decode_filter_check(#{ <<"#p">> := TagPublicKeys }) + when not is_list(TagPublicKeys) -> + {error, [{tag_public_keys, TagPublicKeys}]}; +decode_filter_check(#{ <<"kinds">> := Kinds }) + when not is_list(Kinds) -> + {error, [{kinds, Kinds}]}; +decode_filter_check(Filter) -> + decode_filter_ids(Filter, #filter{}). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +% @todo add decode prefixes for ids +decode_filter_ids(#{ <<"ids">> := EventIds } = Filter, Buffer) + when is_list(EventIds) -> + case decode_prefixes(EventIds) of + {ok, Prefixes} -> + Next = Buffer#filter{ event_ids = Prefixes }, + decode_filter_authors(Filter, Next); + Elsewise -> + Elsewise + end; +decode_filter_ids(Filter, Buffer) + when is_map(Filter) -> + decode_filter_authors(Filter, Buffer). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_prefixes(Prefixes) -> + decode_prefixes(Prefixes, []). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_prefixes([], Buffer) -> {ok, lists:reverse(Buffer)}; +decode_prefixes([Prefix|Rest], Buffer) -> + case is_hex(Prefix) of + true -> + Decoded = hex_to_binary(Prefix), + decode_prefixes(Rest, [Decoded|Buffer]); + false -> + {error, [{prefix, Prefix}]} + end; +decode_prefixes(Prefixes,_) -> + {error, [{prefixes, Prefixes}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +% @todo add decode prefixes for authors +decode_filter_authors(#{ <<"authors">> := Authors } = Filter, Buffer) + when is_list(Authors) -> + case decode_prefixes(Authors) of + {ok, Prefixes} -> + Next = Buffer#filter{ authors = Prefixes }, + decode_filter_kinds(Filter, Next); + Elsewise -> Elsewise + end; +decode_filter_authors(Filter, Buffer) + when is_map(Filter) -> + decode_filter_kinds(Filter, Buffer). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_filter_kinds(#{ <<"kinds">> := Kinds } = Filter, Buffer) -> + Next = Buffer#filter{ kinds = kinds(Kinds) }, + decode_filter_tag_event_ids(Filter, Next); +decode_filter_kinds(Filter, Buffer) + when is_map(Filter) -> + decode_filter_tag_event_ids(Filter, Buffer). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +% @todo add decode for tags event ids +decode_filter_tag_event_ids(#{ <<"#e">> := TagEventIds } = Filter, Buffer) -> + Parsed = lists:map(fun(X) -> hex_to_binary(X) end, TagEventIds), + Next = Buffer#filter{ tag_event_ids = Parsed }, + decode_filter_tag_public_keys(Filter, Next); +decode_filter_tag_event_ids(Filter, Buffer) + when is_map(Filter) -> + decode_filter_tag_public_keys(Filter, Buffer). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +% @todo add decode for tags public keys +decode_filter_tag_public_keys(#{ <<"#p">> := TagPublicKeys } = Filter, Buffer) -> + Parsed = lists:map(fun(X) -> hex_to_binary(X) end, TagPublicKeys), + Next = Buffer#filter{ tag_public_keys = Parsed }, + decode_filter_since(Filter, Next); +decode_filter_tag_public_keys(Filter, Buffer) + when is_map(Filter) -> + decode_filter_since(Filter, Buffer). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_filter_since(#{ <<"since">> := Since } = Filter, Buffer) -> + Next = Buffer#filter{ since = erlang:posixtime_to_universaltime(Since) }, + decode_filter_until(Filter, Next); +decode_filter_since(Filter, Buffer) + when is_map(Filter) -> + decode_filter_until(Filter, Buffer). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_filter_until(#{ <<"until">> := Until } = Filter, Buffer) -> + Next = Buffer#filter{ until = erlang:posixtime_to_universaltime(Until) }, + decode_filter_limit(Filter, Next); +decode_filter_until(Filter, Buffer) + when is_map(Filter) -> + decode_filter_limit(Filter, Buffer). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_filter_limit(#{ <<"limit">> := Limit } = _Filter, Buffer) -> + {ok, Buffer#filter{ limit = Limit }}; +decode_filter_limit(Filter, Buffer) + when is_map(Filter) -> + {ok, Buffer}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec decode_filter_test() -> any(). +decode_filter_test() -> + [?assertEqual({ok, #filter{}}, decode_filter(#{})) + ,?assertEqual({ok, #filter{ since = {{2020,01,01}, {00,00,00}} + , until = {{2021,01,01}, {00,00,00}} + , limit = 100 + }} + ,decode_filter(#{ <<"since">> => 1577836800 + , <<"until">> => 1609459200 + , <<"limit">> => 100 + }) + ) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc +%% @end +%%-------------------------------------------------------------------- +decode_request(SubscriptionId, Filters, Labels) -> + case decode_filters(Filters) of + {ok, F} -> + Request = #request{ subscription_id = SubscriptionId + , filter = F + }, + {ok, Request, Labels}; + Elsewise -> Elsewise + end. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc +%% @end +%%-------------------------------------------------------------------- +decode_eose(Value, Labels) -> + case decode_subscription_id(Value) of + {ok, SubscriptionId} -> + Eose = #eose{ id = SubscriptionId }, + {ok, Eose, Labels}; + _Elsewise -> + {error, [{eose, Value}]} + end. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +decode_notice(Value, Labels) + when is_binary(Value) -> + Notice = #notice{ message = Value }, + {ok, Notice, Labels}; +decode_notice(Value, _Labels) -> + {error, [{notice, Value}]}. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +decode_close(Value, Labels) + when is_binary(Value) -> + case decode_subscription_id(Value) of + {ok, SubscriptionId} -> + Close = #close{ subscription_id = SubscriptionId }, + {ok, Close, Labels}; + Elsewise -> Elsewise + end; +decode_close(SubscriptionId, _Labels) -> + {error, [{subscription_id, SubscriptionId}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +decode_subscription_id(<>) -> + case re:run(SubscriptionId, <<"^\\w+$">>) of + {match, _} -> {ok, SubscriptionId}; + _ -> {error, [{subscription_id, SubscriptionId}]} + end. %%-------------------------------------------------------------------- %% @doc kind/1 function helps to convert nostr kind from atom to @@ -38,13 +1427,11 @@ encode(Message) -> Kind :: kind(), Return :: kind(). -?KIND(0, metadata); -?KIND(1, short_text_note); -?KIND(2, recommended_relay); -?KIND(3, contacts); -?KIND(4, encrypted_direct_messages); -?KIND(5, event_deletion); -?KIND(7, reaction). +?KIND(0, set_metadata); +?KIND(1, text_note); +?KIND(2, recommend_server); +?KIND(7, reaction); +kind(_) -> unsupported. %%-------------------------------------------------------------------- %% @doc kinds/1 function converts a list of atoms into integers. @@ -56,32 +1443,291 @@ encode(Message) -> kinds(Kinds) -> Converter = fun(Kind) when is_atom(Kind) -> kind(Kind); - (Kind) when is_integer(Kind) -> Kind + (Kind) when is_integer(Kind) -> kind(Kind) end, lists:map(Converter, Kinds). %%-------------------------------------------------------------------- -%% @doc wrapper around since/1. +%% @doc `integer_to_hex/1' converts integer to hexadecimal bitstring. +%% %% @end -%% @TODO create spec %%-------------------------------------------------------------------- --spec since() -> Return when - Return :: pos_integer(). +-spec integer_to_hex(Integer) -> Hexadecimal when + Integer :: pos_integer() | bitstring() | string(), + Hexadecimal :: bitstring(). -since() -> - since(10). +integer_to_hex(Integer) + when is_integer(Integer) andalso Integer >= 0 -> + << <<(char_to_lower(X))>> || <> <= integer_to_binary(Integer, 16) >>; +integer_to_hex(List) + when is_list(List) -> + integer_to_hex(list_to_integer(List)); +integer_to_hex(Bitstring) + when is_bitstring(Bitstring) -> + integer_to_hex(binary_to_integer(Bitstring)). + +% @hidden +-spec integer_to_hex_test() -> any(). +integer_to_hex_test() -> + ?assertEqual(<<"0">>, integer_to_hex(0)), + ?assertEqual(<<"0">>, integer_to_hex("0")), + ?assertEqual(<<"0">>, integer_to_hex(<<"0">>)), + ?assertEqual(<<"1">>, integer_to_hex(1)), + ?assertEqual(<<"1">>, integer_to_hex("1")), + ?assertEqual(<<"1">>, integer_to_hex(<<"1">>)), + ?assertEqual(<<"f">>, integer_to_hex(15)), + ?assertEqual(<<"f">>, integer_to_hex("15")), + ?assertEqual(<<"f">>, integer_to_hex(<<"15">>)), + ?assertEqual(<<"ffff">>, integer_to_hex(65535)), + ?assertEqual(<<"ffff">>, integer_to_hex("65535")), + ?assertEqual(<<"ffff">>, integer_to_hex(<<"65535">>)), + ?assertEqual(<<"ffffffff">>, integer_to_hex(4294967295)), + ?assertEqual(<<"ffffffff">>, integer_to_hex("4294967295")), + ?assertEqual(<<"ffffffff">>, integer_to_hex(<<"4294967295">>)), + ?assertEqual(<<"5132e8bf2ac08dc03016dda748ab8c9c1207d595afb35b87539413b99932ad72">> + ,integer_to_hex(36727289447488093764767047625565761236828060469984325220555173745007757798770)). %%-------------------------------------------------------------------- -%% @doc since/1 returns a Posix Timestamp with a shift in second. +%% @doc `hex_to_integer/1' converts an hexadecimal bitstring to +%% integer. It's a wrapper around `erlang:binary_to_integer/1' +%% function. +%% +%% @see erlang:binary_to_integer/2 %% @end -%% @TODO create spec %%-------------------------------------------------------------------- --spec since(Shift) -> Return when - Shift :: pos_integer(), - Return :: pos_integer(). +-spec hex_to_integer(Hexadecimal) -> Integer when + Hexadecimal :: bitstring(), + Integer :: pos_integer() | bitstring() | string(). -since(Shift) - when Shift > 0 -> - UniversalTime = erlang:universaltime(), - PosixTime = erlang:universaltime_to_posixtime(UniversalTime), - PosixTime-Shift. +hex_to_integer(Hexadecimal) -> + erlang:binary_to_integer(Hexadecimal, 16). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec hex_to_integer_test() -> any(). +hex_to_integer_test() -> + [?assertEqual(36727289447488093764767047625565761236828060469984325220555173745007757798770 + ,hex_to_integer(<<"5132e8bf2ac08dc03016dda748ab8c9c1207d595afb35b87539413b99932ad72">>)) + ]. + +%%-------------------------------------------------------------------- +%% @doc `binary_to_hex/1' converts a raw bitstring to hexadecimal +%% string. +%% +%% @end +%%-------------------------------------------------------------------- +-spec binary_to_hex(Bitstring) -> Hexadecimal when + Bitstring :: iodata(), + Hexadecimal :: bitstring(). + +binary_to_hex(Bitstring) -> + Convert = fun(Char) -> + <> = integer_to_binary(Char, 16), + char_to_lower(Hex) + end, + << <<(Convert(X))>> || <> <= Bitstring >>. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec binary_to_hex_test() -> any(). +binary_to_hex_test() -> + [?assertEqual(<<"0123456789abcdef">> + ,binary_to_hex(<< <> || X <- lists:seq(0,15) >>)) + ,?assertEqual(<<"ffffffff">> + ,binary_to_hex(<< <<15:4>> || _ <- lists:seq(0,7) >>)) + ]. + +%%-------------------------------------------------------------------- +%% @doc `hex_to_binary/1' converts an hexadecimal string to a raw +%% binary. +%% +%% == Examples == +%% +%% ``` +%% <<1,35,69,103,137,171,205,239>> = nostrlib:hex_to_binary(<<"0123456789abcdef">>). +%% ''' +%% +%% @end +%%-------------------------------------------------------------------- +-spec hex_to_binary(Hexadecimal) -> Binary when + Hexadecimal :: binary(), + Binary :: binary(). + +hex_to_binary(Hexadecimal) -> + << <<(char_to_hex(X)):4>> || <> <= Hexadecimal >>. + +% @hidden +-spec hex_to_binary_test() -> any(). +hex_to_binary_test() -> + [?assertEqual(<<1,35,69,103,137,171,205,239>> + ,hex_to_binary(<<"0123456789abcdef">>)) + ,?assertEqual(<< <<15:4>> || _ <- lists:seq(0,7) >> + ,hex_to_binary(<<"ffffffff">>)) + ]. + +%%-------------------------------------------------------------------- +%% @doc `is_hex/1' checks if an hex string is valid. +%% +%% == Examples == +%% +%% ``` +%% true = nostrlib:is_hex(<<"0123456789abcdef">>). +%% false = nostrlib:is_hex(<<"test">>). +%% ''' +%% +%% @see check_hex/1 +%% @end +%%-------------------------------------------------------------------- +-spec is_hex(Hexadecimal) -> Return when + Hexadecimal :: iodata(), + Return :: boolean(). + +is_hex(Hexadecimal) -> + case check_hex(Hexadecimal, <<>>) of + {ok, _} -> true; + {error, _} -> false + end. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec is_hex_test() -> any(). +is_hex_test() -> + [?assertEqual(true, is_hex(<<"0123456789abcdef">>)) + ,?assertEqual(false, is_hex(<<"test">>)) + ]. + +%%-------------------------------------------------------------------- +%% @doc `check_hex/1' analyses the content of a supposed hexadecimal +%% bitstring and returns if it if valid, else returns the reason of +%% the problem. This function only accept lowercase hexadecimal. +%% +%% == Examples == +%% +%% ``` +%% {ok, <<"0123456789abcdef">>} = nostrlib:check_hex(<<"0123456789abcdef">>). +%% +%% {error,[{char,<<"_">>} +%% ,{offset,10} +%% ,{valid,<<"0123456789">>} +%% ,{rest,<<"abcdef">>}] = check_hex(<<"0123456789_abcdef">>). +%% ''' +%% +%% @end +%%-------------------------------------------------------------------- +-spec check_hex(Hexadecimal) -> Return when + Hexadecimal :: iodata(), + Return :: {ok, Hexadecimal} + | {error, proplists:proplists()}. + +check_hex(Hexadecimal) -> + check_hex(Hexadecimal, <<>>). + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec check_hex_test() -> any(). +check_hex_test() -> + [?assertEqual({ok, <<"0123456789abcdef">>} + ,check_hex(<<"0123456789abcdef">>)) + ,?assertEqual({error,[{char,<<"_">>},{offset,10},{valid,<<"0123456789">>},{rest,<<"abcdef">>}]} + ,check_hex(<<"0123456789_abcdef">>)) + ]. + +%%-------------------------------------------------------------------- +%% @hidden +%%-------------------------------------------------------------------- +-spec check_hex(Hexadecimal, Buffer) -> Return when + Hexadecimal :: bitstring(), + Buffer :: bitstring(), + Return :: {ok, Hexadecimal} + | {error, proplists:proplists()}. + +check_hex(<<>>, Buffer) -> {ok, Buffer}; +check_hex(<>, Buffer) + when Char >= $0 andalso Char =< $9 -> + check_hex(Rest, <>); +check_hex(<>, Buffer) + when Char >= $a andalso Char =< $f -> + check_hex(Rest, <>); +check_hex(<>, Buffer) -> + Offset = byte_size(Buffer), + Message = [{char, Char}, {offset, Offset}, {valid, Buffer}, {rest, Rest}], + {error, Message}; +check_hex(Value, Buffer) -> + {error, [{value, Value}, {buffer, Buffer}]}. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function used to change uppercase hex to lowercase. +%% @end +%%-------------------------------------------------------------------- +-spec char_to_lower(Integer) -> Integer when + Integer :: pos_integer(). + +char_to_lower($A) -> $a; +char_to_lower($B) -> $b; +char_to_lower($C) -> $c; +char_to_lower($D) -> $d; +char_to_lower($E) -> $e; +char_to_lower($F) -> $f; +char_to_lower(Integer) + when Integer >= $0 andalso Integer =< $9 -> + Integer. + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc internal function used to convert ASCII numbers to +%% integers. Accept only lowercase hexadecimal code. +%% @end +%%-------------------------------------------------------------------- +-spec char_to_hex(Char) -> Integer when + Char :: pos_integer(), + Integer :: pos_integer(). + +char_to_hex($0) -> 0; +char_to_hex($1) -> 1; +char_to_hex($2) -> 2; +char_to_hex($3) -> 3; +char_to_hex($4) -> 4; +char_to_hex($5) -> 5; +char_to_hex($6) -> 6; +char_to_hex($7) -> 7; +char_to_hex($8) -> 8; +char_to_hex($9) -> 9; +char_to_hex($a) -> 10; +char_to_hex($b) -> 11; +char_to_hex($c) -> 12; +char_to_hex($d) -> 13; +char_to_hex($e) -> 14; +char_to_hex($f) -> 15. + +%%-------------------------------------------------------------------- +%% @doc `create_subscription_id/0' generate a new subscription id +%% based on `crypto:strong_rand_bytes/1'. It returns a base64 encoded +%% string. +%% +%% From the specification: +%% +%% `' is an arbitrary, non-empty string of max +%% length `64' chars, that should be used to represent a +%% subscription. +%% +%% == Examples == +%% +%% ``` +%% <> = nostrlib:new_subscription_id(). +%% ''' +%% +%% @todo use base32 string instead of base64. +%% @see crypto:strong_rand_bytes/1 +%% @see base64:encode/1 +%% @end +%%-------------------------------------------------------------------- +-spec new_subscription_id() -> Return when + Return :: bitstring(). + +new_subscription_id() -> + base64:encode(crypto:strong_rand_bytes(24)). diff --git a/src/nostrlib_client.erl b/src/nostrlib_client.erl index 18c6ff8..a948fa0 100644 --- a/src/nostrlib_client.erl +++ b/src/nostrlib_client.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() -> - <> = 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>> - || <> <= 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]. diff --git a/src/nostrlib_decoder.erl b/src/nostrlib_decoder.erl deleted file mode 100644 index 3816874..0000000 --- a/src/nostrlib_decoder.erl +++ /dev/null @@ -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 -%%%=================================================================== --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}. diff --git a/src/nostrlib_event.erl b/src/nostrlib_event.erl new file mode 100644 index 0000000..fa5e9c8 --- /dev/null +++ b/src/nostrlib_event.erl @@ -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). + + + diff --git a/src/nostrlib_identity.erl b/src/nostrlib_identity.erl new file mode 100644 index 0000000..cd008f4 --- /dev/null +++ b/src/nostrlib_identity.erl @@ -0,0 +1,5 @@ +%%%=================================================================== +%%% @doc DRAFT +%%% @end +%%%=================================================================== +-module(nostrlib_identity). diff --git a/src/nostrlib_kind.erl b/src/nostrlib_kind.erl deleted file mode 100644 index 0f45fbe..0000000 --- a/src/nostrlib_kind.erl +++ /dev/null @@ -1,66 +0,0 @@ -%%%=================================================================== -%%% @doc -%%% @end -%%% @author Mathieu Kerjouan -%%%=================================================================== --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). diff --git a/src/nostrlib_relay.erl b/src/nostrlib_relay.erl deleted file mode 100644 index 4d4334d..0000000 --- a/src/nostrlib_relay.erl +++ /dev/null @@ -1,22 +0,0 @@ -%%%=================================================================== -%%% @doc -%%% @end -%%% @author Mathieu Kerjouan -%%%=================================================================== --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]. diff --git a/src/nostrlib_schnorr.erl b/src/nostrlib_schnorr.erl new file mode 100644 index 0000000..9397ed8 --- /dev/null +++ b/src/nostrlib_schnorr.erl @@ -0,0 +1,1035 @@ +%%%=================================================================== +%%% @author Mathieu Kerjouan +%%% @copyright 2023 Mathieu Kerjouan +%%% @since nostr/0.1.0 (2023) +%%% @doc +%%% +%%% This module implement Schnorr signature like defined in the Bitcoin +%%% BIP-0340 specification using only Erlang. +%%% +%%% Indeed, the Schnorr signature scheme is used by Bitcoin but also +%%% by nostr. All messages coming from clients are signed using it, +%%% and the relay should be able to validate them. This feature is +%%% mandatory and critical for the nostr project. +%%% +%%% The code present in this module has been optimized using +%%% `crypto:mod_pow/3' function from `crypto' module. More tests can +%%% be found in `extra' directory at the root of this project. +%%% +%%% == Examples == +%%% +%%% ``` +%%% % generate a private key using secp256k1 cipher +%%% {ok, PrivateKey} = nostrlib_schnorr:new_privatekey(). +%%% +%%% % generate the public key from the private key +%%% {ok, PublicKey} = nostrlib_schnorr:new_publickey(PrivateKey). +%%% +%%% % Create a message +%%% Message = <<"Hello Joe, how are you?">>. +%%% +%%% % Create the SHA256 checksum of this message +%%% Hash = crypto:hash(sha256, Message). +%%% +%%% % Sign the hash of the message +%%% % Note: nostrlib_schnorr:sign/2 can also be used +%%% {ok, Signature} = nostrlib_schnorr:sign(Hash, PrivateKey, <<0:256>>). +%%% +%%% % Valid the signature with the PublicKey +%%% true = nostrlib_schnorr:verify(Hash, PublicKey, Signature). +%%% ''' +%%% +%%% == modulo function == +%%% +%%% `erlang:rem/2' and `erlang:div/2' are not compatible with Schnorr +%%% signature -- at least the one implemented by Bitcoin. A modulo +%%% function needs to be created, a functional was found on stack +%%% overflow. The `crypto:mod_pow/3' function can be used to generate +%%% modulo from positive integer using the second argument (`P') to 1. +%%% +%%% == pow function == +%%% +%%% `crypto:mod_pow/3' does not support negative integer, it must +%%% support negative integer like the pow function provided by +%%% python. This function is working for positive numbers though. +%%% +%%% ``` +%%% # Python +%%% 33 == pow(-123456789, 1, 123) +%%% 23664 == pow(-987654321, 1, 65535) +%%% +%%% # Erlang +%%% <<"F">> = crypto:mod_pow(-123456789, 1, 123). +%%% <> = crypto:mod_pow(-987654321, 1, 65535). +%%% X2 = 23665. +%%% ''' +%%% +%%% @end +%%%=================================================================== +-module(nostrlib_schnorr). +-export([new_privatekey/0, new_publickey/1]). +-export([sign/2, sign/3]). +-export([verify/3]). +-export([mod/2, pow/3]). +% only exported during debug +% -export([point_add/2, point_mul/2]). +% -export([point_to_bitstring/1, bytes_from_int/1, lift_x/1]). +% -export([integer_to_bitstring/1, bitstring_to_integer/1]). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("kernel/include/logger.hrl"). + +-define(P, 16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F). +-define(N, 16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141). +-define(GX, 16#79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798). +-define(GY, 16#483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8). +-define(G, #point{ x = ?GX, y = ?GY }). + +-record(point, { x = 0 :: pos_integer() + , y = 0 :: pos_integer() + }). +-type point() :: #point{ x :: pos_integer() + , y :: pos_integer() + } + | infinity. + +-type private_key() :: <<_:256>>. +-type public_key() :: <<_:256>>. +-type message() :: <<_:256>>. +-type aux_rand() :: <<_:256>>. +-type signature() :: <<_:512>>. + +%%-------------------------------------------------------------------- +%% required for eunit. +%%-------------------------------------------------------------------- +-spec test() -> any(). + +%%-------------------------------------------------------------------- +%% @doc internal function. `point_add/2' adds two points. This +%% function is not optimized and will have huge impact on performance, +%% in particular during the exponentiation. +%% +%% @end +%%-------------------------------------------------------------------- +-spec point_add(Point1, Point2) -> Return when + Point1 :: point(), + Point2 :: point(), + Return :: point(). + +point_add(infinity, #point{} = P2) -> P2; +point_add(#point{} = P1, infinity) -> P1; +point_add(#point{x = XP1, y = YP1} = _P1, #point{x = XP2, y = YP2} = _P2) + when XP1 =:= XP2 andalso YP1 =/= YP2 -> + infinity; +point_add(#point{x = XP1, y = YP1} = P1, #point{x = XP2, y = _YP2} = P2) + when P1 =:= P2 -> + Pow = pow(2*YP1, ?P-2, ?P), + Lam = mod((3 * XP1 * XP1 * Pow), ?P), + X3 = mod( (Lam*Lam-XP1-XP2), ?P), + Y3 = Lam * (XP1-X3) - YP1, + ModY3 = mod(Y3, ?P), + #point{ x = X3, y = ModY3 }; +point_add(#point{x = XP1, y = YP1} = _P1, #point{x = XP2, y = YP2} = _P2) -> + Pow = pow(XP2-XP1, ?P-2, ?P), + Lam = mod((YP2-YP1)*Pow, ?P), + X3 = mod(Lam*Lam-XP1-XP2, ?P), + Y3 = Lam * (XP1-X3) - YP1, + ModY3 = mod(Y3, ?P), + #point{ x = X3, y = ModY3 }. + +% @hidden +-spec point_add_test() -> any(). +point_add_test() -> + [?assertEqual(#point{x = 7, y = 11} + ,point_add(infinity, #point{ x = 7, y = 11}) + ) + ,?assertEqual(#point{x = 7, y = 11} + ,point_add(#point{ x = 7, y = 11 }, infinity) + ) + ,?assertEqual(#point{ x = 19378428157484735184523243358891984578749728838671251419826579141819453736404 + , y = 44205000258235380766042078705889964821895425212793847935799688109705992950649 + } + ,point_add(#point{x = 7, y = 11}, #point{x = 7, y = 11}) + ) + ,?assertEqual(#point{ x = 115792089237316195423570985008687907853269984665640564039457584007908834671650 + , y = 115792089237316195423570985008687907853269984665640564039457584007908834671636 + } + ,point_add(#point{x = 3, y = 11}, #point{ x = 11, y = 3}) + ) + ,?assertEqual(#point{ x = 28948022309329048855892746252171976963317496166410141009864396001977208667908 + , y = 14474011154664524427946373126085988481658748083205070504932198000988604333969 + } + ,point_add(#point{x = 3, y = 5}, #point{ x = 7, y = 11}) + ) + % random numbers from crypto:strong_rand_bytes + % <> = crypto:strong_rand_bytes(1024). + ,?assertEqual(#point{ x = 91135211804769037289181822946249074482714108779522925302592554235721783701018 + , y = 99818033513876009352157021758714302832184620200363201766386068538701879468152 + } + ,point_add(#point{ x = 141593973711105839125463102363107836958 + , y = 329489256900559930863793082810166473960 + } + ,#point{ x = 234618553877540191490899838365674539395 + , y = 89765200862939297360272288140060207649 + } + ) + ) + % <> = crypto:strong_rand_bytes(1024). + ,?assertEqual(#point{ x = 40492007733098845228301650448206050367870738761190187486523881458865268785647 + , y = 7382127217435614743656716445293306338341893567227196890616741654871654947558 + } + ,point_add(#point{ x = 99149246872437019612261312727375821210201487809380798340591244968847435110958 + , y = 75919448780511037489073435567726392226548250036757700517067622240324034566273 + } + ,#point{ x = 41628706388483764512884286855448126539167618097769771973505584034538908295376 + , y = 6436500457272134772481425159511058557726236442881064554112153466415715261526 + } + ) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. `point_mul/2' multiplies a point with an +%% integer. +%% +%% @end +%%-------------------------------------------------------------------- +-spec point_mul(Point, N) -> Return when + Point :: point(), + N :: pos_integer(), + Return :: point(). + +point_mul(#point{} = Point, N) -> + point_mul(Point, N, infinity, 0). + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @end +%%-------------------------------------------------------------------- +point_mul(_Point, _N, R, 256) -> R; +point_mul(Point, N, R, I) -> + case (N bsr I) band 1 of + 0 -> + P = point_add(Point, Point), + point_mul(P, N, R, I+1); + _T -> + R2 = point_add(R, Point), + P = point_add(Point, Point), + point_mul(P, N, R2, I+1) + end. + +% @hidden +-spec point_mul_test() -> any(). +point_mul_test() -> + [?assertEqual(#point{ x = 7, y = 7 } + ,point_mul(#point{ x = 7, y = 7 }, 1) + ) + ,?assertEqual(#point{ x = 28948022309329048855892746252171976963317496166410141009864396001977208668012 + , y = 101318078082651670995624611882601919371611236582435493534525386006920230336761 + } + ,point_mul(#point{ x = 7, y = 7 }, 2) + ) + ,?assertEqual(#point{ x = 113983170870405637072057152569562644400358531773776591071171880583320981987473 + , y = 2397548289122980910071032066274387186495158503098094242655101888740367798284 + } + ,point_mul(#point{ x = 3, y = 11 }, 23) + ) + ,?assertEqual(#point{ x = 7290227233335871060690792451044222428311888608390440755173487812836481398552 + , y = 10409433877825173591058732575755507734177582538799672995274584861774214108018 + } + ,point_mul(#point{x = 98721398621, y = 7987213}, 6784312) + ) + + % random numbers from crypto:strong_rand_bytes + % <> = crypto:strong_rand_bytes(1024). + ,?assertEqual(#point{ x = 14653810375998584450416643383712972377040960434358203450412140382669449377871 + , y = 101615459546039329619859114806133454376029017792297498784803970473114837648526 + } + ,point_mul(#point{ x = 141593973711105839125463102363107836958 + , y = 329489256900559930863793082810166473960 + } + ,89765200862939297360272288140060207649 + ) + ) + + % <> = crypto:strong_rand_bytes(1024). + ,?assertEqual(#point{ x = 109145713281741604754885712729628231331329135401249140446457991728192817682717 + , y = 98912812288377654917975538622885692727087477773812060039032656542892668293152 + } + ,point_mul(#point{ x = 99149246872437019612261312727375821210201487809380798340591244968847435110958 + , y = 75919448780511037489073435567726392226548250036757700517067622240324034566273 + } + , 6436500457272134772481425159511058557726236442881064554112153466415715261526 + ) + ) + + ,?assertEqual(#point{ x = 87919591022248410098849921382602642724620778780113673678898283161568451288436 + , y = 82771203004193098477539928827975513383082404516251638949313570671552988484250 + } + ,point_mul(#point{ x = 112711660439710606056748659173929673102114977341539408544630613555209775888121 + , y = 25583027980570883691656905877401976406448868254816295069919888960541586679410 + } + , 67071769870995747205322053780378091752498503321064312642056330450148171233907 + ) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc `modular_pow/3' is an alternative to `crypto:mod_pow/3' and +%% `math:pow/2' modular pow implementation using right to left +%% exponentiation. +%% +%% This code is not cryptographicaly safe. When a negative number is +%% given, the time used to compute it take twice the +%% time. `crypto:mod_pow/3' needs to be modified accordingly. +%% +%% The exponentiation methods given by Erlang/OTP do not fit the +%% requirement provided by the main implementation of BIP-0340. This +%% function was created to temporarily fix this issue. Many better +%% implementation and algorithms exist. See: +%% +%%
  • [https://en.wikipedia.org/wiki/Modular_exponentiation#Right-to-left_binary_method] +%%
  • +%%
  • [https://www.johndcook.com/blog/2008/12/10/fast-exponentiation] +%%
  • +%%
  • [https://mathstats.uncg.edu/sites/pauli/112/HTML/secfastexp.html] +%%
  • +%%
  • [https://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.99.1460] +%%
  • +%% +%% Different implementations have been tested without great success +%% for the moment. +%% +%% @end +%%-------------------------------------------------------------------- +-spec pow(X, Y, Modulo) -> Return when + X :: integer(), + Y :: integer(), + Modulo :: pos_integer(), + Return :: integer(). + +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 -> + % @todo this part of the code is impacted by crypto:mod_pow/3 when + % a negative value is added, the output is not compatible with the + % implementation. Question: is it possible to compute all those + % values without negative numbers? + case 1 band Exponent of + 1 -> + modular_pow(Base, Exponent, Modulus, Base); + 0 -> + modular_pow(Base, Exponent, Modulus, 1) + end. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% @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. + +% @hidden +-spec pow_test() -> any(). +pow_test() -> + [?assertEqual(0, pow(0,0,1)) + ,?assertEqual(0, pow(?P, ?P, ?P)) + ,?assertEqual(1, pow(1,0,1)) + ,?assertEqual(0, pow(0,1,1)) + ,?assertEqual(88966338620437832068624569303567783236486510711125279810536434685834011611583 + ,pow(-109145713281741604754885712729628231331329135401249140446457991728192817682717 + ,28897801297922945330451835637322792013828657623029579235286557361627672604967 + ,?P) + ) + ,?assertEqual(26825750616878363354946415705120124616783473954515284228921149322074823060080 + ,pow(109145713281741604754885712729628231331329135401249140446457991728192817682717 + ,28897801297922945330451835637322792013828657623029579235286557361627672604967 + ,?P) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% thanks to [https://stackoverflow.com/a/2386387/6782635], Erlang is +%% using truncated division, Python is using floored modulo based on +%% euclidian division. The divrem function from python is defined +%% here:
  • +%% [https://hg.python.org/cpython/file/tip/Objects/longobject.c#l2607] +%%
  • . Another implementation is required, but it should do the +%% job for now. The Art of programming volume 2 needs to be checked as +%% well. +%% +%% @end +%%-------------------------------------------------------------------- +-spec mod(X, Y) -> Return when + X :: integer(), + Y :: pos_integer(), + Return :: pos_integer(). + +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. + +% @hidden +-spec mod_test() -> any(). +mod_test() -> + [?assertEqual(0, mod(0,0)) + ,?assertEqual(22452309387972768763530205817659855289843162532160402740598319643309799867816 + ,mod(109145713281741604754885712729628231331329135401249140446457991728192817682717 + ,28897801297922945330451835637322792013828657623029579235286557361627672604967 + ) + ) + ,?assertEqual(6445491909950176566921629819662936723985495090869176494688237718317872737151 + ,mod(-109145713281741604754885712729628231331329135401249140446457991728192817682717 + ,28897801297922945330451835637322792013828657623029579235286557361627672604967 + ) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. `lift_x/1' lifts the point. +%% @end +%%-------------------------------------------------------------------- +-spec lift_x(Integer) -> Point when + Integer :: integer(), + Point :: point(). + +lift_x(X) when X >= ?P -> infinity; +lift_x(X) -> + Y1 = mod((pow(X, 3, ?P) + 7), ?P), + Y2 = pow(Y1, (?P+1) div 4, ?P), + case pow(Y2, 2, ?P) =/= Y1 of + true -> infinity; + false -> + case Y2 band 1 =:= 0 of + true -> #point{x = X, y = Y2}; + false -> #point{x = X, y = ?P-Y2} + end + end. + +% @hidden +-spec lift_x_test() -> any(). +lift_x_test() -> + [?assertEqual(#point{ x = 1 + , y = 29896722852569046015560700294576055776214335159245303116488692907525646231534 + } + ,lift_x(1) + ) + ,?assertEqual(#point{ x = 123 + , y = 57446492628264891784098775050622253460841288276710978755323331825353875991360 + } + ,lift_x(123) + ) + ,?assertEqual(infinity + ,lift_x(35068317372709509953717756927105732741704429749274497856780391249648561587686) + ) + ,?assertEqual(#point{ x = 35068317372709509953717756927105732741707686 + , y = 88331955267511973018408495437670803136063821269883896203017701759027241157628 + } + ,lift_x(35068317372709509953717756927105732741707686) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc `new_privatekey/0' creates a new secp256k1 private key based +%% on `crypto:generate_key/2' function. +%% +%% @see crypto:generate_key/2 +%% @end +%%-------------------------------------------------------------------- +-spec new_privatekey() -> PrivateKey when + PrivateKey :: {ok, private_key()}. + +new_privatekey() -> + {_PublicKey, PrivateKey} = crypto:generate_key(ecdh, secp256k1), + {ok, PrivateKey}. + +%%-------------------------------------------------------------------- +%% @doc `new_publickey/1' generates a public key from a private key. +%% +%% @see new_privatekey/0 +%% @todo allow integers to be used to generate a public key. +%% @end +%%-------------------------------------------------------------------- +-spec new_publickey(PrivateKey) -> PublicKey when + PrivateKey :: private_key(), + PublicKey :: {ok, public_key()} + | {error, proplists:proplists() | atom()}. + +new_publickey(PrivateKey) + when byte_size(PrivateKey) =/= 32 -> + {error, [{message, "The private key must be a 32-byte array."}]}; +new_publickey(<<0:256>>) -> + {error, [{message, "The private key must be an integer in the range (1..n-1)."}]}; +new_publickey(<>) + when D0 >= ?N -> + {error, [{message, "The private key must be an integer in the range (1..n-1)."}]}; +new_publickey(PrivateKey) + when is_bitstring(PrivateKey) -> + D0 = bitstring_to_integer(PrivateKey), + new_publickey(D0); +new_publickey(PrivateKey) + when is_integer(PrivateKey) + andalso PrivateKey >= 1 + andalso PrivateKey =< (?N-1) -> + Point = point_mul(?G, PrivateKey), + case Point of + infinity -> {error, infinity}; + #point{} -> {ok, point_to_bitstring(Point)} + end. + +% @hidden +-spec new_publickey_test() -> any(). +new_publickey_test() -> + [?assertEqual({error,[{message,"The private key must be a 32-byte array."}]} + ,new_publickey(<<>>) + ) + ,?assertEqual({error,[{message,"The private key must be an integer in the range (1..n-1)."}]} + ,new_publickey(<<0:256>>) + ) + ,?assertEqual({error,[{message,"The private key must be an integer in the range (1..n-1)."}]} + ,new_publickey(<>) + ) + ,?assertEqual({ok, <<121,190,102,126,249,220,187,172,85,160,98,149,206 + ,135,11,7,2,155,252,219,45,206,40,217,89,242,129,91 + ,22,248,23,152>>} + ,new_publickey(<<1:256>>) + ) + ,?assertEqual({ok, <<27,56,144,58,67,247,241,20,237,69,0,180,234,199,8 + ,63,222,254,206,28,242,156,99,82,141,86,52,70,249 + ,114,193,128>>} + ,new_publickey(<<255:256>>) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. `point_to_bitstring/1' converts a point to +%% its binary form. Only x coordinate is extracted. +%% +%% @end +%% -------------------------------------------------------------------- +-spec point_to_bitstring(Point) -> Integer when + Point :: point(), + Integer :: pos_integer(). + +point_to_bitstring(#point{x = X1} = _Point) -> + integer_to_bitstring(X1). + +%%-------------------------------------------------------------------- +%% @doc `sign/2' signs a message with a private key. +%% +%% @see sign/3 +%% @end +%%-------------------------------------------------------------------- +-spec sign(Message, PrivateKey) -> Return when + Message :: message(), + PrivateKey :: private_key(), + Return :: {ok, signature()} + | {error, proplists:proplists()}. +sign(Message, PrivateKey) -> + sign(Message, PrivateKey, <<0:256>>). + +%%-------------------------------------------------------------------- +%% @doc `sign/3' signs a message with a private key and auxiliary +%% values. +%% +%% @end +%%-------------------------------------------------------------------- +-spec sign(Message, PrivateKey, AuxRand) -> Return when + Message :: message(), + PrivateKey :: private_key(), + AuxRand :: aux_rand(), + Return :: {ok, signature()} + | {error, proplists:proplists()}. + +sign(Message, _, _) + when byte_size(Message) =/= 32 -> + {error, [{message, "The message must be a 32-byte array."}]}; +sign(_, <<0:256>>, _) -> + {error, [{message, "The private key must be an integer in the range (1..n-1)."}]}; +sign(_, <>, _) + when D0 >= ?N -> + {error, [{message, "The private key must be an integer in the range (1..n-1)."}]}; +sign(_, PrivateKey, _) + when byte_size(PrivateKey) =/= 32 -> + {error, [{message, "The private key must be a 32-byte array."}]}; +sign(_, _, AuxRand) + when byte_size(AuxRand) =/= 32 -> + {error, [{message, "The aux_rand value must be 32 bytes."}]}; +sign(<>, <<16#B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF:256>> = PrivateKey, <>) -> + ?LOG_WARNING("Private Key \"~p\" should be used for development purpose only.", [PrivateKey]), + D0 = 16#B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF, + Point = point_mul(?G, D0), + sign1(Message, D0, AuxRand, Point); +sign(<>, <<16#C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9:256>> = PrivateKey, <>) -> + ?LOG_WARNING("Private Key \"~p\" should be used for development purpose only.", [PrivateKey]), + D0 = 16#C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9, + Point = point_mul(?G, D0), + sign1(Message, D0, AuxRand, Point); +sign(<>, <<16#0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710:256>> = PrivateKey, <>) -> + ?LOG_WARNING("Private Key \"~p\" should be used for development purpose only.", [PrivateKey]), + D0 = 16#0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710, + Point = point_mul(?G, D0), + sign1(Message, D0, AuxRand, Point); +sign(<>, <> = _PrivateKey, <>) -> + Point = point_mul(?G, D0), + sign1(Message, D0, AuxRand, Point). + +% @hidden +-spec sign_test() -> any(). +sign_test() -> + [?assertEqual({ok, + <<233,7,131,31,128,132,141,16,105,165,55,27,64,36,16 + ,54,75,223,28,95,131,7,176,8,76,85,241,206,45,202 + ,130,21,37,246,106,74,133,234,139,113,228,130,167 + ,79,56,45,44,229,235,238,232,253,178,23,47,71,125 + ,244,144,13,49,5,54,192>> } + ,sign(<<0:256>>, <<3:256>>, <<0:256>>) + ) + + ,?assertEqual({ok, + <<104,150,189,96,238,174,41,109,180,138,34,159,247 + ,29,254,7,27,222,65,62,109,67,249,23,220,141,207 + ,140,120,222,51,65,137,6,209,26,201,118,171,204 + ,178,11,9,18,146,191,244,234,137,126,252,182,57 + ,234,135,28,250,149,246,222,51,158,75,10>> + } + ,sign(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>> + ,<<16#B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF:256>> + ,<<16#0000000000000000000000000000000000000000000000000000000000000001:256>> + ) + ) + + ,?assertEqual({ok, + <<88,49,170,238,215,180,75,183,78,94,171,148,186 + ,157,66,148,196,155,207,42,96,114,141,139,76,32 + ,15,80,221,49,60,27,171,116,88,121,165,173,149,74 + ,114,196,90,145,195,165,29,60,122,222,169,141,130 + ,248,72,30,14,30,3,103,74,111,63,183>> + } + ,sign(<<16#7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C:256>> + ,<<16#C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9:256>> + ,<<16#C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906:256>> + ) + ) + ,?assertEqual({ok, + <<126,176,80,151,87,226,70,241,148,73,136,86,81,97 + ,28,185,101,236,193,161,135,221,81,182,79,218,30,220 + ,150,55,213,236,151,88,43,156,177,61,179,147,55,5,179 + ,43,169,130,175,90,242,95,215,136,129,235,179,39,113 + ,252,89,34,239,198,110,163>> + } + ,sign(<<16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:256>> + ,<<16#0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710:256>> + ,<<16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:256>> + ) + ) + ,?assertEqual({error, [{message, "The message must be a 32-byte array."}]} + ,sign(<<>>, <<>>, <<>>) + ) + ,?assertEqual({error, [{message, "The private key must be a 32-byte array."}]} + ,sign(<<0:256>>, <<>>, <<>>) + ) + ,?assertEqual({error, [{message, "The aux_rand value must be 32 bytes."}]} + ,sign(<<0:256>>, <<1:256>>, <<>>) + ) + ,?assertEqual({error, [{message, "The private key must be an integer in the range (1..n-1)."}]} + ,sign(<<0:256>>, <<0:256>>, <<0:256>>) + ) + ,?assertEqual({error, [{message, "The private key must be an integer in the range (1..n-1)."}]} + ,sign(<<0:256>>, <>, <<0:256>>) + ) + ,?assertEqual({error, [{message, "The private key must be an integer in the range (1..n-1)."}]} + ,sign(<<0:256>>, <<(?N+1):256>>, <<0:256>>) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +sign1(_Message, _D0, _AuxRand, infinity) -> + {error, [{message, "p is set to infinity."}]}; +sign1(Message, D0, AuxRand, Point) -> + D = case has_even_y(Point) of + true -> D0; + false -> ?N-D0 + end, + T = sign_tag_aux(D, AuxRand), + K0 = sign_tag_nonce(T, Point, Message), + sign2(Message, D0, AuxRand, Point, D, K0). + +% @hidden +-spec sign1_test() -> any(). +sign1_test() -> + [?assertEqual({error, [{message, "p is set to infinity."}]} + ,sign1(0,0,0,infinity)) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +sign2(_Message, _D0, _AuxRand, _Point, _D, 0) -> + {error, [{message, "Failure. This happens only with negligible probability."}]}; +sign2(Message, D0, AuxRand, Point, D, K0) -> + R = point_mul(?G, K0), + sign3(Message, D0, AuxRand, Point, D, K0, R). + +% @hidden +-spec sign2_test() -> any(). +sign2_test() -> + [?assertEqual({error, [{message, "Failure. This happens only with negligible probability."}]} + ,sign2(<<>>, <<>>, <<>>, #point{}, <<>>, 0)) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +sign3(_Message, _D0, _AuxRand, _Point, _D, _K0, infinity) -> + {error, [{message, "r is set to infinity"}]}; +sign3(Message, _D0, _AuxRand, Point, D, K0, R) -> + K = case has_even_y(R) of + false -> ?N - K0; + true -> K0 + end, + E = sign_tag_challenge(R, Point, Message), + Signature = sign_make(R, K, E, D), + case verify(Message, point_to_bitstring(Point), Signature) of + true -> {ok, Signature}; + false -> {error, [{message, "The created signature does not pass verification."}]} + end. + +% @hidden +-spec sign3_test() -> any(). +sign3_test() -> + [?assertEqual({error, [{message, "r is set to infinity"}]} + ,sign3(<<>>, <<>>, <<>>, #point{}, <<>>, <<>>, infinity)) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +sign_make(R, K, E, D) -> + Mod = mod(K+E*D, ?N), + Head = point_to_bitstring(R), + Tail = integer_to_bitstring(Mod), + <>. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +sign_tag_aux(D, AuxRand) -> + T_Tagged = tagged_hash(<<"BIP0340/aux">>, AuxRand), + IntegerD = integer_to_bitstring(D), + crypto:exor(IntegerD, T_Tagged). + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +sign_tag_nonce(T, Point, Message) -> + K0_Tag = <<"BIP0340/nonce">>, + K0_Message = <>, + K0_Tagged = tagged_hash(K0_Tag, K0_Message), + mod(bitstring_to_integer(K0_Tagged), ?N). + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +sign_tag_challenge(R, Point, Message) -> + E_Tag = <<"BIP0340/challenge">>, + E_Message = <<(point_to_bitstring(R))/bitstring + ,(point_to_bitstring(Point))/bitstring + ,Message/bitstring>>, + E_Tagged = tagged_hash(E_Tag, E_Message), + mod(bitstring_to_integer(E_Tagged), ?N). + +%%-------------------------------------------------------------------- +%% @doc `verify/3' checks a message with its signature and a public +%% key. +%% +%% @end +%%-------------------------------------------------------------------- +-spec verify(Message, PublicKey, Signature) -> Return when + Message :: message(), + PublicKey :: public_key(), + Signature :: signature(), + Return :: boolean(). + +verify(Message, _, _) + when byte_size(Message) =/= 32 -> + {error, [{"The message must be a 32-byte array."}]}; +verify(_, PublicKey, _) + when byte_size(PublicKey) =/= 32 -> + {error, [{"The public key must be a 32-byte array."}]}; +verify(_, <<0:256>> = _PublicKey, _) -> + {error, [{"The public key must be an integer in the range (1..n-1)."}]}; +verify(_, _, Signature) + when byte_size(Signature) =/= 64 -> + {error, [{"The signature must be a 64-byte array"}]}; +verify(Message, PublicKey, <> = _Signature) -> + P = lift_x(bitstring_to_integer(PublicKey)), + Verif1 = (P =:= infinity) orelse (RSign >= P) orelse (SSign >= ?N), + case Verif1 of + true -> false; + false -> + E = verify_tag(Message, PublicKey, RSign), + RPoint_A = point_mul(?G, SSign), + RPoint_B = point_mul(P, ?N-E), + RPoint = point_add(RPoint_A, RPoint_B), + verify2(RPoint, RSign) + end. + +% @hidden +-spec verify_test() -> any(). +verify_test() -> + [?assertEqual({error,[{"The message must be a 32-byte array."}]} + ,verify(<<>>, <<>>, <<>>) + ) + ,?assertEqual({error,[{"The public key must be a 32-byte array."}]} + ,verify(<<0:256>>, <<>>, <<>>) + ) + ,?assertEqual({error,[{"The signature must be a 64-byte array"}]} + ,verify(<<0:256>>, <<1:256>>, <<0>>) + ) + ,?assertEqual({error, [{"The public key must be an integer in the range (1..n-1)."}]} + ,verify(<<0:256>>, <<0:256>>, <<0:512>>) + ) + ,?assertEqual(true + ,verify(<<16#0000000000000000000000000000000000000000000000000000000000000000:256>> + ,<<16#F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9:256>> + ,<<16#E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0:512>> + ) + ) + + ,?assertEqual(true + ,verify(<<16#4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703:256>> + ,<<16#D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9:256>> + ,<<16#00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4:512>> + ) + ) + + ,?assertEqual(true, + verify(<<16#4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703:256>>, + <<16#D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9:256>>, + <<16#00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34:256>>, + <<16#6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659:256>>, + <<16#6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141:512>>)) + + ,?assertEqual(false, + verify(<<16#243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89:256>>, + <<16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30:256>>, + <<16#6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B:512>>)) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +verify2(infinity, _) -> false; +verify2(#point{x = X} = _RPoint, RSign) when X =/= RSign -> false; +verify2(RPoint, _RSign) -> + case not has_even_y(RPoint) of + true -> false; + false -> true + end. + +%%-------------------------------------------------------------------- +%% @doc internal function. +%% +%% @end +%%-------------------------------------------------------------------- +verify_tag(Message, PublicKey, R) -> + E_Tag = <<"BIP0340/challenge">>, + E_Message = <>, + E_Tagged = bitstring_to_integer(tagged_hash(E_Tag, E_Message)), + mod(E_Tagged, ?N). + +%%-------------------------------------------------------------------- +%% @doc internal function. `tagged_hash/2' creates a new tagged hash. +%% +%% @end +%%-------------------------------------------------------------------- +-spec tagged_hash(Tag, Message) -> Hash when + Tag :: bitstring(), + Message :: iodata(), + Hash :: iodata(). + +-define(SHA256_TAG_NONCE, <<7,73,119,52,167,155,203,53,91,155,140 + ,125,3,79,18,28,244,52,215,62,247,45 + ,218,25,135,0,97,251,82,191,235,47>>). + +-define(SHA256_TAG_AUX, <<241,239,78,94,192,99,202,218,109,148,202 + ,250,157,152,126,160,105,38,88,57,236,193 + ,31,151,45,119,165,46,216,193,204,144>>). + +-define(SHA256_TAG_CHALLENGE, <<123,181,45,122,159,239,88,50,62,177 + ,191,122,64,125,179,130,210,243,242 + ,216,27,177,34,79,73,254,81,143,109 + ,72,211,124>>). + +tagged_hash(<<"BIP0340/aux">>, Message) -> + TagHash = ?SHA256_TAG_AUX, + crypto:hash(sha256, <>); +tagged_hash(<<"BIP0340/challenge">>, Message) -> + TagHash = ?SHA256_TAG_CHALLENGE, + crypto:hash(sha256, <>); +tagged_hash(<<"BIP0340/nonce">>, Message) -> + TagHash = ?SHA256_TAG_NONCE, + crypto:hash(sha256, <>); +tagged_hash(Tag, Message) -> + TagHash = crypto:hash(sha256, Tag), + crypto:hash(sha256, <>). + +% @hidden +-spec tagged_hash_test() -> any(). +tagged_hash_test() -> + [?assertEqual(<<45,186,93,188,51,158,115,22,174,162,104,63,175 + ,131,156,27,123,30,226,49,61,183,146,17,37,136 + ,17,141,240,102,170,53>> + ,tagged_hash(<<>>, <<>>) + ) + ,?assertEqual(<<3,4,228,53,11,241,70,225,142,233,227,234,108,197 + ,232,2,211,243,63,103,51,176,168,109,51,125,59,57 + ,238,89,167,104>> + ,tagged_hash(<<"test">>, <<"test">>) + ) + ,?assertEqual(<<84,241,105,207,201,226,229,114,116,128,68,31,144 + ,186,37,196,136,244,97,199,11,94,165,220,170,247 + ,175,105,39,10,165,20>> + ,tagged_hash(<<"BIP0340/aux">>, <<0:256>>) + ) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. `has_even_y/1' returns true if y is even. +%% +%% @end +%%-------------------------------------------------------------------- +-spec has_even_y(point()) -> boolean. + +has_even_y(#point{ y = Y } = _Point) -> Y rem 2 =:= 0; +has_even_y(_) -> false. + +% @hidden +-spec has_even_y_test() -> any(). +has_even_y_test() -> + [?assertEqual(false, has_even_y(#point{ y = 1 })) + ,?assertEqual(false, has_even_y(infinity)) + ,?assertEqual(true, has_even_y(#point{ y = 2 })) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. `integer_to_bitstring/1' converts an +%% integer to a bitstring. +%% +%% @see erlang:integer_to_binary/2 +%% @end +%%-------------------------------------------------------------------- +-spec integer_to_bitstring(Integer) -> Bitstring when + Integer :: pos_integer(), + Bitstring :: iodata(). + +integer_to_bitstring(Integer) -> + Size = byte_size(erlang:integer_to_binary(Integer, 2)), + <<0:(256-Size), Integer:Size/unsigned-integer>>. + +% @hidden +-spec integer_to_bitstring_test() -> any(). +integer_to_bitstring_test() -> + [?assertEqual(<<0:256>>, integer_to_bitstring(0)) + ,?assertEqual(<<1:256>>, integer_to_bitstring(1)) + ,?assertEqual(<<65535:256>>, integer_to_bitstring(65535)) + ]. + +%%-------------------------------------------------------------------- +%% @doc internal function. `bitstring_to_integer/1' convers a +%% bitstring to an integer. +%% +%% @see crypto:bytes_to_integer/1 +%% @end +%%-------------------------------------------------------------------- +-spec bitstring_to_integer(Bitstring) -> Integer when + Bitstring :: iodata(), + Integer :: pos_integer(). +bitstring_to_integer(Bitstring) -> + crypto:bytes_to_integer(Bitstring). + +% @hidden +-spec bitstring_to_integer_test() -> any(). +bitstring_to_integer_test() -> + [?assertEqual(123, bitstring_to_integer(<<123>>)) + ]. diff --git a/src/nostrlib_secp256k1.erl b/src/nostrlib_secp256k1.erl deleted file mode 100644 index 0d52ea0..0000000 --- a/src/nostrlib_secp256k1.erl +++ /dev/null @@ -1,19 +0,0 @@ -%%%=================================================================== -%%% @doc -%%% @end -%%% @author Mathieu Kerjouan -%%%=================================================================== --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). diff --git a/src/nostrlib_tags.erl b/src/nostrlib_tags.erl deleted file mode 100644 index a7013a1..0000000 --- a/src/nostrlib_tags.erl +++ /dev/null @@ -1,67 +0,0 @@ -%%%=================================================================== -%%% @doc -%%% @end -%%% @author Mathieu Kerjouan -%%%=================================================================== --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]. diff --git a/src/nostrlib_url.erl b/src/nostrlib_url.erl new file mode 100644 index 0000000..79b7afe --- /dev/null +++ b/src/nostrlib_url.erl @@ -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. + + diff --git a/test/nostrlib_SUITE.erl b/test/nostrlib_SUITE.erl index 9ac56bf..e8ac063 100644 --- a/test/nostrlib_SUITE.erl +++ b/test/nostrlib_SUITE.erl @@ -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">>])). diff --git a/test/nostrlib_SUITE_data/valid_eose.json b/test/nostrlib_SUITE_data/valid_eose.json new file mode 100644 index 0000000..98e38ed --- /dev/null +++ b/test/nostrlib_SUITE_data/valid_eose.json @@ -0,0 +1 @@ +["EOSE","9635033750818420944"] diff --git a/test/nostrlib_SUITE_data/valid_event_kind0.json b/test/nostrlib_SUITE_data/valid_event_kind0.json new file mode 100644 index 0000000..decaa41 --- /dev/null +++ b/test/nostrlib_SUITE_data/valid_event_kind0.json @@ -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"}] diff --git a/test/nostrlib_SUITE_data/valid_event_kind1.json b/test/nostrlib_SUITE_data/valid_event_kind1.json new file mode 100644 index 0000000..68eb9f6 --- /dev/null +++ b/test/nostrlib_SUITE_data/valid_event_kind1.json @@ -0,0 +1 @@ +["EVENT","5452643154455862",{"pubkey":"e623bb2e90351b30818de33debd506aa9eae04d8268be65ceb2dcc1ef6881765","content":"Rest isn't stagnation.","id":"1e76eecdcd101063df6c56afd58d90a1d9e81265ea255148c6fc9789d168420e","created_at":1677417294,"sig":"1b9096d0c8f48aed807f148a4b35b97cc9b28cc8ea698e4ea6d8fda405c463569eb8bed01a77596a4ffa2daab89b7601d08d4646d2f8a82e7cd63388bb1f88e1","kind":1,"tags":[]}] diff --git a/test/nostrlib_SUITE_data/valid_event_kind1_with_tags.json b/test/nostrlib_SUITE_data/valid_event_kind1_with_tags.json new file mode 100644 index 0000000..ba084ef --- /dev/null +++ b/test/nostrlib_SUITE_data/valid_event_kind1_with_tags.json @@ -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"}] diff --git a/test/nostrlib_SUITE_data/valid_event_kind2.json b/test/nostrlib_SUITE_data/valid_event_kind2.json new file mode 100644 index 0000000..7b13f24 --- /dev/null +++ b/test/nostrlib_SUITE_data/valid_event_kind2.json @@ -0,0 +1 @@ +["EVENT","14983797176934273925",{"id":"e7f5850dd535feba822e35747d022ea7c29d7c0f226f75f08af433abb20357f6","pubkey":"5132e8bf2ac08dc03016dda748ab8c9c1207d595afb35b87539413b99932ad72","created_at":1675315873,"kind":2,"tags":[],"content":"wss://rsslay.fiatjaf.com","sig":"c0d56d5f45c0bebc97c980e42b906ea336d9a21806b3813bf819982e3ba1816d1673c3ae4c23366afac61e1bfb5cdc880318c5ef5808e28252cb14189ef2faad"}] diff --git a/test/nostrlib_SUITE_data/valid_event_kind7.json b/test/nostrlib_SUITE_data/valid_event_kind7.json new file mode 100644 index 0000000..9afbfe6 --- /dev/null +++ b/test/nostrlib_SUITE_data/valid_event_kind7.json @@ -0,0 +1 @@ +["EVENT","5452643154455862",{"id":"fb9af93e73551efd48e381fe320bb543e7bc119a83b702628b76b1a7ac5e75ca","pubkey":"0e1314f29c0a64ec3679671c59dabaf655594e1ca6bbd8712366e3ac175a964f","created_at":1677417344,"kind":7,"tags":[["e","76918fd820d4165419796e699693123e1c47955c5d89d1fc91100f3b4235608d"],["p","c49f8402ef410fce5a1e5b2e6da06f351804988dd2e0bad18ae17a50fc76c221"]],"content":"+","sig":"0815450293a7c4e3079ae3d62baeba5e441ca026eb4a0fa8c145e8ad969673ad155cb2db7916393db6db0b562b06f86acc7dc38b5e1bad238aaa534a27f91161"}] diff --git a/test/nostrlib_SUITE_data/valid_event_request.json b/test/nostrlib_SUITE_data/valid_event_request.json new file mode 100644 index 0000000..777031f --- /dev/null +++ b/test/nostrlib_SUITE_data/valid_event_request.json @@ -0,0 +1 @@ +["REQ","5452643154455862",{"kinds":[0,1,2,7],"since":1677330792,"limit":450}] diff --git a/test/nostrlib_schnorr_SUITE.erl b/test/nostrlib_schnorr_SUITE.erl new file mode 100644 index 0000000..8e87e6c --- /dev/null +++ b/test/nostrlib_schnorr_SUITE.erl @@ -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, <>} = 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). diff --git a/test/nostrlib_schnorr_SUITE_data/test-vectors.csv b/test/nostrlib_schnorr_SUITE_data/test-vectors.csv new file mode 100644 index 0000000..a1a63e1 --- /dev/null +++ b/test/nostrlib_schnorr_SUITE_data/test-vectors.csv @@ -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