Compare commits

..
Author SHA1 Message Date
William Casarin c12da6a229 Command result notices
This introduces a new notice-like event with more structure so that clients
can know if an event is sucessfully written to the database. Clients
can't really do much with the current NOTICE messages, but with these
structured result messages, it can know if an event was sucessfully
saved.

Whenever there is an error and we have an id available (event id, sub
id, etc) we return these structured OK events instead.

Example:

When saving the following event:
    ["EVENT",{"id": "event_id" }]

The server will now return events in the following format:
    ["OK", event_id, "true|false", message]

For example, on a successful save:
    ["OK", "event_id", "true"]

If we already have the event:
    ["OK", "event_id", "true", "duplicate"]

If the event is rejected:
    ["OK", "event_id", "false", "you are blocked"]

If a subscription fails:
    ["OK", "sub_id", "false", "Too many subscriptions"]

NIP coming soon!
2022-11-09 07:44:39 -06:00
11 changed files with 112 additions and 257 deletions
Generated
+29 -29
View File
@@ -200,9 +200,9 @@ checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.76" version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@@ -212,9 +212,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.23" version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"num-integer", "num-integer",
@@ -381,9 +381,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.82" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a"
dependencies = [ dependencies = [
"cc", "cc",
"cxxbridge-flags", "cxxbridge-flags",
@@ -393,9 +393,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx-build" name = "cxx-build"
version = "1.0.82" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827"
dependencies = [ dependencies = [
"cc", "cc",
"codespan-reporting", "codespan-reporting",
@@ -408,15 +408,15 @@ dependencies = [
[[package]] [[package]]
name = "cxxbridge-flags" name = "cxxbridge-flags"
version = "1.0.82" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a"
[[package]] [[package]]
name = "cxxbridge-macro" name = "cxxbridge-macro"
version = "1.0.82" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -438,9 +438,9 @@ dependencies = [
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.6" version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
@@ -789,9 +789,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.23" version = "0.14.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -872,9 +872,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.2" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [ dependencies = [
"autocfg 1.1.0", "autocfg 1.1.0",
"hashbrown 0.12.3", "hashbrown 0.12.3",
@@ -1095,7 +1095,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]] [[package]]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.7.3" version = "0.7.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitcoin_hashes", "bitcoin_hashes",
@@ -1664,9 +1664,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.7.0" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -1684,9 +1684,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.28" version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
@@ -1836,9 +1836,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.88" version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa",
@@ -1997,9 +1997,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.22.0" version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
dependencies = [ dependencies = [
"autocfg 1.1.0", "autocfg 1.1.0",
"bytes", "bytes",
@@ -2367,9 +2367,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.2.2" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]
+1 -9
View File
@@ -1,15 +1,7 @@
[package] [package]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.7.3" version = "0.7.1"
edition = "2021" edition = "2021"
authors = ["Greg Heartsfield <scsibug@imap.cc>"]
description = "A relay implementation for the Nostr protocol"
readme = "README.md"
homepage = "https://sr.ht/~gheartsfield/nostr-rs-relay/"
repository = "https://git.sr.ht/~gheartsfield/nostr-rs-relay"
license = "MIT"
keywords = ["nostr", "server"]
categories = ["network-programming", "web-programming"]
[dependencies] [dependencies]
tracing = "0.1.36" tracing = "0.1.36"
+2 -7
View File
@@ -1,21 +1,16 @@
FROM docker.io/library/rust:1.65.0@sha256:1bca14676a365d0ed37a1e2a1da86c2bcf883fdf6e6886469434763d94d4afd5 as builder FROM docker.io/library/rust:1.65.0@sha256:1bca14676a365d0ed37a1e2a1da86c2bcf883fdf6e6886469434763d94d4afd5 as builder
RUN USER=root cargo install cargo-auditable
RUN USER=root cargo new --bin nostr-rs-relay RUN USER=root cargo new --bin nostr-rs-relay
WORKDIR ./nostr-rs-relay WORKDIR ./nostr-rs-relay
COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.toml ./Cargo.toml
COPY ./Cargo.lock ./Cargo.lock COPY ./Cargo.lock ./Cargo.lock
# build dependencies only (caching) RUN cargo build --release
RUN cargo auditable build --release --locked
# get rid of starter project code
RUN rm src/*.rs RUN rm src/*.rs
# copy project source code
COPY ./src ./src COPY ./src ./src
# build auditable release using locked deps
RUN rm ./target/release/deps/nostr*relay* RUN rm ./target/release/deps/nostr*relay*
RUN cargo auditable build --release --locked RUN cargo build --release
FROM docker.io/library/debian:bullseye-20221024-slim@sha256:76cdda8fe5eb597ef5e712e4c9a9f5f1fb119e69f353daaa7bd6d0f6e66e541d FROM docker.io/library/debian:bullseye-20221024-slim@sha256:76cdda8fe5eb597ef5e712e4c9a9f5f1fb119e69f353daaa7bd6d0f6e66e541d
+9 -27
View File
@@ -1,8 +1,8 @@
# [nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay) # [nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay)
This is a [nostr](https://github.com/nostr-protocol/nostr) relay, This is a [nostr](https://github.com/nostr-protocol/nostr) relay, written in
written in Rust. It currently supports the entire relay protocol, and Rust. It currently supports the entire relay protocol, and has a
persists data with SQLite. SQLite persistence layer.
The project master repository is available on The project master repository is available on
[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is [sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is
@@ -26,7 +26,6 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
- [x] NIP-12: [Generic Tag Queries](https://github.com/nostr-protocol/nips/blob/master/12.md) - [x] NIP-12: [Generic Tag Queries](https://github.com/nostr-protocol/nips/blob/master/12.md)
- [x] NIP-15: [End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md) - [x] NIP-15: [End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md)
- [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md) - [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md)
- [x] NIP-20: [Command Results](https://github.com/nostr-protocol/nips/blob/master/20.md)
- [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_) - [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_)
- [x] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md) - [x] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md)
@@ -37,32 +36,15 @@ application. Use a bind mount to store the SQLite database outside of
the container image, and map the container's 8080 port to a host port the container image, and map the container's 8080 port to a host port
(7000 in the example below). (7000 in the example below).
The examples below start a rootless podman container, mapping a local
data directory and config file.
```console ```console
$ podman build -t nostr-rs-relay . $ docker build -t nostr-rs-relay .
$ mkdir data $ docker run -it -p 7000:8080 \
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind nostr-rs-relay
$ podman unshare chown 100:100 data [2021-12-31T19:58:31Z INFO nostr_rs_relay] listening on: 0.0.0.0:8080
[2021-12-31T19:58:31Z INFO nostr_rs_relay::db] opened database "/usr/src/app/db/nostr.db" for writing
$ podman run -it --rm -p 7000:8080 \ [2021-12-31T19:58:31Z INFO nostr_rs_relay::db] DB version = 2
--user=100:100 \
-v $(pwd)/data:/usr/src/app/db:Z \
-v $(pwd)/config.toml:/usr/src/app/config.toml:ro,Z \
--name nostr-relay nostr-rs-relay:latest
Nov 19 15:31:15.013 INFO nostr_rs_relay: Starting up from main
Nov 19 15:31:15.017 INFO nostr_rs_relay::server: listening on: 0.0.0.0:8080
Nov 19 15:31:15.019 INFO nostr_rs_relay::server: db writer created
Nov 19 15:31:15.019 INFO nostr_rs_relay::server: control message listener started
Nov 19 15:31:15.019 INFO nostr_rs_relay::db: Built a connection pool "event writer" (min=1, max=4)
Nov 19 15:31:15.019 INFO nostr_rs_relay::db: opened database "/usr/src/app/db/nostr.db" for writing
Nov 19 15:31:15.019 INFO nostr_rs_relay::schema: DB version = 0
Nov 19 15:31:15.054 INFO nostr_rs_relay::schema: database pragma/schema initialized to v7, and ready
Nov 19 15:31:15.054 INFO nostr_rs_relay::schema: All migration scripts completed successfully. Welcome to v7.
Nov 19 15:31:15.521 INFO nostr_rs_relay::db: Built a connection pool "client query" (min=4, max=128)
``` ```
Use a `nostr` client such as Use a `nostr` client such as
+15 -34
View File
@@ -159,9 +159,8 @@ pub async fn db_writer(
event.get_event_id_prefix() event.get_event_id_prefix()
); );
notice_tx notice_tx
.try_send(Notice::blocked( .try_send(Notice::message(
event.id, "pubkey is not allowed to publish to this relay".to_owned(),
"pubkey is not allowed to publish to this relay",
)) ))
.ok(); .ok();
continue; continue;
@@ -193,9 +192,9 @@ pub async fn db_writer(
event.get_author_prefix() event.get_author_prefix()
); );
notice_tx notice_tx
.try_send(Notice::blocked( .try_send(Notice::message(
event.id, "NIP-05 verification is no longer valid (expired/wrong domain)"
"NIP-05 verification is no longer valid (expired/wrong domain)", .to_owned(),
)) ))
.ok(); .ok();
continue; continue;
@@ -207,9 +206,8 @@ pub async fn db_writer(
event.get_author_prefix() event.get_author_prefix()
); );
notice_tx notice_tx
.try_send(Notice::blocked( .try_send(Notice::message(
event.id, "NIP-05 verification needed to publish events".to_owned(),
"NIP-05 verification needed to publish events",
)) ))
.ok(); .ok();
continue; continue;
@@ -252,8 +250,9 @@ pub async fn db_writer(
} }
Err(err) => { Err(err) => {
warn!("event insert failed: {:?}", err); warn!("event insert failed: {:?}", err);
let msg = "relay experienced an error trying to publish the latest event"; let msg =
notice_tx.try_send(Notice::error(event.id, msg)).ok(); "relay experienced an error trying to publish the latest event".into();
notice_tx.try_send(Notice::err_msg(msg, event.id)).ok();
} }
} }
} }
@@ -435,7 +434,7 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
return (empty_query, empty_params); return (empty_query, empty_params);
} }
let mut query = "SELECT DISTINCT(e.content), e.created_at FROM event e".to_owned(); let mut query = "SELECT DISTINCT(e.content), e.created_at FROM event e ".to_owned();
// query parameters for SQLite // query parameters for SQLite
let mut params: Vec<Box<dyn ToSql>> = vec![]; let mut params: Vec<Box<dyn ToSql>> = vec![];
@@ -471,14 +470,8 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
} }
} }
} }
if !authvec.is_empty() {
let authors_clause = format!("({})", auth_searches.join(" OR ")); let authors_clause = format!("({})", auth_searches.join(" OR "));
filter_components.push(authors_clause); filter_components.push(authors_clause);
} else {
// if the authors list was empty, we should never return
// any results.
filter_components.push("false".to_owned());
}
} }
// Query for Kind // Query for Kind
if let Some(ks) = &f.kinds { if let Some(ks) = &f.kinds {
@@ -511,14 +504,8 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
} }
} }
} }
if !idvec.is_empty() {
let id_clause = format!("({})", id_searches.join(" OR ")); let id_clause = format!("({})", id_searches.join(" OR "));
filter_components.push(id_clause); filter_components.push(id_clause);
} else {
// if the ids list was empty, we should never return
// any results.
filter_components.push("false".to_owned());
}
} }
// Query for tags // Query for tags
if let Some(map) = &f.tags { if let Some(map) = &f.tags {
@@ -594,6 +581,7 @@ fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
.map(|s| format!("SELECT content, created_at FROM ({})", s)) .map(|s| format!("SELECT content, created_at FROM ({})", s))
.collect(); .collect();
let query: String = subqueries_selects.join(" UNION "); let query: String = subqueries_selects.join(" UNION ");
trace!("final query string: {}", query);
(query, params) (query, params)
} }
@@ -619,8 +607,6 @@ pub async fn db_query(
trace!("SQL generated in {:?}", start.elapsed()); trace!("SQL generated in {:?}", start.elapsed());
// show pool stats // show pool stats
debug!("DB pool stats: {:?}", pool.state()); debug!("DB pool stats: {:?}", pool.state());
// cutoff for displaying slow queries
let slow_cutoff = Duration::from_millis(200);
let start = Instant::now(); let start = Instant::now();
if let Ok(conn) = pool.get() { if let Ok(conn) = pool.get() {
// execute the query. Don't cache, since queries vary so much. // execute the query. Don't cache, since queries vary so much.
@@ -629,16 +615,11 @@ pub async fn db_query(
let mut first_result = true; let mut first_result = true;
while let Some(row) = event_rows.next()? { while let Some(row) = event_rows.next()? {
if first_result { if first_result {
let first_result_elapsed = start.elapsed();
// logging for slow queries
if first_result_elapsed >= slow_cutoff {
info!("final query string (slow): {}", q);
} else {
trace!("final query string: {}", q);
}
debug!( debug!(
"time to first result: {:?} (cid={}, sub={:?})", "time to first result: {:?} (cid={}, sub={:?})",
first_result_elapsed, client_id, sub.id start.elapsed(),
client_id,
sub.id
); );
first_result = false; first_result = false;
} }
+3 -9
View File
@@ -17,16 +17,10 @@ pub enum Error {
ConnWriteError, ConnWriteError,
#[error("EVENT parse failed")] #[error("EVENT parse failed")]
EventParseFailed, EventParseFailed,
#[error("CLOSE message parse failed")] #[error("ClOSE message parse failed")]
CloseParseFailed, CloseParseFailed,
#[error("Event invalid signature")] #[error("Event validation failed")]
EventInvalidSignature, EventInvalid,
#[error("Event invalid id")]
EventInvalidId,
#[error("Event malformed pubkey")]
EventMalformedPubkey,
#[error("Event could not canonicalize")]
EventCouldNotCanonicalize,
#[error("Event too large")] #[error("Event too large")]
EventMaxLengthError(usize), EventMaxLengthError(usize),
#[error("Subscription identifier max length exceeded")] #[error("Subscription identifier max length exceeded")]
+12 -18
View File
@@ -27,12 +27,6 @@ pub struct EventCmd {
event: Event, event: Event,
} }
impl EventCmd {
pub fn event_id(&self) -> &str {
&self.event.id
}
}
/// Parsed nostr event. /// Parsed nostr event.
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct Event { pub struct Event {
@@ -89,13 +83,13 @@ impl From<EventCmd> for Result<Event> {
// ensure command is correct // ensure command is correct
if ec.cmd != "EVENT" { if ec.cmd != "EVENT" {
Err(CommandUnknownError) Err(CommandUnknownError)
} else { } else if ec.event.is_valid() {
ec.event.validate().map(|_| {
let mut e = ec.event; let mut e = ec.event;
e.build_index(); e.build_index();
e.update_delegation(); e.update_delegation();
e Ok(e)
}) } else {
Err(EventInvalid)
} }
} }
} }
@@ -226,7 +220,7 @@ impl Event {
} }
/// Check if this event has a valid signature. /// Check if this event has a valid signature.
fn validate(&self) -> Result<()> { fn is_valid(&self) -> bool {
// TODO: return a Result with a reason for invalid events // TODO: return a Result with a reason for invalid events
// validation is performed by: // validation is performed by:
// * parsing JSON string into event fields // * parsing JSON string into event fields
@@ -235,8 +229,8 @@ impl Event {
// * serialize with no spaces/newlines // * serialize with no spaces/newlines
let c_opt = self.to_canonical(); let c_opt = self.to_canonical();
if c_opt.is_none() { if c_opt.is_none() {
debug!("could not canonicalize"); debug!("event could not be canonicalized");
return Err(EventCouldNotCanonicalize); return false;
} }
let c = c_opt.unwrap(); let c = c_opt.unwrap();
// * compute the sha256sum. // * compute the sha256sum.
@@ -245,21 +239,21 @@ impl Event {
// * ensure the id matches the computed sha256sum. // * ensure the id matches the computed sha256sum.
if self.id != hex_digest { if self.id != hex_digest {
debug!("event id does not match digest"); debug!("event id does not match digest");
return Err(EventInvalidId); return false;
} }
// * validate the message digest (sig) using the pubkey & computed sha256 message hash. // * validate the message digest (sig) using the pubkey & computed sha256 message hash.
let sig = schnorr::Signature::from_str(&self.sig).unwrap(); let sig = schnorr::Signature::from_str(&self.sig).unwrap();
if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) { if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
if let Ok(pubkey) = XOnlyPublicKey::from_str(&self.pubkey) { if let Ok(pubkey) = XOnlyPublicKey::from_str(&self.pubkey) {
SECP.verify_schnorr(&sig, &msg, &pubkey) let verify = SECP.verify_schnorr(&sig, &msg, &pubkey);
.map_err(|_| EventInvalidSignature) matches!(verify, Ok(()))
} else { } else {
debug!("client sent malformed pubkey"); debug!("client sent malformed pubkey");
Err(EventMalformedPubkey) false
} }
} else { } else {
info!("error converting digest to secp256k1 message"); info!("error converting digest to secp256k1 message");
Err(EventInvalidSignature) false
} }
} }
+1 -1
View File
@@ -35,7 +35,7 @@ impl From<config::Info> for RelayInfo {
description: i.description, description: i.description,
pubkey: i.pubkey, pubkey: i.pubkey,
contact: i.contact, contact: i.contact,
supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 20, 22, 26]), supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 22, 26]),
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()), software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
version: CARGO_PKG_VERSION.map(|x| x.to_owned()), version: CARGO_PKG_VERSION.map(|x| x.to_owned()),
} }
+20 -58
View File
@@ -1,15 +1,13 @@
use crate::error;
pub enum EventResultStatus { pub enum EventResultStatus {
Saved, Saved,
Duplicate, Duplicate,
Invalid, Error(String),
Blocked,
RateLimited,
Error,
} }
pub struct EventResult { pub struct EventResult {
pub id: String, pub id: String,
pub msg: String,
pub status: EventResultStatus, pub status: EventResultStatus,
} }
@@ -18,69 +16,33 @@ pub enum Notice {
EventResult(EventResult), EventResult(EventResult),
} }
impl EventResultStatus {
pub fn to_bool(&self) -> bool {
match self {
Self::Saved => true,
Self::Duplicate => true,
Self::Invalid => false,
Self::Blocked => false,
Self::RateLimited => false,
Self::Error => false,
}
}
pub fn prefix(&self) -> &'static str {
match self {
Self::Saved => "saved",
Self::Duplicate => "duplicate",
Self::Invalid => "invalid",
Self::Blocked => "blocked",
Self::RateLimited => "rate-limited",
Self::Error => "error",
}
}
}
impl Notice { impl Notice {
//pub fn err(err: error::Error, id: String) -> Notice { pub fn err(err: error::Error, id: String) -> Notice {
// Notice::err_msg(format!("{}", err), id) Notice::err_msg(format!("{}", err), id)
//} }
pub fn message(msg: String) -> Notice { pub fn message(msg: String) -> Notice {
Notice::Message(msg) Notice::Message(msg)
} }
fn prefixed(id: String, msg: &str, status: EventResultStatus) -> Notice {
let msg = format!("{}: {}", status.prefix(), msg);
Notice::EventResult(EventResult { id, msg, status })
}
pub fn invalid(id: String, msg: &str) -> Notice {
Notice::prefixed(id, msg, EventResultStatus::Invalid)
}
pub fn blocked(id: String, msg: &str) -> Notice {
Notice::prefixed(id, msg, EventResultStatus::Blocked)
}
pub fn rate_limited(id: String, msg: &str) -> Notice {
Notice::prefixed(id, msg, EventResultStatus::RateLimited)
}
pub fn duplicate(id: String) -> Notice {
Notice::prefixed(id, "", EventResultStatus::Duplicate)
}
pub fn error(id: String, msg: &str) -> Notice {
Notice::prefixed(id, msg, EventResultStatus::Error)
}
pub fn saved(id: String) -> Notice { pub fn saved(id: String) -> Notice {
Notice::EventResult(EventResult { Notice::EventResult(EventResult {
id, id,
msg: "".into(),
status: EventResultStatus::Saved, status: EventResultStatus::Saved,
}) })
} }
pub fn duplicate(id: String) -> Notice {
Notice::EventResult(EventResult {
id,
status: EventResultStatus::Duplicate,
})
}
pub fn err_msg(msg: String, id: String) -> Notice {
Notice::EventResult(EventResult {
id,
status: EventResultStatus::Error(msg),
})
}
} }
+3 -51
View File
@@ -20,7 +20,7 @@ pragma mmap_size = 536870912; -- 512MB of mmap
"##; "##;
/// Latest database version /// Latest database version
pub const DB_VERSION: usize = 9; pub const DB_VERSION: usize = 7;
/// Schema definition /// Schema definition
const INIT_SQL: &str = formatcp!( const INIT_SQL: &str = formatcp!(
@@ -48,10 +48,10 @@ content TEXT NOT NULL -- serialized json of event object
-- Event Indexes -- Event Indexes
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash); CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
CREATE INDEX IF NOT EXISTS author_index ON event(author);
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at); CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
CREATE INDEX IF NOT EXISTS author_index ON event(author);
CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by); CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at); CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
-- Tag Table -- Tag Table
-- Tag values are stored as either a BLOB (if they come in as a -- Tag values are stored as either a BLOB (if they come in as a
@@ -157,13 +157,6 @@ pub fn upgrade_db(conn: &mut PooledConnection) -> Result<()> {
if curr_version == 6 { if curr_version == 6 {
curr_version = mig_6_to_7(conn)?; curr_version = mig_6_to_7(conn)?;
} }
if curr_version == 7 {
curr_version = mig_7_to_8(conn)?;
}
if curr_version == 8 {
curr_version = mig_8_to_9(conn)?;
}
if curr_version == DB_VERSION { if curr_version == DB_VERSION {
info!( info!(
"All migration scripts completed successfully. Welcome to v{}.", "All migration scripts completed successfully. Welcome to v{}.",
@@ -380,44 +373,3 @@ PRAGMA user_version = 7;
} }
Ok(7) Ok(7)
} }
fn mig_7_to_8(conn: &mut PooledConnection) -> Result<usize> {
info!("database schema needs update from 7->8");
// Remove redundant indexes, and add a better multi-column index.
let upgrade_sql = r##"
DROP INDEX IF EXISTS created_at_index;
DROP INDEX IF EXISTS kind_index;
CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at);
PRAGMA user_version = 8;
"##;
match conn.execute_batch(upgrade_sql) {
Ok(()) => {
info!("database schema upgraded v7 -> v8");
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be upgraded");
}
}
Ok(8)
}
fn mig_8_to_9(conn: &mut PooledConnection) -> Result<usize> {
info!("database schema needs update from 8->9");
// Those old indexes were actually helpful...
let upgrade_sql = r##"
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
CREATE INDEX IF NOT EXISTS event_composite_index ON event(kind,created_at);
PRAGMA user_version = 8;
"##;
match conn.execute_batch(upgrade_sql) {
Ok(()) => {
info!("database schema upgraded v8 -> v9");
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be upgraded");
}
}
Ok(8)
}
+11 -8
View File
@@ -10,7 +10,7 @@ use crate::event::Event;
use crate::event::EventCmd; use crate::event::EventCmd;
use crate::info::RelayInfo; use crate::info::RelayInfo;
use crate::nip05; use crate::nip05;
use crate::notice::Notice; use crate::notice::{EventResultStatus, Notice};
use crate::subscription::Subscription; use crate::subscription::Subscription;
use futures::SinkExt; use futures::SinkExt;
use futures::StreamExt; use futures::StreamExt;
@@ -409,7 +409,11 @@ fn convert_to_msg(msg: String, max_bytes: Option<usize>) -> Result<NostrMessage>
fn make_notice_message(notice: Notice) -> Message { fn make_notice_message(notice: Notice) -> Message {
let json = match notice { let json = match notice {
Notice::Message(ref msg) => json!(["NOTICE", msg]), Notice::Message(ref msg) => json!(["NOTICE", msg]),
Notice::EventResult(ref res) => json!(["OK", res.id, res.status.to_bool(), res.msg]), Notice::EventResult(ref res) => match &res.status {
EventResultStatus::Saved => json!(["OK", res.id, "true"]),
EventResultStatus::Duplicate => json!(["OK", res.id, "true", "duplicate"]),
EventResultStatus::Error(msg) => json!(["OK", res.id, "false", msg]),
},
}; };
Message::text(json.to_string()) Message::text(json.to_string())
@@ -572,7 +576,6 @@ async fn nostr_server(
Ok(NostrMessage::EventMsg(ec)) => { Ok(NostrMessage::EventMsg(ec)) => {
// An EventCmd needs to be validated to be converted into an Event // An EventCmd needs to be validated to be converted into an Event
// handle each type of message // handle each type of message
let evid = ec.event_id().to_owned();
let parsed : Result<Event> = Result::<Event>::from(ec); let parsed : Result<Event> = Result::<Event>::from(ec);
match parsed { match parsed {
Ok(e) => { Ok(e) => {
@@ -587,15 +590,15 @@ async fn nostr_server(
} else { } else {
info!("client: {} sent a far future-dated event", cid); info!("client: {} sent a far future-dated event", cid);
if let Some(fut_sec) = settings.options.reject_future_seconds { if let Some(fut_sec) = settings.options.reject_future_seconds {
let msg = format!("The event created_at field is out of the acceptable range (+{}sec) for this relay.",fut_sec); let msg = format!("The event created_at field is out of the acceptable range (+{}sec) for this relay and was not stored.",fut_sec);
let notice = Notice::invalid(e.id, &msg); let notice = Notice::err_msg(msg, e.id);
ws_stream.send(make_notice_message(notice)).await.ok(); ws_stream.send(make_notice_message(notice)).await.ok();
} }
} }
}, },
Err(e) => { Err(_) => {
info!("client: {} sent an invalid event", cid); info!("client: {} sent an invalid event", cid);
ws_stream.send(make_notice_message(Notice::invalid(evid, &format!("{}", e)))).await.ok(); ws_stream.send(make_notice_message(Notice::message("event was invalid".into()))).await.ok();
} }
} }
}, },
@@ -617,7 +620,7 @@ async fn nostr_server(
}, },
Err(e) => { Err(e) => {
info!("Subscription error: {}", e); info!("Subscription error: {}", e);
ws_stream.send(make_notice_message(Notice::message(format!("Subscription error: {}", e)))).await.ok(); ws_stream.send(make_notice_message(Notice::err(e, s.id))).await.ok();
} }
} }
}, },