tinyrelayfile
tinyrelay/protocol/nostr/event.go/

protocol/nostr/event.go raw
1package nostr
2
3import (
4 "bytes"
5 "crypto/rand"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "strconv"
12 "strings"
13
14 "fiatjaf.com/nostr"
15 "fiatjaf.com/nostr/nip13"
16 "github.com/btcsuite/btcd/btcec/v2"
17 "github.com/btcsuite/btcd/btcec/v2/schnorr"
18)
19
20type Event struct {
21 ID string `json:"id"`
22 PubKey string `json:"pubkey"`
23 CreatedAt int64 `json:"created_at"`
24 Kind int `json:"kind"`
25 Tags [][]string `json:"tags"`
26 Content string `json:"content"`
27 Sig string `json:"sig"`
28}
29
30// Validate checks the wire shape, canonical ID and Schnorr signature of e.
31// Kind-specific checks are applied by the protocol gate that owns the kind.
32func Validate(e Event) error {
33 if !hex64(e.ID) {
34 return errors.New("invalid: bad id")
35 }
36 if !hex64(e.PubKey) {
37 return errors.New("invalid: bad pubkey")
38 }
39 if len(e.Sig) != 128 || !isHex(e.Sig) {
40 return errors.New("invalid: bad sig")
41 }
42 if e.Kind < 0 || e.Kind > 65535 {
43 return errors.New("invalid: kind out of range")
44 }
45 if e.CreatedAt < 0 {
46 return errors.New("invalid: bad created_at")
47 }
48 if e.Tags == nil {
49 return errors.New("invalid: tags must be a list")
50 }
51 for _, tag := range e.Tags {
52 if len(tag) == 0 {
53 return errors.New("invalid: bad tag")
54 }
55 }
56 serialized, err := serializeForID(e)
57 if err != nil {
58 return errors.New("invalid: cannot serialize event")
59 }
60 sum := sha256.Sum256(serialized)
61 if e.ID != hex.EncodeToString(sum[:]) || !verifySignature(e, sum[:]) {
62 return errors.New("invalid: id or signature does not match content")
63 }
64 return nil
65}
66
67// Parse decodes and validates one complete JSON event object.
68func Parse(raw []byte) (Event, error) {
69 if err := requireFields(raw); err != nil {
70 return Event{}, err
71 }
72 var e Event
73 if err := json.Unmarshal(raw, &e); err != nil {
74 return Event{}, fmt.Errorf("invalid event JSON: %w", err)
75 }
76 if err := Validate(e); err != nil {
77 return Event{}, err
78 }
79 return e, nil
80}
81
82// Canonical encodes e as the JSON representation used on relay boundaries.
83func Canonical(e Event) ([]byte, error) {
84 var out bytes.Buffer
85 encoder := json.NewEncoder(&out)
86 encoder.SetEscapeHTML(false)
87 if err := encoder.Encode(e); err != nil {
88 return nil, err
89 }
90 return bytes.TrimSuffix(out.Bytes(), []byte{'\n'}), nil
91}
92
93// Tag returns the first value for name, or an empty string when the tag is
94// absent or has no value.
95func Tag(e Event, name string) string {
96 for _, tag := range e.Tags {
97 if len(tag) > 0 && tag[0] == name && len(tag) > 1 {
98 return tag[1]
99 }
100 }
101 return ""
102}
103
104// TagValues returns every first value belonging to tags named name.
105func TagValues(e Event, name string) []string {
106 values := make([]string, 0)
107 for _, tag := range e.Tags {
108 if len(tag) > 1 && tag[0] == name {
109 values = append(values, tag[1])
110 }
111 }
112 return values
113}
114
115// Expiration returns the positive Unix timestamp in the expiration tag.
116// Malformed, missing and nonpositive values return zero.
117func Expiration(e Event) int64 {
118 v, err := strconv.ParseInt(Tag(e, "expiration"), 10, 64)
119 if err != nil || v <= 0 {
120 return 0
121 }
122 return v
123}
124
125// Difficulty returns the NIP-13 proof-of-work difficulty encoded by id.
126func Difficulty(id string) int {
127 parsed, err := nostr.IDFromHex(id)
128 if err != nil {
129 return 0
130 }
131 return nip13.Difficulty(parsed)
132}
133
134// CommittedDifficulty returns the target in a nonce tag's third field.
135func CommittedDifficulty(e Event) int {
136 for _, tag := range e.Tags {
137 if len(tag) > 2 && tag[0] == "nonce" {
138 n, _ := strconv.Atoi(tag[2])
139 return n
140 }
141 }
142 return 0
143}
144
145// Sign derives the public key, ID and Schnorr signature for e from secretHex.
146// A nil tag slice is encoded as an empty tag list before signing.
147func Sign(e *Event, secretHex string) error {
148 secret, err := decodeSecret(secretHex)
149 if err != nil {
150 return err
151 }
152 if e.Tags == nil {
153 e.Tags = [][]string{}
154 }
155 key, pubkey := btcec.PrivKeyFromBytes(secret[:])
156 e.PubKey = hex.EncodeToString(schnorr.SerializePubKey(pubkey))
157 serialized, err := serializeForID(*e)
158 if err != nil {
159 return fmt.Errorf("serialize event: %w", err)
160 }
161 sum := sha256.Sum256(serialized)
162 signature, err := schnorr.Sign(key, sum[:])
163 if err != nil {
164 return fmt.Errorf("sign event: %w", err)
165 }
166 e.ID = hex.EncodeToString(sum[:])
167 e.Sig = hex.EncodeToString(signature.Serialize())
168 return nil
169}
170
171// GenerateKey creates a random nonzero 32-byte private key in hexadecimal.
172func GenerateKey() (string, error) {
173 for {
174 key := make([]byte, 32)
175 if _, err := rand.Read(key); err != nil {
176 return "", fmt.Errorf("generate private key: %w", err)
177 }
178 if !allZero(key) {
179 return hex.EncodeToString(key), nil
180 }
181 }
182}
183
184// PublicKey derives the x-only Schnorr public key for secretHex.
185func PublicKey(secretHex string) (string, error) {
186 secret, err := decodeSecret(secretHex)
187 if err != nil {
188 return "", err
189 }
190 _, pubkey := btcec.PrivKeyFromBytes(secret[:])
191 return hex.EncodeToString(schnorr.SerializePubKey(pubkey)), nil
192}
193
194// IsEphemeral reports whether k is in Nostr's ephemeral kind range.
195func IsEphemeral(k int) bool { return k >= 20000 && k < 30000 }
196
197// IsReplaceable reports whether k is a replaceable event kind.
198func IsReplaceable(k int) bool { return k == 0 || k == 3 || k >= 10000 && k < 20000 }
199
200// IsAddressable reports whether k is an addressable event kind.
201func IsAddressable(k int) bool { return k >= 30000 && k < 40000 }
202
203func hex64(s string) bool { return len(s) == 64 && isHex(s) }
204func isHex(s string) bool {
205 for _, char := range s {
206 if char >= '0' && char <= '9' || char >= 'a' && char <= 'f' {
207 continue
208 }
209 return false
210 }
211 return true
212}
213
214func serializeForID(e Event) ([]byte, error) {
215 value := []any{0, e.PubKey, e.CreatedAt, e.Kind, e.Tags, e.Content}
216 var out bytes.Buffer
217 encoder := json.NewEncoder(&out)
218 encoder.SetEscapeHTML(false)
219 if err := encoder.Encode(value); err != nil {
220 return nil, err
221 }
222 serialized := bytes.TrimSuffix(out.Bytes(), []byte{'\n'})
223 serialized = bytes.ReplaceAll(serialized, []byte(`\u2028`), []byte("\u2028"))
224 serialized = bytes.ReplaceAll(serialized, []byte(`\u2029`), []byte("\u2029"))
225 return serialized, nil
226}
227
228func verifySignature(e Event, hash []byte) bool {
229 pubkey, err := hex.DecodeString(e.PubKey)
230 if err != nil {
231 return false
232 }
233 key, err := schnorr.ParsePubKey(pubkey)
234 if err != nil {
235 return false
236 }
237 signature, err := hex.DecodeString(e.Sig)
238 if err != nil {
239 return false
240 }
241 sig, err := schnorr.ParseSignature(signature)
242 return err == nil && sig.Verify(hash, key)
243}
244
245func decodeSecret(value string) ([32]byte, error) {
246 var secret [32]byte
247 if len(value) != 64 {
248 return secret, errors.New("invalid secret key: expected 64 hex characters")
249 }
250 if _, err := hex.Decode(secret[:], []byte(strings.ToLower(value))); err != nil {
251 return secret, fmt.Errorf("invalid secret key: %w", err)
252 }
253 if allZero(secret[:]) {
254 return secret, errors.New("invalid secret key")
255 }
256 return secret, nil
257}
258
259func allZero(value []byte) bool {
260 for _, item := range value {
261 if item != 0 {
262 return false
263 }
264 }
265 return true
266}
267
268func requireFields(raw []byte) error {
269 var fields map[string]json.RawMessage
270 if err := json.Unmarshal(raw, &fields); err != nil {
271 return fmt.Errorf("invalid event JSON: %w", err)
272 }
273 for _, field := range []string{"id", "pubkey", "created_at", "kind", "tags", "content", "sig"} {
274 if _, ok := fields[field]; !ok {
275 return fmt.Errorf("invalid: missing %s", field)
276 }
277 }
278 return nil
279}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs