github.com/decred/dcrd/blockchain@v1.2.1/compress_test.go (about) 1 // Copyright (c) 2015-2016 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package blockchain 6 7 import ( 8 "bytes" 9 "encoding/hex" 10 "testing" 11 ) 12 13 // hexToBytes converts the passed hex string into bytes and will panic if there 14 // is an error. This is only provided for the hard-coded constants so errors in 15 // the source code can be detected. It will only (and must only) be called with 16 // hard-coded values. 17 func hexToBytes(s string) []byte { 18 b, err := hex.DecodeString(s) 19 if err != nil { 20 panic("invalid hex in source file: " + s) 21 } 22 return b 23 } 24 25 // TestVLQ ensures the variable length quantity serialization, deserialization, 26 // and size calculation works as expected. 27 func TestVLQ(t *testing.T) { 28 t.Parallel() 29 30 tests := []struct { 31 val uint64 32 serialized []byte 33 }{ 34 {0, hexToBytes("00")}, 35 {1, hexToBytes("01")}, 36 {127, hexToBytes("7f")}, 37 {128, hexToBytes("8000")}, 38 {129, hexToBytes("8001")}, 39 {255, hexToBytes("807f")}, 40 {256, hexToBytes("8100")}, 41 {16383, hexToBytes("fe7f")}, 42 {16384, hexToBytes("ff00")}, 43 {16511, hexToBytes("ff7f")}, // Max 2-byte value 44 {16512, hexToBytes("808000")}, 45 {16513, hexToBytes("808001")}, 46 {16639, hexToBytes("80807f")}, 47 {32895, hexToBytes("80ff7f")}, 48 {2113663, hexToBytes("ffff7f")}, // Max 3-byte value 49 {2113664, hexToBytes("80808000")}, 50 {270549119, hexToBytes("ffffff7f")}, // Max 4-byte value 51 {270549120, hexToBytes("8080808000")}, 52 {2147483647, hexToBytes("86fefefe7f")}, 53 {2147483648, hexToBytes("86fefeff00")}, 54 {4294967295, hexToBytes("8efefefe7f")}, // Max uint32, 5 bytes 55 } 56 57 for _, test := range tests { 58 // Ensure the function to calculate the serialized size without 59 // actually serializing the value is calculated properly. 60 gotSize := serializeSizeVLQ(test.val) 61 if gotSize != len(test.serialized) { 62 t.Errorf("serializeSizeVLQ: did not get expected size "+ 63 "for %d - got %d, want %d", test.val, gotSize, 64 len(test.serialized)) 65 continue 66 } 67 68 // Ensure the value serializes to the expected bytes. 69 gotBytes := make([]byte, gotSize) 70 gotBytesWritten := putVLQ(gotBytes, test.val) 71 if !bytes.Equal(gotBytes, test.serialized) { 72 t.Errorf("putVLQUnchecked: did not get expected bytes "+ 73 "for %d - got %x, want %x", test.val, gotBytes, 74 test.serialized) 75 continue 76 } 77 if gotBytesWritten != len(test.serialized) { 78 t.Errorf("putVLQUnchecked: did not get expected number "+ 79 "of bytes written for %d - got %d, want %d", 80 test.val, gotBytesWritten, len(test.serialized)) 81 continue 82 } 83 84 // Ensure the serialized bytes deserialize to the expected 85 // value. 86 gotVal, gotBytesRead := deserializeVLQ(test.serialized) 87 if gotVal != test.val { 88 t.Errorf("deserializeVLQ: did not get expected value "+ 89 "for %x - got %d, want %d", test.serialized, 90 gotVal, test.val) 91 continue 92 } 93 if gotBytesRead != len(test.serialized) { 94 t.Errorf("deserializeVLQ: did not get expected number "+ 95 "of bytes read for %d - got %d, want %d", 96 test.serialized, gotBytesRead, 97 len(test.serialized)) 98 continue 99 } 100 } 101 } 102 103 // TestScriptCompression ensures the domain-specific script compression and 104 // decompression works as expected. 105 func TestScriptCompression(t *testing.T) { 106 t.Parallel() 107 108 tests := []struct { 109 name string 110 version uint32 111 scriptVersion uint16 112 uncompressed []byte 113 compressed []byte 114 }{ 115 { 116 name: "nil", 117 version: 1, 118 scriptVersion: 0, 119 uncompressed: nil, 120 compressed: hexToBytes("40"), 121 }, 122 { 123 name: "pay-to-pubkey-hash 1", 124 version: 1, 125 scriptVersion: 0, 126 uncompressed: hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"), 127 compressed: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 128 }, 129 { 130 name: "pay-to-pubkey-hash 2", 131 version: 1, 132 scriptVersion: 0, 133 uncompressed: hexToBytes("76a914e34cce70c86373273efcc54ce7d2a491bb4a0e8488ac"), 134 compressed: hexToBytes("00e34cce70c86373273efcc54ce7d2a491bb4a0e84"), 135 }, 136 { 137 name: "pay-to-script-hash 1", 138 version: 1, 139 scriptVersion: 0, 140 uncompressed: hexToBytes("a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87"), 141 compressed: hexToBytes("01da1745e9b549bd0bfa1a569971c77eba30cd5a4b"), 142 }, 143 { 144 name: "pay-to-script-hash 2", 145 version: 1, 146 scriptVersion: 0, 147 uncompressed: hexToBytes("a914f815b036d9bbbce5e9f2a00abd1bf3dc91e9551087"), 148 compressed: hexToBytes("01f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"), 149 }, 150 { 151 name: "pay-to-pubkey compressed 0x02", 152 version: 1, 153 scriptVersion: 0, 154 uncompressed: hexToBytes("2102192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4ac"), 155 compressed: hexToBytes("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), 156 }, 157 { 158 name: "pay-to-pubkey compressed 0x03", 159 version: 1, 160 scriptVersion: 0, 161 uncompressed: hexToBytes("2103b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65ac"), 162 compressed: hexToBytes("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"), 163 }, 164 { 165 name: "pay-to-pubkey uncompressed 0x04 even", 166 version: 1, 167 scriptVersion: 0, 168 uncompressed: hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"), 169 compressed: hexToBytes("04192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), 170 }, 171 { 172 name: "pay-to-pubkey uncompressed 0x04 odd", 173 version: 1, 174 scriptVersion: 0, 175 uncompressed: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), 176 compressed: hexToBytes("0511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), 177 }, 178 { 179 name: "pay-to-pubkey invalid pubkey", 180 version: 1, 181 scriptVersion: 0, 182 uncompressed: hexToBytes("3302aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"), 183 compressed: hexToBytes("633302aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"), 184 }, 185 { 186 name: "null data", 187 version: 1, 188 scriptVersion: 0, 189 uncompressed: hexToBytes("6a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), 190 compressed: hexToBytes("626a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), 191 }, 192 { 193 name: "requires 2 size bytes - data push 200 bytes", 194 version: 1, 195 scriptVersion: 0, 196 uncompressed: append(hexToBytes("4cc8"), bytes.Repeat([]byte{0x00}, 200)...), 197 // [0x80, 0x50] = 208 as a variable length quantity 198 // [0x4c, 0xc8] = OP_PUSHDATA1 200 199 compressed: append(hexToBytes("810a4cc8"), bytes.Repeat([]byte{0x00}, 200)...), 200 }, 201 } 202 203 for _, test := range tests { 204 // Ensure the function to calculate the serialized size without 205 // actually serializing the value is calculated properly. 206 gotSize := compressedScriptSize(test.scriptVersion, test.uncompressed, 207 test.version) 208 if gotSize != len(test.compressed) { 209 t.Errorf("compressedScriptSize (%s): did not get "+ 210 "expected size - got %d, want %d", test.name, 211 gotSize, len(test.compressed)) 212 continue 213 } 214 215 // Ensure the script compresses to the expected bytes. 216 gotCompressed := make([]byte, gotSize) 217 gotBytesWritten := putCompressedScript(gotCompressed, test.scriptVersion, 218 test.uncompressed, test.version) 219 if !bytes.Equal(gotCompressed, test.compressed) { 220 t.Errorf("putCompressedScript (%s): did not get "+ 221 "expected bytes - got %x, want %x", test.name, 222 gotCompressed, test.compressed) 223 continue 224 } 225 if gotBytesWritten != len(test.compressed) { 226 t.Errorf("putCompressedScript (%s): did not get "+ 227 "expected number of bytes written - got %d, "+ 228 "want %d", test.name, gotBytesWritten, 229 len(test.compressed)) 230 continue 231 } 232 233 // Ensure the compressed script size is properly decoded from 234 // the compressed script. 235 gotDecodedSize := decodeCompressedScriptSize(test.compressed, 236 test.version) 237 if gotDecodedSize != len(test.compressed) { 238 t.Errorf("decodeCompressedScriptSize (%s): did not get "+ 239 "expected size - got %d, want %d", test.name, 240 gotDecodedSize, len(test.compressed)) 241 continue 242 } 243 244 // Ensure the script decompresses to the expected bytes. 245 gotDecompressed := decompressScript(test.compressed, test.version) 246 if !bytes.Equal(gotDecompressed, test.uncompressed) { 247 t.Errorf("decompressScript (%s): did not get expected "+ 248 "bytes - got %x, want %x", test.name, 249 gotDecompressed, test.uncompressed) 250 continue 251 } 252 } 253 } 254 255 // TestScriptCompressionErrors ensures calling various functions related to 256 // script compression with incorrect data returns the expected results. 257 func TestScriptCompressionErrors(t *testing.T) { 258 t.Parallel() 259 260 // A nil script must result in a decoded size of 0. 261 if gotSize := decodeCompressedScriptSize(nil, 1); gotSize != 0 { 262 t.Fatalf("decodeCompressedScriptSize with nil script did not "+ 263 "return 0 - got %d", gotSize) 264 } 265 266 // A nil script must result in a nil decompressed script. 267 if gotScript := decompressScript(nil, 1); gotScript != nil { 268 t.Fatalf("decompressScript with nil script did not return nil "+ 269 "decompressed script - got %x", gotScript) 270 } 271 272 // A compressed script for a pay-to-pubkey (uncompressed) that results 273 // in an invalid pubkey must result in a nil decompressed script. 274 compressedScript := hexToBytes("04012d74d0cb94344c9569c2e77901573d8d" + 275 "7903c3ebec3a957724895dca52c6b4") 276 if gotScript := decompressScript(compressedScript, 1); gotScript != nil { 277 t.Fatalf("decompressScript with compressed pay-to-"+ 278 "uncompressed-pubkey that is invalid did not return "+ 279 "nil decompressed script - got %x", gotScript) 280 } 281 } 282 283 // TestAmountCompression ensures the domain-specific transaction output amount 284 // compression and decompression works as expected. 285 func TestAmountCompression(t *testing.T) { 286 t.Parallel() 287 288 tests := []struct { 289 name string 290 uncompressed uint64 291 compressed uint64 292 }{ 293 { 294 name: "0 DCR (sometimes used in nulldata)", 295 uncompressed: 0, 296 compressed: 0, 297 }, 298 { 299 name: "546 atoms (current network dust value)", 300 uncompressed: 546, 301 compressed: 4911, 302 }, 303 { 304 name: "0.00001 DCR (typical transaction fee)", 305 uncompressed: 1000, 306 compressed: 4, 307 }, 308 { 309 name: "0.0001 DCR (typical transaction fee)", 310 uncompressed: 10000, 311 compressed: 5, 312 }, 313 { 314 name: "0.12345678 DCR", 315 uncompressed: 12345678, 316 compressed: 111111101, 317 }, 318 { 319 name: "0.5 DCR", 320 uncompressed: 50000000, 321 compressed: 48, 322 }, 323 { 324 name: "1 DCR", 325 uncompressed: 100000000, 326 compressed: 9, 327 }, 328 { 329 name: "5 DCR", 330 uncompressed: 500000000, 331 compressed: 49, 332 }, 333 { 334 name: "21000000 DCR (max minted coins)", 335 uncompressed: 2100000000000000, 336 compressed: 21000000, 337 }, 338 } 339 340 for _, test := range tests { 341 // Ensure the amount compresses to the expected value. 342 gotCompressed := compressTxOutAmount(test.uncompressed) 343 if gotCompressed != test.compressed { 344 t.Errorf("compressTxOutAmount (%s): did not get "+ 345 "expected value - got %d, want %d", test.name, 346 gotCompressed, test.compressed) 347 continue 348 } 349 350 // Ensure the value decompresses to the expected value. 351 gotDecompressed := decompressTxOutAmount(test.compressed) 352 if gotDecompressed != test.uncompressed { 353 t.Errorf("decompressTxOutAmount (%s): did not get "+ 354 "expected value - got %d, want %d", test.name, 355 gotDecompressed, test.uncompressed) 356 continue 357 } 358 } 359 } 360 361 // TestCompressedTxOut ensures the transaction output serialization and 362 // deserialization works as expected. 363 func TestCompressedTxOut(t *testing.T) { 364 t.Parallel() 365 366 tests := []struct { 367 name string 368 amount uint64 369 scriptVersion uint16 370 pkScript []byte 371 compPkScript []byte 372 version uint32 373 compressed []byte 374 hasAmount bool 375 isCompressed bool 376 }{ 377 { 378 name: "nulldata with 0 DCR", 379 amount: 0, 380 scriptVersion: 0, 381 pkScript: hexToBytes("6a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), 382 compPkScript: hexToBytes("626a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), 383 version: 1, 384 compressed: hexToBytes("00626a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), 385 hasAmount: false, 386 isCompressed: false, 387 }, 388 { 389 name: "pay-to-pubkey-hash dust, no amount", 390 amount: 0, 391 scriptVersion: 0, 392 pkScript: hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"), 393 compPkScript: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 394 version: 1, 395 compressed: hexToBytes("00001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 396 hasAmount: false, 397 isCompressed: false, 398 }, 399 { 400 name: "pay-to-pubkey-hash dust, no amount, precompressed", 401 amount: 0, 402 scriptVersion: 0, 403 pkScript: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 404 compPkScript: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 405 version: 1, 406 compressed: hexToBytes("00001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 407 hasAmount: false, 408 isCompressed: true, 409 }, 410 { 411 name: "pay-to-pubkey-hash dust, amount", 412 amount: 546, 413 scriptVersion: 0, 414 pkScript: hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"), 415 compPkScript: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 416 version: 1, 417 compressed: hexToBytes("a52f00001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 418 hasAmount: true, 419 isCompressed: false, 420 }, 421 { 422 name: "pay-to-pubkey-hash dust, amount, precompressed", 423 amount: 546, 424 scriptVersion: 0, 425 pkScript: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 426 compPkScript: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 427 version: 1, 428 compressed: hexToBytes("a52f00001018853670f9f3b0582c5b9ee8ce93764ac32b93"), 429 hasAmount: true, 430 isCompressed: true, 431 }, 432 { 433 name: "pay-to-pubkey uncompressed, no amount", 434 amount: 0, 435 scriptVersion: 0, 436 pkScript: hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"), 437 compPkScript: hexToBytes("04192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), 438 version: 1, 439 compressed: hexToBytes("0004192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), 440 hasAmount: false, 441 isCompressed: false, 442 }, 443 { 444 name: "pay-to-pubkey uncompressed 1 DCR, amount present", 445 amount: 100000000, 446 scriptVersion: 0, 447 pkScript: hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"), 448 compPkScript: hexToBytes("04192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), 449 version: 1, 450 compressed: hexToBytes("090004192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), 451 hasAmount: true, 452 isCompressed: false, 453 }, 454 } 455 456 for _, test := range tests { 457 targetSz := compressedTxOutSize(0, test.scriptVersion, test.pkScript, currentCompressionVersion, test.isCompressed, test.hasAmount) - 1 458 target := make([]byte, targetSz) 459 putCompressedScript(target, test.scriptVersion, test.pkScript, currentCompressionVersion) 460 461 // Ensure the function to calculate the serialized size without 462 // actually serializing the txout is calculated properly. 463 gotSize := compressedTxOutSize(test.amount, test.scriptVersion, 464 test.pkScript, test.version, test.isCompressed, test.hasAmount) 465 if gotSize != len(test.compressed) { 466 t.Errorf("compressedTxOutSize (%s): did not get "+ 467 "expected size - got %d, want %d", test.name, 468 gotSize, len(test.compressed)) 469 continue 470 } 471 472 // Ensure the txout compresses to the expected value. 473 gotCompressed := make([]byte, gotSize) 474 gotBytesWritten := putCompressedTxOut(gotCompressed, 475 test.amount, test.scriptVersion, test.pkScript, 476 test.version, test.isCompressed, test.hasAmount) 477 if !bytes.Equal(gotCompressed, test.compressed) { 478 t.Errorf("compressTxOut (%s): did not get expected "+ 479 "bytes - got %x, want %x", test.name, 480 gotCompressed, test.compressed) 481 continue 482 } 483 if gotBytesWritten != len(test.compressed) { 484 t.Errorf("compressTxOut (%s): did not get expected "+ 485 "number of bytes written - got %d, want %d", 486 test.name, gotBytesWritten, 487 len(test.compressed)) 488 continue 489 } 490 491 // Ensure the serialized bytes are decoded back to the expected 492 // compressed values. 493 gotAmount, gotScrVersion, gotScript, gotBytesRead, err := 494 decodeCompressedTxOut(test.compressed, test.version, 495 test.hasAmount) 496 if err != nil { 497 t.Errorf("decodeCompressedTxOut (%s): unexpected "+ 498 "error: %v", test.name, err) 499 continue 500 } 501 if gotAmount != int64(test.amount) { 502 t.Errorf("decodeCompressedTxOut (%s): did not get "+ 503 "expected amount - got %d, want %d", 504 test.name, gotAmount, test.amount) 505 continue 506 } 507 if gotScrVersion != test.scriptVersion { 508 t.Errorf("decodeCompressedTxOut (%s): did not get "+ 509 "expected script version - got %d, want %d", 510 test.name, gotScrVersion, test.scriptVersion) 511 continue 512 } 513 if !bytes.Equal(gotScript, test.compPkScript) { 514 t.Errorf("decodeCompressedTxOut (%s): did not get "+ 515 "expected script - got %x, want %x", 516 test.name, gotScript, test.compPkScript) 517 continue 518 } 519 if gotBytesRead != len(test.compressed) { 520 t.Errorf("decodeCompressedTxOut (%s): did not get "+ 521 "expected number of bytes read - got %d, want %d", 522 test.name, gotBytesRead, len(test.compressed)) 523 continue 524 } 525 526 // Ensure the compressed values decompress to the expected 527 // txout. 528 gotScript = decompressScript(gotScript, test.version) 529 localScript := make([]byte, len(test.pkScript)) 530 copy(localScript, test.pkScript) 531 if test.isCompressed { 532 localScript = decompressScript(localScript, test.version) 533 } 534 if !bytes.Equal(gotScript, localScript) { 535 t.Errorf("decompressTxOut (%s): did not get expected "+ 536 "script - got %x, want %x", test.name, 537 gotScript, test.pkScript) 538 continue 539 } 540 } 541 } 542 543 // TestTxOutCompressionErrors ensures calling various functions related to 544 // txout compression with incorrect data returns the expected results. 545 func TestTxOutCompressionErrors(t *testing.T) { 546 t.Parallel() 547 548 // A compressed txout with a value and missing compressed script must error. 549 compressedTxOut := hexToBytes("00") 550 _, _, _, _, err := decodeCompressedTxOut(compressedTxOut, 1, true) 551 if !isDeserializeErr(err) { 552 t.Fatalf("decodeCompressedTxOut with value and missing "+ 553 "compressed script did not return expected error type "+ 554 "- got %T, want errDeserialize", err) 555 } 556 557 // A compressed txout without a value and with an empty compressed 558 // script returns empty but is valid. 559 compressedTxOut = hexToBytes("00") 560 _, _, _, _, err = decodeCompressedTxOut(compressedTxOut, 1, false) 561 if err != nil { 562 t.Fatalf("decodeCompressedTxOut with missing compressed script "+ 563 "did not return expected error type - got %T, want "+ 564 "errDeserialize", err) 565 } 566 567 // A compressed txout with short compressed script must error. 568 compressedTxOut = hexToBytes("0010") 569 _, _, _, _, err = decodeCompressedTxOut(compressedTxOut, 1, false) 570 if !isDeserializeErr(err) { 571 t.Fatalf("decodeCompressedTxOut with short compressed script "+ 572 "did not return expected error type - got %T, want "+ 573 "errDeserialize", err) 574 } 575 }