identity.go raw

   1  package nostr
   2  
   3  import (
   4  	"crypto/rand"
   5  	"encoding/hex"
   6  	"encoding/json"
   7  	"fmt"
   8  	"time"
   9  
  10  	"github.com/btcsuite/btcd/btcec/v2"
  11  	"github.com/btcsuite/btcd/btcec/v2/schnorr"
  12  )
  13  
  14  // Identity is a Nostr keypair — the organism's external face.
  15  // Distinct from the birthmark (internal fingerprint), the identity
  16  // is what other Nostr clients and relays see.
  17  type Identity struct {
  18  	PrivKey *btcec.PrivateKey
  19  	PubKey  string // 32-byte hex x-only pubkey
  20  }
  21  
  22  // NewIdentity generates a fresh Nostr identity from crypto/rand entropy.
  23  func NewIdentity() (*Identity, error) {
  24  	var seed [32]byte
  25  	if _, err := rand.Read(seed[:]); err != nil {
  26  		return nil, fmt.Errorf("entropy: %w", err)
  27  	}
  28  
  29  	privKey, _ := btcec.PrivKeyFromBytes(seed[:])
  30  	pubKey := schnorr.SerializePubKey(privKey.PubKey())
  31  
  32  	return &Identity{
  33  		PrivKey: privKey,
  34  		PubKey:  hex.EncodeToString(pubKey),
  35  	}, nil
  36  }
  37  
  38  // PrivKeyHex returns the private key as a hex string for signing.
  39  func (id *Identity) PrivKeyHex() string {
  40  	return hex.EncodeToString(id.PrivKey.Serialize())
  41  }
  42  
  43  // Metadata holds NIP-01 kind 0 metadata fields.
  44  type Metadata struct {
  45  	Name    string `json:"name"`
  46  	About   string `json:"about"`
  47  	Picture string `json:"picture,omitempty"`
  48  }
  49  
  50  // ComposeMetadata creates a signed kind 0 event describing this instance.
  51  func (id *Identity) ComposeMetadata(instanceID uint32, generation int, sporeHash string) (*Event, error) {
  52  	meta := Metadata{
  53  		Name:  fmt.Sprintf("dendrite-inst%d", instanceID),
  54  		About: fmt.Sprintf("dendrite lattice organism, generation %d, spore %s", generation, truncHash(sporeHash)),
  55  	}
  56  
  57  	content, err := json.Marshal(meta)
  58  	if err != nil {
  59  		return nil, err
  60  	}
  61  
  62  	ev := &Event{
  63  		CreatedAt: time.Now().Unix(),
  64  		Kind:      0,
  65  		Tags:      [][]string{},
  66  		Content:   string(content),
  67  	}
  68  
  69  	if err := ev.Sign(id.PrivKeyHex()); err != nil {
  70  		return nil, fmt.Errorf("sign metadata: %w", err)
  71  	}
  72  
  73  	return ev, nil
  74  }
  75  
  76  // ComposeStatus creates a signed kind 1 event describing current lattice state.
  77  func (id *Identity) ComposeStatus(instanceID uint32, generation int, totalNodes int, occupied int, occupancy float64, fitness float64, peerCount int) (*Event, error) {
  78  	content := fmt.Sprintf(
  79  		"dendrite inst%d gen%d: %d nodes, %d occupied (%.0f%%), fitness=%.3f, %d peers",
  80  		instanceID, generation, totalNodes, occupied, occupancy*100, fitness, peerCount,
  81  	)
  82  
  83  	ev := &Event{
  84  		CreatedAt: time.Now().Unix(),
  85  		Kind:      1,
  86  		Tags: [][]string{
  87  			{"t", "dendrite"},
  88  			{"t", "lattice"},
  89  			{"t", fmt.Sprintf("gen%d", generation)},
  90  		},
  91  		Content: content,
  92  	}
  93  
  94  	if err := ev.Sign(id.PrivKeyHex()); err != nil {
  95  		return nil, fmt.Errorf("sign status: %w", err)
  96  	}
  97  
  98  	return ev, nil
  99  }
 100  
 101  // truncHash returns the first 16 chars of a hash string.
 102  func truncHash(h string) string {
 103  	if len(h) > 16 {
 104  		return h[:16]
 105  	}
 106  	return h
 107  }
 108