Skip to content
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

NOM-04: Relay-published indexes #22

Merged
merged 4 commits into from
Nov 12, 2023
Merged
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
5 changes: 5 additions & 0 deletions example.nomen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ data = "nomen.db"
[nostr]
relays = ["wss://relay.damus.io"]

# Per NOM-04 spec: Publish indexes to relays
secret = "nsec1..."
publish = true
well-known = true

[server]
bind = "0.0.0.0:8080"
without_explorer = false
Expand Down
39 changes: 39 additions & 0 deletions nomen/src/config/cfg.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::path::PathBuf;

use anyhow::bail;
use bitcoin::Network;
use nostr_sdk::{
prelude::{FromSkStr, ToBech32},
Options,
};
use sqlx::{sqlite, SqlitePool};

use crate::util::Nsec;

use super::{Cli, ConfigFile};

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -70,6 +73,19 @@ impl Config {
Ok((keys, client))
}

pub async fn nostr_keys_client(
&self,
keys: &nostr_sdk::Keys,
) -> anyhow::Result<nostr_sdk::Client> {
let client = nostr_sdk::Client::with_opts(keys, Options::new().wait_for_send(true));
let relays = self.relays();
for relay in relays {
client.add_relay(relay, None).await?;
}
client.connect().await;
Ok(client)
}

pub async fn nostr_random_client(
&self,
) -> anyhow::Result<(nostr_sdk::Keys, nostr_sdk::Client)> {
Expand All @@ -86,6 +102,29 @@ impl Config {
}
}

pub fn validate(&self) -> anyhow::Result<()> {
if self.missing_secret_key() {
bail!("Config: Secret key required for relay publising");
}
Ok(())
}

fn missing_secret_key(&self) -> bool {
(self.publish_index() || self.well_known()) && self.file.nostr.secret.is_none()
}

pub fn publish_index(&self) -> bool {
self.file.nostr.publish.unwrap_or_default()
}

pub fn well_known(&self) -> bool {
self.file.nostr.well_known.unwrap_or_default()
}

pub fn secret_key(&self) -> Option<Nsec> {
self.file.nostr.secret
}

fn rpc_cookie(&self) -> Option<PathBuf> {
self.file.rpc.cookie.clone()
}
Expand Down
12 changes: 12 additions & 0 deletions nomen/src/config/config_file.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::path::PathBuf;

use bitcoin::Network;
use nostr_sdk::Keys;
use serde::{Deserialize, Serialize};

use crate::util::Nsec;

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ServerConfig {
pub bind: Option<String>,
Expand Down Expand Up @@ -50,11 +53,20 @@ impl RpcConfig {
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct NostrConfig {
pub relays: Option<Vec<String>>,
pub secret: Option<Nsec>,
pub publish: Option<bool>,
pub well_known: Option<bool>,
}
impl NostrConfig {
fn example() -> NostrConfig {
NostrConfig {
relays: Some(vec!["wss://relay.damus.io".into()]),
secret: Keys::generate()
.secret_key()
.ok()
.map(std::convert::Into::into),
publish: Some(true),
well_known: Some(true),
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion nomen/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ use sqlx::{Sqlite, SqlitePool};
mod index;
mod name;
mod raw;
pub mod relay_index;
pub mod stats;

static MIGRATIONS: [&str; 14] = [
static MIGRATIONS: [&str; 15] = [
"CREATE TABLE event_log (id INTEGER PRIMARY KEY, created_at, type, data);",
"CREATE TABLE index_height (blockheight INTEGER PRIMARY KEY, blockhash);",
"CREATE TABLE raw_blockchain (id INTEGER PRIMARY KEY, blockhash, txid, blocktime, blockheight, txheight, vout, data, indexed_at);",
Expand Down Expand Up @@ -49,6 +50,7 @@ static MIGRATIONS: [&str; 14] = [
"CREATE TABLE name_events (name, fingerprint, nsid, pubkey, created_at, event_id, records, indexed_at, raw_event);",
"CREATE UNIQUE INDEX name_events_unique_idx ON name_events(name, pubkey);",
"CREATE INDEX name_events_created_at_idx ON name_events(created_at);",
"CREATE TABLE relay_index_queue (name);"
];

pub async fn initialize(config: &Config) -> anyhow::Result<SqlitePool> {
Expand Down
39 changes: 39 additions & 0 deletions nomen/src/db/relay_index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use sqlx::Sqlite;

pub async fn queue(
conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy,
name: &str,
) -> anyhow::Result<()> {
sqlx::query("INSERT INTO relay_index_queue (name) VALUES (?)")
.bind(name)
.execute(conn)
.await?;
Ok(())
}

#[derive(sqlx::FromRow, Debug)]
pub struct Name {
pub name: String,
pub pubkey: String,
pub records: String,
}

pub async fn fetch_all(
conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy,
) -> anyhow::Result<Vec<Name>> {
let results = sqlx::query_as::<_, Name>(
"SELECT vnr.name, vnr.pubkey, COALESCE(vnr.records, '{}') as records
FROM valid_names_records_vw vnr
JOIN relay_index_queue riq ON vnr.name = riq.name;",
)
.fetch_all(conn)
.await?;
Ok(results)
}

pub async fn clear(conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy) -> anyhow::Result<()> {
sqlx::query("DELETE FROM relay_index_queue;")
.execute(conn)
.await?;
Ok(())
}
1 change: 1 addition & 0 deletions nomen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fn parse_config() -> anyhow::Result<Config> {
};

let config = Config::new(cli, file);
config.validate()?;

tracing::debug!("Config loaded: {config:?}");

Expand Down
1 change: 1 addition & 0 deletions nomen/src/subcommands/index/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ async fn index_output(conn: &SqlitePool, index: BlockchainIndex) -> anyhow::Resu
}
}
}
db::relay_index::queue(conn, name).await?;
}
} else {
db::insert_blockchain_index(conn, &index).await?;
Expand Down
1 change: 1 addition & 0 deletions nomen/src/subcommands/index/events/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod event_data;
mod records;
pub mod relay_index;

pub use event_data::*;
pub use records::*;
6 changes: 4 additions & 2 deletions nomen/src/subcommands/index/events/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,19 @@ async fn save_event(pool: &SqlitePool, ed: EventData) -> anyhow::Result<()> {

db::update_v0_index(pool, name.as_ref(), &pubkey, calculated_nsid).await?;

db::relay_index::queue(pool, name.as_ref()).await?;

Ok(())
}

async fn latest_events(
config: &Config,
pool: &sqlx::Pool<sqlx::Sqlite>,
) -> anyhow::Result<Vec<Event>> {
let index_height = db::last_records_time(pool).await?;
let records_time = db::last_records_time(pool).await? + 1;
let filter = Filter::new()
.kind(NameKind::Name.into())
.since(index_height.into());
.since(records_time.into());

let (_keys, client) = config.nostr_random_client().await?;
let events = client
Expand Down
65 changes: 65 additions & 0 deletions nomen/src/subcommands/index/events/relay_index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::collections::HashMap;

use nostr_sdk::{EventBuilder, Keys, Tag};
use secp256k1::SecretKey;
use serde::Serialize;
use sqlx::SqlitePool;

use crate::{
config::Config,
db::{self, relay_index::Name},
};

pub async fn publish(config: &Config, pool: &SqlitePool) -> anyhow::Result<()> {
if !config.publish_index() {
return Ok(());
}
let sk: SecretKey = config
.secret_key()
.expect("Missing config validation for secret")
.into();
let keys = Keys::new(sk);
let client = config.nostr_keys_client(&keys).await?;
tracing::info!("Publishing relay index.");
let names = db::relay_index::fetch_all(pool).await?;

send_event(names, keys, &client).await?;

db::relay_index::clear(pool).await?;
client.disconnect().await.ok();
Ok(())
}

async fn send_event(
names: Vec<Name>,
keys: Keys,
client: &nostr_sdk::Client,
) -> Result<(), anyhow::Error> {
for name in names {
let records: HashMap<String, String> = serde_json::from_str(&name.records)?;
let content = Content {
name: name.name.clone(),
pubkey: name.pubkey,
records,
};
let content_serialize = serde_json::to_string(&content)?;
let event = EventBuilder::new(
nostr_sdk::Kind::ParameterizedReplaceable(38301),
content_serialize,
&[Tag::Identifier(name.name.clone())],
)
.to_event(&keys)?;

if client.send_event(event).await.is_err() {
tracing::error!("Unable to broadcast event during relay index publish");
}
}
Ok(())
}

#[derive(Serialize)]
struct Content {
name: String,
pubkey: String,
records: HashMap<String, String>,
}
1 change: 1 addition & 0 deletions nomen/src/subcommands/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub async fn index(config: &Config) -> anyhow::Result<()> {
let pool = config.sqlite().await?;
blockchain::index(config, &pool).await?;
events::records(config, &pool).await?;
events::relay_index::publish(config, &pool).await?;

db::save_event(&pool, "index", "").await?;
Ok(())
Expand Down
44 changes: 34 additions & 10 deletions nomen/src/subcommands/server/explorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use serde::Deserialize;
use crate::{
db::{self, NameDetails},
subcommands::util::{extend_psbt, name_event},
util::{format_time, KeyVal, Pubkey},
util::{format_time, KeyVal, Npub},
};

use super::{AppState, WebError};
Expand Down Expand Up @@ -129,7 +129,7 @@ pub struct NewNameTemplate {
pub struct NewNameForm {
upgrade: bool,
name: String,
pubkey: Pubkey,
pubkey: Npub,
psbt: String,
}

Expand Down Expand Up @@ -197,7 +197,7 @@ pub struct NewRecordsTemplate {
#[derive(Deserialize)]
pub struct NewRecordsQuery {
name: Option<String>,
pubkey: Option<Pubkey>,
pubkey: Option<Npub>,
}

pub async fn new_records_form(
Expand Down Expand Up @@ -240,7 +240,7 @@ async fn records_from_query(query: &NewRecordsQuery, state: &AppState) -> Result
pub struct NewRecordsForm {
records: String,
name: String,
pubkey: Pubkey,
pubkey: Npub,
}

#[allow(clippy::unused_async)]
Expand Down Expand Up @@ -297,7 +297,7 @@ pub mod transfer {

use crate::{
subcommands::{AppState, WebError},
util::Pubkey,
util::Npub,
};

#[derive(askama::Template)]
Expand All @@ -307,8 +307,8 @@ pub mod transfer {
#[derive(Deserialize)]
pub struct InitiateTransferForm {
name: String,
pubkey: Pubkey,
old_pubkey: Pubkey,
pubkey: Npub,
old_pubkey: Npub,
}

#[allow(clippy::unused_async)]
Expand All @@ -320,8 +320,8 @@ pub mod transfer {
#[template(path = "transfer/sign.html")]
pub struct SignEventTemplate {
name: String,
pubkey: Pubkey,
old_pubkey: Pubkey,
pubkey: Npub,
old_pubkey: Npub,
event: String,
}

Expand All @@ -346,7 +346,7 @@ pub mod transfer {
#[derive(Deserialize)]
pub struct FinalTransferForm {
name: String,
pubkey: Pubkey,
pubkey: Npub,
sig: Signature,
}

Expand Down Expand Up @@ -376,3 +376,27 @@ pub mod transfer {
})
}
}

pub mod well_known {
use axum::{extract::State, Json};
use nostr_sdk::Keys;

use crate::subcommands::{AppState, WebError};

#[allow(clippy::unused_async)]
pub async fn nomen(
State(state): State<AppState>,
) -> anyhow::Result<Json<serde_json::Value>, WebError> {
let sk = state.config.secret_key().ok_or(anyhow::anyhow!(
"Config: secret key required for .well-known"
))?;
let pk = Keys::new(*sk.as_ref()).public_key();
let result = serde_json::json!({
"indexer": {
"pubkey": pk.to_string()
}
});

Ok(Json(result))
}
}
Loading