// Copyright (c) 2015-2016 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package blockchain import ( "bytes" "errors" "math/big" "reflect" "testing" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/wire" ) // TestErrNotInMainChain ensures the functions related to errNotInMainChain work // as expected. func TestErrNotInMainChain(t *testing.T) { errStr := "no block at height 1 exists" err := error(errNotInMainChain(errStr)) // Ensure the stringized output for the error is as expected. if err.Error() != errStr { t.Fatalf("errNotInMainChain retuned unexpected error string - "+ "got %q, want %q", err.Error(), errStr) } // Ensure error is detected as the correct type. if !isNotInMainChainErr(err) { t.Fatalf("isNotInMainChainErr did not detect as expected type") } err = errors.New("something else") if isNotInMainChainErr(err) { t.Fatalf("isNotInMainChainErr detected incorrect type") } } // maybeDecompress decompresses the amount and public key script fields of the // stxo and marks it decompressed if needed. func (o *spentTxOut) maybeDecompress(version int32) { // Nothing to do if it's not compressed. if !o.compressed { return } o.amount = int64(decompressTxOutAmount(uint64(o.amount))) o.pkScript = decompressScript(o.pkScript, version) o.compressed = false } // TestStxoSerialization ensures serializing and deserializing spent transaction // output entries works as expected. func TestStxoSerialization(t *testing.T) { t.Parallel() tests := []struct { name string stxo spentTxOut txVersion int32 // When the txout is not fully spent. serialized []byte }{ // From block 170 in main blockchain. { name: "Spends last output of coinbase", stxo: spentTxOut{ amount: 5000000000, pkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), isCoinBase: true, height: 9, version: 1, }, serialized: hexToBytes("1301320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), }, // Adapted from block 100025 in main blockchain. { name: "Spends last output of non coinbase", stxo: spentTxOut{ amount: 13761000000, pkScript: hexToBytes("76a914b2fb57eadf61e106a100a7445a8c3f67898841ec88ac"), isCoinBase: false, height: 100024, version: 1, }, serialized: hexToBytes("8b99700186c64700b2fb57eadf61e106a100a7445a8c3f67898841ec"), }, // Adapted from block 100025 in main blockchain. { name: "Does not spend last output", stxo: spentTxOut{ amount: 34405000000, pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), version: 1, }, txVersion: 1, serialized: hexToBytes("0091f20f006edbc6c4d31bae9f1ccc38538a114bf42de65e86"), }, } for _, test := range tests { // Ensure the function to calculate the serialized size without // actually serializing it is calculated properly. gotSize := spentTxOutSerializeSize(&test.stxo) if gotSize != len(test.serialized) { t.Errorf("spentTxOutSerializeSize (%s): did not get "+ "expected size - got %d, want %d", test.name, gotSize, len(test.serialized)) continue } // Ensure the stxo serializes to the expected value. gotSerialized := make([]byte, gotSize) gotBytesWritten := putSpentTxOut(gotSerialized, &test.stxo) if !bytes.Equal(gotSerialized, test.serialized) { t.Errorf("putSpentTxOut (%s): did not get expected "+ "bytes - got %x, want %x", test.name, gotSerialized, test.serialized) continue } if gotBytesWritten != len(test.serialized) { t.Errorf("putSpentTxOut (%s): did not get expected "+ "number of bytes written - got %d, want %d", test.name, gotBytesWritten, len(test.serialized)) continue } // Ensure the serialized bytes are decoded back to the expected // stxo. var gotStxo spentTxOut gotBytesRead, err := decodeSpentTxOut(test.serialized, &gotStxo, test.txVersion) if err != nil { t.Errorf("decodeSpentTxOut (%s): unexpected error: %v", test.name, err) continue } gotStxo.maybeDecompress(test.stxo.version) if !reflect.DeepEqual(gotStxo, test.stxo) { t.Errorf("decodeSpentTxOut (%s) mismatched entries - "+ "got %v, want %v", test.name, gotStxo, test.stxo) continue } if gotBytesRead != len(test.serialized) { t.Errorf("decodeSpentTxOut (%s): did not get expected "+ "number of bytes read - got %d, want %d", test.name, gotBytesRead, len(test.serialized)) continue } } } // TestStxoDecodeErrors performs negative tests against decoding spent // transaction outputs to ensure error paths work as expected. func TestStxoDecodeErrors(t *testing.T) { t.Parallel() tests := []struct { name string stxo spentTxOut txVersion int32 // When the txout is not fully spent. serialized []byte bytesRead int // Expected number of bytes read. errType error }{ { name: "nothing serialized", stxo: spentTxOut{}, serialized: hexToBytes(""), errType: errDeserialize(""), bytesRead: 0, }, { name: "no data after header code w/o version", stxo: spentTxOut{}, serialized: hexToBytes("00"), errType: errDeserialize(""), bytesRead: 1, }, { name: "no data after header code with version", stxo: spentTxOut{}, serialized: hexToBytes("13"), errType: errDeserialize(""), bytesRead: 1, }, { name: "no data after version", stxo: spentTxOut{}, serialized: hexToBytes("1301"), errType: errDeserialize(""), bytesRead: 2, }, { name: "no serialized tx version and passed -1", stxo: spentTxOut{}, txVersion: -1, serialized: hexToBytes("003205"), errType: AssertError(""), bytesRead: 1, }, { name: "incomplete compressed txout", stxo: spentTxOut{}, txVersion: 1, serialized: hexToBytes("0032"), errType: errDeserialize(""), bytesRead: 2, }, } for _, test := range tests { // Ensure the expected error type is returned. gotBytesRead, err := decodeSpentTxOut(test.serialized, &test.stxo, test.txVersion) if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { t.Errorf("decodeSpentTxOut (%s): expected error type "+ "does not match - got %T, want %T", test.name, err, test.errType) continue } // Ensure the expected number of bytes read is returned. if gotBytesRead != test.bytesRead { t.Errorf("decodeSpentTxOut (%s): unexpected number of "+ "bytes read - got %d, want %d", test.name, gotBytesRead, test.bytesRead) continue } } } // TestSpendJournalSerialization ensures serializing and deserializing spend // journal entries works as expected. func TestSpendJournalSerialization(t *testing.T) { t.Parallel() tests := []struct { name string entry []spentTxOut blockTxns []*wire.MsgTx utxoView *UtxoViewpoint serialized []byte }{ // From block 2 in main blockchain. { name: "No spends", entry: nil, blockTxns: nil, utxoView: NewUtxoViewpoint(), serialized: nil, }, // From block 170 in main blockchain. { name: "One tx with one input spends last output of coinbase", entry: []spentTxOut{{ amount: 5000000000, pkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), isCoinBase: true, height: 9, version: 1, }}, blockTxns: []*wire.MsgTx{{ // Coinbase omitted. Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: wire.OutPoint{ Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), Index: 0, }, SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), Sequence: 0xffffffff, }}, TxOut: []*wire.TxOut{{ Value: 1000000000, PkScript: hexToBytes("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac"), }, { Value: 4000000000, PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), }}, LockTime: 0, }}, utxoView: NewUtxoViewpoint(), serialized: hexToBytes("1301320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), }, // Adapted from block 100025 in main blockchain. { name: "Two txns when one spends last output, one doesn't", entry: []spentTxOut{{ amount: 34405000000, pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), version: 1, }, { amount: 13761000000, pkScript: hexToBytes("76a914b2fb57eadf61e106a100a7445a8c3f67898841ec88ac"), isCoinBase: false, height: 100024, version: 1, }}, blockTxns: []*wire.MsgTx{{ // Coinbase omitted. Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: wire.OutPoint{ Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), Index: 1, }, SignatureScript: hexToBytes("493046022100c167eead9840da4a033c9a56470d7794a9bb1605b377ebe5688499b39f94be59022100fb6345cab4324f9ea0b9ee9169337534834638d818129778370f7d378ee4a325014104d962cac5390f12ddb7539507065d0def320d68c040f2e73337c3a1aaaab7195cb5c4d02e0959624d534f3c10c3cf3d73ca5065ebd62ae986b04c6d090d32627c"), Sequence: 0xffffffff, }}, TxOut: []*wire.TxOut{{ Value: 5000000, PkScript: hexToBytes("76a914f419b8db4ba65f3b6fcc233acb762ca6f51c23d488ac"), }, { Value: 34400000000, PkScript: hexToBytes("76a914cadf4fc336ab3c6a4610b75f31ba0676b7f663d288ac"), }}, LockTime: 0, }, { Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: wire.OutPoint{ Hash: *newHashFromStr("92fbe1d4be82f765dfabc9559d4620864b05cc897c4db0e29adac92d294e52b7"), Index: 0, }, SignatureScript: hexToBytes("483045022100e256743154c097465cf13e89955e1c9ff2e55c46051b627751dee0144183157e02201d8d4f02cde8496aae66768f94d35ce54465bd4ae8836004992d3216a93a13f00141049d23ce8686fe9b802a7a938e8952174d35dd2c2089d4112001ed8089023ab4f93a3c9fcd5bfeaa9727858bf640dc1b1c05ec3b434bb59837f8640e8810e87742"), Sequence: 0xffffffff, }}, TxOut: []*wire.TxOut{{ Value: 5000000, PkScript: hexToBytes("76a914a983ad7c92c38fc0e2025212e9f972204c6e687088ac"), }, { Value: 13756000000, PkScript: hexToBytes("76a914a6ebd69952ab486a7a300bfffdcb395dc7d47c2388ac"), }}, LockTime: 0, }}, utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{ *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): { version: 1, isCoinBase: false, blockHeight: 100024, sparseOutputs: map[uint32]*utxoOutput{ 1: { amount: 34405000000, pkScript: hexToBytes("76a9142084541c3931677527a7eafe56fd90207c344eb088ac"), }, }, }, }}, serialized: hexToBytes("8b99700186c64700b2fb57eadf61e106a100a7445a8c3f67898841ec0091f20f006edbc6c4d31bae9f1ccc38538a114bf42de65e86"), }, // Hand crafted. { name: "One tx, two inputs from same tx, neither spend last output", entry: []spentTxOut{{ amount: 165125632, pkScript: hexToBytes("51"), version: 1, }, { amount: 154370000, pkScript: hexToBytes("51"), version: 1, }}, blockTxns: []*wire.MsgTx{{ // Coinbase omitted. Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: wire.OutPoint{ Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), Index: 1, }, SignatureScript: hexToBytes(""), Sequence: 0xffffffff, }, { PreviousOutPoint: wire.OutPoint{ Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), Index: 2, }, SignatureScript: hexToBytes(""), Sequence: 0xffffffff, }}, TxOut: []*wire.TxOut{{ Value: 165125632, PkScript: hexToBytes("51"), }, { Value: 154370000, PkScript: hexToBytes("51"), }}, LockTime: 0, }}, utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{ *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): { version: 1, isCoinBase: false, blockHeight: 100000, sparseOutputs: map[uint32]*utxoOutput{ 0: { amount: 165712179, pkScript: hexToBytes("51"), }, }, }, }}, serialized: hexToBytes("0087bc3707510084c3d19a790751"), }, } for i, test := range tests { // Ensure the journal entry serializes to the expected value. gotBytes := serializeSpendJournalEntry(test.entry) if !bytes.Equal(gotBytes, test.serialized) { t.Errorf("serializeSpendJournalEntry #%d (%s): "+ "mismatched bytes - got %x, want %x", i, test.name, gotBytes, test.serialized) continue } // Deserialize to a spend journal entry. gotEntry, err := deserializeSpendJournalEntry(test.serialized, test.blockTxns, test.utxoView) if err != nil { t.Errorf("deserializeSpendJournalEntry #%d (%s) "+ "unexpected error: %v", i, test.name, err) continue } for stxoIdx := range gotEntry { stxo := &gotEntry[stxoIdx] stxo.maybeDecompress(test.entry[stxoIdx].version) } // Ensure that the deserialized spend journal entry has the // correct properties. if !reflect.DeepEqual(gotEntry, test.entry) { t.Errorf("deserializeSpendJournalEntry #%d (%s) "+ "mismatched entries - got %v, want %v", i, test.name, gotEntry, test.entry) continue } } } // TestSpendJournalErrors performs negative tests against deserializing spend // journal entries to ensure error paths work as expected. func TestSpendJournalErrors(t *testing.T) { t.Parallel() tests := []struct { name string blockTxns []*wire.MsgTx utxoView *UtxoViewpoint serialized []byte errType error }{ // Adapted from block 170 in main blockchain. { name: "Force assertion due to missing stxos", blockTxns: []*wire.MsgTx{{ // Coinbase omitted. Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: wire.OutPoint{ Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), Index: 0, }, SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), Sequence: 0xffffffff, }}, LockTime: 0, }}, utxoView: NewUtxoViewpoint(), serialized: hexToBytes(""), errType: AssertError(""), }, { name: "Force deserialization error in stxos", blockTxns: []*wire.MsgTx{{ // Coinbase omitted. Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: wire.OutPoint{ Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), Index: 0, }, SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), Sequence: 0xffffffff, }}, LockTime: 0, }}, utxoView: NewUtxoViewpoint(), serialized: hexToBytes("1301320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a"), errType: errDeserialize(""), }, } for _, test := range tests { // Ensure the expected error type is returned and the returned // slice is nil. stxos, err := deserializeSpendJournalEntry(test.serialized, test.blockTxns, test.utxoView) if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { t.Errorf("deserializeSpendJournalEntry (%s): expected "+ "error type does not match - got %T, want %T", test.name, err, test.errType) continue } if stxos != nil { t.Errorf("deserializeSpendJournalEntry (%s): returned "+ "slice of spent transaction outputs is not nil", test.name) continue } } } // TestUtxoSerialization ensures serializing and deserializing unspent // trasaction output entries works as expected. func TestUtxoSerialization(t *testing.T) { t.Parallel() tests := []struct { name string entry *UtxoEntry serialized []byte }{ // From tx in main blockchain: // 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098 { name: "Only output 0, coinbase", entry: &UtxoEntry{ version: 1, isCoinBase: true, blockHeight: 1, sparseOutputs: map[uint32]*utxoOutput{ 0: { amount: 5000000000, pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), }, }, }, serialized: hexToBytes("010103320496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"), }, // From tx in main blockchain: // 8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb { name: "Only output 1, not coinbase", entry: &UtxoEntry{ version: 1, isCoinBase: false, blockHeight: 100001, sparseOutputs: map[uint32]*utxoOutput{ 1: { amount: 1000000, pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"), }, }, }, serialized: hexToBytes("01858c21040700ee8bd501094a7d5ca318da2506de35e1cb025ddc"), }, // Adapted from tx in main blockchain: // df3f3f442d9699857f7f49de4ff0b5d0f3448bec31cdc7b5bf6d25f2abd637d5 { name: "Only output 2, coinbase", entry: &UtxoEntry{ version: 1, isCoinBase: true, blockHeight: 99004, sparseOutputs: map[uint32]*utxoOutput{ 2: { amount: 100937281, pkScript: hexToBytes("76a914da33f77cee27c2a975ed5124d7e4f7f97513510188ac"), }, }, }, serialized: hexToBytes("0185843c010182b095bf4100da33f77cee27c2a975ed5124d7e4f7f975135101"), }, // Adapted from tx in main blockchain: // 4a16969aa4764dd7507fc1de7f0baa4850a246de90c45e59a3207f9a26b5036f { name: "outputs 0 and 2 not coinbase", entry: &UtxoEntry{ version: 1, isCoinBase: false, blockHeight: 113931, sparseOutputs: map[uint32]*utxoOutput{ 0: { amount: 20000000, pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), }, 2: { amount: 15000000, pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"), }, }, }, serialized: hexToBytes("0185f90b0a011200e2ccd6ec7c6e2e581349c77e067385fa8236bf8a800900b8025be1b3efc63b0ad48e7f9f10e87544528d58"), }, // Adapted from tx in main blockchain: // 4a16969aa4764dd7507fc1de7f0baa4850a246de90c45e59a3207f9a26b5036f { name: "outputs 0 and 2, not coinbase, 1 marked spent", entry: &UtxoEntry{ version: 1, isCoinBase: false, blockHeight: 113931, sparseOutputs: map[uint32]*utxoOutput{ 0: { amount: 20000000, pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), }, 1: { // This won't be serialized. spent: true, amount: 1000000, pkScript: hexToBytes("76a914e43031c3e46f20bf1ccee9553ce815de5a48467588ac"), }, 2: { amount: 15000000, pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"), }, }, }, serialized: hexToBytes("0185f90b0a011200e2ccd6ec7c6e2e581349c77e067385fa8236bf8a800900b8025be1b3efc63b0ad48e7f9f10e87544528d58"), }, // Adapted from tx in main blockchain: // 4a16969aa4764dd7507fc1de7f0baa4850a246de90c45e59a3207f9a26b5036f { name: "outputs 0 and 2, not coinbase, output 2 compressed", entry: &UtxoEntry{ version: 1, isCoinBase: false, blockHeight: 113931, sparseOutputs: map[uint32]*utxoOutput{ 0: { amount: 20000000, pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), }, 2: { // Uncompressed Amount: 15000000 // Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac compressed: true, amount: 137, pkScript: hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"), }, }, }, serialized: hexToBytes("0185f90b0a011200e2ccd6ec7c6e2e581349c77e067385fa8236bf8a800900b8025be1b3efc63b0ad48e7f9f10e87544528d58"), }, // Adapted from tx in main blockchain: // 4a16969aa4764dd7507fc1de7f0baa4850a246de90c45e59a3207f9a26b5036f { name: "outputs 0 and 2, not coinbase, output 2 compressed, packed indexes reversed", entry: &UtxoEntry{ version: 1, isCoinBase: false, blockHeight: 113931, sparseOutputs: map[uint32]*utxoOutput{ 0: { amount: 20000000, pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), }, 2: { // Uncompressed Amount: 15000000 // Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac compressed: true, amount: 137, pkScript: hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"), }, }, }, serialized: hexToBytes("0185f90b0a011200e2ccd6ec7c6e2e581349c77e067385fa8236bf8a800900b8025be1b3efc63b0ad48e7f9f10e87544528d58"), }, // From tx in main blockchain: // 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098 { name: "Only output 0, coinbase, fully spent", entry: &UtxoEntry{ version: 1, isCoinBase: true, blockHeight: 1, sparseOutputs: map[uint32]*utxoOutput{ 0: { spent: true, amount: 5000000000, pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), }, }, }, serialized: nil, }, // Adapted from tx in main blockchain: // 1b02d1c8cfef60a189017b9a420c682cf4a0028175f2f563209e4ff61c8c3620 { name: "Only output 22, not coinbase", entry: &UtxoEntry{ version: 1, isCoinBase: false, blockHeight: 338156, sparseOutputs: map[uint32]*utxoOutput{ 22: { spent: false, amount: 366875659, pkScript: hexToBytes("a9141dd46a006572d820e448e12d2bbb38640bc718e687"), }, }, }, serialized: hexToBytes("0193d06c100000108ba5b9e763011dd46a006572d820e448e12d2bbb38640bc718e6"), }, } for i, test := range tests { // Ensure the utxo entry serializes to the expected value. gotBytes, err := serializeUtxoEntry(test.entry) if err != nil { t.Errorf("serializeUtxoEntry #%d (%s) unexpected "+ "error: %v", i, test.name, err) continue } if !bytes.Equal(gotBytes, test.serialized) { t.Errorf("serializeUtxoEntry #%d (%s): mismatched "+ "bytes - got %x, want %x", i, test.name, gotBytes, test.serialized) continue } // Don't try to deserialize if the test entry was fully spent // since it will have a nil serialization. if test.entry.IsFullySpent() { continue } // Deserialize to a utxo entry. utxoEntry, err := deserializeUtxoEntry(test.serialized) if err != nil { t.Errorf("deserializeUtxoEntry #%d (%s) unexpected "+ "error: %v", i, test.name, err) continue } // Ensure that the deserialized utxo entry has the same // properties for the containing transaction and block height. if utxoEntry.Version() != test.entry.Version() { t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ "version: got %d, want %d", i, test.name, utxoEntry.Version(), test.entry.Version()) continue } if utxoEntry.IsCoinBase() != test.entry.IsCoinBase() { t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ "coinbase flag: got %v, want %v", i, test.name, utxoEntry.IsCoinBase(), test.entry.IsCoinBase()) continue } if utxoEntry.BlockHeight() != test.entry.BlockHeight() { t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ "block height: got %d, want %d", i, test.name, utxoEntry.BlockHeight(), test.entry.BlockHeight()) continue } if utxoEntry.IsFullySpent() != test.entry.IsFullySpent() { t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ "fully spent: got %v, want %v", i, test.name, utxoEntry.IsFullySpent(), test.entry.IsFullySpent()) continue } // Ensure all of the outputs in the test entry match the // spentness of the output in the deserialized entry and the // deserialized entry does not contain any additional utxos. var numUnspent int for outputIndex := range test.entry.sparseOutputs { gotSpent := utxoEntry.IsOutputSpent(outputIndex) wantSpent := test.entry.IsOutputSpent(outputIndex) if !wantSpent { numUnspent++ } if gotSpent != wantSpent { t.Errorf("deserializeUtxoEntry #%d (%s) output "+ "#%d: mismatched spent: got %v, want "+ "%v", i, test.name, outputIndex, gotSpent, wantSpent) continue } } if len(utxoEntry.sparseOutputs) != numUnspent { t.Errorf("deserializeUtxoEntry #%d (%s): mismatched "+ "number of unspent outputs: got %d, want %d", i, test.name, len(utxoEntry.sparseOutputs), numUnspent) continue } // Ensure all of the amounts and scripts of the utxos in the // deserialized entry match the ones in the test entry. for outputIndex := range utxoEntry.sparseOutputs { gotAmount := utxoEntry.AmountByIndex(outputIndex) wantAmount := test.entry.AmountByIndex(outputIndex) if gotAmount != wantAmount { t.Errorf("deserializeUtxoEntry #%d (%s) "+ "output #%d: mismatched amounts: got "+ "%d, want %d", i, test.name, outputIndex, gotAmount, wantAmount) continue } gotPkScript := utxoEntry.PkScriptByIndex(outputIndex) wantPkScript := test.entry.PkScriptByIndex(outputIndex) if !bytes.Equal(gotPkScript, wantPkScript) { t.Errorf("deserializeUtxoEntry #%d (%s) "+ "output #%d mismatched scripts: got "+ "%x, want %x", i, test.name, outputIndex, gotPkScript, wantPkScript) continue } } } } // TestUtxoEntryHeaderCodeErrors performs negative tests against unspent // transaction output header codes to ensure error paths work as expected. func TestUtxoEntryHeaderCodeErrors(t *testing.T) { t.Parallel() tests := []struct { name string entry *UtxoEntry code uint64 bytesRead int // Expected number of bytes read. errType error }{ { name: "Force assertion due to fully spent tx", entry: &UtxoEntry{}, errType: AssertError(""), bytesRead: 0, }, } for _, test := range tests { // Ensure the expected error type is returned and the code is 0. code, gotBytesRead, err := utxoEntryHeaderCode(test.entry, 0) if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { t.Errorf("utxoEntryHeaderCode (%s): expected error "+ "type does not match - got %T, want %T", test.name, err, test.errType) continue } if code != 0 { t.Errorf("utxoEntryHeaderCode (%s): unexpected code "+ "on error - got %d, want 0", test.name, code) continue } // Ensure the expected number of bytes read is returned. if gotBytesRead != test.bytesRead { t.Errorf("utxoEntryHeaderCode (%s): unexpected number "+ "of bytes read - got %d, want %d", test.name, gotBytesRead, test.bytesRead) continue } } } // TestUtxoEntryDeserializeErrors performs negative tests against deserializing // unspent transaction outputs to ensure error paths work as expected. func TestUtxoEntryDeserializeErrors(t *testing.T) { t.Parallel() tests := []struct { name string serialized []byte errType error }{ { name: "no data after version", serialized: hexToBytes("01"), errType: errDeserialize(""), }, { name: "no data after block height", serialized: hexToBytes("0101"), errType: errDeserialize(""), }, { name: "no data after header code", serialized: hexToBytes("010102"), errType: errDeserialize(""), }, { name: "not enough bytes for unspentness bitmap", serialized: hexToBytes("01017800"), errType: errDeserialize(""), }, { name: "incomplete compressed txout", serialized: hexToBytes("01010232"), errType: errDeserialize(""), }, } for _, test := range tests { // Ensure the expected error type is returned and the returned // entry is nil. entry, err := deserializeUtxoEntry(test.serialized) if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { t.Errorf("deserializeUtxoEntry (%s): expected error "+ "type does not match - got %T, want %T", test.name, err, test.errType) continue } if entry != nil { t.Errorf("deserializeUtxoEntry (%s): returned entry "+ "is not nil", test.name) continue } } } // TestBestChainStateSerialization ensures serializing and deserializing the // best chain state works as expected. func TestBestChainStateSerialization(t *testing.T) { t.Parallel() workSum := new(big.Int) tests := []struct { name string state bestChainState serialized []byte }{ { name: "genesis", state: bestChainState{ hash: *newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"), height: 0, totalTxns: 1, workSum: func() *big.Int { workSum.Add(workSum, CalcWork(486604799)) return new(big.Int).Set(workSum) }(), // 0x0100010001 }, serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000000000000100000000000000050000000100010001"), }, { name: "block 1", state: bestChainState{ hash: *newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"), height: 1, totalTxns: 2, workSum: func() *big.Int { workSum.Add(workSum, CalcWork(486604799)) return new(big.Int).Set(workSum) }(), // 0x0200020002 }, serialized: hexToBytes("4860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000010000000200000000000000050000000200020002"), }, } for i, test := range tests { // Ensure the state serializes to the expected value. gotBytes := serializeBestChainState(test.state) if !bytes.Equal(gotBytes, test.serialized) { t.Errorf("serializeBestChainState #%d (%s): mismatched "+ "bytes - got %x, want %x", i, test.name, gotBytes, test.serialized) continue } // Ensure the serialized bytes are decoded back to the expected // state. state, err := deserializeBestChainState(test.serialized) if err != nil { t.Errorf("deserializeBestChainState #%d (%s) "+ "unexpected error: %v", i, test.name, err) continue } if !reflect.DeepEqual(state, test.state) { t.Errorf("deserializeBestChainState #%d (%s) "+ "mismatched state - got %v, want %v", i, test.name, state, test.state) continue } } } // TestBestChainStateDeserializeErrors performs negative tests against // deserializing the chain state to ensure error paths work as expected. func TestBestChainStateDeserializeErrors(t *testing.T) { t.Parallel() tests := []struct { name string serialized []byte errType error }{ { name: "nothing serialized", serialized: hexToBytes(""), errType: database.Error{ErrorCode: database.ErrCorruption}, }, { name: "short data in hash", serialized: hexToBytes("0000"), errType: database.Error{ErrorCode: database.ErrCorruption}, }, { name: "short data in work sum", serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000001000000000000000500000001000100"), errType: database.Error{ErrorCode: database.ErrCorruption}, }, } for _, test := range tests { // Ensure the expected error type and code is returned. _, err := deserializeBestChainState(test.serialized) if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { t.Errorf("deserializeBestChainState (%s): expected "+ "error type does not match - got %T, want %T", test.name, err, test.errType) continue } if derr, ok := err.(database.Error); ok { tderr := test.errType.(database.Error) if derr.ErrorCode != tderr.ErrorCode { t.Errorf("deserializeBestChainState (%s): "+ "wrong error code got: %v, want: %v", test.name, derr.ErrorCode, tderr.ErrorCode) continue } } } }