NAME
Net::Nostr::Event - Nostr protocol event object
SYNOPSIS
use Net::Nostr::Event;
use Net::Nostr::Key;
# Typical usage: create via Key (sets pubkey and signs automatically)
my $key = Net::Nostr::Key->new;
my $event = $key->create_event(kind => 1, content => 'hello', tags => []);
say $event->id; # 64-char hex sha256
say $event->sig; # 128-char hex signature
# Manual construction (local builder -- defaults created_at, tags, id)
my $event = Net::Nostr::Event->new(
pubkey => $key->pubkey_hex,
kind => 1,
content => 'hello world',
tags => [['t', 'nostr']],
created_at => 1700000000,
);
# Parse from wire (strict -- all 7 fields required, no defaults)
my $event = Net::Nostr::Event->from_wire(\%hash);
say $event->json_serialize; # canonical JSON array for hashing
my $hash = $event->to_hash; # { id, pubkey, created_at, kind, tags, content, sig }
DESCRIPTION
Represents a Nostr event as defined by NIP-01. Handles canonical JSON serialization, automatic ID computation, kind classification, and signature verification.
Events are immutable after construction. The body fields (id, pubkey, created_at, kind, tags, content) are read-only. The only writable field is sig, which does not participate in the event ID computation. Tags are deep-copied on input and output so that callers cannot invalidate an event through retained references. This prevents a class of bugs where mutating a field silently invalidates the event ID and any existing signature.
CONSTRUCTOR
new
my $event = Net::Nostr::Event->new(
pubkey => $hex_pubkey,
kind => 1,
content => 'hello',
tags => [['p', $pubkey]],
created_at => time(),
sig => $hex_sig,
);
Strict builder for local event construction. pubkey, kind, and content are required. tags defaults to [], created_at defaults to time(), and id is automatically computed from the canonical serialization. If id is passed explicitly, it is preserved as-is.
For events parsed from the wire, use "from_wire" instead, which requires all seven NIP-01 fields and does not apply any defaults.
Croaks if any required field is missing or if values fail format validation:
pubkeymust be 64-character lowercase hexkindmust be an integer between 0 and 65535contentmust be definedsig, if provided, must be 128-character lowercase hexid, if provided, must be 64-character lowercase hexUnknown arguments are rejected
from_wire
my $event = Net::Nostr::Event->from_wire(\%hash);
Strict wire parser. Constructs an event from a hashref received over the wire (e.g. from JSON-decoded protocol messages). All seven NIP-01 event fields are required: id, pubkey, created_at, kind, tags, content, sig. No defaults are applied. Croaks if any field is missing, undefined, or fails format validation.
This is the entry point used by "parse" in Net::Nostr::Message for EVENT and AUTH messages. Use "new" for local event construction where defaults (created_at, tags, id) are convenient.
my $hash = { id => '...', pubkey => '...', created_at => 1000,
kind => 1, tags => [], content => 'hi', sig => '...' };
my $event = Net::Nostr::Event->from_wire($hash);
ACCESSORS
All body accessors are read-only. Attempting to set them after construction will croak. sig is the only writable accessor.
id
my $id = $event->id; # '3bf0c63f...' (64-char hex)
Returns the event ID, a SHA-256 hex digest of the canonical serialization. Read-only.
pubkey
my $pubkey = $event->pubkey;
Returns the author's public key as a 64-character hex string. Read-only.
created_at
my $ts = $event->created_at; # Unix timestamp
Returns the event creation timestamp. Read-only.
kind
my $kind = $event->kind; # 1
Returns the event kind (integer). Read-only.
tags
my $tags = $event->tags; # [['p', 'abc...'], ['e', 'def...']]
Returns a deep copy of the tags arrayref. Each tag is an arrayref of strings. Read-only. All tags must be provided at construction time.
Tags are deep-copied both on input (during construction) and on output (from this accessor and "to_hash"), so callers cannot accidentally mutate the event's internal state through retained references.
content
my $content = $event->content;
Returns the event content string. Read-only.
sig
my $sig = $event->sig; # get
$event->sig($hex_signature); # set
Gets or sets the Schnorr signature as a 128-character lowercase hex string. This is the only writable field because the signature does not participate in event ID computation. Setting undef clears the signature. The setter croaks if the value is defined but not valid 128-char lowercase hex.
json_serialize
my $json = $event->json_serialize;
Returns the canonical JSON serialization used for ID computation: [0, pubkey, created_at, kind, tags, content]. The output is UTF-8 encoded with no extra whitespace.
to_hash
my $hash = $event->to_hash;
# { id => '...', pubkey => '...', created_at => 1000,
# kind => 1, tags => [...], content => '...', sig => '...' }
Returns a hashref with all seven event fields. The tags value is a deep copy, so mutating it will not affect the event. Useful for JSON encoding the full event object.
METHODS
difficulty
my $bits = $event->difficulty; # e.g. 21
Returns the Proof of Work difficulty of the event, defined as the number of leading zero bits in the event ID (NIP-13). For example, an ID starting with 000006d8 has 21 leading zero bits.
my $event = $key->create_event(kind => 1, content => 'hello', tags => []);
my $mined = $event->mine(16);
say $mined->difficulty; # >= 16
committed_target_difficulty
my $target = $event->committed_target_difficulty; # e.g. 20, or undef
Returns the committed target difficulty from the nonce tag's third entry (NIP-13), or undef if no nonce tag or no target is present. This allows clients and relays to reject events where the miner committed to a lower difficulty than required, even if the actual difficulty happens to be higher.
my $mined = $event->mine(20);
say $mined->committed_target_difficulty; # 20
mine
my $mined = $event->mine($target_difficulty);
Returns a new Net::Nostr::Event with a nonce tag that gives the event at least $target_difficulty leading zero bits in its ID (NIP-13). The original event is not modified. The nonce tag's third entry records the committed target difficulty.
The returned event is unsigned -- call $key->sign_event($mined) to sign it after mining.
my $event = $key->create_event(kind => 1, content => 'hello', tags => []);
my $mined = $event->mine(20);
$key->sign_event($mined);
say $mined->difficulty; # >= 20
Existing tags are preserved. If the event already has a nonce tag, it is replaced. The created_at timestamp is updated during mining.
Since the NIP-01 event ID does not commit to the signature, mining can be delegated to a third party (delegated Proof of Work).
d_tag
my $d = $event->d_tag; # '' if no d tag
Returns the value of the first d tag, or empty string if none exists. Used for addressable event deduplication (kinds 30000-39999).
my $event = Net::Nostr::Event->new(
pubkey => 'a' x 64, kind => 30023,
content => '', tags => [['d', 'my-article']],
);
say $event->d_tag; # 'my-article'
expiration
my $ts = $event->expiration; # Unix timestamp, or undef
Returns the value of the expiration tag (NIP-40) as a number, or undef if the event has no expiration tag.
my $event = Net::Nostr::Event->new(
pubkey => 'a' x 64, kind => 1, content => 'temp',
tags => [['expiration', '1600000000']],
);
say $event->expiration; # 1600000000
is_expired
my $bool = $event->is_expired;
my $bool = $event->is_expired($now);
Returns true if the event has an expiration tag (NIP-40) and the expiration time has passed. Accepts an optional Unix timestamp to compare against (defaults to time()). Returns false if there is no expiration tag.
if ($event->is_expired) {
# ignore or discard the event
}
content_warning
my $reason = $event->content_warning; # string, '' or undef
Returns the value of the content-warning tag (NIP-36), or undef if the event has no content warning tag. Returns an empty string if the tag is present but has no reason.
my $event = Net::Nostr::Event->new(
pubkey => 'a' x 64, kind => 1, content => 'sensitive',
tags => [['content-warning', 'spoiler']],
);
say $event->content_warning; # 'spoiler'
has_content_warning
my $bool = $event->has_content_warning;
Returns true if the event has a content-warning tag (NIP-36). Clients can use this to hide content until the user opts in.
if ($event->has_content_warning) {
# hide content behind a warning
}
content_warning_tag
my $tag = Net::Nostr::Event->content_warning_tag('spoiler');
my $tag = Net::Nostr::Event->content_warning_tag();
Class method that creates a content-warning tag arrayref, suitable for inclusion in an event's tags. The reason is optional.
my $event = Net::Nostr::Event->new(
pubkey => 'a' x 64,
kind => 1,
content => 'spoiler content',
tags => [Net::Nostr::Event->content_warning_tag('spoiler')],
);
is_regular
$event->is_regular; # true for kinds 1, 2, 4-44, 1000-9999
Returns true if the event kind is a regular (non-replaceable, non-ephemeral, non-addressable) kind.
is_replaceable
$event->is_replaceable; # true for kinds 0, 3, 10000-19999
Returns true if the event kind is replaceable (only latest per pubkey+kind is kept).
is_ephemeral
$event->is_ephemeral; # true for kinds 20000-29999
Returns true if the event kind is ephemeral (broadcast but never stored).
is_addressable
$event->is_addressable; # true for kinds 30000-39999
Returns true if the event kind is addressable (only latest per pubkey+kind+d_tag is kept).
is_protected
$event->is_protected; # true if ["-"] tag is present
Returns true if the event contains a ["-"] tag (NIP-70). Protected events can only be published to relays by their author. Relays MUST reject protected events unless the client has authenticated (NIP-42) as the event's pubkey.
validate
$event->validate; # croaks on failure, returns 1 on success
Full cryptographic validation: recomputes the event ID from the canonical serialization, compares it to the stored id, and verifies the Schnorr signature against the event's own pubkey. Croaks if the event is unsigned, the ID does not match, or the signature is invalid.
This is the method callers should use to verify events received from untrusted sources (relays, peers, files).
my $event = Net::Nostr::Message->parse($json)->event;
$event->validate; # croaks if tampered or forged
verify_sig
my $valid = $event->verify_sig($key);
Low-level signature check: verifies the Schnorr signature against the stored id using the given Net::Nostr::Key object. Croaks if the key's pubkey does not match $event->pubkey. Does not recompute the event ID -- use "validate" for full verification.
my $key = Net::Nostr::Key->new;
my $event = $key->create_event(kind => 1, content => 'signed', tags => []);
say $event->verify_sig($key); # 1