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