commit 72f8a1aa5c2c1e7ffba2cd897f5c992febc44674
parent 274c61bb723bcd305ed6d3486e6e645c9bbd0d0f
Author: Greg Heartsfield <scsibug@imap.cc>
Date: Sun, 16 Oct 2022 15:25:06 -0500
feat(NIP-26): allow searches for delegated public keys
Implements core NIP-26 delegated event functionality. Events can
include a `delegation` tag that provides a signature and restrictions
on which events can be delegated.
Notable points on the implementation so far:
* Schema has been upgraded to include an index and new column.
* Basic rune parsing/evaluation to implement the example event in the
NIP, but no more.
* No special logic for deletion.
* No migration logic for determining delegated authors for
already-stored events.
Diffstat:
11 files changed, 568 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1042,6 +1042,7 @@ dependencies = [
"r2d2",
"r2d2_sqlite",
"rand 0.8.5",
+ "regex",
"rusqlite",
"secp256k1",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
@@ -32,6 +32,7 @@ http = { version = "0.2" }
parse_duration = "2"
rand = "0.8"
const_format = "0.2.28"
+regex = "1"
[dev-dependencies]
anyhow = "1"
diff --git a/README.md b/README.md
@@ -27,6 +27,7 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
- [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-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)
## Quick Start
diff --git a/src/db.rs b/src/db.rs
@@ -142,12 +142,15 @@ pub async fn db_writer(
if next_event.is_none() {
break;
}
+ // track if an event write occurred; this is used to
+ // update the rate limiter
let mut event_write = false;
let subm_event = next_event.unwrap();
let event = subm_event.event;
let notice_tx = subm_event.notice_tx;
// check if this event is authorized.
if let Some(allowed_addrs) = whitelist {
+ // TODO: incorporate delegated pubkeys
// if the event address is not in allowed_addrs.
if !allowed_addrs.contains(&event.pubkey) {
info!(
@@ -284,12 +287,13 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
let tx = conn.transaction()?;
// get relevant fields from event and convert to blobs.
let id_blob = hex::decode(&e.id).ok();
- let pubkey_blob = hex::decode(&e.pubkey).ok();
+ let pubkey_blob: Option<Vec<u8>> = hex::decode(&e.pubkey).ok();
+ let delegator_blob: Option<Vec<u8>> = e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok());
let event_str = serde_json::to_string(&e).ok();
// ignore if the event hash is a duplicate.
let mut ins_count = tx.execute(
- "INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, strftime('%s','now'), FALSE);",
- params![id_blob, e.created_at, e.kind, pubkey_blob, event_str]
+ "INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);",
+ params![id_blob, e.created_at, e.kind, pubkey_blob, delegator_blob, event_str]
)?;
if ins_count == 0 {
// if the event was a duplicate, no need to insert event or
@@ -439,16 +443,22 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
for auth in authvec {
match hex_range(auth) {
Some(HexSearch::Exact(ex)) => {
- auth_searches.push("author=?".to_owned());
+ auth_searches.push("author=? OR delegated_by=?".to_owned());
+ params.push(Box::new(ex.clone()));
params.push(Box::new(ex));
}
Some(HexSearch::Range(lower, upper)) => {
- auth_searches.push("(author>? AND author<?)".to_owned());
+ auth_searches.push(
+ "(author>? AND author<?) OR (delegated_by>? AND delegated_by<?)".to_owned(),
+ );
+ params.push(Box::new(lower.clone()));
+ params.push(Box::new(upper.clone()));
params.push(Box::new(lower));
params.push(Box::new(upper));
}
Some(HexSearch::LowerOnly(lower)) => {
- auth_searches.push("author>?".to_owned());
+ auth_searches.push("author>? OR delegated_by>?".to_owned());
+ params.push(Box::new(lower.clone()));
params.push(Box::new(lower));
}
None => {
diff --git a/src/delegation.rs b/src/delegation.rs
@@ -0,0 +1,416 @@
+//! Event parsing and validation
+use crate::error::Error;
+use crate::error::Result;
+use crate::event::Event;
+use bitcoin_hashes::{sha256, Hash};
+use lazy_static::lazy_static;
+use regex::Regex;
+use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey};
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
+use tracing::{debug, info};
+
+// This handles everything related to delegation, in particular the
+// condition/rune parsing and logic.
+
+// Conditions are poorly specified, so we will implement the minimum
+// necessary for now.
+
+// fields MUST be either "kind" or "created_at".
+// operators supported are ">", "<", "=", "!".
+// no operations on 'content' are supported.
+
+// this allows constraints for:
+// valid date ranges (valid from X->Y dates).
+// specific kinds (publish kind=1,5)
+// kind ranges (publish ephemeral events, kind>19999&kind<30001)
+
+// for more complex scenarios (allow delegatee to publish ephemeral
+// AND replacement events), it may be necessary to generate and use
+// different condition strings, since we do not support grouping or
+// "OR" logic.
+
+lazy_static! {
+ /// Secp256k1 verification instance.
+ pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
+pub enum Field {
+ Kind,
+ CreatedAt,
+}
+
+impl FromStr for Field {
+ type Err = Error;
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ if value == "kind" {
+ Ok(Field::Kind)
+ } else if value == "created_at" {
+ Ok(Field::CreatedAt)
+ } else {
+ Err(Error::DelegationParseError)
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
+pub enum Operator {
+ LessThan,
+ GreaterThan,
+ Equals,
+ NotEquals,
+}
+impl FromStr for Operator {
+ type Err = Error;
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ if value == "<" {
+ Ok(Operator::LessThan)
+ } else if value == ">" {
+ Ok(Operator::GreaterThan)
+ } else if value == "=" {
+ Ok(Operator::Equals)
+ } else if value == "!" {
+ Ok(Operator::NotEquals)
+ } else {
+ Err(Error::DelegationParseError)
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
+pub struct ConditionQuery {
+ pub(crate) conditions: Vec<Condition>,
+}
+
+impl ConditionQuery {
+ pub fn allows_event(&self, event: &Event) -> bool {
+ // check each condition, to ensure that the event complies
+ // with the restriction.
+ for c in &self.conditions {
+ if !c.allows_event(event) {
+ // any failing conditions invalidates the delegation
+ // on this event
+ return false;
+ }
+ }
+ // delegation was permitted unconditionally, or all conditions
+ // were true
+ true
+ }
+}
+
+// Verify that the delegator approved the delegation; return a ConditionQuery if so.
+pub fn validate_delegation(
+ delegator: &str,
+ delegatee: &str,
+ cond_query: &str,
+ sigstr: &str,
+) -> Option<ConditionQuery> {
+ // form the token
+ let tok = format!("nostr:delegation:{}:{}", delegatee, cond_query);
+ // form SHA256 hash
+ let digest: sha256::Hash = sha256::Hash::hash(tok.as_bytes());
+ let sig = schnorr::Signature::from_str(sigstr).unwrap();
+ if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
+ if let Ok(pubkey) = XOnlyPublicKey::from_str(delegator) {
+ let verify = SECP.verify_schnorr(&sig, &msg, &pubkey);
+ if verify.is_ok() {
+ // return the parsed condition query
+ cond_query.parse::<ConditionQuery>().ok()
+ } else {
+ debug!("client sent an delegation signature that did not validate");
+ None
+ }
+ } else {
+ debug!("client sent malformed delegation pubkey");
+ None
+ }
+ } else {
+ info!("error converting delegation digest to secp256k1 message");
+ None
+ }
+}
+
+/// Parsed delegation condition
+/// see https://github.com/nostr-protocol/nips/pull/28#pullrequestreview-1084903800
+/// An example complex condition would be: kind=1,2,3&created_at<1665265999
+#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
+pub struct Condition {
+ pub(crate) field: Field,
+ pub(crate) operator: Operator,
+ pub(crate) values: Vec<u64>,
+}
+
+impl Condition {
+ /// Check if this condition allows the given event to be delegated
+ pub fn allows_event(&self, event: &Event) -> bool {
+ // determine what the right-hand side of the operator is
+ let resolved_field = match &self.field {
+ Field::Kind => event.kind,
+ Field::CreatedAt => event.created_at,
+ };
+ match &self.operator {
+ Operator::LessThan => {
+ // the less-than operator is only valid for single values.
+ if self.values.len() == 1 {
+ if let Some(v) = self.values.first() {
+ return resolved_field < *v;
+ }
+ }
+ }
+ Operator::GreaterThan => {
+ // the greater-than operator is only valid for single values.
+ if self.values.len() == 1 {
+ if let Some(v) = self.values.first() {
+ return resolved_field > *v;
+ }
+ }
+ }
+ Operator::Equals => {
+ // equals is interpreted as "must be equal to at least one provided value"
+ return self.values.iter().any(|&x| resolved_field == x);
+ }
+ Operator::NotEquals => {
+ // not-equals is interpreted as "must not be equal to any provided value"
+ // this is the one case where an empty list of values could be allowed; even though it is a pointless restriction.
+ return self.values.iter().all(|&x| resolved_field != x);
+ }
+ }
+ false
+ }
+}
+
+fn str_to_condition(cs: &str) -> Option<Condition> {
+ // a condition is a string (alphanum+underscore), an operator (<>=!), and values (num+comma)
+ lazy_static! {
+ static ref RE: Regex = Regex::new("([[:word:]]+)([<>=!]+)([,[[:digit:]]]*)").unwrap();
+ }
+ // match against the regex
+ let caps = RE.captures(cs)?;
+ let field = caps.get(1)?.as_str().parse::<Field>().ok()?;
+ let operator = caps.get(2)?.as_str().parse::<Operator>().ok()?;
+ // values are just comma separated numbers, but all must be parsed
+ let rawvals = caps.get(3)?.as_str();
+ let values = rawvals
+ .split_terminator(',')
+ .map(|n| n.parse::<u64>().ok())
+ .collect::<Option<Vec<_>>>()?;
+ // convert field string into Field
+ Some(Condition {
+ field,
+ operator,
+ values,
+ })
+}
+
+/// Parse a condition query from a string slice
+impl FromStr for ConditionQuery {
+ type Err = Error;
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ // split the string with '&'
+ let mut conditions = vec![];
+ let condstrs = value.split_terminator('&');
+ // parse each individual condition
+ for c in condstrs {
+ conditions.push(str_to_condition(c).ok_or(Error::DelegationParseError)?);
+ }
+ Ok(ConditionQuery { conditions })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // parse condition strings
+ #[test]
+ fn parse_empty() -> Result<()> {
+ // given an empty condition query, produce an empty vector
+ let empty_cq = ConditionQuery { conditions: vec![] };
+ let parsed = "".parse::<ConditionQuery>()?;
+ assert_eq!(parsed, empty_cq);
+ Ok(())
+ }
+
+ // parse field 'kind'
+ #[test]
+ fn test_kind_field_parse() -> Result<()> {
+ let field = "kind".parse::<Field>()?;
+ assert_eq!(field, Field::Kind);
+ Ok(())
+ }
+ // parse field 'created_at'
+ #[test]
+ fn test_created_at_field_parse() -> Result<()> {
+ let field = "created_at".parse::<Field>()?;
+ assert_eq!(field, Field::CreatedAt);
+ Ok(())
+ }
+ // parse unknown field
+ #[test]
+ fn unknown_field_parse() {
+ let field = "unk".parse::<Field>();
+ assert!(field.is_err());
+ }
+
+ // parse a full conditional query with an empty array
+ #[test]
+ fn parse_kind_equals_empty() -> Result<()> {
+ // given an empty condition query, produce an empty vector
+ let kind_cq = ConditionQuery {
+ conditions: vec![Condition {
+ field: Field::Kind,
+ operator: Operator::Equals,
+ values: vec![],
+ }],
+ };
+ let parsed = "kind=".parse::<ConditionQuery>()?;
+ assert_eq!(parsed, kind_cq);
+ Ok(())
+ }
+ // parse a full conditional query with a single value
+ #[test]
+ fn parse_kind_equals_singleval() -> Result<()> {
+ // given an empty condition query, produce an empty vector
+ let kind_cq = ConditionQuery {
+ conditions: vec![Condition {
+ field: Field::Kind,
+ operator: Operator::Equals,
+ values: vec![1],
+ }],
+ };
+ let parsed = "kind=1".parse::<ConditionQuery>()?;
+ assert_eq!(parsed, kind_cq);
+ Ok(())
+ }
+ // parse a full conditional query with multiple values
+ #[test]
+ fn parse_kind_equals_multival() -> Result<()> {
+ // given an empty condition query, produce an empty vector
+ let kind_cq = ConditionQuery {
+ conditions: vec![Condition {
+ field: Field::Kind,
+ operator: Operator::Equals,
+ values: vec![1, 2, 4],
+ }],
+ };
+ let parsed = "kind=1,2,4".parse::<ConditionQuery>()?;
+ assert_eq!(parsed, kind_cq);
+ Ok(())
+ }
+ // parse multiple conditions
+ #[test]
+ fn parse_multi_conditions() -> Result<()> {
+ // given an empty condition query, produce an empty vector
+ let cq = ConditionQuery {
+ conditions: vec![
+ Condition {
+ field: Field::Kind,
+ operator: Operator::GreaterThan,
+ values: vec![10000],
+ },
+ Condition {
+ field: Field::Kind,
+ operator: Operator::LessThan,
+ values: vec![20000],
+ },
+ Condition {
+ field: Field::Kind,
+ operator: Operator::NotEquals,
+ values: vec![10001],
+ },
+ Condition {
+ field: Field::CreatedAt,
+ operator: Operator::LessThan,
+ values: vec![1665867123],
+ },
+ ],
+ };
+ let parsed =
+ "kind>10000&kind<20000&kind!10001&created_at<1665867123".parse::<ConditionQuery>()?;
+ assert_eq!(parsed, cq);
+ Ok(())
+ }
+ fn simple_event() -> Event {
+ Event {
+ id: "0".to_owned(),
+ pubkey: "0".to_owned(),
+ delegated_by: None,
+ created_at: 0,
+ kind: 0,
+ tags: vec![],
+ content: "".to_owned(),
+ sig: "0".to_owned(),
+ tagidx: None,
+ }
+ }
+ // Check for condition logic on event w/ empty values
+ #[test]
+ fn condition_with_empty_values() {
+ let mut c = Condition {
+ field: Field::Kind,
+ operator: Operator::GreaterThan,
+ values: vec![],
+ };
+ let e = simple_event();
+ assert!(!c.allows_event(&e));
+ c.operator = Operator::LessThan;
+ assert!(!c.allows_event(&e));
+ c.operator = Operator::Equals;
+ assert!(!c.allows_event(&e));
+ // Not Equals applied to an empty list *is* allowed
+ // (pointless, but logically valid).
+ c.operator = Operator::NotEquals;
+ assert!(c.allows_event(&e));
+ }
+
+ // Check for condition logic on event w/ single value
+ #[test]
+ fn condition_kind_gt_event_single() {
+ let c = Condition {
+ field: Field::Kind,
+ operator: Operator::GreaterThan,
+ values: vec![10],
+ };
+ let mut e = simple_event();
+ // kind is not greater than 10, not allowed
+ e.kind = 1;
+ assert!(!c.allows_event(&e));
+ // kind is greater than 10, allowed
+ e.kind = 100;
+ assert!(c.allows_event(&e));
+ // kind is 10, not allowed
+ e.kind = 10;
+ assert!(!c.allows_event(&e));
+ }
+ // Check for condition logic on event w/ multi values
+ #[test]
+ fn condition_with_multi_values() {
+ let mut c = Condition {
+ field: Field::Kind,
+ operator: Operator::Equals,
+ values: vec![0, 10, 20],
+ };
+ let mut e = simple_event();
+ // Allow if event kind is in list for Equals
+ e.kind = 10;
+ assert!(c.allows_event(&e));
+ // Deny if event kind is not in list for Equals
+ e.kind = 11;
+ assert!(!c.allows_event(&e));
+ // Deny if event kind is in list for NotEquals
+ e.kind = 10;
+ c.operator = Operator::NotEquals;
+ assert!(!c.allows_event(&e));
+ // Allow if event kind is not in list for NotEquals
+ e.kind = 99;
+ c.operator = Operator::NotEquals;
+ assert!(c.allows_event(&e));
+ // Always deny if GreaterThan/LessThan for a list
+ c.operator = Operator::LessThan;
+ assert!(!c.allows_event(&e));
+ c.operator = Operator::GreaterThan;
+ assert!(!c.allows_event(&e));
+ }
+}
diff --git a/src/error.rs b/src/error.rs
@@ -50,6 +50,8 @@ pub enum Error {
HyperError(hyper::Error),
#[error("Hex encoding error")]
HexError(hex::FromHexError),
+ #[error("Delegation parse error")]
+ DelegationParseError,
#[error("Unknown/Undocumented")]
UnknownError,
}
diff --git a/src/event.rs b/src/event.rs
@@ -1,4 +1,5 @@
//! Event parsing and validation
+use crate::delegation::validate_delegation;
use crate::error::Error::*;
use crate::error::Result;
use crate::nip05;
@@ -31,6 +32,8 @@ pub struct EventCmd {
pub struct Event {
pub id: String,
pub(crate) pubkey: String,
+ #[serde(skip)]
+ pub(crate) delegated_by: Option<String>,
pub(crate) created_at: u64,
pub(crate) kind: u64,
#[serde(deserialize_with = "tag_from_string")]
@@ -83,6 +86,7 @@ impl From<EventCmd> for Result<Event> {
} else if ec.event.is_valid() {
let mut e = ec.event;
e.build_index();
+ e.update_delegation();
Ok(e)
} else {
Err(EventInvalid)
@@ -110,6 +114,50 @@ impl Event {
None
}
+ // is this event delegated (properly)?
+ // does the signature match, and are conditions valid?
+ // if so, return an alternate author for the event
+ pub fn delegated_author(&self) -> Option<String> {
+ // is there a delegation tag?
+ let delegation_tag: Vec<String> = self
+ .tags
+ .iter()
+ .filter(|x| x.len() == 4)
+ .filter(|x| x.get(0).unwrap() == "delegation")
+ .take(1)
+ .next()?
+ .to_vec(); // get first tag
+
+ //let delegation_tag = self.tag_values_by_name("delegation");
+ // delegation tags should have exactly 3 elements after the name (pubkey, condition, sig)
+ // the event is signed by the delagatee
+ let delegatee = &self.pubkey;
+ // the delegation tag references the claimed delagator
+ let delegator: &str = delegation_tag.get(1)?;
+ let querystr: &str = delegation_tag.get(2)?;
+ let sig: &str = delegation_tag.get(3)?;
+
+ // attempt to get a condition query; this requires the delegation to have a valid signature.
+ if let Some(cond_query) = validate_delegation(delegator, delegatee, querystr, sig) {
+ // The signature was valid, now we ensure the delegation
+ // condition is valid for this event:
+ if cond_query.allows_event(self) {
+ // since this is allowed, we will provide the delegatee
+ Some(delegator.into())
+ } else {
+ debug!("an event failed to satisfy delegation conditions");
+ None
+ }
+ } else {
+ debug!("event had had invalid delegation signature");
+ None
+ }
+ }
+
+ /// Update delegation status
+ fn update_delegation(&mut self) {
+ self.delegated_by = self.delegated_author();
+ }
/// Build an event tag index
fn build_index(&mut self) {
// if there are no tags; just leave the index as None
@@ -145,7 +193,7 @@ impl Event {
self.pubkey.chars().take(8).collect()
}
- /// Retrieve tag values
+ /// Retrieve tag initial values across all tags matching the name
pub fn tag_values_by_name(&self, tag_name: &str) -> Vec<String> {
self.tags
.iter()
@@ -269,6 +317,7 @@ mod tests {
Event {
id: "0".to_owned(),
pubkey: "0".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: vec![],
@@ -350,6 +399,7 @@ mod tests {
let e = Event {
id: "999".to_owned(),
pubkey: "012345".to_owned(),
+ delegated_by: None,
created_at: 501234,
kind: 1,
tags: vec![],
@@ -367,6 +417,7 @@ mod tests {
let e = Event {
id: "999".to_owned(),
pubkey: "012345".to_owned(),
+ delegated_by: None,
created_at: 501234,
kind: 1,
tags: vec![
@@ -389,10 +440,38 @@ mod tests {
}
#[test]
+ fn event_no_tag_select() {
+ let e = Event {
+ id: "999".to_owned(),
+ pubkey: "012345".to_owned(),
+ delegated_by: None,
+ created_at: 501234,
+ kind: 1,
+ tags: vec![
+ vec!["j".to_owned(), "abc".to_owned()],
+ vec!["e".to_owned(), "foo".to_owned()],
+ vec!["e".to_owned(), "baz".to_owned()],
+ vec![
+ "p".to_owned(),
+ "aaaa".to_owned(),
+ "ws://example.com".to_owned(),
+ ],
+ ],
+ content: "this is a test".to_owned(),
+ sig: "abcde".to_owned(),
+ tagidx: None,
+ };
+ let v = e.tag_values_by_name("x");
+ // asking for tags that don't exist just returns zero-length vector
+ assert_eq!(v.len(), 0);
+ }
+
+ #[test]
fn event_canonical_with_tags() {
let e = Event {
id: "999".to_owned(),
pubkey: "012345".to_owned(),
+ delegated_by: None,
created_at: 501234,
kind: 1,
tags: vec![
diff --git a/src/info.rs b/src/info.rs
@@ -35,7 +35,7 @@ impl From<config::Info> for RelayInfo {
description: i.description,
pubkey: i.pubkey,
contact: i.contact,
- supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 22]),
+ 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()),
version: CARGO_PKG_VERSION.map(|x| x.to_owned()),
}
diff --git a/src/lib.rs b/src/lib.rs
@@ -2,6 +2,7 @@ pub mod close;
pub mod config;
pub mod conn;
pub mod db;
+pub mod delegation;
pub mod error;
pub mod event;
pub mod hexrange;
diff --git a/src/schema.rs b/src/schema.rs
@@ -20,7 +20,7 @@ pragma mmap_size = 536870912; -- 512MB of mmap
"##;
/// Latest database version
-pub const DB_VERSION: usize = 6;
+pub const DB_VERSION: usize = 7;
/// Schema definition
const INIT_SQL: &str = formatcp!(
@@ -40,6 +40,7 @@ event_hash BLOB NOT NULL, -- 4-byte hash
first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970)
created_at INTEGER NOT NULL, -- when the event was authored
author BLOB NOT NULL, -- author pubkey
+delegated_by BLOB, -- delegator pubkey (NIP-26)
kind INTEGER NOT NULL, -- event kind
hidden INTEGER, -- relevant for queries
content TEXT NOT NULL -- serialized json of event object
@@ -49,6 +50,7 @@ content TEXT NOT NULL -- serialized json of event object
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
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 kind_index ON event(kind);
-- Tag Table
@@ -152,6 +154,9 @@ pub fn upgrade_db(conn: &mut PooledConnection) -> Result<()> {
if curr_version == 5 {
curr_version = mig_5_to_6(conn)?;
}
+ if curr_version == 6 {
+ curr_version = mig_6_to_7(conn)?;
+ }
if curr_version == DB_VERSION {
info!(
"All migration scripts completed successfully. Welcome to v{}.",
@@ -348,3 +353,23 @@ fn mig_5_to_6(conn: &mut PooledConnection) -> Result<usize> {
info!("vacuumed DB after tags rebuild in {:?}", start.elapsed());
Ok(6)
}
+
+fn mig_6_to_7(conn: &mut PooledConnection) -> Result<usize> {
+ info!("database schema needs update from 6->7");
+ // only change is adding a hidden column to events.
+ let upgrade_sql = r##"
+ALTER TABLE event ADD delegated_by BLOB;
+CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
+PRAGMA user_version = 7;
+"##;
+ match conn.execute_batch(upgrade_sql) {
+ Ok(()) => {
+ info!("database schema upgraded v6 -> v7");
+ }
+ Err(err) => {
+ error!("update failed: {}", err);
+ panic!("database could not be upgraded");
+ }
+ }
+ Ok(7)
+}
diff --git a/src/subscription.rs b/src/subscription.rs
@@ -217,6 +217,17 @@ impl ReqFilter {
.unwrap_or(true)
}
+ fn delegated_authors_match(&self, event: &Event) -> bool {
+ if let Some(delegated_pubkey) = &event.delegated_by {
+ self.authors
+ .as_ref()
+ .map(|vs| prefix_match(vs, delegated_pubkey))
+ .unwrap_or(true)
+ } else {
+ false
+ }
+ }
+
fn tag_match(&self, event: &Event) -> bool {
// get the hashset from the filter.
if let Some(map) = &self.tags {
@@ -248,7 +259,7 @@ impl ReqFilter {
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
&& self.until.map(|t| event.created_at < t).unwrap_or(true)
&& self.kind_match(event.kind)
- && self.authors_match(event)
+ && (self.authors_match(event) || self.delegated_authors_match(event))
&& self.tag_match(event)
&& !self.force_no_match
}
@@ -308,6 +319,7 @@ mod tests {
let e = Event {
id: "foo".to_owned(),
pubkey: "abcd".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: Vec::new(),
@@ -326,6 +338,7 @@ mod tests {
let e = Event {
id: "abcd".to_owned(),
pubkey: "".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: Vec::new(),
@@ -344,6 +357,7 @@ mod tests {
let e = Event {
id: "abcde".to_owned(),
pubkey: "".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: Vec::new(),
@@ -363,6 +377,7 @@ mod tests {
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
+ delegated_by: None,
created_at: 50,
kind: 0,
tags: Vec::new(),
@@ -386,6 +401,7 @@ mod tests {
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
+ delegated_by: None,
created_at: 150,
kind: 0,
tags: Vec::new(),
@@ -407,6 +423,7 @@ mod tests {
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
+ delegated_by: None,
created_at: 50,
kind: 0,
tags: Vec::new(),
@@ -425,6 +442,7 @@ mod tests {
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
+ delegated_by: None,
created_at: 1001,
kind: 0,
tags: Vec::new(),
@@ -443,6 +461,7 @@ mod tests {
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: Vec::new(),
@@ -461,6 +480,7 @@ mod tests {
let e = Event {
id: "123".to_owned(),
pubkey: "abc".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: Vec::new(),
@@ -479,6 +499,7 @@ mod tests {
let e = Event {
id: "123".to_owned(),
pubkey: "bcd".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: Vec::new(),
@@ -497,6 +518,7 @@ mod tests {
let e = Event {
id: "123".to_owned(),
pubkey: "xyz".to_owned(),
+ delegated_by: None,
created_at: 0,
kind: 0,
tags: Vec::new(),