trust_act.go raw
1 package directory
2
3 import (
4 "crypto/rand"
5 "strconv"
6 "strings"
7 "time"
8
9 "next.orly.dev/pkg/lol/chk"
10 "next.orly.dev/pkg/lol/errorf"
11 "next.orly.dev/pkg/nostr/encoders/event"
12 "next.orly.dev/pkg/nostr/encoders/tag"
13 )
14
15 // TrustAct represents a complete Trust Act event (Kind 39101)
16 // with typed access to its components.
17 type TrustAct struct {
18 Event *event.E
19 TargetPubkey string
20 TrustLevel TrustLevel
21 RelayURL string
22 Expiry *time.Time
23 Reason TrustReason
24 ReplicationKinds []uint16
25 IdentityTag *IdentityTag
26 }
27
28 // IdentityTag represents the I tag with npub identity and proof-of-control.
29 type IdentityTag struct {
30 NPubIdentity string
31 Nonce string
32 Signature string
33 }
34
35 // NewTrustAct creates a new Trust Act event.
36 func NewTrustAct(
37 pubkey []byte,
38 targetPubkey string,
39 trustLevel TrustLevel,
40 relayURL string,
41 expiry *time.Time,
42 reason TrustReason,
43 replicationKinds []uint16,
44 identityTag *IdentityTag,
45 ) (ta *TrustAct, err error) {
46
47 // Validate required fields
48 if len(pubkey) != 32 {
49 return nil, errorf.E("pubkey must be 32 bytes")
50 }
51 if targetPubkey == "" {
52 return nil, errorf.E("target pubkey is required")
53 }
54 if len(targetPubkey) != 64 {
55 return nil, errorf.E("target pubkey must be 64 hex characters")
56 }
57 if err = ValidateTrustLevel(trustLevel); chk.E(err) {
58 return
59 }
60 if relayURL == "" {
61 return nil, errorf.E("relay URL is required")
62 }
63
64 // Create base event
65 ev := CreateBaseEvent(pubkey, TrustActKind)
66
67 // Add required tags
68 ev.Tags.Append(tag.NewFromAny(string(PubkeyTag), targetPubkey))
69 ev.Tags.Append(tag.NewFromAny(string(TrustLevelTag), strconv.FormatUint(uint64(trustLevel), 10)))
70 ev.Tags.Append(tag.NewFromAny(string(RelayTag), relayURL))
71
72 // Add optional expiry
73 if expiry != nil {
74 ev.Tags.Append(tag.NewFromAny(string(ExpiryTag), strconv.FormatInt(expiry.Unix(), 10)))
75 }
76
77 // Add reason
78 if reason != "" {
79 ev.Tags.Append(tag.NewFromAny(string(ReasonTag), string(reason)))
80 }
81
82 // Add replication kinds (K tag)
83 if len(replicationKinds) > 0 {
84 var kindStrings []string
85 for _, k := range replicationKinds {
86 kindStrings = append(kindStrings, strconv.FormatUint(uint64(k), 10))
87 }
88 ev.Tags.Append(tag.NewFromAny(string(KTag), strings.Join(kindStrings, ",")))
89 }
90
91 // Add identity tag if provided
92 if identityTag != nil {
93 if err = identityTag.Validate(); chk.E(err) {
94 return
95 }
96 ev.Tags.Append(tag.NewFromAny(string(ITag),
97 identityTag.NPubIdentity,
98 identityTag.Nonce,
99 identityTag.Signature))
100 }
101
102 ta = &TrustAct{
103 Event: ev,
104 TargetPubkey: targetPubkey,
105 TrustLevel: trustLevel,
106 RelayURL: relayURL,
107 Expiry: expiry,
108 Reason: reason,
109 ReplicationKinds: replicationKinds,
110 IdentityTag: identityTag,
111 }
112
113 return
114 }
115
116 // ParseTrustAct parses an event into a TrustAct structure
117 // with validation.
118 func ParseTrustAct(ev *event.E) (ta *TrustAct, err error) {
119 if ev == nil {
120 return nil, errorf.E("event cannot be nil")
121 }
122
123 // Validate event kind
124 if ev.Kind != TrustActKind.K {
125 return nil, errorf.E("invalid event kind: expected %d, got %d",
126 TrustActKind.K, ev.Kind)
127 }
128
129 // Extract required tags
130 pTag := ev.Tags.GetFirst(PubkeyTag)
131 if pTag == nil {
132 return nil, errorf.E("missing p tag")
133 }
134
135 trustLevelTag := ev.Tags.GetFirst(TrustLevelTag)
136 if trustLevelTag == nil {
137 return nil, errorf.E("missing trust_level tag")
138 }
139
140 relayTag := ev.Tags.GetFirst(RelayTag)
141 if relayTag == nil {
142 return nil, errorf.E("missing relay tag")
143 }
144
145 // Validate trust level
146 var trustLevelValue uint64
147 if trustLevelValue, err = strconv.ParseUint(string(trustLevelTag.Value()), 10, 8); chk.E(err) {
148 return nil, errorf.E("invalid trust level: %w", err)
149 }
150 trustLevel := TrustLevel(trustLevelValue)
151 if err = ValidateTrustLevel(trustLevel); chk.E(err) {
152 return
153 }
154
155 // Parse optional expiry
156 var expiry *time.Time
157 expiryTag := ev.Tags.GetFirst(ExpiryTag)
158 if expiryTag != nil {
159 var expiryUnix int64
160 if expiryUnix, err = strconv.ParseInt(string(expiryTag.Value()), 10, 64); chk.E(err) {
161 return nil, errorf.E("invalid expiry timestamp: %w", err)
162 }
163 expiryTime := time.Unix(expiryUnix, 0)
164 expiry = &expiryTime
165 }
166
167 // Parse optional reason
168 var reason TrustReason
169 reasonTag := ev.Tags.GetFirst(ReasonTag)
170 if reasonTag != nil {
171 reason = TrustReason(reasonTag.Value())
172 }
173
174 // Parse replication kinds (K tag)
175 var replicationKinds []uint16
176 kTag := ev.Tags.GetFirst(KTag)
177 if kTag != nil {
178 kindStrings := strings.Split(string(kTag.Value()), ",")
179 for _, kindStr := range kindStrings {
180 kindStr = strings.TrimSpace(kindStr)
181 if kindStr == "" {
182 continue
183 }
184 var kind uint64
185 if kind, err = strconv.ParseUint(kindStr, 10, 16); chk.E(err) {
186 return nil, errorf.E("invalid kind in K tag: %s", kindStr)
187 }
188 replicationKinds = append(replicationKinds, uint16(kind))
189 }
190 }
191
192 // Parse identity tag (I tag)
193 var identityTag *IdentityTag
194 iTag := ev.Tags.GetFirst(ITag)
195 if iTag != nil {
196 if identityTag, err = ParseIdentityTag(iTag); chk.E(err) {
197 return
198 }
199 }
200
201 ta = &TrustAct{
202 Event: ev,
203 TargetPubkey: string(pTag.ValueHex()), // ValueHex() handles binary/hex storage
204 TrustLevel: trustLevel,
205 RelayURL: string(relayTag.Value()),
206 Expiry: expiry,
207 Reason: reason,
208 ReplicationKinds: replicationKinds,
209 IdentityTag: identityTag,
210 }
211
212 return
213 }
214
215 // ParseIdentityTag parses an I tag into an IdentityTag structure.
216 func ParseIdentityTag(t *tag.T) (it *IdentityTag, err error) {
217 if t == nil {
218 return nil, errorf.E("tag cannot be nil")
219 }
220
221 if t.Len() < 4 {
222 return nil, errorf.E("I tag must have at least 4 elements")
223 }
224
225 // First element should be "I"
226 if string(t.T[0]) != "I" {
227 return nil, errorf.E("invalid I tag key")
228 }
229
230 it = &IdentityTag{
231 NPubIdentity: string(t.T[1]),
232 Nonce: string(t.T[2]),
233 Signature: string(t.T[3]),
234 }
235
236 if err = it.Validate(); chk.E(err) {
237 return nil, err
238 }
239 return it, nil
240 }
241
242 // Validate performs validation of an IdentityTag.
243 func (it *IdentityTag) Validate() (err error) {
244 if it == nil {
245 return errorf.E("IdentityTag cannot be nil")
246 }
247
248 if it.NPubIdentity == "" {
249 return errorf.E("npub identity is required")
250 }
251
252 if !strings.HasPrefix(it.NPubIdentity, "npub1") {
253 return errorf.E("identity must be npub-encoded")
254 }
255
256 if it.Nonce == "" {
257 return errorf.E("nonce is required")
258 }
259
260 if len(it.Nonce) < 32 { // Minimum 16 bytes hex-encoded
261 return errorf.E("nonce must be at least 16 bytes (32 hex characters)")
262 }
263
264 if it.Signature == "" {
265 return errorf.E("signature is required")
266 }
267
268 if len(it.Signature) != 128 { // 64 bytes hex-encoded
269 return errorf.E("signature must be 64 bytes (128 hex characters)")
270 }
271
272 return nil
273 }
274
275 // Validate performs comprehensive validation of a TrustAct.
276 func (ta *TrustAct) Validate() (err error) {
277 if ta == nil {
278 return errorf.E("TrustAct cannot be nil")
279 }
280
281 if ta.Event == nil {
282 return errorf.E("event cannot be nil")
283 }
284
285 // Validate event signature
286 if _, err = ta.Event.Verify(); chk.E(err) {
287 return errorf.E("invalid event signature: %w", err)
288 }
289
290 // Validate required fields
291 if ta.TargetPubkey == "" {
292 return errorf.E("target pubkey is required")
293 }
294
295 if len(ta.TargetPubkey) != 64 {
296 return errorf.E("target pubkey must be 64 hex characters")
297 }
298
299 if err = ValidateTrustLevel(ta.TrustLevel); chk.E(err) {
300 return
301 }
302
303 if ta.RelayURL == "" {
304 return errorf.E("relay URL is required")
305 }
306
307 // Validate expiry if present
308 if ta.Expiry != nil && ta.Expiry.Before(time.Now()) {
309 return errorf.E("trust act has expired")
310 }
311
312 // Validate identity tag if present
313 if ta.IdentityTag != nil {
314 if err = ta.IdentityTag.Validate(); chk.E(err) {
315 return
316 }
317 }
318
319 return nil
320 }
321
322 // IsExpired returns true if the trust act has expired.
323 func (ta *TrustAct) IsExpired() bool {
324 return ta.Expiry != nil && ta.Expiry.Before(time.Now())
325 }
326
327 // HasReplicationKind returns true if the act includes the specified
328 // kind for replication.
329 func (ta *TrustAct) HasReplicationKind(kind uint16) bool {
330 for _, k := range ta.ReplicationKinds {
331 if k == kind {
332 return true
333 }
334 }
335 return false
336 }
337
338 // ShouldReplicate returns true if an event of the given kind should be
339 // replicated based on this trust act.
340 func (ta *TrustAct) ShouldReplicate(kind uint16) bool {
341 // Directory events are always replicated
342 if IsDirectoryEventKind(kind) {
343 return true
344 }
345
346 // Check if kind is in the replication list
347 return ta.HasReplicationKind(kind)
348 }
349
350 // ShouldReplicateEvent determines whether a specific event should be replicated
351 // based on the trust level using partial replication (random dice-throw).
352 // This function uses crypto/rand for cryptographically secure randomness.
353 func (ta *TrustAct) ShouldReplicateEvent(kind uint16) (shouldReplicate bool, err error) {
354 // Check if kind is eligible for replication
355 if !ta.ShouldReplicate(kind) {
356 return false, nil
357 }
358
359 // Trust level of 100 means always replicate
360 if ta.TrustLevel == TrustLevelFull {
361 return true, nil
362 }
363
364 // Trust level of 0 means never replicate
365 if ta.TrustLevel == TrustLevelNone {
366 return false, nil
367 }
368
369 // Generate cryptographically secure random number 0-100
370 var randomBytes [1]byte
371 if _, err = rand.Read(randomBytes[:]); chk.E(err) {
372 return false, errorf.E("failed to generate random number: %w", err)
373 }
374
375 // Scale byte value (0-255) to 0-100 range
376 randomValue := uint8((uint16(randomBytes[0]) * 101) / 256)
377
378 // Replicate if random value is less than or equal to trust level
379 shouldReplicate = randomValue <= uint8(ta.TrustLevel)
380 return
381 }
382
383 // GetTargetPubkey returns the target relay's public key.
384 func (ta *TrustAct) GetTargetPubkey() string {
385 return ta.TargetPubkey
386 }
387
388 // GetTrustLevel returns the trust level.
389 func (ta *TrustAct) GetTrustLevel() TrustLevel {
390 return ta.TrustLevel
391 }
392
393 // GetRelayURL returns the target relay's URL.
394 func (ta *TrustAct) GetRelayURL() string {
395 return ta.RelayURL
396 }
397
398 // GetExpiry returns the expiry time, or nil if no expiry is set.
399 func (ta *TrustAct) GetExpiry() *time.Time {
400 return ta.Expiry
401 }
402
403 // GetReason returns the reason for the trust relationship.
404 func (ta *TrustAct) GetReason() TrustReason {
405 return ta.Reason
406 }
407
408 // GetReplicationKinds returns the list of event kinds to replicate.
409 func (ta *TrustAct) GetReplicationKinds() []uint16 {
410 return ta.ReplicationKinds
411 }
412
413 // GetIdentityTag returns the identity tag, or nil if not present.
414 func (ta *TrustAct) GetIdentityTag() *IdentityTag {
415 return ta.IdentityTag
416 }
417