generate.go raw
1 package crypto
2
3 import (
4 "context"
5 "crypto/rand"
6 "encoding/binary"
7 "errors"
8 "fmt"
9
10 "git.mleku.dev/mleku/dendrite/pkg/axiom"
11 "git.mleku.dev/mleku/dendrite/pkg/dissolve"
12 "git.mleku.dev/mleku/dendrite/pkg/grow"
13 "git.mleku.dev/mleku/dendrite/pkg/lattice"
14 "git.mleku.dev/mleku/dendrite/pkg/ratio"
15 )
16
17 // Generate produces a self-contained keypair. It builds a lattice from
18 // scratch, seeds it with random elements, runs dissolution to establish
19 // equilibrium, and extracts the keypair.
20 //
21 // tags defines the constraint type layers. factory maps each tag to
22 // a constraint implementation — this becomes the private key (trapdoor).
23 func Generate(params Params, tags []string, factory func(string) axiom.Constraint) (*KeyPair, error) {
24 if !params.Valid() {
25 return nil, errors.New("crypto: invalid parameters")
26 }
27 if len(tags) == 0 {
28 return nil, errors.New("crypto: no type tags")
29 }
30 if factory == nil {
31 return nil, errors.New("crypto: nil constraint factory")
32 }
33
34 // 1. Build lattice with N nodes distributed across tags.
35 l := lattice.New()
36 nodesPerTag := params.N / len(tags)
37 if nodesPerTag < 1 {
38 nodesPerTag = 1
39 }
40
41 type tagGroup struct {
42 tag string
43 nodes []*lattice.Node
44 }
45 groups := make([]tagGroup, len(tags))
46
47 for i, tag := range tags {
48 groups[i].tag = tag
49 for range nodesPerTag {
50 n := l.AddNode([]axiom.Constraint{factory(tag)})
51 n.SetEnergy(true)
52 groups[i].nodes = append(groups[i].nodes, n)
53 }
54 }
55
56 // 2. Ring topology within each type + cross-type bridges.
57 for _, g := range groups {
58 for j := range g.nodes {
59 l.Connect(g.nodes[j], g.nodes[(j+1)%len(g.nodes)])
60 }
61 }
62 for i := 0; i < len(groups); i++ {
63 for j := i + 1; j < len(groups); j++ {
64 a, b := groups[i].nodes, groups[j].nodes
65 step := max(1, min(len(a), len(b))/3)
66 for k := 0; k < min(len(a), len(b)); k += step {
67 l.Connect(a[k], b[k])
68 }
69 }
70 }
71
72 // 3. Seed with random elements via Brownian walk growth.
73 // Each seed element carries random permutation and projection data
74 // so the resulting lattice has a unique structural fingerprint.
75 seedCount := l.Size() / 2 // aim for ~50% occupancy
76 solution := make(chan axiom.Element, seedCount)
77 for i := range seedCount {
78 tag := tags[i%len(tags)]
79 val := randomByte()
80 rb := randomByte()
81 rb2 := randomByte()
82 solution <- seedElement{
83 tag: tag,
84 val: val,
85 perm: rb % 6,
86 projVertex: rb2 & 0x07,
87 projKey: (rb2 >> 3) & 0x07,
88 projPath: randomUint16(),
89 }
90 }
91 close(solution)
92
93 events := make(chan grow.Event, seedCount*2)
94 ctx := context.Background()
95 grow.Run(ctx, l, solution, grow.Config{
96 MaxSteps: params.MaxWalkSteps,
97 Workers: 4,
98 }, events)
99 close(events)
100 for range events {
101 }
102
103 // 4. Dissolution passes to establish equilibrium.
104 for range params.DissolutionPasses {
105 dissolved := make(chan axiom.Element, l.Size())
106 dissEvents := make(chan dissolve.Event, l.Size())
107 dissolve.ScanOnce(l, dissolve.Config{
108 Threshold: params.SmoothingParam,
109 }, dissolved, dissEvents)
110 close(dissolved)
111 close(dissEvents)
112 for range dissolved {
113 }
114 for range dissEvents {
115 }
116 }
117
118 // 5. Extract keypair.
119 return GenerateKeyPair(l, params, factory), nil
120 }
121
122 // seedElement is a random element used during key generation.
123 // It carries random permutation and projection data so that
124 // each generated keypair produces a unique structural fingerprint
125 // (PermDist, ProjDist) in the spore. Without these, all lattices
126 // built from the same tags/params would be structurally identical.
127 type seedElement struct {
128 tag string
129 val byte
130 perm uint8 // random S_3 permutation (0-5)
131 projVertex uint8 // random projection vertex (0-7)
132 projKey uint8 // random projection key (0-7)
133 projPath uint16 // random rendering path
134 }
135
136 func (e seedElement) Type() string { return e.tag }
137 func (e seedElement) Value() any { return e.val }
138 func (e seedElement) Permutation() uint8 { return e.perm }
139 func (e seedElement) ProjectionVertex() uint8 { return e.projVertex }
140 func (e seedElement) ProjectionKey() uint8 { return e.projKey }
141 func (e seedElement) ProjectionPath() uint16 { return e.projPath }
142
143 // randomByte returns a cryptographically random byte.
144 func randomByte() byte {
145 var b [1]byte
146 if _, err := rand.Read(b[:]); err != nil {
147 panic(fmt.Sprintf("crypto/rand: %v", err))
148 }
149 return b[0]
150 }
151
152 // randomUint16 returns a cryptographically random uint16.
153 func randomUint16() uint16 {
154 var b [2]byte
155 if _, err := rand.Read(b[:]); err != nil {
156 panic(fmt.Sprintf("crypto/rand: %v", err))
157 }
158 return binary.LittleEndian.Uint16(b[:])
159 }
160
161 // cloneLattice creates a new lattice with the same topology and occupancy.
162 // The clone is independent — mutations don't affect the original.
163 func cloneLattice(src *lattice.Lattice, factory func(string) axiom.Constraint) *lattice.Lattice {
164 dst := lattice.New()
165 srcNodes := src.Nodes()
166
167 // Recreate all nodes with same constraints.
168 dstNodes := make([]*lattice.Node, len(srcNodes))
169 for i, n := range srcNodes {
170 constraints := n.Constraints()
171 clonedConstraints := make([]axiom.Constraint, len(constraints))
172 for j, c := range constraints {
173 if factory != nil {
174 clonedConstraints[j] = factory(c.Tag())
175 } else {
176 clonedConstraints[j] = c
177 }
178 }
179 dstNodes[i] = dst.AddNode(clonedConstraints)
180
181 // Copy energy state.
182 h := n.Hexagram()
183 dstNodes[i].SetEnergy(h.Inner().Energy())
184
185 // Copy projection and permutation.
186 dstNodes[i].SetPermutation(n.Permutation())
187 dstNodes[i].SetProjection(n.ProjectionVertex(), n.ProjectionKey(), n.ProjectionPath())
188 }
189
190 // Recreate neighbor connections.
191 for i, n := range srcNodes {
192 for _, nb := range n.Neighbors() {
193 nbID := int(nb.ID())
194 if nbID > i { // only connect once per pair
195 dst.Connect(dstNodes[i], dstNodes[nbID])
196 }
197 }
198 }
199
200 // Re-bond occupied sites.
201 for i, n := range srcNodes {
202 if n.Occupied() {
203 occ := n.Occupant()
204 dstNodes[i].Bond(occ)
205 }
206 }
207
208 return dst
209 }
210
211 // occupiedCount returns the number of occupied nodes in a lattice.
212 func occupiedCount(l *lattice.Lattice) int {
213 count := 0
214 for _, n := range l.Nodes() {
215 if n.Occupied() {
216 count++
217 }
218 }
219 return count
220 }
221
222 // Unused import guard for ratio — needed by dissolution Config.
223 var _ = ratio.Zero
224