package nostr import ( "bytes" "crypto/sha256" "errors" "strconv" "git.smesh.lol/nostrino/pkg/crypto/secp256k1" ) // Event is a NIP-01 event. Binary fields are fixed-size so that an event is // self-contained and cannot alias the JSON buffer it was parsed from. type Event struct { ID [IDLen]byte PubKey [PubLen]byte CreatedAt int64 Kind int Tags Tags Content string Sig [SigLen]byte } // AppendCanonical appends the NIP-01 canonical serialization to dst. func (e *Event) AppendCanonical(dst []byte) []byte { dst = append(dst, "[0,"...) dst = AppendJSONString(dst, Hex(e.PubKey[:])) dst = append(dst, ',') dst = strconv.AppendInt(dst, e.CreatedAt, 10) dst = append(dst, ',') dst = strconv.AppendInt(dst, int64(e.Kind), 10) dst = append(dst, ',') dst = e.Tags.AppendJSON(dst) dst = append(dst, ',') dst = AppendJSONString(dst, e.Content) return append(dst, ']') } // Canonical returns the NIP-01 canonical serialization. func (e *Event) Canonical() []byte { return e.AppendCanonical(make([]byte, 0, 192+len(e.Content))) } // ComputeID sets ID to the SHA-256 of the canonical serialization. func (e *Event) ComputeID() { sum := sha256.Sum256(e.Canonical()) copy(e.ID[:], sum[:]) } // CheckID reports whether ID matches the canonical serialization. func (e *Event) CheckID() bool { sum := sha256.Sum256(e.Canonical()) return bytes.Equal(sum[:], e.ID[:]) } // Verify checks both the ID and the BIP-340 signature. func (e *Event) Verify() error { if !e.CheckID() { return errors.New("nostr: event id does not match contents") } if !secp256k1.Verify(e.PubKey, e.ID, e.Sig) { return errors.New("nostr: invalid signature") } return nil } // Sign computes the ID and signs it. Used by the relay for its own events and // by the tests; relays do not sign inbound events. func (e *Event) Sign(seckey [32]byte) error { pub, ok := secp256k1.PubKey(seckey) if !ok { return errors.New("nostr: invalid secret key") } copy(e.PubKey[:], pub[:]) e.ComputeID() sig, ok := secp256k1.Sign(seckey, e.ID) if !ok { return errors.New("nostr: signing failed") } copy(e.Sig[:], sig[:]) return nil } // PubKeyHex returns the x-only public key for a secret key as lowercase hex. func PubKeyHex(sk [32]byte) (string, error) { pub, ok := secp256k1.PubKey(sk) if !ok { return "", errors.New("nostr: invalid secret key") } return Hex(pub[:]), nil } // AppendJSON appends the event's wire form to dst. func (e *Event) AppendJSON(dst []byte) []byte { dst = append(dst, `{"id":"`...) dst = AppendHex(dst, e.ID[:]) dst = append(dst, `","pubkey":"`...) dst = AppendHex(dst, e.PubKey[:]) dst = append(dst, `","created_at":`...) dst = strconv.AppendInt(dst, e.CreatedAt, 10) dst = append(dst, `,"kind":`...) dst = strconv.AppendInt(dst, int64(e.Kind), 10) dst = append(dst, `,"tags":`...) dst = e.Tags.AppendJSON(dst) dst = append(dst, `,"content":`...) dst = AppendJSONString(dst, e.Content) dst = append(dst, `,"sig":"`...) dst = AppendHex(dst, e.Sig[:]) return append(dst, '"', '}') } // MarshalJSON implements json.Marshaler. func (e *Event) MarshalJSON() ([]byte, error) { return e.AppendJSON(nil), nil } // --- kind classes (NIP-01 / NIP-33) --- // IsReplaceable reports whether only the newest event per (kind, pubkey) is // kept: kind 0, kind 3, and 10000..19999. func (e *Event) IsReplaceable() bool { return e.Kind == 0 || e.Kind == 3 || (e.Kind >= 10000 && e.Kind < 20000) } // IsAddressable reports whether events are keyed by (kind, pubkey, d-tag). func (e *Event) IsAddressable() bool { return e.Kind >= 30000 && e.Kind < 40000 } // IsEphemeral reports whether an event must not be stored: 20000..29999. func (e *Event) IsEphemeral() bool { return e.Kind >= 20000 && e.Kind < 30000 } // IsDeletion reports whether the event is a NIP-09 deletion request. func (e *Event) IsDeletion() bool { return e.Kind == 5 } // Expiration returns the NIP-40 expiration timestamp, or 0 when absent or // malformed. func (e *Event) Expiration() int64 { v, ok := e.Tags.First("expiration") if !ok { return 0 } n, err := strconv.ParseInt(v, 10, 64) if err != nil || n <= 0 { return 0 } return n } // Expired reports whether the event carries an expiration in the past. func (e *Event) Expired(now int64) bool { exp := e.Expiration() return exp != 0 && exp <= now } // IsProtected reports whether the event carries the NIP-70 "-" marker. func (e *Event) IsProtected() bool { return e.Tags.HasName("-") } // Clone returns a deep copy. Domains hand events to each other by value; the // tag slices and tag strings must not be shared across a domain boundary. func (e *Event) Clone() Event { out := *e out.Tags = make(Tags, len(e.Tags)) for i, tag := range e.Tags { cp := make(Tag, len(tag)) copy(cp, tag) out.Tags[i] = cp } return out }