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.
This commit is contained in:
niamtokik
2023-02-26 14:33:56 +00:00
parent d07a6dbc55
commit 1984e20549
53 changed files with 5700 additions and 739 deletions

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ _build
rebar3.crashdump
doc
*~
**.trace

View File

@@ -1,2 +1,3 @@
erlang 25.1.2
rebar 3.20.0
pandoc 3.1.1

View File

@@ -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)

View File

@@ -31,7 +31,7 @@ moment.
Here the list of currently supported
[nips](https://github.com/nostr-protocol/nips):
- [ ] [nip/01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [x] [nip/01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [ ] [nip/02: Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)
- [ ] [nip/03: OpenTimestamps Attestations for Events](https://github.com/nostr-protocol/nips/blob/master/03.md)
- [ ] [nip/04: Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
@@ -64,6 +64,11 @@ Here the list of currently supported
- [ ] [nip/56: Reporting](https://github.com/nostr-protocol/nips/blob/master/56.md)
- [ ] [nip/57: Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md)
- [ ] [nip/65: Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
- [ ] [nip/78: Arbitrary custom app data](https://github.com/nostr-protocol/nips/blob/master/78.md)
## Other Implementation (required by nostr)
- [x] [BIP-0340: Schnorr Signatures for secp256k1](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)
## Build

16
extra/README.md Normal file
View File

@@ -0,0 +1,16 @@
# Extra Scripts, Tests and Benchmarks
## modular pow function check
Scripts used to check and validate the modular pow function
implementation in Erlang.
- [python script (`check_pow.py`)](check_pow.py)
- [erlang code (`pow.erl`)](pow.erl)
## floored modulo operator check
Scripts used to validated the floored modulo implementation in Erlang.
- [python script (`check_mod.py`)](check_mod.py)
- [erlang code (`mod.erl`)](mod.erl)

24
extra/check_mod.py Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
# check_mod.py
"""
This script is used to check custom modulo operator created
in Erlang for the nostr project.
"""
import sys
import string
import secrets
limit = 256
if len(sys.argv) > 1:
limit = int(sys.argv[1])
generator = secrets.SystemRandom()
start = -(2**256)
end = 2**256
for i in range(limit):
a = generator.randrange(start, end)
m = generator.randrange(0,end)
r = a % m
l = ",".join([str(i),str(a),str(m),str(r)])
print(l)

25
extra/check_pow.py Normal file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python3
# check_pow.py
"""
This script is used to check custom pow function created
in Erlang for the nostr project.
"""
import sys
import string
import secrets
limit = 256
if len(sys.argv) > 1:
limit = int(sys.argv[1])
generator = secrets.SystemRandom()
start = -(2**256)
end = 2**256
for i in range(limit):
a = generator.randrange(start, end)
b = generator.randrange(0,end)
m = generator.randrange(0,end)
p = pow(a, b, m)
l = ",".join([str(i),str(a),str(b),str(m),str(p)])
print(l)

56
extra/mod.erl Normal file
View File

@@ -0,0 +1,56 @@
%%%===================================================================
%%% @doc This script parse the output of the `check_mod.py' module
%%% present in `extra' directory. The `nostrlib:mod/2' function must
%%% be exported and this script must be executed at the root of the
%%% project.
%%%
%%% @end
%%%===================================================================
-module(mod).
-export([check/0, check/1]).
-define(PYTHON, "python3").
-define(CHECK_SCRIPT, "check_mod.py").
%%--------------------------------------------------------------------
%% @doc `check/0' executes the script using python and check its
%% output.
%%
%% @end
%% --------------------------------------------------------------------
check() ->
Path = filename:join("extra", ?CHECK_SCRIPT),
Command = string:join([?PYTHON, Path], " "),
Return = os:cmd(Command),
Content = list_to_bitstring(Return),
check_content(Content).
%%--------------------------------------------------------------------
%% @doc `check/1' read a file containing the output of check_mod.py.
%% @end
%%--------------------------------------------------------------------
check(File) ->
{ok, Content} = file:read_file(File),
check_content(Content).
%%--------------------------------------------------------------------
%% @doc `check_content/1' returns true if the results are valid,
%% return false if something was wrong
%%
%% @end
%%--------------------------------------------------------------------
check_content(Content) ->
Lines = re:split(Content, "\n"),
Splitted = lists:map(fun(X) -> re:split(X, ",") end, Lines),
Result = [ { binary_to_integer(I)
, binary_to_integer(R) =:=
nostrlib_schnorr:mod(binary_to_integer(A)
,binary_to_integer(M))}
|| [I,A,M,R] <- Splitted
],
Filter = lists:filter(fun({_,false}) -> true;
(_) -> false
end, Result),
case Filter of
_ when Filter =:= [] -> {ok, length(Result)};
Elsewise -> {error, Elsewise}
end.

57
extra/pow.erl Normal file
View File

@@ -0,0 +1,57 @@
%%%===================================================================
%%% @doc This script parse the output of the `check_pow.py' module
%%% present in `extra' directory. The `nostrlib:pow/2' function must
%%% be exported and this script must be executed at the root of the
%%% project.
%%%
%%% @end
%%%===================================================================
-module(pow).
-export([check/0, check/1]).
-define(PYTHON, "python3").
-define(CHECK_SCRIPT, "check_pow.py").
%%--------------------------------------------------------------------
%% @doc `check/0' executes the script using python and check its
%% output.
%%
%% @end
%% --------------------------------------------------------------------
check() ->
Path = filename:join("extra", ?CHECK_SCRIPT),
Command = string:join([?PYTHON, Path], " "),
Return = os:cmd(Command),
Content = list_to_bitstring(Return),
check_content(Content).
%%--------------------------------------------------------------------
%% @doc `check/1' read a file containing the output of check_pow.py.
%% @end
%%--------------------------------------------------------------------
check(File) ->
{ok, Content} = file:read_file(File),
check_content(Content).
%%--------------------------------------------------------------------
%% @doc `check_content/1' returns true if the results are valid,
%% return false if something was wrong
%%
%% @end
%%--------------------------------------------------------------------
check_content(Content) ->
Lines = re:split(Content, "\n"),
Splitted = lists:map(fun(X) -> re:split(X, ",") end, Lines),
Result = [ { binary_to_integer(I)
, binary_to_integer(P) =:=
nostrlib_schnorr:pow(binary_to_integer(A)
,binary_to_integer(B)
,binary_to_integer(M))}
|| [I,A,B,M,P] <- Splitted
],
Filter = lists:filter(fun({_,false}) -> true;
(_) -> false
end, Result),
case Filter of
_ when Filter =:= [] -> {ok, length(Result)};
Elsewise -> {error, Elsewise}
end.

View File

@@ -1,9 +1,181 @@
%%%-------------------------------------------------------------------
%%%===================================================================
%%% @doc type and data-structure used in nostrlib_decoder.erl
%%% module. Can be easily imported in other modules as needed.
%%%
%%%-------------------------------------------------------------------
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-type to_be_defined() :: any().
%% A type created to point out a type to define.
%%--------------------------------------------------------------------
%% A macro used to translate a kind as integer or atom.
%%--------------------------------------------------------------------
-define(KIND(K_INTEGER, K_ATOM),
kind(K_INTEGER) -> K_ATOM;
kind(K_ATOM) -> K_INTEGER
).
%%%===================================================================
%%% @doc encoded JSON messages
%%% @end
%%%===================================================================
-type encoded_event() :: binary() | bitstring() | iodata().
%%%===================================================================
%%% @doc decoded types and data-structures
%%% @end
%%%===================================================================
-type decoded_event_id() :: undefined | <<_:256>>.
%% event_id() is an event id as defined in NIP/01.
-type decoded_private_key() :: <<_:256>>.
%% A private key as a bitstring.
-type decoded_public_key() :: <<_:256>>.
%% A public key as a bistring.
-type decoded_created_at() :: pos_integer().
%% An Unix timestamp as integer.
-type decoded_kind() :: atom() | 0 | 1 | 2 | 7 | pos_integer().
%% A kind represented as positive integer
-type decoded_kinds() :: [decoded_kind(), ...].
%% A list of kinds
-type decoded_content() :: bitstring() | iodata().
%% The main payload of the message as raw string.
-type decoded_signature() :: <<_:512>>.
%% A signature as an hexadecimal string.
-type decoded_event_ids() :: [decoded_event_id(), ...].
%% A list of event id.
-type decoded_prefix() :: bitstring().
%% A prefix as an hexadecimal string.
-type decoded_author() :: decoded_public_key() | decoded_prefix().
%% An author as defined in NIP/01, can be a public key or a prefix.
-type decoded_authors() :: [decoded_author(), ...].
%% A list of authors.
-type decoded_tag_event_ids() :: decoded_event_ids().
%% A tag event_ids, an alias for event id used in tags.
-type decoded_tag_event_public_keys() :: [decoded_public_key(), ...].
%% A tag containing a list of public keys.
-type decoded_since() :: pos_integer().
%% An Unix timestamp as positive integer.
-type decoded_until() :: pos_integer().
%% An Unix timestamp as positive integer.
-type decoded_limit() :: pos_integer().
%% A limit of event as positive integer
-type decoded_message() :: bitstring().
%% A raw message, used in notice.
-type decoded_subscription_id() :: bitstring().
%% A subscription id as a random string.
%%--------------------------------------------------------------------
%% A tag record.
%%--------------------------------------------------------------------
-record(tag, { name = undefined :: public_key | event_id
, value = undefined :: undefined | bitstring()
, params = [] :: list()
}).
-type decoded_tag() :: #tag{ params :: undefined }.
%% A tag as represented in NIP/01. It can describe an event id or a
%% public key.
-type decoded_tags() :: [decoded_tag(), ...].
%% A list of tag, used in events.
%%--------------------------------------------------------------------
%% A full event record.
%%--------------------------------------------------------------------
-record(event, { id = undefined :: decoded_event_id()
, public_key = undefined :: decoded_public_key()
, created_at = undefined :: decoded_created_at()
, kind = undefined :: decoded_kind()
, tags = [] :: decoded_tags()
, content = undefined :: decoded_content()
, signature = undefined :: decoded_signature()
}).
-type decoded_event() :: #event{signature :: decoded_signature()}.
%%--------------------------------------------------------------------
%% A full filter record.
%%--------------------------------------------------------------------
-record(filter, { event_ids = [] :: decoded_event_ids()
, authors = [] :: decoded_authors()
, kinds = [] :: decoded_kinds()
, tag_event_ids = [] :: decoded_tag_event_ids()
, tag_public_keys = [] :: decoded_tag_event_public_keys()
, since = undefined :: decoded_since()
, until = undefined :: decoded_until()
, limit = undefined :: decoded_limit()
}).
-type decoded_filter() :: #filter{limit :: decoded_limit()}.
%%--------------------------------------------------------------------
%% A full request record.
%%--------------------------------------------------------------------
-record(request, { subscription_id = undefined :: decoded_subscription_id()
, filter = #filter{} :: [decoded_filter(), ...]
}).
-type decoded_request() :: #request{filter :: [decoded_filter(), ...]}.
%%--------------------------------------------------------------------
%% A full close record.
%%--------------------------------------------------------------------
-record(close, { subscription_id = undefined :: decoded_subscription_id()
}).
-type decoded_close() :: #close{subscription_id :: decoded_subscription_id()}.
%%--------------------------------------------------------------------
%% A full notice record.
%%--------------------------------------------------------------------
-record(notice, { message = undefined :: decoded_message() }).
-type decoded_notice() :: #notice{ message :: decoded_message() }.
%%--------------------------------------------------------------------
%% A full eose record.
%%--------------------------------------------------------------------
-record(eose, { id = undefined :: decoded_subscription_id() }).
-type decoded_eose() :: #eose{ id :: decoded_subscription_id() }.
%%--------------------------------------------------------------------
%% A full subscription record.
%%--------------------------------------------------------------------
-type decoded_subscription_content() :: decoded_event()
| decoded_request().
-record(subscription, { id = undefined :: decoded_subscription_id()
, content = undefined :: decoded_subscription_content()
}).
-type decoded_subscription() :: #subscription{ content :: decoded_subscription_content() }.
%%--------------------------------------------------------------------
%% A type representing all decoded messages available.
%%--------------------------------------------------------------------
-type decoded_messages() :: decoded_event()
| decoded_request()
| decoded_close()
| decoded_notice()
| decoded_subscription()
| decoded_eose().
%%%===================================================================
%%% @doc encoded types and data-structure.
%%% @end
%%%===================================================================
-type message() :: list().
%% A nostr message.
@@ -21,11 +193,3 @@
-type event() :: map().
%% A nostr event.
%%--------------------------------------------------------------------
%% A macro used to translate a kind as integer or atom.
%%--------------------------------------------------------------------
-define(KIND(K_INTEGER, K_ATOM),
kind(K_INTEGER) -> K_ATOM;
kind(K_ATOM) -> K_INTEGER
).

View File

@@ -0,0 +1 @@

View File

@@ -1,140 +0,0 @@
%%%===================================================================
%%% @doc type and data-structure used in nostrlib_decoder.erl
%%% module. Can be easily imported in other modules as needed.
%%%
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-type event_id() :: undefined | bitstring().
%% event_id() is an event id as defined in NIP/01.
-type public_key() :: bitstring().
%% A public key as an hexdecimal string.
-type created_at() :: pos_integer().
%% An Unix timestamp as integer.
-type kind() :: 0 | 1 | 2 | 7 | pos_integer().
%% A kind represented as positive integer
-type kinds() :: [kind(), ...].
%% A list of kinds
-type tag() :: [bitstring(), ...].
%% A tag as represented in NIP/01. It can describe an event id or a
%% public key.
-type tags() :: [tag(), ...].
%% A list of tag, used in events.
-type content() :: bitstring().
%% The main payload of the message as raw string.
-type signature() :: bitstring().
%% A signature as an hexadecimal string.
-type event_ids() :: [event_id(), ...].
%% A list of event id.
-type prefix() :: bitstring().
%% A prefix as an hexadecimal string.
-type author() :: public_key() | prefix().
%% An author as defined in NIP/01, can be a public key or a prefix.
-type authors() :: [author(), ...].
%% A list of authors.
-type tag_event_ids() :: event_ids().
%% A tag event_ids, an alias for event id used in tags.
-type tag_event_public_keys() :: [public_key(), ...].
%% A tag containing a list of public keys.
-type since() :: pos_integer().
%% An Unix timestamp as positive integer.
-type until() :: pos_integer().
%% An Unix timestamp as positive integer.
-type limit() :: pos_integer().
%% A limit of event as positive integer
-type message() :: bitstring().
%% A raw message, used in notice.
-type subscription_id() :: bitstring().
%% A subscription id as a random string.
%%--------------------------------------------------------------------
%% A full event record.
%%--------------------------------------------------------------------
-record(event, { id :: event_id()
, public_key :: public_key()
, created_at :: created_at()
, kind :: kind()
, tags :: tags()
, content :: content()
, signature :: signature()
}).
-type event() :: #event{signature :: signature()}.
%%--------------------------------------------------------------------
%% A full filter record.
%%--------------------------------------------------------------------
-record(filter, { event_ids :: event_ids()
, authors :: authors()
, kinds :: kinds()
, tag_event_ids :: tag_event_ids()
, tag_public_keys :: tag_event_public_keys()
, since :: since()
, until :: until()
, limit :: limit()
}).
-type filter() :: #filter{limit :: limit()}.
%%--------------------------------------------------------------------
%% A full request record.
%%--------------------------------------------------------------------
-record(request, { subscription_id :: subscription_id()
, filter :: filter()
}).
-type request() :: #request{filter :: filter()}.
%%--------------------------------------------------------------------
%% A full close record.
%%--------------------------------------------------------------------
-record(close, { subscription_id :: subscription_id() }).
-type close() :: #close{subscription_id :: subscription_id()}.
%%--------------------------------------------------------------------
%% A full notice record.
%%--------------------------------------------------------------------
-record(notice, { message :: message() }).
-type notice() :: #notice{}.
%%--------------------------------------------------------------------
%% A full eose record.
%%--------------------------------------------------------------------
-record(eose, { id :: subscription_id() }).
-type eose() :: #eose{}.
%%--------------------------------------------------------------------
%% A full subscription record.
%%--------------------------------------------------------------------
-type subscription_content() :: event()
| request().
-record(subscription, { id :: subscription_id()
, content :: subscription_content()
}).
-type subscription() :: #subscription{}.
%%--------------------------------------------------------------------
%% A type representing all decoded messages available.
%%--------------------------------------------------------------------
-type decoded_messages() :: event()
| request()
| close()
| notice()
| subscription()
| eose().

View File

@@ -0,0 +1,552 @@
---
date: 2023-03-10
title: Schnorr Signature Scheme in Erlang
subtitle: |
Implementing the Schnorr signature scheme in Erlang with
minimal dependencies
author: Mathieu Kerjouan
keywords: erlang,otp,nostr,schnorr,design,interfaces
license: CC BY-NC-ND
abstract: |
Nostr protocol was mainly designed by Bitcoin developers, or at
least, people attracted by the Bitcoin ecosystem. Unfortunately,
Erlang/OTP do not have all the cryptographic primitives to build a
fully compliant nostr application from scratch without using
external dependencies. Indeed, Erlang/OTP is already delivered with
`crypto` and `public_key` modules to deal with classical
cryptographic functionalities, principally used by SSL/TLS
protocol stack. Bitcoin is using, on its side, lot of "unconventional"
features -- or not generally offered by default libraries.
This article is the first one implementing cryptographic functions
in Erlang. `nostr` protocol relies on elliptic curve cryptography, in
particular `secp256k1` curve. Instead of using Elliptic Curve
Digital Signature Algorithm (ECDSA) or Edwards-curve Digital
Signature Algorithm (EdDSA), nostr is using Schnorr signature scheme
like the Bitcoin project and defined in BIP-0340.
toc: true
hyperrefoptions:
- linktoc=all
---
---
This article has been redacted in February and March 2023. It
describes the methodologies applied and describe the methods used to
implement the first NIP from `nostr` protocol in Erlang with a minimal
amount of dependencies. The following code has been tested using
[Erlang/OTP R25](https://www.erlang.org/news/157) running on
[OPENBSD-72-CURRENT](openbsd.org/) and [Parrot
Linux](https://parrotsec.org/) (a Debian like distribution).
# Schnorr Signature in Pure Erlang
Erlang is a multi-purpose language using its own virtual machine
called the BEAM [^wikipedia-beam] -- a short name for Bogdan's Erlang
Abstract Machine -- and greatly inspired by the Warren Abstract
Machine [^wikipedia-wam] (WAM). It was created to help creating
distributed enviroment. Usually, low level languages are used to
implement cryptographic functions, this article will show its possible
to have decent performances with great features when using high level
languages.
The first Schnorr signature scheme designed for Bitcoin
[^wikipedia-bitcoin] was defined in BIP-0340
[^bip-0340-specification]. This scheme can also be found in BIP-0341
[^bip-0341-specification] and BIP-0342 [^bip-0342-specification]
specifications. The implementation reference was made in Python
[^bip-0340-implementation] [^bip-0340-test-vectors].
Schnorr signature [^wikipedia-schnorr-signature] -- likes any other
cryptographic scheme -- is hard to implement. Fortunately, because
Bitcoin is used by lot of people around the world, this signature
protocol has been explained many times and anyone can find interesting
resources on this topic [^youtube-christof-paar-ecc]
[^youtube-pieter-wuille-schnorr]
[^youtube-cihangir-tezcan-schnorr-multi-signature]
[^youtube-bill-buchanan-schnorr] [^youtube-theoretically] without
buying a single book.
## Collecting Information
Starting with a small overview [^weboftrust-schnorr-signature], Schnorr
signature scheme seems to have a strong security proofs, to be simple,
to be fast and to have the property to create secrets without
additional exchange.
Many implementation of this scheme can be found out there, in C
[^c-libecc] [^c-cschnorr], C++ [^cpp-schnor], Elixir [^elixir-k256]
[^elixir-bitcoinex] [^elixir-bitcoinex-schnorr], Go
[^go-schnorr-signature], Python [^python-schnorr-example]
[^python-taproot] [^python-solcrypto], Java [^java-samourai-schorr] or
Javascript [^javascript-schnorr] [^nodejs-schnorr-signature].
## Implementing Schnorr Signature Scheme
Like previously said, the reference implementation was made in
Python. This language is not using the same paradigm than Erlang,
Python is a script-like language using oriented object programming,
while Erlang is a distributed and functional programming language. The
way the code will be created will be quite different, and will require
small adaptation. Modulo operator and `pow` functions are, for
example, not the same in these two universes. Functions with same
behaviors than these two will need to be created on the Erlang side, a
dedicated section for each of them will be available in this part of
the article.
Dealing with asymetric cryptography -- even more elliptic curves --
can be quite complex and a well designed interface should avoid even
smallest frictions with the developers. Functions to create standard
and secure keys should be available. In this implementation, the
`nostrlib_schnorr:new_privatekey/0` will be a wrapper of the
[`crypto:generate_key/2`](https://www.erlang.org/doc/man/crypto.html#generate_key-2)
function. A secp256k1 private key a 32bytes (256bits) random number
and could also have been generated using
[`crypto:strong_rand_bytes/1`](https://www.erlang.org/doc/man/crypto.html#strong_rand_bytes-1)
function as well but we are assuming the functions provided by the
Erlang team in [`crypto`](https://www.erlang.org/doc/man/crypto.html)
module are already doing all the validation and are configuring the
recommended parameterss [^secp256k1-recommended-parameters].
```erlang
% generate a private key using crypto:generate_key/2
{_PublicKey, <<PrivateKey:256/bitstring>>}
= crypto:generate_key(ecdh, secp256k1).
% generate a private key using crypto:strong_rand_bytes/1
<<PrivateKey:256/bitstring>>
= crypto:strong_rand_bytes(32).
% generate a private key using nostrlib_schnorr:new_private_key/0.
{ok, <<PrivateKey:256/bitstring>>}
= nostrlib_schnorr:new_private_key().
```
A public key, derived from the private key, is also required. The one
provided by
[`crypto:generate_key/2`](https://www.erlang.org/doc/man/crypto.html#generate_key-2)
is not suitable for our needs. A specific point on the curve is
required and defined in BIP-0340, an operation on this same point is
also required. The function to generate a public key with `nostrlib`
is `nostrlib_schnorr:new_publickey/1`, where the first and only
argument to pass is a valid private key (32bytes length bitstring).
```erlang
{ok, <<PublicKey:256>>}
= nostrlib_schnorr:new_public_key(PrivateKey).
```
The Schnorr signature scheme can only sign 32bytes (256bits) messages,
the BIP-0340 specification uses SHA256 as main hashing function to
produce a 256bits fixed length hash as message. The `crypto` module
offers the function
[`crypto:hash/2`](https://www.erlang.org/doc/man/crypto.html#hash-2)
to generate this payload, where the first argument will be the atom
`sha256` and the second argument will be the content to hash. This
value can be signed by the functions `nostrlib_schnorr:sign/2` or
`nostrlib_schnorr:sign/3` where the first argument is the hash
previously generated and the second argument is the private key. The
signature returned is a 64bytes (512bits) fixed length bitstring.
```erlang
% create a hash from data, in this example, a raw bitstring.
<<Message:256/bitstring>>
= crypto:hash(sha256, <<"my data">>).
% create a signature with default aux_data (set to 0).
{ok, <<Signature:512/bitstring>>}
= nostrlib_schnorr:sign(Message, PrivateKey).
% create a signature with aux_data set to 0 (manually).
{ok, <<Signature:512bitstring>>}
= nostrlib_schnorr:sign(Message, PrivateKey, <<0:256>>).
```
![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)

View File

@@ -0,0 +1,184 @@
# ANNEXE 1 - BIP-0340 Implementation in Python
Here the steps to extract and execute
[BIP-0340](https://github.com/bitcoin/bips/tree/master/bip-0340)
implementation in Python with debug mode activated.
```sh
git clone https://github.com/bitcoin/bips
cd bips/bip-0340
sed -i "s/^DEBUG = False/DEBUG = True/" reference.py
python reference.py
```
Here the output:
```
Test vector #0:
Variables in function schnorr_sign at line 118:
msg == 0x0000000000000000000000000000000000000000000000000000000000000000
seckey == 0x0000000000000000000000000000000000000000000000000000000000000003
aux_rand == 0x0000000000000000000000000000000000000000000000000000000000000000
d0 == 0x0000000000000000000000000000000000000000000000000000000000000003
P == ('0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9',
'0x388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672')
d == 0x0000000000000000000000000000000000000000000000000000000000000003
t == 0x54f169cfc9e2e5727480441f90ba25c488f461c70b5ea5dcaaf7af69270aa517
k0 == 0x1d2dc1652fee3ad08434469f9ad30536a5787feccfa308e8fb396c8030dd1c69
R == ('0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215',
'0x849b08486a5b16ea5fd009a3ade472b48a2dc817aeebc33ab4fa25ebbd599f27')
k == 0xe2d23e9ad011c52f7bcbb960652cfac815365cf9dfa59752c498f20c9f5924d8
e == 0x6bb6b93a91f2ecc0cd924f4f9baabb5e6eb21745bb00f2cebdaac908bb5d86ce
sig == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
Variables in function schnorr_verify at line 141:
msg == 0x0000000000000000000000000000000000000000000000000000000000000000
pubkey == 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
sig == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
P == ('0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9',
'0x388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672')
r == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215
s == 0x25f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
e == 0x6bb6b93a91f2ecc0cd924f4f9baabb5e6eb21745bb00f2cebdaac908bb5d86ce
R == ('0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215',
'0x7b64f7b795a4e915a02ff65c521b8d4b75d237e851143cc54b05da1342a65d08')
* Passed signing test.
Variables in function schnorr_verify at line 141:
msg == 0x0000000000000000000000000000000000000000000000000000000000000000
pubkey == 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
sig == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
P == ('0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9',
'0x388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672')
r == 0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215
s == 0x25f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0
e == 0x6bb6b93a91f2ecc0cd924f4f9baabb5e6eb21745bb00f2cebdaac908bb5d86ce
R == ('0xe907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215',
'0x7b64f7b795a4e915a02ff65c521b8d4b75d237e851143cc54b05da1342a65d08')
* Passed verification test.
Test vector #1:
Variables in function schnorr_sign at line 118:
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
seckey == 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef
aux_rand == 0x0000000000000000000000000000000000000000000000000000000000000001
d0 == 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
d == 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef
t == 0x5966c1816cb27e627c8543d63478bdce03c1b115838e469a416b0899fe5723dd
k0 == 0xf7becdac22c3d61a97ff4e84a004e1c4919c0e0c51f50dd5bee15c9cbd27318e
R == ('0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341',
'0x20bc7663da14be43c22eb2ccb49cd746573b2766b277273fcb20a2f6c1a60c6c')
k == 0xf7becdac22c3d61a97ff4e84a004e1c4919c0e0c51f50dd5bee15c9cbd27318e
e == 0xcfb58e748d9648b71fdc909fb7432fc0c954da5bd75cdc9d4804d32648f9839a
sig == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
Variables in function schnorr_verify at line 141:
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
sig == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
r == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341
s == 0x8906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
e == 0xcfb58e748d9648b71fdc909fb7432fc0c954da5bd75cdc9d4804d32648f9839a
R == ('0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341',
'0x20bc7663da14be43c22eb2ccb49cd746573b2766b277273fcb20a2f6c1a60c6c')
* Passed signing test.
Variables in function schnorr_verify at line 141:
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
sig == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
r == 0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341
s == 0x8906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a
e == 0xcfb58e748d9648b71fdc909fb7432fc0c954da5bd75cdc9d4804d32648f9839a
R == ('0x6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341', '0x20bc7663da14be43c22eb2ccb49cd746573b2766b277273fcb20a2f6c1a60c6c')
* Passed verification test.
Test vector #2:
Variables in function schnorr_sign at line 118:
msg == 0x7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c
seckey == 0xc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9
aux_rand == 0xc87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906
d0 == 0xc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9
P == ('0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8',
'0xf594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394')
d == 0xc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9
t == 0xbad44f6e1f50e3c2ad9d2ba5768ad6ab84dc4b8f9b893444234a9c39e8fc58e1
k0 == 0xf5878384ed63c5ec428e7ab31bdb446b6884dfad76b7e0599af3f5e838409aab
R == ('0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b',
'0x5f193d22f6f1d925a7f8c4ceff20cc2a53ba1c3310ca843cf83156c1514bb284')
k == 0xf5878384ed63c5ec428e7ab31bdb446b6884dfad76b7e0599af3f5e838409aab
e == 0x9bc1ba4a0abbc0792066b2ca0ef771d88af676b322a83dd7517f7c1fd149215a
sig == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
Variables in function schnorr_verify at line 141:
msg == 0x7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c
pubkey == 0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8
sig == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
P == ('0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8',
'0xf594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394')
r == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b
s == 0xab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
e == 0x9bc1ba4a0abbc0792066b2ca0ef771d88af676b322a83dd7517f7c1fd149215a
R == ('0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b',
'0x5f193d22f6f1d925a7f8c4ceff20cc2a53ba1c3310ca843cf83156c1514bb284')
* Passed signing test.
Variables in function schnorr_verify at line 141:
msg == 0x7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c
pubkey == 0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8
sig == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
P == ('0xdd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8',
'0xf594bb5f72b37faae396a4259ea64ed5e6fdeb2a51c6467582b275925fab1394')
r == 0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b
s == 0xab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7
e == 0x9bc1ba4a0abbc0792066b2ca0ef771d88af676b322a83dd7517f7c1fd149215a
R == ('0x5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b',
'0x5f193d22f6f1d925a7f8c4ceff20cc2a53ba1c3310ca843cf83156c1514bb284')
* Passed verification test.
...
Test vector #12:
Variables in function schnorr_verify at line 134:
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
sig == 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f69e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
r == 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
s == 0x69e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
* Passed verification test.
Test vector #13:
Variables in function schnorr_verify at line 134:
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
pubkey == 0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659
sig == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e177769fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
P == ('0xdff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659',
'0x2ce19b946c4ee58546f5251d441a065ea50735606985e5b228788bec4e582898')
r == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e177769
s == 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
* Passed verification test.
Test vector #14:
Variables in function schnorr_verify at line 134:
msg == 0x243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89
pubkey == 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc30
sig == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e17776969e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
P == None
r == 0x6cff5c3ba86c69ea4b7376f31a9bcb4f74c1976089b2d9963da2e5543e177769
s == 0x69e89b4c5564d00349106b8497785dd7d1d713a8ae82b32fa79d5f7fc407d39b
* Passed verification test.
All test vectors passed.
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -0,0 +1,650 @@
---
date: 2023-03-10
title: Nostr NIP/01 in Pure Erlang
subtitle: |
Implementing the first Nostr Implementation Possibility in pure
Erlang with minimal dependencies
author: Mathieu Kerjouan
keywords: erlang,otp,nostr,nip/01,schnorr,design,interfaces
license: CC BY-NC-ND
abstract: |
Designing and creating the first brick of a library is an
exhausting process, even in high level language like Erlang.
The goal of this article is to explain and detail NIP/01
specification then implement it.
toc: true
hyperrefoptions:
- linktoc=all
---
---
This article has been redacted in February and March 2023. It
describes the methodologies applied and describe the methods used to
implement the first NIP from nostr protocol in Erlang with a minimal
amount of dependencies. The following code has been tested using
[Erlang/OTP R25](https://www.erlang.org/news/157) running on
[OPENBSD-72-CURRENT](openbsd.org/) and [Parrot
Linux](https://parrotsec.org/) (a Debian like distribution).
# Nostr NIP/01 Library Design and Implementation
NIP/01[^nip-01-specification] is the first NIP describing the nostr
protocol. This is a mandatory specification defining the basic
data-structures and how relays and clients communicate together.
![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

View File

@@ -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}]).
```

View File

@@ -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])
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -9,6 +9,17 @@ what was their ideas.
**All articles MUST be licensed under [CC
BY-NC-ND](https://creativecommons.org/licenses/by-nc-nd/4.0/)**.
- [Implementing nostr Client in Erlang](0003-implementing-nostr-client-in-erlang)
- [From Erlang to nostr](0002-from-erlang-to-nostr)
- [Create Github Actions Workflow](0001-create-github-actions-workflow)
| Date | Title | Author | Notes |
|------------|-----------------------------|--------|-------|
| 2023-03-09 | [Nostr NIP/01 in Pure Erlang](0005-implementing-nip-01-standard-in-pure-erlang) | Mathieu Kerjouan | R25
| 2023-03-09 | [Schnorr signature scheme in Erlang](0004-schnorr-signature-scheme-in-erlang) | Mathieu Kerjouan | R25
| 2023-02-25 | [Implementing nostr Client in Erlang](0003-implementing-nostr-client-in-erlang) | Mathieu Kerjouan | R25
| 2023-02-10 | [From Erlang to nostr](0002-from-erlang-to-nostr) | Mathieu Kerjouan | R25
| 2023-02-10 | [Create Github Actions Workflow](0001-create-github-actions-workflow) | Mathieu Kerjouan | R25
The codes presented in these articles are usually tested under OpenBSD
and ParrotLinux (Debian-like distribution) with the latest major
release of Erlang (R25).
These articles are also available in EPUB, PDF and HTML files. A
template is available in [`_template`](_template) directory.

11
notes/_template/README.md Normal file
View File

@@ -0,0 +1,11 @@
---
date: 2023-02-25
title:
subtitle:
author:
keywords:
license: CC BY-NC-ND
abstract:
---

View File

@@ -14,6 +14,8 @@
,stdlib
,cowboy
,gun
,crypto
,public_key
]}
,{optional_applications, []}
,{env, []}

View File

@@ -1,13 +1,41 @@
%%%===================================================================
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%% @doc nostr_client module is the main interface to communicate with
%%% all the nostr Erlang client application. It will offer all
%%% important function to be used by anyone.
%%%
%%% == Examples ==
%%%
%%% ```
%%% % load records
%%% rr(nostrlib).
%%%
%%% % set some variables
%%% Host = "relay.nostrich.de".
%%% Filter = #filter{ limit = 1 }.
%%%
%%% % create a new connection
%%% {ok, Connection} = nostr_client:connect(Host).
%%%
%%% % create a new subscription
%%% {ok, Subscription} = nostr_client:request(Host, Filter).
%%%
%%% % close the current active connection
%%% ok = nostrlib_client:close(Host, Subscription).
%%%
%%% % send and event
%%% Opts = [{private_key, PrivateKey}].
%%% ok = nostrlib_client:event(Host, text_note, <<"hello">>, Opts).
%%% '''
%%%
%%% @todo replace host by another ID.
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-module(nostr_client).
-export([connect/1, connect/2]).
-export([event/4]).
-export([request/2, request/3]).
-export([close/2, close/3]).
-include_lib("kernel/include/logger.hrl").
-include("nostrlib.hrl").
@@ -23,6 +51,7 @@
-spec connect(Host) -> Return when
Host :: host(),
Return :: any(). % TODO: check the return function of gun module.
connect(Host) ->
DefaultOptions = [],
connect(Host, DefaultOptions).
@@ -36,5 +65,131 @@ connect(Host) ->
Host :: host(),
Options :: options(),
Return :: any(). % TODO: check the return function of gun module.
connect(Host, Options) ->
nostr_client_connection:start([{host, Host}, {options, Options}]).
nostr_manager_client_sup:start_client_sup([{host, Host}, {options, Options}]).
%%--------------------------------------------------------------------
%% @doc `event/4' function send an event to an active connection.
%% @end
%%--------------------------------------------------------------------
-spec event(Host, Kind, Content, Opts) -> Return when
Host :: string(),
Kind :: atom(),
Content :: bitstring(),
Opts :: proplists:proplists(),
Return :: ok.
event(Host, set_metadata, Content, Opts)
when is_map(Content) ->
case get_connection(Host) of
{ok, Connection} ->
Metadata = thoas:encode(Content),
Event = #event{ kind = set_metadata, content = Metadata },
{ok, Payload} = nostrlib:encode(Event, Opts),
nostr_client_connection:send_raw(Connection, Payload);
Elsewise -> Elsewise
end;
event(Host, text_note, Content, Opts)
when is_binary(Content) ->
case get_connection(Host) of
{ok, Connection} ->
Event = #event{ kind = text_note, content = Content},
{ok, Payload} = nostrlib:encode(Event, Opts),
nostr_client_connection:send_raw(Connection, Payload);
Elsewise -> Elsewise
end;
event(Host, recommend_server, Content, Opts) ->
case nostrlib_url:check(Content, Opts) of
{ok, Url} ->
case get_connection(Host) of
{ok, Connection} ->
Event = #event{ kind = recommend_server, content = Url },
{ok, Payload} = nostrlib:encode(Event, Opts),
nostr_client_connection:send_raw(Connection, Payload);
Elsewise -> Elsewise
end;
Elsewise -> Elsewise
end;
event(_,Kind,_,_) ->
{error, [{kind, Kind},{message, unsupported}]}.
%%--------------------------------------------------------------------
%% @doc `request/2'
%%
%% @see request/3
%% @end
%%--------------------------------------------------------------------
-spec request(Host, Filter) -> Return when
Host :: string(),
Filter :: decoded_filter(),
Return :: {ok, bitstring()}.
request(Host, Filter) ->
request(Host, Filter, []).
%%--------------------------------------------------------------------
%% @doc `request/3' function send a request to an active connection.
%% @end
%%--------------------------------------------------------------------
-spec request(Host, Filter, Opts) -> Return when
Host :: string(),
Filter :: decoded_filter(),
Opts :: proplists:proplists(),
Return :: {ok, bitstring()}.
request(Host, Filter, Opts) ->
case get_connection(Host) of
{ok, Connection} ->
SubscriptionId = nostrlib:new_subscription_id(),
Request = #request{ subscription_id = SubscriptionId
, filter = Filter },
{ok, Payload} = nostrlib:encode(Request, Opts),
ok = nostr_client_connection:send_raw(Connection, Payload),
{ok, SubscriptionId};
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @doc `close/2'
%%
%% @see close/3
%% @end
%%--------------------------------------------------------------------
-spec close(Host, SubscriptionId) -> Return when
Host :: string(),
SubscriptionId :: binary(),
Return :: ok.
close(Host, SubscriptionId) ->
close(Host, SubscriptionId, []).
%%--------------------------------------------------------------------
%% @doc `close/3' function closes an active subscription.
%%
%% @end
%%--------------------------------------------------------------------
-spec close(Host, SubscriptionId, Opts) -> Return when
Host :: string(),
SubscriptionId :: iodata(),
Opts :: proplists:proplists(),
Return :: ok.
close(Host, SubscriptionId, Opts) ->
case get_connection(Host) of
{ok, Connection} ->
Close = #close{ subscription_id = SubscriptionId },
{ok, Payload} = nostrlib:encode(Close, Opts),
nostr_client_connection:send_raw(Connection, Payload);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @hidden
%%--------------------------------------------------------------------
get_connection(Host) ->
case pg:get_members(client, {Host, connection}) of
[] -> {error, [{host, Host}, {connection, not_connected}]};
[Connection] -> {ok, Connection}
end.

View File

@@ -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).

View File

@@ -81,4 +81,3 @@ spec_controller(Args) ->
Return :: supervisor:startchild_ret().
start_controller(Pid, Args) ->
supervisor:start_child(Pid, spec_controller(Args)).

View File

@@ -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) ->

View File

@@ -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)].
%%--------------------------------------------------------------------
%%

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,6 @@
-export([event/1]).
-export([request/2]).
-export([close/1]).
-export([create_subscription_id/0]).
-export([create_event_id/1]).
-include_lib("eunit/include/eunit.hrl").
-include("nostrlib.hrl").
@@ -37,36 +35,6 @@ request(SubscriptionId, Filters)
when is_list(Filters) ->
[<<"REQ">>, SubscriptionId, filters(Filters)].
-spec request_test() -> any().
request_test() ->
In_001 = request(<<"721983390570381">>, #{
kinds => [0,1,2,7],
since => 1676057052,
limit => 450
}),
Out_001 = [
<<"REQ">>,
<<"721983390570381">>,
#{ kinds => [0,1,2,7], since => 1676057052, limit => 450}
],
?assertEqual(Out_001, In_001),
% Check if the conversion from atom to integer is working
In_002 = request(
<<"721983390570381">>,
#{ kinds => [nostrlib:kind(0)
,nostrlib:kind(1)
,nostrlib:kind(2)
,nostrlib:kind(7)],
since => 1676057052, limit => 450
}),
Out_002 = [
<<"REQ">>,
<<"721983390570381">>,
#{ kinds => [0,1,2,7], since => 1676057052, limit => 450
}],
?assertEqual(Out_002, In_002).
%%--------------------------------------------------------------------
%% @TODO this function is used internally and should not be exported
%%--------------------------------------------------------------------
@@ -133,61 +101,3 @@ close(SubscriptionId)
when is_bitstring(SubscriptionId) ->
[<<"CLOSE">>, SubscriptionId].
%%--------------------------------------------------------------------
%% @doc create_subscription_id/0 generate a new subscription id based
%% on crypto:strong_rand_bytes/1 function and returning a
%% random integer as string.
%% @end
%% @TODO review the way to generate subscription id.
%%--------------------------------------------------------------------
-spec create_subscription_id() -> Return when
Return :: pos_integer().
create_subscription_id() ->
<<SubscriptionIdRaw:64/integer>> = crypto:strong_rand_bytes(8),
erlang:integer_to_binary(SubscriptionIdRaw).
%%--------------------------------------------------------------------
%% @doc create_event_id/1 generates a compatible checksum based on
%% the given event.
%%
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
%% @end
%%--------------------------------------------------------------------
-spec create_event_id(Event) -> Return when
Event :: event(),
Return :: iodata().
create_event_id(Event) ->
SerializedEvent = serialize_event(Event),
SerializedJson = thoas:encode(SerializedEvent),
BinaryHash = crypto:hash(sha256, SerializedJson),
UppercaseHash = << <<(erlang:integer_to_binary(X, 16))/bitstring>>
|| <<X:4>> <= BinaryHash >>,
string:lowercase(UppercaseHash).
-spec create_event_id_test() -> any().
create_event_id_test() ->
% a random event from an open relay
Event = #{ <<"content">> => <<240,159,164,153>>
, <<"created_at">> => 1676749221
, <<"id">> => <<"5b5479e7adc2a7902572c2ee5325c2db6c31097fa7f4b86bb7e586d3ee7249ea">>
, <<"kind">> => 7
, <<"pubkey">> => <<"52e98835d909f73315eb391faa203506aa30bc533290a937a0c84db3eba16573">>
, <<"sig">> => <<"1e7e9802c604482e0d9b076bc6d89d37135ac00bbffce4a0bd9162f8d52569ed71db43c6a0c435005b2087fec8af260b86d30437a867b1984bc8cab99e607b30">>
, <<"tags">> =>[[<<"p">>,<<"7b3f7803750746f455413a221f80965eecb69ef308f2ead1da89cc2c8912e968">>,<<"wss://relay.damus.io">>]
,[<<"e">>,<<"b221a746d78058a7a3403bba4b0b123d36c827635e3eba0dcf8f564e9fc013d4">>,<<"wss://nostr-pub.wellorder.net">>,<<"root">>]
,[<<"e">>,<<"d8ca85941e1aab6a475736f20ef6c7b62c77477899594b5d2ac011eba5282954">>]
,[<<"p">>,<<"7ecd3fe6353ec4c53672793e81445c2a319ccf0a298a91d77adcfa386b52f30d">>]]
},
#{ <<"id">> := Id } = Event,
?assertEqual(Id, create_event_id(Event)).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
serialize_event(#{ <<"pubkey">> := PublicKey
, <<"created_at">> := CreatedAt
, <<"kind">> := Kind
, <<"tags">> := Tags
, <<"content">> := Content
} = _Event) ->
[0 ,PublicKey,CreatedAt,Kind,Tags,Content].

View File

@@ -1,263 +0,0 @@
%%%===================================================================
%%% @doc DRAFT: `nostrlib_decoder' module offers a way to decode the message
%%% coming from the connection and used in the router. The generated
%%% Erlang term is a record (or a tuple if used in other language).
%%%
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-module(nostrlib_decoder).
-export([decode/1]).
-include_lib("eunit/include/eunit.hrl").
-include("nostrlib_decoder.hrl").
%%--------------------------------------------------------------------
%% spec used for eunit
%%--------------------------------------------------------------------
-spec test() -> any().
%%--------------------------------------------------------------------
%% @doc `decode/1' decodes a raw message coming from a connection,
%% parse it with thoas and then convert it to a record defined in
%% `nostrlib_decoder.hrl' file.
%%
%% @todo creates more test
%% @todo creates examples
%% @see thoas:decode/1
%% @end
%%--------------------------------------------------------------------
-spec decode(Bitstring) -> Return when
Bitstring :: bitstring(),
Return :: decoded_messages().
decode(Bitstring) ->
case thoas:decode(Bitstring) of
{ok, Json} ->
decode_message(Json);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% Random messages from open relay
%%--------------------------------------------------------------------
-spec decode_test() -> any().
decode_test() ->
% decode a valid json message of kind 1
IN_001 = <<"[\"EVENT\",\"8034879165223001\",{\"id\":\"2f0e96269",
"b7ece13f63c39a26fd0b3ee1e6a41afd4f8aab0cb8afdcb9ab3",
"a64e\",\"pubkey\":\"a3eb29554bd27fca7f53f66272e4bb5",
"9d066f2f31708cf341540cb4729fbd841\",\"created_at\":",
"1677096834,\"kind\":1,\"tags\":[[\"e\",\"5aec5f6b41",
"844cd9fd7635f98fcd5ac814ac1618fc8a896d78011973ebc17",
"6ec\"],[\"p\",\"e623bb2e90351b30818de33debd506aa9ea",
"e04d8268be65ceb2dcc1ef6881765\"],[\"p\",\"472f440f2",
"9ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669",
"301e\"]],\"content\":\"#[2] has convinced me\",\"si",
"g\":\"430506421243e3cf0e737efd22101d38765ddd5235a9d",
"47602adf0c5a4dbe63a9c9d6e267375bfcfb4d95d1558135bd9",
"2ad26b1b86437ff27c63ff713d2825e3\"}]">>,
OUT_001 = {ok, #subscription{ id = <<"8034879165223001">>
, content = #event{ id = <<"2f0e96269b7ece13f63c39a26fd0b3ee1e6a41afd4f8aab0cb8afdcb9ab3a64e">>
, public_key = <<"a3eb29554bd27fca7f53f66272e4bb59d066f2f31708cf341540cb4729fbd841">>
, created_at = 1677096834
, kind = 1
, tags = [[<<"e">>,<<"5aec5f6b41844cd9fd7635f98fcd5ac814ac1618fc8a896d78011973ebc176ec">>]
,[<<"p">>,<<"e623bb2e90351b30818de33debd506aa9eae04d8268be65ceb2dcc1ef6881765">>]
,[<<"p">>,<<"472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e">>]
]
, content = <<"#[2] has convinced me">>
, signature = <<"430506421243e3cf0e737e"
,"fd22101d38765ddd5235a9"
,"d47602adf0c5a4dbe63a9c"
,"9d6e267375bfcfb4d95d15"
,"58135bd92ad26b1b86437f"
,"f27c63ff713d2825e3">>}
}
},
?assertEqual(OUT_001, decode(IN_001)),
% decode an unsupported but valid json message
IN_002 = <<"[]">>,
OUT_002 = {error, {unsupported, []}},
?assertEqual(OUT_002, decode(IN_002)),
% decode another valid json with utf8 symbol.
IN_003 = <<91,34,69,86,69,78,84,34,44,34,49,53,52,56,48,50,
48,48,48,57,54,57,53,48,56,51,49,51,53,34,44,123,
34,105,100,34,58,34,48,52,56,53,98,56,48,97,100,
98,54,97,102,49,48,100,51,53,56,48,99,52,52,57,97,
57,98,49,101,50,102,54,57,54,55,55,102,49,52,101,
54,49,98,49,54,100,102,97,53,100,101,51,51,98,49,
99,54,54,56,101,102,53,102,97,34,44,34,112,117,98,
107,101,121,34,58,34,56,97,57,56,49,102,49,97,101,
51,102,97,98,51,51,48,48,98,53,52,56,99,52,102,50,
48,54,53,52,99,98,48,102,49,100,51,53,48,52,57,56,
99,52,98,54,54,56,52,57,98,55,51,101,56,53,52,54,
48,48,49,100,99,97,48,34,44,34,99,114,101,97,116,
101,100,95,97,116,34,58,49,54,55,55,51,48,53,56,
54,49,44,34,107,105,110,100,34,58,49,44,34,116,97,
103,115,34,58,91,91,34,101,34,44,34,102,57,55,97,
100,56,54,48,49,51,53,102,102,52,49,50,53,49,51,
52,98,57,48,52,100,57,49,102,57,53,52,98,55,49,54,
100,54,50,52,50,51,49,57,54,57,101,50,48,49,97,97,
49,54,99,99,50,55,54,55,100,55,49,97,57,34,93,44,
91,34,112,34,44,34,100,101,57,48,99,53,100,98,51,
54,97,52,48,49,49,102,57,100,53,56,52,100,102,99,
49,56,100,101,49,97,53,55,50,52,54,56,54,56,54,55,
57,56,52,55,57,51,101,102,53,50,54,51,51,49,98,53,
49,102,56,98,52,51,101,57,34,93,93,44,34,99,111,
110,116,101,110,116,34,58,34,68,97,109,110,32,119,
101,108,108,44,32,73,32,99,97,110,226,128,153,116,
32,101,118,101,110,32,99,111,117,110,116,32,116,
104,97,116,33,34,44,34,115,105,103,34,58,34,52,54,
52,49,50,102,54,49,99,100,100,49,48,54,102,102,98,
51,52,101,57,101,55,49,52,99,57,56,99,48,54,100,
97,55,55,51,51,98,100,51,100,53,55,98,101,100,53,
98,53,55,100,101,49,97,55,57,97,53,55,50,98,102,
99,99,101,53,50,98,55,97,102,52,50,97,97,50,54,
100,53,101,48,50,56,48,52,97,97,53,54,56,102,53,
55,54,99,49,97,99,56,54,53,100,55,98,53,57,102,97,
54,51,48,52,100,55,97,53,55,99,101,99,99,57,98,99,
53,57,49,99,34,125,93>>,
OUT_003 = {ok,#subscription{ id = <<"1548020009695083135">>
, content = #event{ id = <<"0485b80adb6af10d3580c449a9b1e2f69677f14e61b16dfa5de33b1c668ef5fa">>
, public_key = <<"8a981f1ae3fab3300b548c4f20654cb0f1d350498c4b66849b73e8546001dca0">>
, created_at = 1677305861
, kind = 1
, tags = [[<<"e">>,<<"f97ad860135ff4125134b904d91f954b716d624231969e201aa16cc2767d71a9">>]
,[<<"p">>,<<"de90c5db36a4011f9d584dfc18de1a5724686867984793ef526331b51f8b43e9">>]]
, content = <<68,97,109,110,32,119,101,108,108,44,32,73,32,99,97
,110,226,128,153,116,32,101,118,101,110,32,99,111
,117,110,116,32,116,104,97,116,33>>
, signature = <<"46412f61cdd106ffb34e9e714c98c06da"
,"7733bd3d57bed5b57de1a79a572bfcce5"
,"2b7af42aa26d5e02804aa568f576c1ac8"
,"65d7b59fa6304d7a57cecc9bc591c">>
}
}
},
?assertEqual(OUT_003, decode(IN_003)).
% kind 0
% <<"[\"EVENT\",\"17771556064953075220123\",{\"id\":\"433e78562c17101284e130026c0b4ac82a41d442576f92fae929b6fbe441cd3b\",\"pubkey\":\"02748827a1016a393c780aec1d96191a3b8df1c397d09351029cbb25b2d83443\",\"created_at\":1676898304,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"berean\\\",\\\"nip05\\\":\\\"berean@nostrplebs.com\\\",\\\"picture\\\":\\\"https://nostr.build/i/p/nostr.build_b764966a4970638de60956883c63fc4e0a8d8bf1d7c54e0da5562f402e716c2c.jpg\\\",\\\"banner\\\":\\\"\\\",\\\"about\\\":\\\"Don't Trust. Verify.\\\\n#Bitcoin #Plebchain\\\",\\\"lud06\\\":\\\"\\\",\\\"lud16\\\":\\\"lastingbead65@walletofsatoshi.com\\\",\\\"username\\\":\\\"berean\\\",\\\"display_name\\\":\\\"berean\\\",\\\"displayName\\\":\\\"\\\",\\\"website\\\":\\\"\\\",\\\"Tags\\\":\\\"#bitcoin #nostr #plebchain\\\",\\\"nip05valid\\\":true,\\\"followingCount\\\":157,\\\"followersCount\\\":153}\",\"sig\":\"1252bfa38c4b93a9551ec04e141e57bbaf1b3696446790bb8c8af50d50f3e3c5f903926e9f4114abc9a3e7f44221d504864599447ffa6fb244e00f1929f951cc\"}]">>
% <<"[\"EVENT\",\"17771556064953075220123\",{\"id\":\"5a18e9add8877b0df36bf1500a16bcacc0e446c7b330b52cda08ecb8f3021656\",\"pubkey\":\"9d96589eee0e57d07a5f1877285f42e8618e40ab2b94546a04dcad5eb8cbd0e8\",\"created_at\":1676898679,\"kind\":0,\"tags\":[],\"content\":\"{\\\"display_name\\\":\\\"\\\",\\\"website\\\":\\\"\\\",\\\"name\\\":\\\"machasm\\\",\\\"about\\\":\\\"Total Nostr Noob\\\",\\\"lud06\\\":\\\"lnbc10n1p3lyhprpp5fnftakvwgxp9vsgn2xdv47n276vksc564juld7umgn79fuzs9xgqdqqcqzpgxqyz5vqsp5c2vzcfgvax0tgzc5smg835g665k3px72yt44qtjfsafjcclkgzvq9qyyssqejmspclfnd8val39mnmy8grjn5hkpxemue4tw4kd94uky8cmk9j9re9fu8vtnv0mjeclatyvsdp7hkzu7vl8uml9rg6m4k866xtkawspx4nc93\\\",\\\"banner\\\":\\\"\\\",\\\"picture\\\":\\\"https://imgur.com/gallery/r6m07Dk\\\",\\\"nip05\\\":\\\"machasm@nostrplebs.com\\\",\\\"nip05_updated_at\\\":1676845714}\",\"sig\":\"8524a862fd059a6dc9d0199888f77b531487c9962e99e3753f9640da0c4680b4361db60d07aa44e03ed23ce71478366137c9b0c40748ed710c3929168637f66a\"}]">>
% kind 2
% <<"[\"EVENT\",\"17771556064953075220111\",{\"id\":\"379345c8843c72bbe3c4165df7cb4cf7d88e88964d7b2440a9024ce12ca7140d\",\"pubkey\":\"d0872ed8cd4ef83ab9fc56841dedaee15866aa80eb811959b294e627757a6819\",\"created_at\":1672028822,\"kind\":2,\"tags\":[],\"content\":\"wss://nostr.orangepill.dev\",\"sig\":\"d5b8e7fc9928ec6d40452f9f1be05aca6303f9da85ee128052ce77fc2f27b66629ce4ee5c93231b4f0946d1709f71d3d25c7236dfcbef0a5eb90e86db8683e9a\"}]">>
% <<"[\"EVENT\",\"17771556064953075220111\",{\"id\":\"3cd9aae254c99e4e31ca4ddaa8cc85cb030dcebcd64088bd490d2118eb19aad7\",\"pubkey\":\"b8060b54a86d9a8fab04328ce134f0f2f20d2e2c67c128932d0bfb3732abf1f6\",\"created_at\":1672044455,\"kind\":2,\"tags\":[],\"content\":\"wss://nostr-pub.wellorder.net\",\"sig\":\"4900c0eef7040c63960a99d75c31d3473fe7afc687e6ad337483c7bc8dcbc1793867da316dfa94bd69fffe3c8ee3f27422f739af9383026ef6c8fa3bbe15c55b\"}]">>
% kind 7
% <<"[\"EVENT\",\"17771556064953075220\",{\"id\":\"6bef2129264acfe7fb43d33b418e40f69d511922c7203a1739fff933a80073fc\",\"pubkey\":\"a2d9f796461e3926e82d6ff02661be5fc57d3d6b3b31b6aaf76344db8280e331\",\"created_at\":1677314334,\"kind\":7,\"tags\":[[\"e\",\"7ed274a08644586b6bb369276d58197908dcdec0381d3dfac6328d336a62d69e\"],[\"p\",\"69074169ed68fa74c37d3926359f4100635c37eea5cfece064ed022ed06f792b\"]],\"content\":\"+\",\"sig\":\"bb9aa9df016baf7a8b7c7e302af3f4a9d716aeb734aee22d51e6cfb7df92d1ac2542aceb254533c958169e3f0622ce12b9d6257dac9dd42673b9ab0f21cc3706\"}]">>
%%--------------------------------------------------------------------
%% @doc internal function used to decode every element of a JSON
%% encoded message and convert it to record.
%%
%% @todo creates more test.
%% @end
%%--------------------------------------------------------------------
% decode an event message from client to relay
decode_message([<<"EVENT">>, Event]) ->
decode_message_event(Event);
% decode an event message from relay to client
decode_message([<<"EVENT">>, SubscriptionId, Event]) ->
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
{ok, ParsedEvent} = decode_message_event(Event),
Return = #subscription{ id = ParsedSubscriptionId
, content = ParsedEvent
},
{ok, Return};
% decode a subscription request
decode_message([<<"REQ">>, SubscriptionId, Filter]) ->
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
{ok, ParsedFilter} = decode_message_filter(Filter),
Return = #subscription{ id = ParsedSubscriptionId
, content = ParsedFilter
},
{ok, Return};
% decode a end of subscription request
decode_message([<<"CLOSE">>, SubscriptionId]) ->
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
Return = #close{ subscription_id = ParsedSubscriptionId },
{ok, Return};
% decode a notice message
decode_message([<<"NOTICE">>, Notice]) ->
{ok, ParsedNotice} = decode_message_notice(Notice),
Return = #notice{ message = ParsedNotice },
{ok, Return};
% decode an end of subscription message
decode_message([<<"EOSE">>, SubscriptionId]) ->
{ok, ParsedSubscriptionId} = decode_message_subscription_id(SubscriptionId),
Return = #eose{ id = ParsedSubscriptionId },
{ok, Return};
decode_message(Message) ->
{error, {unsupported, Message}}.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
decode_message_event(#{ <<"id">> := EventId
, <<"pubkey">> := PublicKey
, <<"created_at">> := CreatedAt
, <<"kind">> := Kind
, <<"tags">> := Tags
, <<"content">> := Content
, <<"sig">> := Signature
}) ->
Event = #event{ id = EventId
, created_at = CreatedAt
, public_key = PublicKey
, kind = Kind
, tags = Tags
, content = Content
, signature = Signature
},
{ok, Event}.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
decode_message_subscription_id(SubscriptionId)
when is_bitstring(SubscriptionId) ->
{ok, SubscriptionId}.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
decode_message_notice(Notice)
when is_bitstring(Notice) ->
Notice = #notice{ message = Notice },
{ok, Notice}.
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
decode_message_filter(#{ <<"ids">> := EventIds
, <<"authors">> := Authors
, <<"kinds">> := Kinds
, <<"#e">> := TagEventIds
, <<"#p">> := TagPublicKey
, <<"since">> := Since
, <<"until">> := Until
, <<"limit">> := Limit
}) ->
Filter = #filter{ event_ids = EventIds
, authors = Authors
, kinds = Kinds
, tag_event_ids = TagEventIds
, tag_public_keys = TagPublicKey
, since = Since
, until = Until
, limit = Limit
},
{ok, Filter}.

39
src/nostrlib_event.erl Normal file
View File

@@ -0,0 +1,39 @@
%%%===================================================================
%%% @doc
%%%
%%% == Examples ==
%%%
%%% ```
%%% nostrlib_event:create(metadata, #{}
%%% '''
%%%
%%% Here the manual steps to check a message.
%%%
%%% ```
%%% % 1. read the file (or take it from the wild)
%%% {ok, M} = file:read_file("test/nostrlib_SUITE_data/valid_event_kind1.json").
%%%
%%% % 2. decode the json
%%% {ok, J} = thoas:decode(M).
%%%
%%% % 3. decode the message and convert it to record
%%% {ok,{_,_,E}} = nostrlib_decode:message(J).
%%%
%%% % 4. serialize the message
%%% X = nostrlib_event:serialize(E).
%%%
%%% % 5. create the hash and convert others values from hex to binary
%%% HashMessage = crypto:hash(sha256, X).
%%% PublicKey = nostrlib:hex_to_binary(E#event.public_key).
%%% Signature = nostrlib:hex_to_binary(E#event.signature).
%%%
%%% % 6. verify the message with the public key and the signature.
%%% true = nostrlib_schnorr:verify(HashMessage, PublicKey, Signature).
%%% '''
%%%
%%% @end
%%%===================================================================
-module(nostrlib_event).

View File

@@ -0,0 +1,5 @@
%%%===================================================================
%%% @doc DRAFT
%%% @end
%%%===================================================================
-module(nostrlib_identity).

View File

@@ -1,66 +0,0 @@
%%%===================================================================
%%% @doc
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-module(nostrlib_kind).
-export([metadata/3, text_note/1]).
-export([recommend_server/1, recommend_server/2]).
-include("nostrlib.hrl").
%%--------------------------------------------------------------------
%% @doc
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
%% @end
%%--------------------------------------------------------------------
-spec metadata(Username, About, Picture) -> Return when
Username :: to_be_defined(),
About :: to_be_defined(),
Picture :: to_be_defined(),
Return :: map().
metadata(Username, About, Picture) ->
#{ name => Username
, about => About
, picture => Picture
}.
%%--------------------------------------------------------------------
%% @doc
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
%% @end
%%--------------------------------------------------------------------
-spec text_note(Content) -> Return when
Content :: to_be_defined(),
Return :: map().
text_note(Content) ->
text_note(#{}, Content).
%%--------------------------------------------------------------------
%% @doc
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
%% @end
%%--------------------------------------------------------------------
-spec text_note(Map, Content) -> Return when
Map :: map(),
Content :: to_be_defined(),
Return :: map().
text_note(Map, Content) ->
maps:put(content, Content, Map).
%%--------------------------------------------------------------------
%% @doc
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#basic-event-kinds
%% @end
%%--------------------------------------------------------------------
-spec recommend_server(ServerUrl) -> Return when
ServerUrl :: to_be_defined(),
Return :: map().
recommend_server(ServerUrl) ->
recommend_server(#{}, ServerUrl).
-spec recommend_server(Map, ServerUrl) -> Return when
Map :: map(),
ServerUrl :: to_be_defined(),
Return :: map().
recommend_server(Map, ServerUrl) ->
text_note(Map, ServerUrl).

View File

@@ -1,22 +0,0 @@
%%%===================================================================
%%% @doc
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-module(nostrlib_relay).
-export([notice/1]).
-include("nostrlib.hrl").
%%--------------------------------------------------------------------
%% @doc notice/1 function returns a notification message to the
%% client.
%% see https://github.com/nostr-protocol/nips/blob/master/01.md
%% @end
%%--------------------------------------------------------------------
-spec notice(Message) -> Return when
Message :: iodata(),
Return :: any().
notice(Message)
when is_bitstring(Message) ->
[<<"NOTICE">>, Message].

1035
src/nostrlib_schnorr.erl Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
%%%===================================================================
%%% @doc
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-module(nostrlib_secp256k1).
-export([create_keys/0]).
-include("nostrlib.hrl").
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec create_keys() -> Return when
Return :: {PublicKey, PrivateKey},
PublicKey :: iodata(),
PrivateKey :: iodata().
create_keys() ->
crypto:generate_key(ecdh, secp256k1).

View File

@@ -1,67 +0,0 @@
%%%===================================================================
%%% @doc
%%% @end
%%% @author Mathieu Kerjouan <contact at erlang-punch.com>
%%%===================================================================
-module(nostrlib_tags).
-export([e/2, event/2]).
-export([p/2, public_key/2]).
-include_lib("eunit/include/eunit.hrl").
-include("nostrlib.hrl").
%%--------------------------------------------------------------------
%% extra-specification for eunit.
%%--------------------------------------------------------------------
-spec test() -> any().
%%--------------------------------------------------------------------
%% @doc an alias for event.
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
%% @end
%%--------------------------------------------------------------------
-spec e(EventId, RecommendedRelayUrl) -> Return when
EventId :: iodata(),
RecommendedRelayUrl :: iodata(),
Return :: any().
e(EventId, RecommendedRelayUrl) ->
event(EventId, RecommendedRelayUrl).
%%--------------------------------------------------------------------
%% @doc
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
%% @end
%%--------------------------------------------------------------------
-spec event(EventId, RecommendedRelayUrl) -> Return when
EventId :: iodata(),
RecommendedRelayUrl :: iodata(),
Return :: any().
event(EventId, RecommendedRelayUrl) ->
["e", EventId, RecommendedRelayUrl].
%%--------------------------------------------------------------------
%% @doc An alias for publickey
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
%% @end
%%--------------------------------------------------------------------
-spec p(PublicKey, RecommendedRelayUrl) -> Return when
PublicKey :: iodata(),
RecommendedRelayUrl :: iodata(),
Return :: any().
p(PublicKey, RecommendedRelayUrl) ->
public_key(PublicKey, RecommendedRelayUrl).
%%--------------------------------------------------------------------
%% @doc
%% see https://github.com/nostr-protocol/nips/blob/master/01.md#other-notes
%% @end
%%--------------------------------------------------------------------
-spec public_key(PublicKey, RecommendedRelayUrl) -> Return when
PublicKey :: iodata(),
RecommendedRelayUrl :: iodata(),
Return :: any().
public_key(PublicKey, RecommendedRelayUrl) ->
["p", PublicKey, RecommendedRelayUrl].

87
src/nostrlib_url.erl Normal file
View File

@@ -0,0 +1,87 @@
%%%===================================================================
%%% @doc
%%% @end
%%%===================================================================
-module(nostrlib_url).
-export([check/1]).
-export([check_hostname/1]).
-include_lib("eunit/include/eunit.hrl").
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec test() -> any().
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec check(Url) -> Return when
Url :: iodata(),
Return :: {ok, Url}
| {error, proplists:proplists()}.
check(Url) ->
CheckHost = fun(Hostname, VUrl) ->
case check_hostname(Hostname) of
{ok, Hostname} ->
{ok, VUrl};
Elsewise ->
Elsewise
end
end,
case uri_string:parse(Url) of
#{host := Host, path := <<>>, scheme := <<"ws">>} ->
CheckHost(Host, Url);
#{host := Host, path := <<>>, scheme := <<"wss">>} ->
CheckHost(Host, Url);
#{scheme := Scheme, path := <<>>} ->
{error, [{scheme, Scheme}]};
#{path := Path} ->
{error, [{path, Path}]}
end.
-spec check_test() -> any().
check_test() ->
[?assertEqual({ok, <<"wss://rsslay.fiatjaf.com">>}
, check(<<"wss://rsslay.fiatjaf.com">>))
,?assertEqual({ok, <<"wss://somerelay.com">>}
, check(<<"wss://somerelay.com">>))
,?assertEqual({ok, <<"ws://somerelay.com">>}
, check(<<"ws://somerelay.com">>))
,?assertEqual({ok, <<"wss://d.a.c.are.somerelay.com">>}
, check(<<"wss://d.a.c.are.somerelay.com">>))
,?assertEqual({error, [{path, <<"/test">>}]}
, check(<<"wss://somerelay.com/test">>))
,?assertEqual({error, [{scheme, <<"https">>}]}
,check(<<"https://httprelay.com">>))
,?assertEqual({error, [{hostname, <<"httprelay_!.com">>}]}
,check(<<"wss://httprelay_!.com">>))
].
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec check_hostname() -> iodata().
check_hostname() ->
Pattern = <<"(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)">>,
{ok, Regex} = re:compile(Pattern),
Regex.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec check_hostname(Hostname) -> Return when
Hostname :: bitstring(),
Return :: {ok, Hostname}
| {error, proplists:proplists()}.
check_hostname(Hostname) ->
Regex = check_hostname(),
case re:run(Hostname, Regex) of
{match, _} ->
{ok, Hostname};
_ ->
{error, [{hostname, Hostname}]}
end.

View File

@@ -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.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
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">>])).

View File

@@ -0,0 +1 @@
["EOSE","9635033750818420944"]

View File

@@ -0,0 +1 @@
["EVENT","5452643154455862",{"id":"5da65160faa8f1e0232f5e9f9f36267188581446ca955a71c756591ef0fed9f2","pubkey":"a96ed24d83fb80552b13734babd96562743ede16cb22c306189380c67ab0eb4f","created_at":1677417436,"kind":0,"tags":[],"content":"{\"banner\":\"https://cdn.cdnparenting.com/articles/2021/07/31125225/1817265176.webp\",\"website\":\"\",\"nip05\":\"coldpotato@nostrplebs.com\",\"picture\":\"https://void.cat/d/67H6AKeprGmKq7mp2b9n9r\",\"lud16\":\"coldpotato@stacker.news\",\"display_name\":\"Cold Potato 💜⚡️\",\"about\":\"\",\"name\":\"coldpotato\",\"nip05valid\":false,\"username\":\"coldpotato\",\"displayName\":\"Cold Potato 💜⚡️\",\"lud06\":\"\"}","sig":"266858908030fd70dfb4af8aae3aa7fb3339ba12215222c8a4877056c8d4dee1a520b90cc9a35f9ece38be6f2e4eb1998fe4b0a8864ad5c64e68507f7685fba0"}]

View File

@@ -0,0 +1 @@
["EVENT","5452643154455862",{"pubkey":"e623bb2e90351b30818de33debd506aa9eae04d8268be65ceb2dcc1ef6881765","content":"Rest isn't stagnation.","id":"1e76eecdcd101063df6c56afd58d90a1d9e81265ea255148c6fc9789d168420e","created_at":1677417294,"sig":"1b9096d0c8f48aed807f148a4b35b97cc9b28cc8ea698e4ea6d8fda405c463569eb8bed01a77596a4ffa2daab89b7601d08d4646d2f8a82e7cd63388bb1f88e1","kind":1,"tags":[]}]

View File

@@ -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"}]

View File

@@ -0,0 +1 @@
["EVENT","14983797176934273925",{"id":"e7f5850dd535feba822e35747d022ea7c29d7c0f226f75f08af433abb20357f6","pubkey":"5132e8bf2ac08dc03016dda748ab8c9c1207d595afb35b87539413b99932ad72","created_at":1675315873,"kind":2,"tags":[],"content":"wss://rsslay.fiatjaf.com","sig":"c0d56d5f45c0bebc97c980e42b906ea336d9a21806b3813bf819982e3ba1816d1673c3ae4c23366afac61e1bfb5cdc880318c5ef5808e28252cb14189ef2faad"}]

View File

@@ -0,0 +1 @@
["EVENT","5452643154455862",{"id":"fb9af93e73551efd48e381fe320bb543e7bc119a83b702628b76b1a7ac5e75ca","pubkey":"0e1314f29c0a64ec3679671c59dabaf655594e1ca6bbd8712366e3ac175a964f","created_at":1677417344,"kind":7,"tags":[["e","76918fd820d4165419796e699693123e1c47955c5d89d1fc91100f3b4235608d"],["p","c49f8402ef410fce5a1e5b2e6da06f351804988dd2e0bad18ae17a50fc76c221"]],"content":"+","sig":"0815450293a7c4e3079ae3d62baeba5e441ca026eb4a0fa8c145e8ad969673ad155cb2db7916393db6db0b562b06f86acc7dc38b5e1bad238aaa534a27f91161"}]

View File

@@ -0,0 +1 @@
["REQ","5452643154455862",{"kinds":[0,1,2,7],"since":1677330792,"limit":450}]

View File

@@ -0,0 +1,174 @@
%%%====================================================================
%%% @doc
%%%
%%% @end
%%%====================================================================
-module(nostrlib_schnorr_SUITE).
-export([suite/0]).
-export([init_per_suite/1, end_per_suite/1]).
-export([init_per_group/2, end_per_group/2]).
-export([init_per_testcase/2, end_per_testcase/2]).
-export([groups/0, all/0]).
-export([verification_vectors/0, verification_vectors/1]).
-export([signature_vectors/0, signature_vectors/1]).
-export([common/0, common/1]).
-include_lib("common_test/include/ct.hrl").
-spec suite() -> any().
-spec init_per_suite(any()) -> any().
-spec end_per_suite(any()) -> any().
-spec init_per_group(any(), any()) -> any().
-spec end_per_group(any(), any()) -> any().
-spec init_per_testcase(any(), any()) -> any().
-spec end_per_testcase(any(), any()) -> any().
-spec groups() -> any().
-spec all() -> any().
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
suite() -> [{timetrap,{minutes,10}}].
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
init_per_suite(_Config) -> [].
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
end_per_suite(_Config) -> ok.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
init_per_group(_GroupName, Config) -> Config.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
end_per_group(_GroupName, _Config) -> ok.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
init_per_testcase(_TestCase, Config) -> Config.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
end_per_testcase(_TestCase, _Config) -> ok.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
groups() -> [{vectors, [parallel], [verification_vectors
,signature_vectors
,common]}
].
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
all() -> [{group, vectors, [parallel]}].
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec common() -> any().
common() -> [].
-spec common(Config) -> Return when
Config :: proplists:proplists(),
Return :: any().
common(_Config) ->
{ok, <<PrivateKey:256>>} = nostrlib_schnorr:new_privatekey(),
{ok, <<_:256>>} = nostrlib_schnorr:new_publickey(PrivateKey).
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec verification_vectors() -> any().
verification_vectors() -> [].
-spec verification_vectors(Config) -> Return when
Config :: proplists:proplists(),
Return :: any().
verification_vectors(Config) ->
DataDir = proplists:get_value(data_dir, Config),
VerificationTest = fun (#{ <<"index">> := I
, <<"message">> := M
, <<"public key">> := P
, <<"signature">> := S
, <<"verification result">> := V }) ->
Info = [{index, I}
,{message, M}
,{public_key, P}
,{signature, S}
,{result, V}
],
V = nostrlib_schnorr:verify(M, P, S),
ct:pal(info, "verify (ok): ~p", [Info])
end,
lists:map(VerificationTest, test_vectors(DataDir)).
%%--------------------------------------------------------------------
%% @doc
%% @end
%%--------------------------------------------------------------------
-spec signature_vectors() -> any().
signature_vectors() -> [].
-spec signature_vectors(Config) -> Return when
Config :: proplists:proplists(),
Return :: any().
signature_vectors(Config) ->
DataDir = proplists:get_value(data_dir, Config),
SignatureTest = fun (#{ <<"secret key">> := <<>> }) ->
ignored;
(#{ <<"index">> := I
, <<"message">> := M
, <<"aux_rand">> := A
, <<"signature">> := S
, <<"secret key">> := K }) ->
Info = [{index, I}
,{message, M}
,{secret_key, K}
,{aux_rand, A}
,{signature, S}
],
{ok, S} = nostrlib_schnorr:sign(M, K, A),
ct:pal(info, "signature (ok): ~p", [Info])
end,
lists:map(SignatureTest, test_vectors(DataDir)).
%%--------------------------------------------------------------------
%% @doc internal function. read the test-vectors.csv file from
%% BIP-0340 and converts it in an Erlang like format.
%%
%% @end
%%--------------------------------------------------------------------
test_vectors(Directory) ->
{ok, File} = file:read_file(filename:join(Directory, "test-vectors.csv")),
Lines = re:split(File, "\r\n"),
Fields = lists:map(fun(X) ->
re:split(X, ",")
end, Lines),
Cleaned = lists:filter(fun _F([<<>>]) -> false; _F(_) -> true end, Fields),
[Header|Content] = Cleaned,
Zip = fun _Zip ([<<>>]) -> undefined;
_Zip (C) -> Z = lists:zip(Header, C),
M = maps:from_list(Z),
maps:map(fun _F(<<"aux_rand">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
_F(<<"secret key">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
_F(<<"signature">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):512>>;
_F(<<"public key">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
_F(<<"message">>, Y) when Y =/= <<>> -> <<(binary_to_integer(Y, 16)):256>>;
_F(<<"verification result">>, <<"FALSE">>) -> false;
_F(<<"verification result">>, <<"TRUE">>) -> true;
_F(_X, Y) -> Y
end, M)
end,
lists:map(Zip, Content).

View File

@@ -0,0 +1,16 @@
index,secret key,public key,aux_rand,message,signature,verification result,comment
0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE,
1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE,
2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE,
3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n
4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE,
5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve
6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false
7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message
8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value
9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0
10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1
11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve
12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size
13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order
14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size
1 index secret key public key aux_rand message signature verification result comment
2 0 0000000000000000000000000000000000000000000000000000000000000003 F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0 TRUE
3 1 B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659 0000000000000000000000000000000000000000000000000000000000000001 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A TRUE
4 2 C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9 DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8 C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906 7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C 5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7 TRUE
5 3 0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710 25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3 TRUE test fails if msg is reduced modulo p or n
6 4 D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9 4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703 00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4 TRUE
7 5 EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B FALSE public key not on the curve
8 6 DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2 FALSE has_even_y(R) is false
9 7 DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD FALSE negated message
10 8 DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6 FALSE negated s value
11 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
12 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
13 11 DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B FALSE sig[0:32] is not an X coordinate on the curve
14 12 DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B FALSE sig[0:32] is equal to field size
15 13 DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 FALSE sig[32:64] is equal to curve order
16 14 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30 243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89 6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B FALSE public key is not a valid X coordinate because it exceeds the field size