shadow_compress_test.go raw
1 package crypto
2
3 import (
4 "bytes"
5 "testing"
6
7 "git.mleku.dev/mleku/dendrite/pkg/epoch"
8 "git.mleku.dev/mleku/dendrite/pkg/state"
9 )
10
11 // TestShadowCompressDecompressRoundTrip tests full compress/decompress
12 // cycle with and without auxiliary data.
13 func TestShadowCompressDecompressRoundTrip(t *testing.T) {
14 ep := epoch.Colony
15
16 testCases := []struct {
17 name string
18 data []byte
19 aux []byte
20 }{
21 {"empty", nil, nil},
22 {"single byte", []byte{42}, nil},
23 {"short text", []byte("hello shadow compression"), nil},
24 {"with auxiliary", []byte("primary data stream"), []byte("aux")},
25 {"aux 1 byte", []byte("data for one byte aux"), []byte{0xFF}},
26 {"aux 2 bytes", []byte("data for two byte aux"), []byte{0x42, 0x37}},
27 {"aux 7 bytes", []byte("seven bytes of auxiliary payload here"), []byte("7bytes!")},
28 {"long text", bytes.Repeat([]byte("the shadow between binary and decimal "), 20), nil},
29 {"long with aux", bytes.Repeat([]byte("compress me "), 50), bytes.Repeat([]byte{0xAB}, 10)},
30 }
31
32 for _, tc := range testCases {
33 t.Run(tc.name, func(t *testing.T) {
34 sc, err := ShadowCompress(tc.data, tc.aux, ep)
35 if err != nil {
36 t.Fatalf("ShadowCompress: %v", err)
37 }
38
39 if len(tc.data) == 0 {
40 if sc.OrigLen != 0 {
41 t.Errorf("empty data: OrigLen = %d, want 0", sc.OrigLen)
42 }
43 return
44 }
45
46 if sc.OrigLen != len(tc.data) {
47 t.Errorf("OrigLen = %d, want %d", sc.OrigLen, len(tc.data))
48 }
49
50 data, aux, err := ShadowDecompress(sc, ep)
51 if err != nil {
52 t.Fatalf("ShadowDecompress: %v", err)
53 }
54
55 if !bytes.Equal(data, tc.data) {
56 t.Errorf("data mismatch:\n got: %x\n want: %x", data, tc.data)
57 }
58
59 if tc.aux != nil {
60 if !bytes.Equal(aux, tc.aux) {
61 t.Errorf("auxiliary mismatch:\n got: %x\n want: %x", aux, tc.aux)
62 }
63 }
64 })
65 }
66 }
67
68 // TestShadowCompressWireRoundTrip tests marshal/unmarshal cycle.
69 func TestShadowCompressWireRoundTrip(t *testing.T) {
70 ep := epoch.Colony
71 data := []byte("wire format round-trip test data")
72
73 sc, err := ShadowCompress(data, []byte("aux"), ep)
74 if err != nil {
75 t.Fatalf("ShadowCompress: %v", err)
76 }
77
78 wire, err := MarshalShadowCompressed(sc)
79 if err != nil {
80 t.Fatalf("Marshal: %v", err)
81 }
82
83 sc2, err := UnmarshalShadowCompressed(wire)
84 if err != nil {
85 t.Fatalf("Unmarshal: %v", err)
86 }
87
88 // Verify all fields match.
89 if sc2.OrigLen != sc.OrigLen {
90 t.Errorf("OrigLen: %d vs %d", sc2.OrigLen, sc.OrigLen)
91 }
92 if sc2.TokenCount != sc.TokenCount {
93 t.Errorf("TokenCount: %d vs %d", sc2.TokenCount, sc.TokenCount)
94 }
95 if sc2.EpochDec != sc.EpochDec || sc2.EpochBin != sc.EpochBin {
96 t.Errorf("Epoch: %d/%d vs %d/%d", sc2.EpochDec, sc2.EpochBin, sc.EpochDec, sc.EpochBin)
97 }
98 if sc2.ShadowPayloadLen != sc.ShadowPayloadLen {
99 t.Errorf("ShadowPayloadLen: %d vs %d", sc2.ShadowPayloadLen, sc.ShadowPayloadLen)
100 }
101 if !bytes.Equal(sc2.Primary, sc.Primary) {
102 t.Errorf("Primary stream mismatch")
103 }
104 if !bytes.Equal(sc2.Shadow, sc.Shadow) {
105 t.Errorf("Shadow stream mismatch")
106 }
107 if sc2.FreqTable != sc.FreqTable {
108 t.Errorf("FreqTable mismatch")
109 }
110 if sc2.ContentHash != sc.ContentHash {
111 t.Errorf("ContentHash mismatch")
112 }
113
114 // Verify full round-trip: unmarshal then decompress.
115 recovered, aux, err := ShadowDecompress(sc2, ep)
116 if err != nil {
117 t.Fatalf("ShadowDecompress after unmarshal: %v", err)
118 }
119 if !bytes.Equal(recovered, data) {
120 t.Errorf("data after wire round-trip: %x != %x", recovered, data)
121 }
122 if !bytes.Equal(aux, []byte("aux")) {
123 t.Errorf("aux after wire round-trip: %x", aux)
124 }
125 }
126
127 // TestHuffmanTreeDeterminism verifies that building the Huffman tree
128 // from the same frequency table always produces identical codebooks.
129 func TestHuffmanTreeDeterminism(t *testing.T) {
130 var freq [64]uint32
131 freq[0] = 100
132 freq[1] = 50
133 freq[2] = 25
134 freq[10] = 10
135 freq[63] = 5
136
137 total := int64(190)
138 var allCodes []map[uint8]int // symbol → code length
139
140 for range 100 {
141 tree := buildHuffmanTree(freq, total)
142 codes := make(map[uint8]int)
143 codeMap := make(map[state.Hexagram]huffCode)
144 buildCodes(tree, nil, codeMap)
145 for sym, code := range codeMap {
146 codes[uint8(sym)] = code.length
147 }
148 allCodes = append(allCodes, codes)
149 }
150
151 // All 100 codebooks should be identical.
152 for i := 1; i < len(allCodes); i++ {
153 for sym, length := range allCodes[0] {
154 if allCodes[i][sym] != length {
155 t.Errorf("iteration %d: symbol %d has length %d, want %d",
156 i, sym, allCodes[i][sym], length)
157 }
158 }
159 }
160 }
161
162 // TestShadowCompressRatio verifies that compression produces output
163 // smaller than or comparable to input for repetitive data.
164 func TestShadowCompressRatio(t *testing.T) {
165 ep := epoch.Colony
166
167 // Highly repetitive data should compress well.
168 data := bytes.Repeat([]byte("AAAA"), 1000)
169 sc, err := ShadowCompress(data, nil, ep)
170 if err != nil {
171 t.Fatalf("ShadowCompress: %v", err)
172 }
173
174 compressedSize := len(sc.Primary) + len(sc.Shadow) + shadowCompressedHeaderSize
175 ratio := float64(compressedSize) / float64(len(data))
176 t.Logf("repetitive data: %d bytes → %d bytes (ratio %.2f)",
177 len(data), compressedSize, ratio)
178
179 // The shadow stream adds 6 bits per token regardless of auxiliary
180 // data, so the total output includes structural overhead. For
181 // highly repetitive data the Huffman stream compresses well, but
182 // the shadow stream is proportional to token count. Ratio under
183 // 2.0 indicates the Huffman layer is contributing.
184 if ratio > 2.0 {
185 t.Errorf("compression ratio %.2f too high for repetitive data", ratio)
186 }
187 }
188
189 // TestShadowCompressEpochVariation verifies that different epochs
190 // produce different shadow streams but recover the same data.
191 func TestShadowCompressEpochVariation(t *testing.T) {
192 data := []byte("epoch variation test")
193
194 epochs := []epoch.Epoch{epoch.Colony, epoch.CryptoWalk128, epoch.CryptoWalk256}
195
196 for _, ep := range epochs {
197 sc, err := ShadowCompress(data, nil, ep)
198 if err != nil {
199 t.Fatalf("epoch %s: ShadowCompress: %v", ep, err)
200 }
201
202 recovered, _, err := ShadowDecompress(sc, ep)
203 if err != nil {
204 t.Fatalf("epoch %s: ShadowDecompress: %v", ep, err)
205 }
206
207 if !bytes.Equal(recovered, data) {
208 t.Errorf("epoch %s: recovered != original", ep)
209 }
210 }
211 }
212