Use traits to keep event methods dry

This commit is contained in:
Jon Staab
2026-05-20 14:27:26 -07:00
parent 8ad0bc393b
commit d0709e1811
7 changed files with 138 additions and 61 deletions
+80
View File
@@ -525,6 +525,86 @@ Deserialization does not verify the signature. Parsing is cheap; `verify()`
is not, and there are many places where you want to read an event off disk is not, and there are many places where you want to read an event off disk
without paying for a signature check. without paying for a signature check.
## A shared surface across event stages
There is a lot of overlap between various event structs. A few minimal accessor
traits capture read access to various fields so that we can define extensions
to events without duplication.
```rust {file=coracle-lib/src/events.rs}
pub trait HasTags {
fn tags(&self) -> &Tags;
}
pub trait HasId {
fn id(&self) -> &[u8; 32];
}
impl HasTags for HashedEvent {
fn tags(&self) -> &Tags {
&self.tags
}
}
impl HasTags for Event {
fn tags(&self) -> &Tags {
&self.tags
}
}
impl HasId for HashedEvent {
fn id(&self) -> &[u8; 32] {
&self.id
}
}
impl HasId for Event {
fn id(&self) -> &[u8; 32] {
&self.id
}
}
```
Subsequent chapters will add *extension traits* bounded on these accessors,
supply the method bodies as defaults, and close with a blanket impl over every
type that satisfies the bound. The logic is written once and reaches both event
types — and any event type added later — without another line per struct.
The expiration chapter's version reads like this:
```rust
pub trait EventExtensionExpiration: HasTags {
fn is_expired(&self) -> bool {
is_expired(self.tags())
}
// ...the rest of the expiration queries
}
impl<T: HasTags> EventExtensionExpiration for T {}
```
A trait's methods are only callable where the trait is in scope, so the
library gathers these extension traits in a `prelude`. One glob import brings
the whole behavior-query surface along:
```rust
use coracle_lib::prelude::*;
```
```rust {file=coracle-lib/src/lib.rs}
pub mod prelude;
```
```rust {file=coracle-lib/src/prelude.rs}
//! One-stop import for the extension traits that add behavior-tag queries to
//! events. With `coracle_lib::prelude::*` in scope, methods like
//! `is_expired`, `is_protected`, and `get_pow` become callable on
//! `HashedEvent` and `Event`.
pub use crate::events::{HasId, HasTags};
```
Each later chapter that defines an extension trait adds it to this prelude.
## What's next ## What's next
The next chapter introduces kinds — the integer that decides how content The next chapter introduces kinds — the integer that decides how content
+17 -17
View File
@@ -44,7 +44,7 @@ pub mod pow;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::events::{Event, HashedEvent, OwnedEvent}; use crate::events::{HasId, HasTags, HashedEvent, OwnedEvent};
use crate::tags::{Tag, Tags}; use crate::tags::{Tag, Tags};
``` ```
@@ -119,31 +119,31 @@ commitment was made.
### Methods on event types ### Methods on event types
Both `HashedEvent` and `Event` carry an id and tags, so both get a Proof of work is the first query that needs the event id as well as its
`get_pow` method that delegates to the free function. This lets callers tags, so its extension trait is bounded on both `HasId` and `HasTags` from
write `event.get_pow() >= 20` regardless of whether the event has been the events chapter. The method body delegates to the free function through
signed yet. those accessors, and the blanket impl puts `get_pow` on every type that
satisfies the bound — both `HashedEvent` and `Event` do. Callers write
`event.get_pow() >= 20` regardless of whether the event has been signed yet,
as long as the trait is in scope (via `coracle_lib::prelude::*`).
```rust {file=coracle-lib/src/pow.rs} ```rust {file=coracle-lib/src/pow.rs}
impl HashedEvent { /// Proof-of-work queries on any event that has an id and tags.
pub trait EventExtensionProofOfWork: HasId + HasTags {
/// Return the validated proof-of-work difficulty of this event. /// Return the validated proof-of-work difficulty of this event.
/// ///
/// The result is the minimum of the actual leading zero bits in the /// The result is the minimum of the actual leading zero bits in the
/// event id and the difficulty claimed in the nonce tag. /// event id and the difficulty claimed in the nonce tag.
pub fn get_pow(&self) -> u8 { fn get_pow(&self) -> u8 {
get_pow(&self.id, &self.tags) get_pow(self.id(), self.tags())
} }
} }
impl Event { impl<T: HasId + HasTags> EventExtensionProofOfWork for T {}
/// Return the validated proof-of-work difficulty of this event. ```
///
/// The result is the minimum of the actual leading zero bits in the ```rust {file=coracle-lib/src/prelude.rs}
/// event id and the difficulty claimed in the nonce tag. pub use crate::pow::EventExtensionProofOfWork;
pub fn get_pow(&self) -> u8 {
get_pow(&self.id, &self.tags)
}
}
``` ```
## Mining ## Mining
+24 -32
View File
@@ -33,13 +33,13 @@ pub mod expiration;
//! NIP-40 expiring events: reading the `expiration` tag and checking //! NIP-40 expiring events: reading the `expiration` tag and checking
//! whether an event has expired. //! whether an event has expired.
//! //!
//! The free functions in this module operate on [`Tags`] so they can //! The free functions in this module operate on [`Tags`] so they can be
//! be used wherever a tag slice is in hand. Method versions on //! used wherever a tag slice is in hand. The [`EventExtensionExpiration`]
//! [`HashedEvent`] and [`Event`] delegate to them for a readable call //! trait layers method-call sugar on top for any event type, reachable
//! site. The tag itself is built with [`Tag::expiration`] from the //! through the crate prelude. The tag itself is built with
//! tags module. //! [`Tag::expiration`] from the tags module.
use crate::events::{Event, HashedEvent}; use crate::events::HasTags;
use crate::tags::Tags; use crate::tags::Tags;
use crate::util::now; use crate::util::now;
``` ```
@@ -128,45 +128,37 @@ impl Tag {
## Methods on event types ## Methods on event types
Both `HashedEvent` and `Event` carry a `Tags` field, and callers who Rather than repeat these queries on each event struct, expiration exposes
have an event in hand almost always want to ask questions of its tags them through an extension trait bounded on [`HasTags`]. The method bodies
without a separate import. The method impls delegate to the free live once, as defaults, and a blanket impl makes them available on every
functions verbatim. type that can hand back its tags — `HashedEvent`, `Event`, and anything
added later. With `coracle_lib::prelude::*` in scope, `event.is_expired()`
reads the same whether the event is hashed or signed.
```rust {file=coracle-lib/src/expiration.rs} ```rust {file=coracle-lib/src/expiration.rs}
impl HashedEvent { /// Expiration queries on any event that carries tags.
pub trait EventExtensionExpiration: HasTags {
/// Return the expiration timestamp of this event, if any. /// Return the expiration timestamp of this event, if any.
pub fn get_expiration(&self) -> Option<u64> { fn get_expiration(&self) -> Option<u64> {
get_expiration(&self.tags) get_expiration(self.tags())
} }
/// Whether this event has expired relative to the given timestamp. /// Whether this event has expired relative to the given timestamp.
pub fn is_expired_at(&self, now: u64) -> bool { fn is_expired_at(&self, now: u64) -> bool {
is_expired_at(&self.tags, now) is_expired_at(self.tags(), now)
} }
/// Whether this event has expired relative to the system clock. /// Whether this event has expired relative to the system clock.
pub fn is_expired(&self) -> bool { fn is_expired(&self) -> bool {
is_expired(&self.tags) is_expired(self.tags())
} }
} }
impl Event { impl<T: HasTags> EventExtensionExpiration for T {}
/// Return the expiration timestamp of this event, if any. ```
pub fn get_expiration(&self) -> Option<u64> {
get_expiration(&self.tags)
}
/// Whether this event has expired relative to the given timestamp. ```rust {file=coracle-lib/src/prelude.rs}
pub fn is_expired_at(&self, now: u64) -> bool { pub use crate::expiration::EventExtensionExpiration;
is_expired_at(&self.tags, now)
}
/// Whether this event has expired relative to the system clock.
pub fn is_expired(&self) -> bool {
is_expired(&self.tags)
}
}
``` ```
## Usage patterns ## Usage patterns
+14 -12
View File
@@ -48,7 +48,7 @@ pub mod protected;
//! challenge followed by a pubkey comparison) needs a live, authenticated //! challenge followed by a pubkey comparison) needs a live, authenticated
//! connection and is handled in the relay layer. //! connection and is handled in the relay layer.
use crate::events::{Event, HashedEvent}; use crate::events::HasTags;
use crate::tags::Tags; use crate::tags::Tags;
``` ```
@@ -91,23 +91,25 @@ impl Tag {
## Methods on event types ## Methods on event types
A caller holding an event wants to ask whether it is protected without A caller holding an event wants to ask whether it is protected without
reaching for its tags directly. Both `HashedEvent` and `Event` carry a reaching for its tags directly. Following the pattern from expiration, the
`Tags` field, so the method facades delegate to the free function. query is an extension trait bounded on [`HasTags`], so one default method
serves every event type. With `coracle_lib::prelude::*` in scope,
`event.is_protected()` works on both `HashedEvent` and `Event`.
```rust {file=coracle-lib/src/protected.rs} ```rust {file=coracle-lib/src/protected.rs}
impl HashedEvent { /// Protected-event queries on any event that carries tags.
pub trait EventExtensionProtected: HasTags {
/// Whether this event carries the NIP-70 protected marker. /// Whether this event carries the NIP-70 protected marker.
pub fn is_protected(&self) -> bool { fn is_protected(&self) -> bool {
is_protected(&self.tags) is_protected(self.tags())
} }
} }
impl Event { impl<T: HasTags> EventExtensionProtected for T {}
/// Whether this event carries the NIP-70 protected marker. ```
pub fn is_protected(&self) -> bool {
is_protected(&self.tags) ```rust {file=coracle-lib/src/prelude.rs}
} pub use crate::protected::EventExtensionProtected;
}
``` ```
## Usage patterns ## Usage patterns
+1
View File
@@ -1,5 +1,6 @@
use coracle_lib::events::{EventContent, HashedEvent}; use coracle_lib::events::{EventContent, HashedEvent};
use coracle_lib::expiration::{get_expiration, is_expired, is_expired_at}; use coracle_lib::expiration::{get_expiration, is_expired, is_expired_at};
use coracle_lib::prelude::*;
use coracle_lib::keys::SecretKey; use coracle_lib::keys::SecretKey;
use coracle_lib::tags::{Tag, Tags}; use coracle_lib::tags::{Tag, Tags};
+1
View File
@@ -1,5 +1,6 @@
use coracle_lib::events::{EventContent, OwnedEvent}; use coracle_lib::events::{EventContent, OwnedEvent};
use coracle_lib::keys::SecretKey; use coracle_lib::keys::SecretKey;
use coracle_lib::prelude::*;
use coracle_lib::pow::{get_leading_zero_bits, get_pow, mine_pow, mine_pow_batch}; use coracle_lib::pow::{get_leading_zero_bits, get_pow, mine_pow, mine_pow_batch};
use coracle_lib::tags::{Tag, Tags}; use coracle_lib::tags::{Tag, Tags};
+1
View File
@@ -1,5 +1,6 @@
use coracle_lib::events::{EventContent, HashedEvent}; use coracle_lib::events::{EventContent, HashedEvent};
use coracle_lib::keys::SecretKey; use coracle_lib::keys::SecretKey;
use coracle_lib::prelude::*;
use coracle_lib::protected::is_protected; use coracle_lib::protected::is_protected;
use coracle_lib::tags::{Tag, Tags}; use coracle_lib::tags::{Tag, Tags};