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