compress_test.go raw
1 package blockchain
2
3 import (
4 "bytes"
5 "encoding/hex"
6 "testing"
7 )
8
9 // hexToBytes converts the passed hex string into bytes and will panic if there is an error. This is only provided for
10 // the hard-coded constants so errors in the source code can be detected. It will only (and must only) be called with
11 // hard-coded values.
12 func hexToBytes(s string) []byte {
13 b, e := hex.DecodeString(s)
14 if e != nil {
15 panic("invalid hex in source file: " + s)
16 }
17 return b
18 }
19
20 // TestVLQ ensures the variable length quantity serialization, deserialization, and size calculation works as expected.
21 func TestVLQ(t *testing.T) {
22 t.Parallel()
23 tests := []struct {
24 val uint64
25 serialized []byte
26 }{
27 {0, hexToBytes("00")},
28 {1, hexToBytes("01")},
29 {127, hexToBytes("7f")},
30 {128, hexToBytes("8000")},
31 {129, hexToBytes("8001")},
32 {255, hexToBytes("807f")},
33 {256, hexToBytes("8100")},
34 {16383, hexToBytes("fe7f")},
35 {16384, hexToBytes("ff00")},
36 {16511, hexToBytes("ff7f")}, // Max 2-byte value
37 {16512, hexToBytes("808000")},
38 {16513, hexToBytes("808001")},
39 {16639, hexToBytes("80807f")},
40 {32895, hexToBytes("80ff7f")},
41 {2113663, hexToBytes("ffff7f")}, // Max 3-byte value
42 {2113664, hexToBytes("80808000")},
43 {270549119, hexToBytes("ffffff7f")}, // Max 4-byte value
44 {270549120, hexToBytes("8080808000")},
45 {2147483647, hexToBytes("86fefefe7f")},
46 {2147483648, hexToBytes("86fefeff00")},
47 {4294967295, hexToBytes("8efefefe7f")}, // Max uint32, 5 bytes
48 // Max uint64, 10 bytes
49 {18446744073709551615, hexToBytes("80fefefefefefefefe7f")},
50 }
51 for _, test := range tests {
52 // Ensure the function to calculate the serialized size without actually serializing the value is calculated
53 // properly.
54 gotSize := serializeSizeVLQ(test.val)
55 if gotSize != len(test.serialized) {
56 t.Errorf("serializeSizeVLQ: did not get expected size "+
57 "for %d - got %d, want %d", test.val, gotSize,
58 len(test.serialized),
59 )
60 continue
61 }
62 // Ensure the value serializes to the expected bytes.
63 gotBytes := make([]byte, gotSize)
64 gotBytesWritten := putVLQ(gotBytes, test.val)
65 if !bytes.Equal(gotBytes, test.serialized) {
66 t.Errorf("putVLQUnchecked: did not get expected bytes "+
67 "for %d - got %x, want %x", test.val, gotBytes,
68 test.serialized,
69 )
70 continue
71 }
72 if gotBytesWritten != len(test.serialized) {
73 t.Errorf("putVLQUnchecked: did not get expected number "+
74 "of bytes written for %d - got %d, want %d",
75 test.val, gotBytesWritten, len(test.serialized),
76 )
77 continue
78 }
79 // Ensure the serialized bytes deserialize to the expected value.
80 gotVal, gotBytesRead := deserializeVLQ(test.serialized)
81 if gotVal != test.val {
82 t.Errorf("deserializeVLQ: did not get expected value "+
83 "for %x - got %d, want %d", test.serialized,
84 gotVal, test.val,
85 )
86 continue
87 }
88 if gotBytesRead != len(test.serialized) {
89 t.Errorf("deserializeVLQ: did not get expected number "+
90 "of bytes read for %d - got %d, want %d",
91 test.serialized, gotBytesRead,
92 len(test.serialized),
93 )
94 continue
95 }
96 }
97 }
98
99 // TestScriptCompression ensures the domain-specific script compression and decompression works as expected.
100 func TestScriptCompression(t *testing.T) {
101 t.Parallel()
102 tests := []struct {
103 name string
104 uncompressed []byte
105 compressed []byte
106 }{
107 {
108 name: "nil",
109 uncompressed: nil,
110 compressed: hexToBytes("06"),
111 },
112 {
113 name: "pay-to-pubkey-hash 1",
114 uncompressed: hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"),
115 compressed: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
116 },
117 {
118 name: "pay-to-pubkey-hash 2",
119 uncompressed: hexToBytes("76a914e34cce70c86373273efcc54ce7d2a491bb4a0e8488ac"),
120 compressed: hexToBytes("00e34cce70c86373273efcc54ce7d2a491bb4a0e84"),
121 },
122 {
123 name: "pay-to-script-hash 1",
124 uncompressed: hexToBytes("a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87"),
125 compressed: hexToBytes("01da1745e9b549bd0bfa1a569971c77eba30cd5a4b"),
126 },
127 {
128 name: "pay-to-script-hash 2",
129 uncompressed: hexToBytes("a914f815b036d9bbbce5e9f2a00abd1bf3dc91e9551087"),
130 compressed: hexToBytes("01f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"),
131 },
132 {
133 name: "pay-to-pubkey compressed 0x02",
134 uncompressed: hexToBytes("2102192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4ac"),
135 compressed: hexToBytes("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"),
136 },
137 {
138 name: "pay-to-pubkey compressed 0x03",
139 uncompressed: hexToBytes("2103b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65ac"),
140 compressed: hexToBytes("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"),
141 },
142 {
143 name: "pay-to-pubkey uncompressed 0x04 even",
144 uncompressed: hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"),
145 compressed: hexToBytes("04192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"),
146 },
147 {
148 name: "pay-to-pubkey uncompressed 0x04 odd",
149 uncompressed: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"),
150 compressed: hexToBytes("0511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"),
151 },
152 {
153 name: "pay-to-pubkey invalid pubkey",
154 uncompressed: hexToBytes("3302aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"),
155 compressed: hexToBytes("293302aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"),
156 },
157 {
158 name: "null data",
159 uncompressed: hexToBytes("6a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"),
160 compressed: hexToBytes("286a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"),
161 },
162 {
163 name: "requires 2 size bytes - data push 200 bytes",
164 uncompressed: append(hexToBytes("4cc8"), bytes.Repeat([]byte{0x00}, 200)...),
165 // [0x80, 0x50] = 208 as a variable length quantity
166 // [0x4c, 0xc8] = OP_PUSHDATA1 200
167 compressed: append(hexToBytes("80504cc8"), bytes.Repeat([]byte{0x00}, 200)...),
168 },
169 }
170 for _, test := range tests {
171 // Ensure the function to calculate the serialized size without actually serializing the value is calculated
172 // properly.
173 gotSize := compressedScriptSize(test.uncompressed)
174 if gotSize != len(test.compressed) {
175 t.Errorf("compressedScriptSize (%s): did not get "+
176 "expected size - got %d, want %d", test.name,
177 gotSize, len(test.compressed),
178 )
179 continue
180 }
181 // Ensure the script compresses to the expected bytes.
182 gotCompressed := make([]byte, gotSize)
183 gotBytesWritten := putCompressedScript(gotCompressed,
184 test.uncompressed,
185 )
186 if !bytes.Equal(gotCompressed, test.compressed) {
187 t.Errorf("putCompressedScript (%s): did not get "+
188 "expected bytes - got %x, want %x", test.name,
189 gotCompressed, test.compressed,
190 )
191 continue
192 }
193 if gotBytesWritten != len(test.compressed) {
194 t.Errorf("putCompressedScript (%s): did not get "+
195 "expected number of bytes written - got %d, "+
196 "want %d", test.name, gotBytesWritten,
197 len(test.compressed),
198 )
199 continue
200 }
201 // Ensure the compressed script size is properly decoded from the compressed script.
202 gotDecodedSize := decodeCompressedScriptSize(test.compressed)
203 if gotDecodedSize != len(test.compressed) {
204 t.Errorf("decodeCompressedScriptSize (%s): did not get "+
205 "expected size - got %d, want %d", test.name,
206 gotDecodedSize, len(test.compressed),
207 )
208 continue
209 }
210 // Ensure the script decompresses to the expected bytes.
211 gotDecompressed := decompressScript(test.compressed)
212 if !bytes.Equal(gotDecompressed, test.uncompressed) {
213 t.Errorf("decompressScript (%s): did not get expected "+
214 "bytes - got %x, want %x", test.name,
215 gotDecompressed, test.uncompressed,
216 )
217 continue
218 }
219 }
220 }
221
222 // TestScriptCompressionErrors ensures calling various functions related to script compression with incorrect data
223 // returns the expected results.
224 func TestScriptCompressionErrors(t *testing.T) {
225 t.Parallel()
226 // A nil script must result in a decoded size of 0.
227 if gotSize := decodeCompressedScriptSize(nil); gotSize != 0 {
228 t.Fatalf("decodeCompressedScriptSize with nil script did not "+
229 "return 0 - got %d", gotSize,
230 )
231 }
232 // A nil script must result in a nil decompressed script.
233 if gotScript := decompressScript(nil); gotScript != nil {
234 t.Fatalf("decompressScript with nil script did not return nil "+
235 "decompressed script - got %x", gotScript,
236 )
237 }
238 // A compressed script for a pay-to-pubkey (uncompressed) that results in an invalid pubkey must result in a nil
239 // decompressed script.
240 compressedScript := hexToBytes("04012d74d0cb94344c9569c2e77901573d8d" +
241 "7903c3ebec3a957724895dca52c6b4",
242 )
243 if gotScript := decompressScript(compressedScript); gotScript != nil {
244 t.Fatalf("decompressScript with compressed pay-to-"+
245 "uncompressed-pubkey that is invalid did not return "+
246 "nil decompressed script - got %x", gotScript,
247 )
248 }
249 }
250
251 // TestAmountCompression ensures the domain-specific transaction output amount compression and decompression works as
252 // expected.
253 func TestAmountCompression(t *testing.T) {
254 t.Parallel()
255 tests := []struct {
256 name string
257 uncompressed uint64
258 compressed uint64
259 }{
260 {
261 name: "0 DUO (sometimes used in nulldata)",
262 uncompressed: 0,
263 compressed: 0,
264 },
265 {
266 name: "546 Satoshi (current network dust value)",
267 uncompressed: 546,
268 compressed: 4911,
269 },
270 {
271 name: "0.00001 DUO (typical transaction fee)",
272 uncompressed: 1000,
273 compressed: 4,
274 },
275 {
276 name: "0.0001 DUO (typical transaction fee)",
277 uncompressed: 10000,
278 compressed: 5,
279 },
280 {
281 name: "0.12345678 DUO",
282 uncompressed: 12345678,
283 compressed: 111111101,
284 },
285 {
286 name: "0.5 DUO",
287 uncompressed: 50000000,
288 compressed: 48,
289 },
290 {
291 name: "1 DUO",
292 uncompressed: 100000000,
293 compressed: 9,
294 },
295 {
296 name: "5 DUO",
297 uncompressed: 500000000,
298 compressed: 49,
299 },
300 {
301 name: "21000000 DUO (max minted coins)",
302 uncompressed: 2100000000000000,
303 compressed: 21000000,
304 },
305 }
306 for _, test := range tests {
307 // Ensure the amount compresses to the expected value.
308 gotCompressed := compressTxOutAmount(test.uncompressed)
309 if gotCompressed != test.compressed {
310 t.Errorf("compressTxOutAmount (%s): did not get "+
311 "expected value - got %d, want %d", test.name,
312 gotCompressed, test.compressed,
313 )
314 continue
315 }
316 // Ensure the value decompresses to the expected value.
317 gotDecompressed := decompressTxOutAmount(test.compressed)
318 if gotDecompressed != test.uncompressed {
319 t.Errorf("decompressTxOutAmount (%s): did not get "+
320 "expected value - got %d, want %d", test.name,
321 gotDecompressed, test.uncompressed,
322 )
323 continue
324 }
325 }
326 }
327
328 // TestCompressedTxOut ensures the transaction output serialization and deserialization works as expected.
329 func TestCompressedTxOut(t *testing.T) {
330 t.Parallel()
331 tests := []struct {
332 name string
333 amount uint64
334 pkScript []byte
335 compressed []byte
336 }{
337 {
338 name: "nulldata with 0 DUO",
339 amount: 0,
340 pkScript: hexToBytes("6a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"),
341 compressed: hexToBytes("00286a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"),
342 },
343 {
344 name: "pay-to-pubkey-hash dust",
345 amount: 546,
346 pkScript: hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"),
347 compressed: hexToBytes("a52f001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
348 },
349 {
350 name: "pay-to-pubkey uncompressed 1 DUO",
351 amount: 100000000,
352 pkScript: hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"),
353 compressed: hexToBytes("0904192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"),
354 },
355 }
356 for _, test := range tests {
357 // Ensure the function to calculate the serialized size without actually serializing the txout is calculated
358 // properly.
359 gotSize := compressedTxOutSize(test.amount, test.pkScript)
360 if gotSize != len(test.compressed) {
361 t.Errorf("compressedTxOutSize (%s): did not get "+
362 "expected size - got %d, want %d", test.name,
363 gotSize, len(test.compressed),
364 )
365 continue
366 }
367 // Ensure the txout compresses to the expected value.
368 gotCompressed := make([]byte, gotSize)
369 gotBytesWritten := putCompressedTxOut(gotCompressed,
370 test.amount, test.pkScript,
371 )
372 if !bytes.Equal(gotCompressed, test.compressed) {
373 t.Errorf("compressTxOut (%s): did not get expected "+
374 "bytes - got %x, want %x", test.name,
375 gotCompressed, test.compressed,
376 )
377 continue
378 }
379 if gotBytesWritten != len(test.compressed) {
380 t.Errorf("compressTxOut (%s): did not get expected "+
381 "number of bytes written - got %d, want %d",
382 test.name, gotBytesWritten,
383 len(test.compressed),
384 )
385 continue
386 }
387 // Ensure the serialized bytes are decoded back to the expected uncompressed values.
388 gotAmount, gotScript, gotBytesRead, e := decodeCompressedTxOut(
389 test.compressed,
390 )
391 if e != nil {
392 t.Errorf("decodeCompressedTxOut (%s): unexpected "+
393 "error: %v", test.name, e,
394 )
395 continue
396 }
397 if gotAmount != test.amount {
398 t.Errorf("decodeCompressedTxOut (%s): did not get "+
399 "expected amount - got %d, want %d",
400 test.name, gotAmount, test.amount,
401 )
402 continue
403 }
404 if !bytes.Equal(gotScript, test.pkScript) {
405 t.Errorf("decodeCompressedTxOut (%s): did not get "+
406 "expected script - got %x, want %x",
407 test.name, gotScript, test.pkScript,
408 )
409 continue
410 }
411 if gotBytesRead != len(test.compressed) {
412 t.Errorf("decodeCompressedTxOut (%s): did not get "+
413 "expected number of bytes read - got %d, want %d",
414 test.name, gotBytesRead, len(test.compressed),
415 )
416 continue
417 }
418 }
419 }
420
421 // TestTxOutCompressionErrors ensures calling various functions related to txout compression with incorrect data returns
422 // the expected results.
423 func TestTxOutCompressionErrors(t *testing.T) {
424 t.Parallel()
425 // A compressed txout with missing compressed script must error.
426 compressedTxOut := hexToBytes("00")
427 _, _, _, e := decodeCompressedTxOut(compressedTxOut)
428 if !isDeserializeErr(e) {
429 t.Fatalf("decodeCompressedTxOut with missing compressed script "+
430 "did not return expected error type - got %T, want "+
431 "errDeserialize", e,
432 )
433 }
434 // A compressed txout with short compressed script must error.
435 compressedTxOut = hexToBytes("0010")
436 _, _, _, e = decodeCompressedTxOut(compressedTxOut)
437 if !isDeserializeErr(e) {
438 t.Fatalf("decodeCompressedTxOut with short compressed script "+
439 "did not return expected error type - got %T, want "+
440 "errDeserialize", e,
441 )
442 }
443 }
444