use std::fmt;
use secp256k1::schnorr::Signature;
use sha2::{Digest, Sha256};
use thiserror::Error;
use crate::event::{Event, Kind, Tag, TagKind, Tags};
use crate::key::{Keys, PublicKey};
use crate::types::Timestamp;
pub const DELEGATION_TAG_KEY: &str = "delegation";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Condition {
Kind(Kind),
CreatedAfter(Timestamp),
CreatedBefore(Timestamp),
}
impl fmt::Display for Condition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Kind(k) => write!(f, "kind={}", k.as_u16()),
Self::CreatedAfter(ts) => write!(f, "created_at>{}", ts.as_secs()),
Self::CreatedBefore(ts) => write!(f, "created_at<{}", ts.as_secs()),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct Conditions {
items: Vec<Condition>,
}
impl Conditions {
#[must_use]
pub const fn new() -> Self {
Self { items: Vec::new() }
}
#[must_use]
pub fn allow_kind(mut self, kind: Kind) -> Self {
self.items.push(Condition::Kind(kind));
self
}
#[must_use]
pub fn after(mut self, ts: Timestamp) -> Self {
self.items.push(Condition::CreatedAfter(ts));
self
}
#[must_use]
pub fn before(mut self, ts: Timestamp) -> Self {
self.items.push(Condition::CreatedBefore(ts));
self
}
pub fn iter(&self) -> impl Iterator<Item = &Condition> {
self.items.iter()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.items.is_empty()
}
#[must_use]
pub fn render(&self) -> String {
let mut out = String::new();
for (i, c) in self.items.iter().enumerate() {
if i > 0 {
out.push('&');
}
out.push_str(&c.to_string());
}
out
}
pub fn parse(s: &str) -> Result<Self, ConditionsError> {
if s.is_empty() {
return Ok(Self::new());
}
let mut items = Vec::with_capacity(s.matches('&').count() + 1);
for raw in s.split('&') {
items.push(parse_one(raw)?);
}
Ok(Self { items })
}
#[must_use]
pub fn matches(&self, kind: Kind, created_at: Timestamp) -> bool {
self.items.iter().all(|c| match c {
Condition::Kind(k) => *k == kind,
Condition::CreatedAfter(ts) => created_at.as_secs() > ts.as_secs(),
Condition::CreatedBefore(ts) => created_at.as_secs() < ts.as_secs(),
})
}
}
fn parse_one(raw: &str) -> Result<Condition, ConditionsError> {
if let Some(rest) = raw.strip_prefix("kind=") {
let n: u16 = rest
.parse()
.map_err(|_| ConditionsError::InvalidValue(raw.to_owned()))?;
return Ok(Condition::Kind(Kind::new(n)));
}
if let Some(rest) = raw.strip_prefix("created_at>") {
let n: u64 = rest
.parse()
.map_err(|_| ConditionsError::InvalidValue(raw.to_owned()))?;
return Ok(Condition::CreatedAfter(Timestamp::from_secs(n)));
}
if let Some(rest) = raw.strip_prefix("created_at<") {
let n: u64 = rest
.parse()
.map_err(|_| ConditionsError::InvalidValue(raw.to_owned()))?;
return Ok(Condition::CreatedBefore(Timestamp::from_secs(n)));
}
Err(ConditionsError::UnsupportedClause(raw.to_owned()))
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ConditionsError {
#[error("unsupported delegation clause `{0}`")]
UnsupportedClause(String),
#[error("invalid value in delegation clause `{0}`")]
InvalidValue(String),
}
#[must_use]
pub fn delegation_message(delegatee: &PublicKey, conditions: &Conditions) -> String {
format!(
"nostr:delegation:{}:{}",
delegatee.to_hex(),
conditions.render()
)
}
#[must_use]
pub fn delegation_hash(delegatee: &PublicKey, conditions: &Conditions) -> [u8; 32] {
let msg = delegation_message(delegatee, conditions);
let digest = Sha256::digest(msg.as_bytes());
digest.into()
}
pub type DelegationToken = Signature;
#[must_use]
pub fn sign_delegation(
delegator: &Keys,
delegatee: &PublicKey,
conditions: &Conditions,
) -> DelegationToken {
let h = delegation_hash(delegatee, conditions);
delegator.sign_schnorr(&h)
}
#[must_use]
pub fn verify_delegation(
delegator: &PublicKey,
delegatee: &PublicKey,
conditions: &Conditions,
token: &DelegationToken,
) -> bool {
let h = delegation_hash(delegatee, conditions);
delegator.verify_schnorr(&h, token)
}
impl Tag {
#[must_use]
pub fn delegation(
delegator: PublicKey,
conditions: &Conditions,
token: &DelegationToken,
) -> Self {
Self::with(
&TagKind::Custom(DELEGATION_TAG_KEY.to_owned()),
[delegator.to_hex(), conditions.render(), token.to_string()],
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Delegation {
pub delegator: PublicKey,
pub conditions: Conditions,
pub token: DelegationToken,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DelegationError {
#[error("event has no well-formed `delegation` tag")]
Missing,
#[error(transparent)]
Pubkey(#[from] crate::key::PublicKeyError),
#[error(transparent)]
Conditions(#[from] ConditionsError),
#[error("invalid delegation token: {0}")]
Token(String),
#[error("delegation token does not verify against the declared delegator")]
InvalidSignature,
#[error("event does not satisfy the delegation conditions")]
ConditionsViolated,
}
pub fn parse_delegation(tags: &Tags) -> Result<Delegation, DelegationError> {
for tag in tags {
if !is_delegation_tag(&tag.kind()) {
continue;
}
let values = tag.values();
let (Some(d), Some(c), Some(t)) = (values.get(1), values.get(2), values.get(3)) else {
return Err(DelegationError::Missing);
};
let delegator = PublicKey::parse(d)?;
let conditions = Conditions::parse(c)?;
let token = t
.parse::<Signature>()
.map_err(|e| DelegationError::Token(e.to_string()))?;
return Ok(Delegation {
delegator,
conditions,
token,
});
}
Err(DelegationError::Missing)
}
fn is_delegation_tag(kind: &TagKind) -> bool {
matches!(kind, TagKind::Custom(s) if s == DELEGATION_TAG_KEY)
}
pub fn verify_event_delegation(event: &Event) -> Result<Delegation, DelegationError> {
let delegation = parse_delegation(&event.tags)?;
if !verify_delegation(
&delegation.delegator,
&event.pubkey,
&delegation.conditions,
&delegation.token,
) {
return Err(DelegationError::InvalidSignature);
}
if !delegation.conditions.matches(event.kind, event.created_at) {
return Err(DelegationError::ConditionsViolated);
}
Ok(delegation)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::EventBuilder;
use crate::event::Kind;
fn fixture_keys(byte: u8) -> Keys {
let hex: String = format!("{byte:064x}");
Keys::parse(&hex).unwrap()
}
#[test]
fn conditions_render_and_parse_round_trip() {
let c = Conditions::new()
.allow_kind(Kind::TEXT_NOTE)
.after(Timestamp::from_secs(1_700_000_000))
.before(Timestamp::from_secs(1_800_000_000));
let rendered = c.render();
assert_eq!(
rendered,
"kind=1&created_at>1700000000&created_at<1800000000"
);
assert_eq!(Conditions::parse(&rendered).unwrap(), c);
}
#[test]
fn empty_conditions_render_to_empty_string() {
let empty = Conditions::new();
assert_eq!(empty.render(), "");
assert_eq!(Conditions::parse("").unwrap(), empty);
assert!(empty.matches(Kind::TEXT_NOTE, Timestamp::from_secs(1)));
}
#[test]
fn parse_rejects_malformed_clauses() {
assert!(matches!(
Conditions::parse("foobar=1"),
Err(ConditionsError::UnsupportedClause(s)) if s == "foobar=1"
));
assert!(matches!(
Conditions::parse("kind=abc"),
Err(ConditionsError::InvalidValue(s)) if s == "kind=abc"
));
}
#[test]
fn matches_enforces_strict_inequalities() {
let c = Conditions::new()
.after(Timestamp::from_secs(100))
.before(Timestamp::from_secs(200));
assert!(c.matches(Kind::TEXT_NOTE, Timestamp::from_secs(150)));
assert!(!c.matches(Kind::TEXT_NOTE, Timestamp::from_secs(100)));
assert!(!c.matches(Kind::TEXT_NOTE, Timestamp::from_secs(200)));
}
#[test]
fn delegation_token_verifies_with_correct_inputs() {
let delegator = fixture_keys(1);
let delegatee = fixture_keys(2);
let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
let token = sign_delegation(&delegator, delegatee.public_key(), &conditions);
assert!(verify_delegation(
delegator.public_key(),
delegatee.public_key(),
&conditions,
&token,
));
}
#[test]
fn delegation_token_fails_when_delegatee_changes() {
let delegator = fixture_keys(1);
let delegatee_a = fixture_keys(2);
let delegatee_b = fixture_keys(3);
let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
let token = sign_delegation(&delegator, delegatee_a.public_key(), &conditions);
assert!(!verify_delegation(
delegator.public_key(),
delegatee_b.public_key(),
&conditions,
&token,
));
}
#[test]
fn delegation_token_fails_when_conditions_change() {
let delegator = fixture_keys(1);
let delegatee = fixture_keys(2);
let signed_conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
let mutated_conditions = Conditions::new().allow_kind(Kind::REACTION);
let token = sign_delegation(&delegator, delegatee.public_key(), &signed_conditions);
assert!(!verify_delegation(
delegator.public_key(),
delegatee.public_key(),
&mutated_conditions,
&token,
));
}
#[test]
fn parse_delegation_round_trips_through_a_built_event() {
let delegator = fixture_keys(1);
let delegatee = fixture_keys(2);
let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
let token = sign_delegation(&delegator, delegatee.public_key(), &conditions);
let event = EventBuilder::text_note("delegated note")
.tag(Tag::delegation(
*delegator.public_key(),
&conditions,
&token,
))
.sign_with_keys(&delegatee)
.unwrap();
let parsed = parse_delegation(&event.tags).unwrap();
assert_eq!(&parsed.delegator, delegator.public_key());
assert_eq!(parsed.conditions, conditions);
let verified = verify_event_delegation(&event).expect("delegation must verify");
assert_eq!(&verified.delegator, delegator.public_key());
}
#[test]
fn verify_event_delegation_rejects_violated_conditions() {
let delegator = fixture_keys(1);
let delegatee = fixture_keys(2);
let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
let token = sign_delegation(&delegator, delegatee.public_key(), &conditions);
let event = EventBuilder::new(Kind::REACTION, "+")
.tag(Tag::delegation(
*delegator.public_key(),
&conditions,
&token,
))
.sign_with_keys(&delegatee)
.unwrap();
assert!(matches!(
verify_event_delegation(&event),
Err(DelegationError::ConditionsViolated)
));
}
#[test]
fn verify_event_delegation_detects_token_tampering() {
let delegator = fixture_keys(1);
let delegatee = fixture_keys(2);
let real = Conditions::new().allow_kind(Kind::TEXT_NOTE);
let real_token = sign_delegation(&delegator, delegatee.public_key(), &real);
let mutated = Conditions::new().allow_kind(Kind::REACTION);
let event = EventBuilder::new(Kind::REACTION, "+")
.tag(Tag::delegation(
*delegator.public_key(),
&mutated,
&real_token,
))
.sign_with_keys(&delegatee)
.unwrap();
assert!(matches!(
verify_event_delegation(&event),
Err(DelegationError::InvalidSignature)
));
}
}