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