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