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