event.go raw
1 package nostr
2
3 import (
4 "bytes"
5 "crypto/sha256"
6 "errors"
7 "strconv"
8
9 "git.smesh.lol/torly/pkg/crypto/secp256k1"
10 )
11
12 // Event is a NIP-01 event. Binary fields are fixed-size so that an event is
13 // self-contained and cannot alias the JSON buffer it was parsed from.
14 type Event struct {
15 ID [IDLen]byte
16 PubKey [PubLen]byte
17 CreatedAt int64
18 Kind int
19 Tags Tags
20 Content string
21 Sig [SigLen]byte
22 }
23
24 // AppendCanonical appends the NIP-01 canonical serialization to dst.
25 func (e *Event) AppendCanonical(dst []byte) []byte {
26 dst = append(dst, "[0,"...)
27 dst = AppendJSONString(dst, Hex(e.PubKey[:]))
28 dst = append(dst, ',')
29 dst = strconv.AppendInt(dst, e.CreatedAt, 10)
30 dst = append(dst, ',')
31 dst = strconv.AppendInt(dst, int64(e.Kind), 10)
32 dst = append(dst, ',')
33 dst = e.Tags.AppendJSON(dst)
34 dst = append(dst, ',')
35 dst = AppendJSONString(dst, e.Content)
36 return append(dst, ']')
37 }
38
39 // Canonical returns the NIP-01 canonical serialization.
40 func (e *Event) Canonical() []byte {
41 return e.AppendCanonical(make([]byte, 0, 192+len(e.Content)))
42 }
43
44 // ComputeID sets ID to the SHA-256 of the canonical serialization.
45 func (e *Event) ComputeID() {
46 sum := sha256.Sum256(e.Canonical())
47 copy(e.ID[:], sum[:])
48 }
49
50 // CheckID reports whether ID matches the canonical serialization.
51 func (e *Event) CheckID() bool {
52 sum := sha256.Sum256(e.Canonical())
53 return bytes.Equal(sum[:], e.ID[:])
54 }
55
56 // Verify checks both the ID and the BIP-340 signature.
57 func (e *Event) Verify() error {
58 if !e.CheckID() {
59 return errors.New("nostr: event id does not match contents")
60 }
61 if !secp256k1.Verify(e.PubKey, e.ID, e.Sig) {
62 return errors.New("nostr: invalid signature")
63 }
64 return nil
65 }
66
67 // Sign computes the ID and signs it. Used by the relay for its own events and
68 // by the tests; relays do not sign inbound events.
69 func (e *Event) Sign(seckey [32]byte) error {
70 pub, ok := secp256k1.PubKey(seckey)
71 if !ok {
72 return errors.New("nostr: invalid secret key")
73 }
74 copy(e.PubKey[:], pub[:])
75 e.ComputeID()
76 sig, ok := secp256k1.Sign(seckey, e.ID)
77 if !ok {
78 return errors.New("nostr: signing failed")
79 }
80 copy(e.Sig[:], sig[:])
81 return nil
82 }
83
84 // PubKeyHex returns the x-only public key for a secret key as lowercase hex.
85 func PubKeyHex(sk [32]byte) (string, error) {
86 pub, ok := secp256k1.PubKey(sk)
87 if !ok {
88 return "", errors.New("nostr: invalid secret key")
89 }
90 return Hex(pub[:]), nil
91 }
92
93 // AppendJSON appends the event's wire form to dst.
94 func (e *Event) AppendJSON(dst []byte) []byte {
95 dst = append(dst, `{"id":"`...)
96 dst = AppendHex(dst, e.ID[:])
97 dst = append(dst, `","pubkey":"`...)
98 dst = AppendHex(dst, e.PubKey[:])
99 dst = append(dst, `","created_at":`...)
100 dst = strconv.AppendInt(dst, e.CreatedAt, 10)
101 dst = append(dst, `,"kind":`...)
102 dst = strconv.AppendInt(dst, int64(e.Kind), 10)
103 dst = append(dst, `,"tags":`...)
104 dst = e.Tags.AppendJSON(dst)
105 dst = append(dst, `,"content":`...)
106 dst = AppendJSONString(dst, e.Content)
107 dst = append(dst, `,"sig":"`...)
108 dst = AppendHex(dst, e.Sig[:])
109 return append(dst, '"', '}')
110 }
111
112 // MarshalJSON implements json.Marshaler.
113 func (e *Event) MarshalJSON() ([]byte, error) { return e.AppendJSON(nil), nil }
114
115 // --- kind classes (NIP-01 / NIP-33) ---
116
117 // IsReplaceable reports whether only the newest event per (kind, pubkey) is
118 // kept: kind 0, kind 3, and 10000..19999.
119 func (e *Event) IsReplaceable() bool {
120 return e.Kind == 0 || e.Kind == 3 || (e.Kind >= 10000 && e.Kind < 20000)
121 }
122
123 // IsAddressable reports whether events are keyed by (kind, pubkey, d-tag).
124 func (e *Event) IsAddressable() bool { return e.Kind >= 30000 && e.Kind < 40000 }
125
126 // IsEphemeral reports whether an event must not be stored: 20000..29999.
127 func (e *Event) IsEphemeral() bool { return e.Kind >= 20000 && e.Kind < 30000 }
128
129 // IsDeletion reports whether the event is a NIP-09 deletion request.
130 func (e *Event) IsDeletion() bool { return e.Kind == 5 }
131
132 // Expiration returns the NIP-40 expiration timestamp, or 0 when absent or
133 // malformed.
134 func (e *Event) Expiration() int64 {
135 v, ok := e.Tags.First("expiration")
136 if !ok {
137 return 0
138 }
139 n, err := strconv.ParseInt(v, 10, 64)
140 if err != nil || n <= 0 {
141 return 0
142 }
143 return n
144 }
145
146 // Expired reports whether the event carries an expiration in the past.
147 func (e *Event) Expired(now int64) bool {
148 exp := e.Expiration()
149 return exp != 0 && exp <= now
150 }
151
152 // IsProtected reports whether the event carries the NIP-70 "-" marker.
153 func (e *Event) IsProtected() bool { return e.Tags.HasName("-") }
154
155 // Clone returns a deep copy. Domains hand events to each other by value; the
156 // tag slices and tag strings must not be shared across a domain boundary.
157 func (e *Event) Clone() Event {
158 out := *e
159 out.Tags = make(Tags, len(e.Tags))
160 for i, tag := range e.Tags {
161 cp := make(Tag, len(tag))
162 copy(cp, tag)
163 out.Tags[i] = cp
164 }
165 return out
166 }
167