compact_v2_test.go raw

   1  package crypto
   2  
   3  import (
   4  	"testing"
   5  
   6  	"git.mleku.dev/mleku/dendrite/pkg/spore"
   7  )
   8  
   9  func TestSubsetRoundTrip(t *testing.T) {
  10  	tests := []struct {
  11  		name    string
  12  		indices []int
  13  		n       int
  14  	}{
  15  		{"single", []int{0}, 256},
  16  		{"first_last", []int{0, 255}, 256},
  17  		{"consecutive", []int{10, 11, 12, 13, 14}, 256},
  18  		{"spread", []int{0, 50, 100, 150, 200, 250}, 256},
  19  		{"large_k", func() []int {
  20  			idx := make([]int, 56)
  21  			for i := range idx {
  22  				idx[i] = i * 4 // every 4th site
  23  			}
  24  			return idx
  25  		}(), 256},
  26  		{"n512", []int{0, 100, 200, 300, 400, 500}, 512},
  27  	}
  28  
  29  	for _, tt := range tests {
  30  		t.Run(tt.name, func(t *testing.T) {
  31  			encoded := encodeSubset(tt.indices, tt.n)
  32  			decoded := decodeSubset(encoded, tt.n, len(tt.indices))
  33  
  34  			if len(decoded) != len(tt.indices) {
  35  				t.Fatalf("length mismatch: got %d, want %d", len(decoded), len(tt.indices))
  36  			}
  37  			for i := range tt.indices {
  38  				if decoded[i] != tt.indices[i] {
  39  					t.Errorf("index %d: got %d, want %d", i, decoded[i], tt.indices[i])
  40  				}
  41  			}
  42  		})
  43  	}
  44  }
  45  
  46  func TestSubsetEncodingSize(t *testing.T) {
  47  	// C(256,56) has 190 bits → 24 bytes.
  48  	nbytes := subsetBytes(256, 56)
  49  	t.Logf("C(256,56) encoding: %d bytes (%d bits)", nbytes, bigBinom(256, 56).BitLen())
  50  	if nbytes != 24 {
  51  		t.Errorf("subset encoding unexpected: %d bytes (want 24)", nbytes)
  52  	}
  53  
  54  	// C(512,56) has 251 bits → 32 bytes.
  55  	nbytes512 := subsetBytes(512, 56)
  56  	t.Logf("C(512,56) encoding: %d bytes (%d bits)", nbytes512, bigBinom(512, 56).BitLen())
  57  	if nbytes512 != 32 {
  58  		t.Errorf("subset encoding unexpected: %d bytes (want 32)", nbytes512)
  59  	}
  60  }
  61  
  62  func TestBitWriterReader(t *testing.T) {
  63  	bw := newBitWriter(16)
  64  	bw.writeBits(5, 3)   // 101
  65  	bw.writeBits(200, 8) // 11001000
  66  	bw.writeBits(3, 3)   // 011
  67  	bw.writeBits(0, 2)   // 00
  68  
  69  	data := bw.bytes()
  70  
  71  	br := newBitReader(data)
  72  	v1, _ := br.readBits(3)
  73  	v2, _ := br.readBits(8)
  74  	v3, _ := br.readBits(3)
  75  	v4, _ := br.readBits(2)
  76  
  77  	if v1 != 5 {
  78  		t.Errorf("v1: got %d, want 5", v1)
  79  	}
  80  	if v2 != 200 {
  81  		t.Errorf("v2: got %d, want 200", v2)
  82  	}
  83  	if v3 != 3 {
  84  		t.Errorf("v3: got %d, want 3", v3)
  85  	}
  86  	if v4 != 0 {
  87  		t.Errorf("v4: got %d, want 0", v4)
  88  	}
  89  }
  90  
  91  func TestMarshalV2RoundTrip(t *testing.T) {
  92  	l := buildMatureLattice()
  93  	params := DefaultParams(Security128)
  94  	kp := GenerateKeyPair(l, params, testFactory)
  95  
  96  	msg := []byte("compact v2 round-trip")
  97  	sig, err := Sign(&kp.Private, msg, params)
  98  	if err != nil {
  99  		t.Fatalf("Sign: %v", err)
 100  	}
 101  
 102  	data, err := sig.MarshalV2(params.N)
 103  	if err != nil {
 104  		t.Fatalf("MarshalV2: %v", err)
 105  	}
 106  
 107  	sig2, consumed, err := UnmarshalSignatureV2(data)
 108  	if err != nil {
 109  		t.Fatalf("UnmarshalSignatureV2: %v", err)
 110  	}
 111  	if consumed != len(data) {
 112  		t.Errorf("consumed %d bytes, data is %d bytes", consumed, len(data))
 113  	}
 114  
 115  	// Occupied count must match.
 116  	origOcc := 0
 117  	for _, s := range sig.Response {
 118  		if s.Occupied {
 119  			origOcc++
 120  		}
 121  	}
 122  	if len(sig2.Response) != origOcc {
 123  		t.Errorf("occupied count: orig=%d, round-trip=%d", origOcc, len(sig2.Response))
 124  	}
 125  
 126  	// Per-site data must match for occupied sites.
 127  	origSites := make([]SiteMark, 0, origOcc)
 128  	for _, s := range sig.Response {
 129  		if s.Occupied {
 130  			origSites = append(origSites, s)
 131  		}
 132  	}
 133  
 134  	for i, s2 := range sig2.Response {
 135  		if i >= len(origSites) {
 136  			break
 137  		}
 138  		s1 := origSites[i]
 139  		if s2.Index != s1.Index {
 140  			t.Errorf("site %d: index %d != %d", i, s2.Index, s1.Index)
 141  		}
 142  		if s2.TypeTag != s1.TypeTag {
 143  			t.Errorf("site %d: tag %q != %q", i, s2.TypeTag, s1.TypeTag)
 144  		}
 145  		if s2.Projection != s1.Projection {
 146  			t.Errorf("site %d: projection %d != %d", i, s2.Projection, s1.Projection)
 147  		}
 148  		if s2.Perm != s1.Perm {
 149  			t.Errorf("site %d: perm %d != %d", i, s2.Perm, s1.Perm)
 150  		}
 151  	}
 152  
 153  	// Proof lengths must be consistent.
 154  	if len(sig2.Proof.LockIns) != len(sig2.Response) {
 155  		t.Error("proof/response length mismatch")
 156  	}
 157  }
 158  
 159  func TestMarshalV2Size(t *testing.T) {
 160  	l := buildMatureLattice()
 161  	params := DefaultParams(Security128)
 162  	kp := GenerateKeyPair(l, params, testFactory)
 163  
 164  	msg := []byte("size test v2")
 165  	sig, err := Sign(&kp.Private, msg, params)
 166  	if err != nil {
 167  		t.Fatalf("Sign: %v", err)
 168  	}
 169  
 170  	data, err := sig.MarshalV2(params.N)
 171  	if err != nil {
 172  		t.Fatalf("MarshalV2: %v", err)
 173  	}
 174  
 175  	v1data, err := sig.Marshal()
 176  	if err != nil {
 177  		t.Fatalf("Marshal V1: %v", err)
 178  	}
 179  
 180  	t.Logf("V1 compact: %d bytes", len(v1data))
 181  	t.Logf("V2 compact: %d bytes", len(data))
 182  	t.Logf("compression ratio: %.1f%%", 100.0*(1.0-float64(len(data))/float64(len(v1data))))
 183  
 184  	// V2 should be under 200 bytes for N=256.
 185  	if len(data) > 200 {
 186  		t.Errorf("V2 signature too large: %d bytes (want <= 200)", len(data))
 187  	}
 188  }
 189  
 190  func TestMarshalV2Deterministic(t *testing.T) {
 191  	l := buildMatureLattice()
 192  	params := DefaultParams(Security128)
 193  	kp := GenerateKeyPair(l, params, testFactory)
 194  
 195  	msg := []byte("deterministic v2")
 196  	sig, err := Sign(&kp.Private, msg, params)
 197  	if err != nil {
 198  		t.Fatalf("Sign: %v", err)
 199  	}
 200  
 201  	data1, err := sig.MarshalV2(params.N)
 202  	if err != nil {
 203  		t.Fatalf("MarshalV2 1: %v", err)
 204  	}
 205  	data2, err := sig.MarshalV2(params.N)
 206  	if err != nil {
 207  		t.Fatalf("MarshalV2 2: %v", err)
 208  	}
 209  
 210  	if string(data1) != string(data2) {
 211  		t.Error("MarshalV2 is not deterministic")
 212  	}
 213  }
 214  
 215  func TestVerifyCompactV2(t *testing.T) {
 216  	l := buildMatureLattice()
 217  	params := DefaultParams(Security128)
 218  	kp := GenerateKeyPair(l, params, testFactory)
 219  
 220  	msg := []byte("verify compact v2")
 221  	sig, err := Sign(&kp.Private, msg, params)
 222  	if err != nil {
 223  		t.Fatalf("Sign: %v", err)
 224  	}
 225  
 226  	data, err := sig.MarshalV2(params.N)
 227  	if err != nil {
 228  		t.Fatalf("MarshalV2: %v", err)
 229  	}
 230  
 231  	sig2, _, err := UnmarshalSignatureV2(data)
 232  	if err != nil {
 233  		t.Fatalf("UnmarshalSignatureV2: %v", err)
 234  	}
 235  
 236  	fp := FingerprintFromSpore(spore.Extract(kp.Private.Lattice))
 237  	if !VerifyCompactV2(fp, msg, sig2) {
 238  		t.Error("VerifyCompactV2 failed on valid signature")
 239  	}
 240  
 241  	// Wrong message should fail.
 242  	if VerifyCompactV2(fp, []byte("wrong message"), sig2) {
 243  		t.Error("VerifyCompactV2 accepted wrong message")
 244  	}
 245  }
 246  
 247  func TestMarshalV2NilSignature(t *testing.T) {
 248  	var s *Signature
 249  	_, err := s.MarshalV2(256)
 250  	if err == nil {
 251  		t.Error("expected error marshaling nil signature")
 252  	}
 253  }
 254  
 255  func TestUnmarshalV2Truncated(t *testing.T) {
 256  	_, _, err := UnmarshalSignatureV2([]byte{0xD2, 0x00})
 257  	if err == nil {
 258  		t.Error("expected error with truncated data")
 259  	}
 260  }
 261  
 262  func TestUnmarshalV2BadMagic(t *testing.T) {
 263  	_, _, err := UnmarshalSignatureV2([]byte{0xFF, 0x00, 0x01, 0x01, 0x01, 'x', 0x01, 0x00})
 264  	if err == nil {
 265  		t.Error("expected error with bad magic")
 266  	}
 267  }
 268