Skip to content

Gossip persistent storage #802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 124 additions & 68 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ negentropy-deprecated = { package = "negentropy", version = "0.3", default-featu
nostr = { version = "0.39", path = "./crates/nostr", default-features = false }
nostr-connect = { version = "0.39", path = "./crates/nostr-connect", default-features = false }
nostr-database = { version = "0.39", path = "./crates/nostr-database", default-features = false }
nostr-gossip = { version = "0.39", path = "./crates/nostr-gossip", default-features = false }
nostr-indexeddb = { version = "0.39", path = "./crates/nostr-indexeddb", default-features = false }
nostr-lmdb = { version = "0.39", path = "./crates/nostr-lmdb", default-features = false }
nostr-ndb = { version = "0.39", path = "./crates/nostr-ndb", default-features = false }
Expand All @@ -46,6 +47,7 @@ web-sys = { version = "0.3", default-features = false }
[patch.crates-io]
# Patch needed to reduce bindings size
bip39 = { git = "https://github.com/yukibtc/rust-bip39", rev = "eade7c56eff5f320e8eb5beee23dd8fb46413938" } # Uses bitcoin_hashes v0.14
rusqlite = { git = "https://github.com/yukibtc/rusqlite", rev = "846aeb3e4088b11d8804bd3261464ba84352a2f6" } # rusqlite v0.32 with WASM support

[profile.release]
lto = true
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The project is split up into several crates in the `crates/` directory:
* [**nostr-lmdb**](./crates/nostr-lmdb): LMDB storage backend
* [**nostr-ndb**](./crates/nostr-ndb): [nostrdb](https://github.com/damus-io/nostrdb) storage backend
* [**nostr-indexeddb**](./crates/nostr-indexeddb): IndexedDB storage backend
* [**nostr-gossip**](./crates/nostr-gossip): Gossip tracker and storage
* [**nostr-relay-pool**](./crates/nostr-relay-pool): Nostr Relay Pool
* [**nostr-sdk**](./crates/nostr-sdk): High level client library
* [**nwc**](./crates/nwc): Nostr Wallet Connect (NWC) client
Expand Down
7 changes: 5 additions & 2 deletions bindings/nostr-sdk-js/.cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ opt-level = 'z' # Optimize for size.
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations.
panic = "abort" # Abort on panic
strip = true # Strip symbols from binary
strip = "debuginfo" # Strip debug symbols from binary. Full strip will cause issues with SQLite

[build]
target = "wasm32-unknown-unknown"
rustflags = ["-C", "panic=abort"]
rustflags = [
"-Cpanic=abort",
"-Ctarget-feature=+bulk-memory" # Required for SQLite WASM
]

[unstable]
unstable-options = true
Expand Down
4 changes: 2 additions & 2 deletions bindings/nostr-sdk-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ crate-type = ["cdylib"]
console_error_panic_hook = "0.1"
js-sys.workspace = true
nostr-connect.workspace = true
nostr-sdk = { workspace = true, default-features = false, features = ["all-nips", "indexeddb"] }
nostr-sdk = { workspace = true, default-features = false, features = ["all-nips", "indexeddb", "gossip"] }
nwc.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
wasm-bindgen = { workspace = true, features = ["std"] }
wasm-bindgen-futures.workspace = true

[package.metadata.wasm-pack.profile.profiling]
wasm-opt = true
wasm-opt = false # Optimizations causes issues to SQLite gossip storage

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] }
31 changes: 31 additions & 0 deletions bindings/nostr-sdk-js/examples/gossip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { Keys, Client, NostrSigner, PublicKey, EventBuilder, loadWasmAsync, initLogger, Gossip, LogLevel, Tag } = require("../");

async function main() {
await loadWasmAsync();

initLogger(LogLevel.info());

let keys = Keys.parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85");
let signer = NostrSigner.keys(keys);

let gossip = Gossip.inMemory()

let client = Client.builder().signer(signer).gossip(gossip).build();

await client.addDiscoveryRelay("wss://relay.damus.io");
await client.addDiscoveryRelay("wss://purplepag.es");

await client.connect();

let pk = PublicKey.parse("npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet");

let builder = EventBuilder.textNote(
"Hello world nostr:npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet",
).tags([Tag.publicKey(pk)]);
let output = await client.sendEventBuilder(builder);
console.log("Event ID", output.id.toBech32());
console.log("Successfully sent to:", output.success);
console.log("Failed to sent to:", output.failed);
}

main();
5 changes: 5 additions & 0 deletions bindings/nostr-sdk-js/src/client/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use wasm_bindgen::prelude::*;
use super::options::JsOptions;
use super::{JsClient, JsNostrSigner};
use crate::database::JsNostrDatabase;
use crate::gossip::JsGossip;
use crate::policy::{FFI2RustAdmitPolicy, JsAdmitPolicy};

#[wasm_bindgen(js_name = ClientBuilder)]
Expand Down Expand Up @@ -41,6 +42,10 @@ impl JsClientBuilder {
self.inner.database(database.deref().clone()).into()
}

pub fn gossip(self, gossip: &JsGossip) -> Self {
self.inner.gossip(gossip.deref().clone()).into()
}

#[wasm_bindgen(js_name = admitPolicy)]
pub fn admit_policy(self, policy: JsAdmitPolicy) -> Self {
self.inner
Expand Down
5 changes: 0 additions & 5 deletions bindings/nostr-sdk-js/src/client/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ impl JsOptions {
self.inner.automatic_authentication(enabled).into()
}

/// Enable gossip model (default: false)
pub fn gossip(self, enable: bool) -> Self {
self.inner.gossip(enable).into()
}

/// Set custom relay limits
#[wasm_bindgen(js_name = relayLimits)]
pub fn relay_limits(self, limits: &JsRelayLimits) -> Self {
Expand Down
31 changes: 31 additions & 0 deletions bindings/nostr-sdk-js/src/gossip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2025 Rust Nostr Developers
// Distributed under the MIT software license

use std::ops::Deref;

use nostr_sdk::prelude::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(js_name = Gossip)]
pub struct JsGossip {
inner: Gossip,
}

impl Deref for JsGossip {
type Target = Gossip;

fn deref(&self) -> &Self::Target {
&self.inner
}
}

#[wasm_bindgen(js_class = Gossip)]
impl JsGossip {
#[wasm_bindgen(js_name = inMemory)]
pub fn in_memory() -> Self {
Self {
inner: Gossip::in_memory(),
}
}
}
1 change: 1 addition & 0 deletions bindings/nostr-sdk-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod connect;
pub mod database;
pub mod duration;
pub mod error;
pub mod gossip;
pub mod logger;
pub mod nwc;
pub mod policy;
Expand Down
2 changes: 2 additions & 0 deletions contrib/scripts/check-crates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ buildargs=(
"-p nostr-relay-builder"
"-p nostr-connect"
"-p nwc"
"-p nostr-gossip"
"-p nostr-sdk" # No default features
"-p nostr-sdk --features all-nips" # Only NIPs features
"-p nostr-sdk --features gossip" # Gossip stuff
"-p nostr-sdk --features tor" # Embedded tor client
"-p nostr-sdk --all-features" # All features
"-p nostr-cli"
Expand Down
1 change: 1 addition & 0 deletions contrib/scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ args=(
"-p nostr-relay-pool"
"-p nwc"
"-p nostr-connect"
"-p nostr-gossip"
"-p nostr-sdk"
"-p nostr-cli"
)
Expand Down
28 changes: 28 additions & 0 deletions crates/nostr-gossip/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "nostr-gossip"
version = "0.39.0"
edition = "2021"
description = "Gossip storage for nostr apps"
authors.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
readme = "README.md"
rust-version.workspace = true
keywords = ["nostr", "gossip"]

[dependencies]
async-utility.workspace = true
lru.workspace = true
nostr = { workspace = true, features = ["std"] }
tracing = { workspace = true, features = ["std"] }

[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }

[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
rusqlite = { version = "0.32", features = ["precompiled-wasm"] }
tokio = { workspace = true, features = ["sync"] }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
13 changes: 13 additions & 0 deletions crates/nostr-gossip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Nostr Gossip

## State

**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways.

## Donations

`rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate).

## License

This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details
51 changes: 51 additions & 0 deletions crates/nostr-gossip/migrations/001_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- Database settings
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA user_version = 1; -- Schema version

CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_key BLOB NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS relays (
id INTEGER PRIMARY KEY AUTOINCREMENT,
relay_url TEXT NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pkid INTEGER NOT NULL, -- Public Key ID
kind INTEGER NOT NULL, -- Kind of the list (i.e., 10002 for NIP65, 10050 for NIP17)
created_at INTEGER NOT NULL DEFAULT 0, -- Event list created at (`created_at` field of event)
last_check INTEGER NOT NULL DEFAULT 0, -- The timestamp of the last check
FOREIGN KEY(pkid) REFERENCES users(id) ON DELETE CASCADE ON UPDATE NO ACTION
);

CREATE UNIQUE INDEX IF NOT EXISTS pubkey_list_idx ON lists(pkid,kind);

CREATE TABLE IF NOT EXISTS relays_by_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pkid INTEGER NOT NULL, -- Public Key ID
listid INTEGER NOT NULL, -- List ID
relayid INTEGER NOT NULL, -- Relay ID
metadata TEXT DEFAULT NULL, -- NIP65 metadata: read, write or NULL
FOREIGN KEY(pkid) REFERENCES users(id) ON DELETE CASCADE ON UPDATE NO ACTION,
FOREIGN KEY(listid) REFERENCES lists(id) ON DELETE CASCADE ON UPDATE NO ACTION,
FOREIGN KEY(relayid) REFERENCES relays(id)
);

CREATE UNIQUE INDEX IF NOT EXISTS pubkey_list_relay_idx ON relays_by_list(pkid,listid,relayid);

-- CREATE TABLE IF NOT EXISTS tracker (
-- id INTEGER PRIMARY KEY AUTOINCREMENT,
-- pkid INTEGER NOT NULL, -- Public Key ID
-- relayid INTEGER NOT NULL, -- Relay ID
-- last_event INTEGER NOT NULL DEFAULT 0, -- Timestamp of the last event seen for the public key on the relay
-- score INTEGER NOT NULL DEFAULT 5, -- Score
-- FOREIGN KEY(pkid) REFERENCES users(id) ON DELETE CASCADE ON UPDATE NO ACTION,
-- FOREIGN KEY(relayid) REFERENCES relays(id)
-- );
--
-- CREATE UNIQUE INDEX IF NOT EXISTS pubkey_relay_idx ON tracker(pkid,relayid);
13 changes: 13 additions & 0 deletions crates/nostr-gossip/src/constant.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2025 Rust Nostr Developers
// Distributed under the MIT software license

use std::num::NonZeroUsize;
use std::time::Duration;

/// Max number of relays allowed in NIP17/NIP65 lists
pub(crate) const MAX_RELAYS_LIST: usize = 5;
pub(crate) const PUBKEY_METADATA_OUTDATED_AFTER: Duration = Duration::from_secs(60 * 60); // 60 min
pub(crate) const CHECK_OUTDATED_INTERVAL: Duration = Duration::from_secs(60 * 5); // 5 min

pub(crate) const CACHE_SIZE: Option<NonZeroUsize> = NonZeroUsize::new(10_000);
Loading
Loading