diff --git a/blockdag/blocknode_test.go b/blockdag/blocknode_test.go index a92cef1a7..0eec8a01a 100644 --- a/blockdag/blocknode_test.go +++ b/blockdag/blocknode_test.go @@ -78,6 +78,9 @@ func TestChainHeight(t *testing.T) { if test.node.chainHeight != test.expectedChainHeight { t.Errorf("block %v expected chain height %v but got %v", test.node, test.expectedChainHeight, test.node.chainHeight) } + if calculateChainHeight(test.node) != test.expectedChainHeight { + t.Errorf("block %v expected calculated chain height %v but got %v", test.node, test.expectedChainHeight, test.node.chainHeight) + } } } diff --git a/blockdag/blockset.go b/blockdag/blockset.go index e37d1d072..4bf1deef5 100644 --- a/blockdag/blockset.go +++ b/blockdag/blockset.go @@ -37,7 +37,10 @@ func (bs blockSet) maxHeight() int32 { func (bs blockSet) highest() *blockNode { var highest *blockNode for _, node := range bs { - if highest == nil || highest.height < node.height || daghash.Less(&node.hash, &highest.hash) { + if highest == nil || + highest.height < node.height || + (highest.height == node.height && daghash.Less(&node.hash, &highest.hash)) { + highest = node } } diff --git a/blockdag/blockset_test.go b/blockdag/blockset_test.go index f91e1bdfe..8e16f0071 100644 --- a/blockdag/blockset_test.go +++ b/blockdag/blockset_test.go @@ -1,6 +1,7 @@ package blockdag import ( + "reflect" "testing" "github.com/daglabs/btcd/dagconfig/daghash" @@ -33,3 +34,303 @@ func TestHashes(t *testing.T) { t.Errorf("TestHashes: hashes are not ordered as expected") } } +func TestBlockSetHighest(t *testing.T) { + node1 := &blockNode{hash: daghash.Hash{10}, height: 1} + node2a := &blockNode{hash: daghash.Hash{20}, height: 2} + node2b := &blockNode{hash: daghash.Hash{21}, height: 2} + node3 := &blockNode{hash: daghash.Hash{30}, height: 3} + + tests := []struct { + name string + set blockSet + expectedHighest *blockNode + }{ + { + name: "empty set", + set: setFromSlice(), + expectedHighest: nil, + }, + { + name: "set with one member", + set: setFromSlice(node1), + expectedHighest: node1, + }, + { + name: "same-height highest members in set", + set: setFromSlice(node2b, node1, node2a), + expectedHighest: node2a, + }, + { + name: "typical set", + set: setFromSlice(node2b, node3, node1, node2a), + expectedHighest: node3, + }, + } + + for _, test := range tests { + highest := test.set.highest() + if highest != test.expectedHighest { + t.Errorf("blockSet.highest: unexpected value in test '%s'. "+ + "Expected: %v, got: %v", test.name, test.expectedHighest, highest) + } + } +} + +func TestBlockSetSubtract(t *testing.T) { + node1 := &blockNode{hash: daghash.Hash{10}} + node2 := &blockNode{hash: daghash.Hash{20}} + node3 := &blockNode{hash: daghash.Hash{30}} + + tests := []struct { + name string + setA blockSet + setB blockSet + expectedResult blockSet + }{ + { + name: "both sets empty", + setA: setFromSlice(), + setB: setFromSlice(), + expectedResult: setFromSlice(), + }, + { + name: "subtract an empty set", + setA: setFromSlice(node1), + setB: setFromSlice(), + expectedResult: setFromSlice(node1), + }, + { + name: "subtract from empty set", + setA: setFromSlice(), + setB: setFromSlice(node1), + expectedResult: setFromSlice(), + }, + { + name: "subtract unrelated set", + setA: setFromSlice(node1), + setB: setFromSlice(node2), + expectedResult: setFromSlice(node1), + }, + { + name: "typical case", + setA: setFromSlice(node1, node2), + setB: setFromSlice(node2, node3), + expectedResult: setFromSlice(node1), + }, + } + + for _, test := range tests { + result := test.setA.subtract(test.setB) + if !reflect.DeepEqual(result, test.expectedResult) { + t.Errorf("blockSet.subtract: unexpected result in test '%s'. "+ + "Expected: %v, got: %v", test.name, test.expectedResult, result) + } + } +} + +func TestBlockSetAddSet(t *testing.T) { + node1 := &blockNode{hash: daghash.Hash{10}} + node2 := &blockNode{hash: daghash.Hash{20}} + node3 := &blockNode{hash: daghash.Hash{30}} + + tests := []struct { + name string + setA blockSet + setB blockSet + expectedResult blockSet + }{ + { + name: "both sets empty", + setA: setFromSlice(), + setB: setFromSlice(), + expectedResult: setFromSlice(), + }, + { + name: "add an empty set", + setA: setFromSlice(node1), + setB: setFromSlice(), + expectedResult: setFromSlice(node1), + }, + { + name: "add to empty set", + setA: setFromSlice(), + setB: setFromSlice(node1), + expectedResult: setFromSlice(node1), + }, + { + name: "add already added member", + setA: setFromSlice(node1, node2), + setB: setFromSlice(node1), + expectedResult: setFromSlice(node1, node2), + }, + { + name: "typical case", + setA: setFromSlice(node1, node2), + setB: setFromSlice(node2, node3), + expectedResult: setFromSlice(node1, node2, node3), + }, + } + + for _, test := range tests { + test.setA.addSet(test.setB) + if !reflect.DeepEqual(test.setA, test.expectedResult) { + t.Errorf("blockSet.addSet: unexpected result in test '%s'. "+ + "Expected: %v, got: %v", test.name, test.expectedResult, test.setA) + } + } +} + +func TestBlockSetAddSlice(t *testing.T) { + node1 := &blockNode{hash: daghash.Hash{10}} + node2 := &blockNode{hash: daghash.Hash{20}} + node3 := &blockNode{hash: daghash.Hash{30}} + + tests := []struct { + name string + set blockSet + slice []*blockNode + expectedResult blockSet + }{ + { + name: "add empty slice to empty set", + set: setFromSlice(), + slice: []*blockNode{}, + expectedResult: setFromSlice(), + }, + { + name: "add an empty slice", + set: setFromSlice(node1), + slice: []*blockNode{}, + expectedResult: setFromSlice(node1), + }, + { + name: "add to empty set", + set: setFromSlice(), + slice: []*blockNode{node1}, + expectedResult: setFromSlice(node1), + }, + { + name: "add already added member", + set: setFromSlice(node1, node2), + slice: []*blockNode{node1}, + expectedResult: setFromSlice(node1, node2), + }, + { + name: "typical case", + set: setFromSlice(node1, node2), + slice: []*blockNode{node2, node3}, + expectedResult: setFromSlice(node1, node2, node3), + }, + } + + for _, test := range tests { + test.set.addSlice(test.slice) + if !reflect.DeepEqual(test.set, test.expectedResult) { + t.Errorf("blockSet.addSlice: unexpected result in test '%s'. "+ + "Expected: %v, got: %v", test.name, test.expectedResult, test.set) + } + } +} + +func TestBlockSetUnion(t *testing.T) { + node1 := &blockNode{hash: daghash.Hash{10}} + node2 := &blockNode{hash: daghash.Hash{20}} + node3 := &blockNode{hash: daghash.Hash{30}} + + tests := []struct { + name string + setA blockSet + setB blockSet + expectedResult blockSet + }{ + { + name: "both sets empty", + setA: setFromSlice(), + setB: setFromSlice(), + expectedResult: setFromSlice(), + }, + { + name: "union against an empty set", + setA: setFromSlice(node1), + setB: setFromSlice(), + expectedResult: setFromSlice(node1), + }, + { + name: "union from an empty set", + setA: setFromSlice(), + setB: setFromSlice(node1), + expectedResult: setFromSlice(node1), + }, + { + name: "union with subset", + setA: setFromSlice(node1, node2), + setB: setFromSlice(node1), + expectedResult: setFromSlice(node1, node2), + }, + { + name: "typical case", + setA: setFromSlice(node1, node2), + setB: setFromSlice(node2, node3), + expectedResult: setFromSlice(node1, node2, node3), + }, + } + + for _, test := range tests { + result := test.setA.union(test.setB) + if !reflect.DeepEqual(result, test.expectedResult) { + t.Errorf("blockSet.union: unexpected result in test '%s'. "+ + "Expected: %v, got: %v", test.name, test.expectedResult, result) + } + } +} + +func TestBlockSetHashesEqual(t *testing.T) { + node1 := &blockNode{hash: daghash.Hash{10}} + node2 := &blockNode{hash: daghash.Hash{20}} + + tests := []struct { + name string + set blockSet + hashes []daghash.Hash + expectedResult bool + }{ + { + name: "empty set, no hashes", + set: setFromSlice(), + hashes: []daghash.Hash{}, + expectedResult: true, + }, + { + name: "empty set, one hash", + set: setFromSlice(), + hashes: []daghash.Hash{node1.hash}, + expectedResult: false, + }, + { + name: "set and hashes of different length", + set: setFromSlice(node1, node2), + hashes: []daghash.Hash{node1.hash}, + expectedResult: false, + }, + { + name: "set equal to hashes", + set: setFromSlice(node1, node2), + hashes: []daghash.Hash{node1.hash, node2.hash}, + expectedResult: true, + }, + { + name: "set equal to hashes, different order", + set: setFromSlice(node1, node2), + hashes: []daghash.Hash{node2.hash, node1.hash}, + expectedResult: true, + }, + } + + for _, test := range tests { + result := test.set.hashesEqual(test.hashes) + if result != test.expectedResult { + t.Errorf("blockSet.hashesEqual: unexpected result in test '%s'. "+ + "Expected: %t, got: %t", test.name, test.expectedResult, result) + } + } +} diff --git a/blockdag/dagio.go b/blockdag/dagio.go index 94d195910..c521064e5 100644 --- a/blockdag/dagio.go +++ b/blockdag/dagio.go @@ -103,272 +103,6 @@ func dbPutVersion(dbTx database.Tx, key []byte, version uint32) error { return dbTx.Metadata().Put(key, serialized[:]) } -// ----------------------------------------------------------------------------- -// The transaction spend journal consists of an entry for each block connected -// to the main chain which contains the transaction outputs the block spends -// serialized such that the order is the reverse of the order they were spent. -// -// This is required because reorganizing the chain necessarily entails -// disconnecting blocks to get back to the point of the fork which implies -// unspending all of the transaction outputs that each block previously spent. -// Since the UTXO set, by definition, only contains unspent transaction outputs, -// the spent transaction outputs must be resurrected from somewhere. There is -// more than one way this could be done, however this is the most straight -// forward method that does not require having a transaction index and unpruned -// blockchain. -// -// NOTE: This format is NOT self describing. The additional details such as -// the number of entries (transaction inputs) are expected to come from the -// block itself and the UTXO set (for legacy entries). The rationale in doing -// this is to save space. This is also the reason the spent outputs are -// serialized in the reverse order they are spent because later transactions are -// allowed to spend outputs from earlier ones in the same block. -// -// The reserved field below used to keep track of the version of the containing -// transaction when the height in the header code was non-zero, however the -// height is always non-zero now, but keeping the extra reserved field allows -// backwards compatibility. -// -// The serialized format is: -// -// [
],... -// -// Field Type Size -// header code VLQ variable -// reserved byte 1 -// compressed txout -// compressed amount VLQ variable -// compressed script []byte variable -// -// The serialized header code format is: -// bit 0 - containing transaction is a coinbase -// bits 1-x - height of the block that contains the spent txout -// -// Example 1: -// From block 170 in main blockchain. -// -// 1300320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c -// <><><------------------------------------------------------------------> -// | | | -// | reserved compressed txout -// header code -// -// - header code: 0x13 (coinbase, height 9) -// - reserved: 0x00 -// - compressed txout 0: -// - 0x32: VLQ-encoded compressed amount for 5000000000 (50 BTC) -// - 0x05: special script type pay-to-pubkey -// - 0x11...5c: x-coordinate of the pubkey -// -// Example 2: -// Adapted from block 100025 in main blockchain. -// -// 8b99700091f20f006edbc6c4d31bae9f1ccc38538a114bf42de65e868b99700086c64700b2fb57eadf61e106a100a7445a8c3f67898841ec -// <----><><----------------------------------------------><----><><----------------------------------------------> -// | | | | | | -// | reserved compressed txout | reserved compressed txout -// header code header code -// -// - Last spent output: -// - header code: 0x8b9970 (not coinbase, height 100024) -// - reserved: 0x00 -// - compressed txout: -// - 0x91f20f: VLQ-encoded compressed amount for 34405000000 (344.05 BTC) -// - 0x00: special script type pay-to-pubkey-hash -// - 0x6e...86: pubkey hash -// - Second to last spent output: -// - header code: 0x8b9970 (not coinbase, height 100024) -// - reserved: 0x00 -// - compressed txout: -// - 0x86c647: VLQ-encoded compressed amount for 13761000000 (137.61 BTC) -// - 0x00: special script type pay-to-pubkey-hash -// - 0xb2...ec: pubkey hash -// ----------------------------------------------------------------------------- - -// spentTxOut contains a spent transaction output and potentially additional -// contextual information such as whether or not it was contained in a coinbase -// transaction, the version of the transaction it was contained in, and which -// block height the containing transaction was included in. As described in -// the comments above, the additional contextual information will only be valid -// when this spent txout is spending the last unspent output of the containing -// transaction. -type spentTxOut struct { - amount int64 // The amount of the output. - pkScript []byte // The public key script for the output. - height int32 // Height of the the block containing the creating tx. - isCoinBase bool // Whether creating tx is a coinbase. -} - -// spentTxOutHeaderCode returns the calculated header code to be used when -// serializing the provided stxo entry. -func spentTxOutHeaderCode(stxo *spentTxOut) uint64 { - // As described in the serialization format comments, the header code - // encodes the height shifted over one bit and the coinbase flag in the - // lowest bit. - headerCode := uint64(stxo.height) << 1 - if stxo.isCoinBase { - headerCode |= 0x01 - } - - return headerCode -} - -// spentTxOutSerializeSize returns the number of bytes it would take to -// serialize the passed stxo according to the format described above. -func spentTxOutSerializeSize(stxo *spentTxOut) int { - size := serializeSizeVLQ(spentTxOutHeaderCode(stxo)) - if stxo.height > 0 { - // The legacy v1 spend journal format conditionally tracked the - // containing transaction version when the height was non-zero, - // so this is required for backwards compat. - size += serializeSizeVLQ(0) - } - return size + compressedTxOutSize(uint64(stxo.amount), stxo.pkScript) -} - -// putSpentTxOut serializes the passed stxo according to the format described -// above directly into the passed target byte slice. The target byte slice must -// be at least large enough to handle the number of bytes returned by the -// spentTxOutSerializeSize function or it will panic. -func putSpentTxOut(target []byte, stxo *spentTxOut) int { - headerCode := spentTxOutHeaderCode(stxo) - offset := putVLQ(target, headerCode) - if stxo.height > 0 { - // The legacy v1 spend journal format conditionally tracked the - // containing transaction version when the height was non-zero, - // so this is required for backwards compat. - offset += putVLQ(target[offset:], 0) - } - return offset + putCompressedTxOut(target[offset:], uint64(stxo.amount), - stxo.pkScript) -} - -// decodeSpentTxOut decodes the passed serialized stxo entry, possibly followed -// by other data, into the passed stxo struct. It returns the number of bytes -// read. -func decodeSpentTxOut(serialized []byte, stxo *spentTxOut) (int, error) { - // Ensure there are bytes to decode. - if len(serialized) == 0 { - return 0, errDeserialize("no serialized bytes") - } - - // Deserialize the header code. - code, offset := deserializeVLQ(serialized) - if offset >= len(serialized) { - return offset, errDeserialize("unexpected end of data after " + - "header code") - } - - // Decode the header code. - // - // Bit 0 indicates containing transaction is a coinbase. - // Bits 1-x encode height of containing transaction. - stxo.isCoinBase = code&0x01 != 0 - stxo.height = int32(code >> 1) - if stxo.height > 0 { - // The legacy v1 spend journal format conditionally tracked the - // containing transaction version when the height was non-zero, - // so this is required for backwards compat. - _, bytesRead := deserializeVLQ(serialized[offset:]) - offset += bytesRead - if offset >= len(serialized) { - return offset, errDeserialize("unexpected end of data " + - "after reserved") - } - } - - // Decode the compressed txout. - amount, pkScript, bytesRead, err := decodeCompressedTxOut( - serialized[offset:]) - offset += bytesRead - if err != nil { - return offset, errDeserialize(fmt.Sprintf("unable to decode "+ - "txout: %v", err)) - } - stxo.amount = int64(amount) - stxo.pkScript = pkScript - return offset, nil -} - -// deserializeSpendJournalEntry decodes the passed serialized byte slice into a -// slice of spent txouts according to the format described in detail above. -// -// Since the serialization format is not self describing, as noted in the -// format comments, this function also requires the transactions that spend the -// txouts. -func deserializeSpendJournalEntry(serialized []byte, txs []*wire.MsgTx) ([]spentTxOut, error) { - // Calculate the total number of stxos. - var numStxos int - for _, tx := range txs { - numStxos += len(tx.TxIn) - } - - // When a block has no spent txouts there is nothing to serialize. - if len(serialized) == 0 { - // Ensure the block actually has no stxos. This should never - // happen unless there is database corruption or an empty entry - // erroneously made its way into the database. - if numStxos != 0 { - return nil, AssertError(fmt.Sprintf("mismatched spend "+ - "journal serialization - no serialization for "+ - "expected %d stxos", numStxos)) - } - - return nil, nil - } - - // Loop backwards through all transactions so everything is read in - // reverse order to match the serialization order. - stxoIdx := numStxos - 1 - offset := 0 - stxos := make([]spentTxOut, numStxos) - for txIdx := len(txs) - 1; txIdx > -1; txIdx-- { - tx := txs[txIdx] - - // Loop backwards through all of the transaction inputs and read - // the associated stxo. - for txInIdx := len(tx.TxIn) - 1; txInIdx > -1; txInIdx-- { - txIn := tx.TxIn[txInIdx] - stxo := &stxos[stxoIdx] - stxoIdx-- - - n, err := decodeSpentTxOut(serialized[offset:], stxo) - offset += n - if err != nil { - return nil, errDeserialize(fmt.Sprintf("unable "+ - "to decode stxo for %v: %v", - txIn.PreviousOutPoint, err)) - } - } - } - - return stxos, nil -} - -// serializeSpendJournalEntry serializes all of the passed spent txouts into a -// single byte slice according to the format described in detail above. -func serializeSpendJournalEntry(stxos []spentTxOut) []byte { - if len(stxos) == 0 { - return nil - } - - // Calculate the size needed to serialize the entire journal entry. - var size int - for i := range stxos { - size += spentTxOutSerializeSize(&stxos[i]) - } - serialized := make([]byte, size) - - // Serialize each individual stxo directly into the slice in reverse - // order one after the other. - var offset int - for i := len(stxos) - 1; i > -1; i-- { - offset += putSpentTxOut(serialized[offset:], &stxos[i]) - } - - return serialized -} - // ----------------------------------------------------------------------------- // The unspent transaction output (UTXO) set consists of an entry for each // unspent output using a format that is optimized to reduce space using domain diff --git a/blockdag/dagio_test.go b/blockdag/dagio_test.go index 2179ee871..2065267bf 100644 --- a/blockdag/dagio_test.go +++ b/blockdag/dagio_test.go @@ -7,13 +7,11 @@ package blockdag import ( "bytes" "errors" - "math" "reflect" "testing" "github.com/daglabs/btcd/dagconfig/daghash" "github.com/daglabs/btcd/database" - "github.com/daglabs/btcd/wire" ) // TestErrNotInDAG ensures the functions related to errNotInDAG work @@ -38,371 +36,6 @@ func TestErrNotInDAG(t *testing.T) { } } -// 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 - 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, - }, - serialized: hexToBytes("1300320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), - }, - // 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, - }, - serialized: hexToBytes("8b99700086c64700b2fb57eadf61e106a100a7445a8c3f67898841ec"), - }, - // Adapted from block 100025 in main blockchain. - { - name: "Does not spend last output, legacy format", - stxo: spentTxOut{ - amount: 34405000000, - pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), - }, - 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) - if err != nil { - t.Errorf("decodeSpentTxOut (%s): unexpected error: %v", - test.name, err) - continue - } - 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 - 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 reserved", - stxo: spentTxOut{}, - serialized: hexToBytes("00"), - errType: errDeserialize(""), - bytesRead: 1, - }, - { - name: "no data after header code with reserved", - stxo: spentTxOut{}, - serialized: hexToBytes("13"), - errType: errDeserialize(""), - bytesRead: 1, - }, - { - name: "no data after reserved", - stxo: spentTxOut{}, - serialized: hexToBytes("1300"), - errType: errDeserialize(""), - bytesRead: 2, - }, - { - name: "incomplete compressed txout", - stxo: spentTxOut{}, - serialized: hexToBytes("1332"), - errType: errDeserialize(""), - bytesRead: 2, - }, - } - - for _, test := range tests { - // Ensure the expected error type is returned. - gotBytesRead, err := decodeSpentTxOut(test.serialized, - &test.stxo) - 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 - serialized []byte - }{ - // From block 2 in main blockchain. - { - name: "No spends", - entry: nil, - blockTxns: nil, - 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, - }}, - blockTxns: []*wire.MsgTx{{ // Coinbase omitted. - Version: 1, - TxIn: []*wire.TxIn{{ - PreviousOutPoint: wire.OutPoint{ - TxID: *newTxIDFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), - Index: 0, - }, - SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), - Sequence: math.MaxUint64, - }}, - TxOut: []*wire.TxOut{{ - Value: 1000000000, - PkScript: hexToBytes("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac"), - }, { - Value: 4000000000, - PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), - }}, - LockTime: 0, - }}, - serialized: hexToBytes("1300320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), - }, - // 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"), - isCoinBase: false, - height: 100024, - }, { - amount: 13761000000, - pkScript: hexToBytes("76a914b2fb57eadf61e106a100a7445a8c3f67898841ec88ac"), - isCoinBase: false, - height: 100024, - }}, - blockTxns: []*wire.MsgTx{{ // Coinbase omitted. - Version: 1, - TxIn: []*wire.TxIn{{ - PreviousOutPoint: wire.OutPoint{ - TxID: *newTxIDFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), - Index: 1, - }, - SignatureScript: hexToBytes("493046022100c167eead9840da4a033c9a56470d7794a9bb1605b377ebe5688499b39f94be59022100fb6345cab4324f9ea0b9ee9169337534834638d818129778370f7d378ee4a325014104d962cac5390f12ddb7539507065d0def320d68c040f2e73337c3a1aaaab7195cb5c4d02e0959624d534f3c10c3cf3d73ca5065ebd62ae986b04c6d090d32627c"), - Sequence: math.MaxUint64, - }}, - TxOut: []*wire.TxOut{{ - Value: 5000000, - PkScript: hexToBytes("76a914f419b8db4ba65f3b6fcc233acb762ca6f51c23d488ac"), - }, { - Value: 34400000000, - PkScript: hexToBytes("76a914cadf4fc336ab3c6a4610b75f31ba0676b7f663d288ac"), - }}, - LockTime: 0, - }, { - Version: 1, - TxIn: []*wire.TxIn{{ - PreviousOutPoint: wire.OutPoint{ - TxID: *newTxIDFromStr("92fbe1d4be82f765dfabc9559d4620864b05cc897c4db0e29adac92d294e52b7"), - Index: 0, - }, - SignatureScript: hexToBytes("483045022100e256743154c097465cf13e89955e1c9ff2e55c46051b627751dee0144183157e02201d8d4f02cde8496aae66768f94d35ce54465bd4ae8836004992d3216a93a13f00141049d23ce8686fe9b802a7a938e8952174d35dd2c2089d4112001ed8089023ab4f93a3c9fcd5bfeaa9727858bf640dc1b1c05ec3b434bb59837f8640e8810e87742"), - Sequence: math.MaxUint64, - }}, - TxOut: []*wire.TxOut{{ - Value: 5000000, - PkScript: hexToBytes("76a914a983ad7c92c38fc0e2025212e9f972204c6e687088ac"), - }, { - Value: 13756000000, - PkScript: hexToBytes("76a914a6ebd69952ab486a7a300bfffdcb395dc7d47c2388ac"), - }}, - LockTime: 0, - }}, - serialized: hexToBytes("8b99700086c64700b2fb57eadf61e106a100a7445a8c3f67898841ec8b99700091f20f006edbc6c4d31bae9f1ccc38538a114bf42de65e86"), - }, - } - - 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) - if err != nil { - t.Errorf("deserializeSpendJournalEntry #%d (%s) "+ - "unexpected error: %v", i, test.name, err) - continue - } - - // 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 - 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{ - TxID: *newTxIDFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), - Index: 0, - }, - SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), - Sequence: math.MaxUint64, - }}, - LockTime: 0, - }}, - serialized: hexToBytes(""), - errType: AssertError(""), - }, - { - name: "Force deserialization error in stxos", - blockTxns: []*wire.MsgTx{{ // Coinbase omitted. - Version: 1, - TxIn: []*wire.TxIn{{ - PreviousOutPoint: wire.OutPoint{ - TxID: *newTxIDFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), - Index: 0, - }, - SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), - Sequence: math.MaxUint64, - }}, - LockTime: 0, - }}, - 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) - 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) { diff --git a/blockdag/error_test.go b/blockdag/error_test.go index e997e4a3d..6f50983be 100644 --- a/blockdag/error_test.go +++ b/blockdag/error_test.go @@ -5,6 +5,7 @@ package blockdag import ( + "fmt" "testing" ) @@ -131,5 +132,14 @@ func TestDeploymentError(t *testing.T) { continue } } - +} + +func TestAssertError(t *testing.T) { + message := "abc 123" + err := AssertError(message) + expectedMessage := fmt.Sprintf("assertion failed: %s", message) + if expectedMessage != err.Error() { + t.Errorf("Unexpected AssertError message. "+ + "Got: %s, want: %s", err.Error(), expectedMessage) + } } diff --git a/blockdag/utxoset.go b/blockdag/utxoset.go index 91de55e9e..234f1c6a4 100644 --- a/blockdag/utxoset.go +++ b/blockdag/utxoset.go @@ -52,20 +52,6 @@ func (entry *UTXOEntry) PkScript() []byte { return entry.pkScript } -// Clone returns a shallow copy of the utxo entry. -func (entry *UTXOEntry) Clone() *UTXOEntry { - if entry == nil { - return nil - } - - return &UTXOEntry{ - amount: entry.amount, - pkScript: entry.pkScript, - blockHeight: entry.blockHeight, - packedFlags: entry.packedFlags, - } -} - // txoFlags is a bitmask defining additional information and state for a // transaction output in a UTXO set. type txoFlags uint8 diff --git a/blockdag/utxoset_test.go b/blockdag/utxoset_test.go index 467e05cbb..42ad18eb0 100644 --- a/blockdag/utxoset_test.go +++ b/blockdag/utxoset_test.go @@ -955,3 +955,107 @@ func (dus *DiffUTXOSet) collection() utxoCollection { return clone.base.collection() } + +func TestUTXOSetAddEntry(t *testing.T) { + hash0, _ := daghash.NewTxIDFromStr("0000000000000000000000000000000000000000000000000000000000000000") + hash1, _ := daghash.NewTxIDFromStr("1111111111111111111111111111111111111111111111111111111111111111") + outPoint0 := wire.NewOutPoint(hash0, 0) + outPoint1 := wire.NewOutPoint(hash1, 0) + utxoEntry0 := NewUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10}, true, 0) + utxoEntry1 := NewUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 20}, false, 1) + + utxoDiff := NewUTXODiff() + + tests := []struct { + name string + outPointToAdd *wire.OutPoint + utxoEntryToAdd *UTXOEntry + expectedUTXODiff *UTXODiff + }{ + { + name: "add an entry", + outPointToAdd: outPoint0, + utxoEntryToAdd: utxoEntry0, + expectedUTXODiff: &UTXODiff{ + toAdd: utxoCollection{*outPoint0: utxoEntry0}, + toRemove: utxoCollection{}, + }, + }, + { + name: "add another entry", + outPointToAdd: outPoint1, + utxoEntryToAdd: utxoEntry1, + expectedUTXODiff: &UTXODiff{ + toAdd: utxoCollection{*outPoint0: utxoEntry0, *outPoint1: utxoEntry1}, + toRemove: utxoCollection{}, + }, + }, + { + name: "add first entry again", + outPointToAdd: outPoint0, + utxoEntryToAdd: utxoEntry0, + expectedUTXODiff: &UTXODiff{ + toAdd: utxoCollection{*outPoint0: utxoEntry0, *outPoint1: utxoEntry1}, + toRemove: utxoCollection{}, + }, + }, + } + + for _, test := range tests { + utxoDiff.AddEntry(*test.outPointToAdd, test.utxoEntryToAdd) + if !reflect.DeepEqual(utxoDiff, test.expectedUTXODiff) { + t.Fatalf("utxoDiff.AddEntry: unexpected utxoDiff in test '%s'. "+ + "Expected: %v, got: %v", test.name, test.expectedUTXODiff, utxoDiff) + } + } +} + +func TestUTXOSetRemoveTxOuts(t *testing.T) { + tx0 := &wire.MsgTx{TxOut: []*wire.TxOut{{PkScript: []byte{1}, Value: 10}}} + tx1 := &wire.MsgTx{TxOut: []*wire.TxOut{{PkScript: []byte{2}, Value: 20}}} + hash0 := tx0.TxID() + hash1 := tx1.TxID() + outPoint0 := wire.NewOutPoint(&hash0, 0) + outPoint1 := wire.NewOutPoint(&hash1, 0) + + utxoDiff := NewUTXODiff() + + tests := []struct { + name string + txToRemove *wire.MsgTx + expectedUTXODiff *UTXODiff + }{ + { + name: "remove a transaction", + txToRemove: tx0, + expectedUTXODiff: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{*outPoint0: nil}, + }, + }, + { + name: "remove another transaction", + txToRemove: tx1, + expectedUTXODiff: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{*outPoint0: nil, *outPoint1: nil}, + }, + }, + { + name: "remove first entry again", + txToRemove: tx0, + expectedUTXODiff: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{*outPoint0: nil, *outPoint1: nil}, + }, + }, + } + + for _, test := range tests { + utxoDiff.RemoveTxOuts(test.txToRemove) + if !reflect.DeepEqual(utxoDiff, test.expectedUTXODiff) { + t.Fatalf("utxoDiff.AddEntry: unexpected utxoDiff in test '%s'. "+ + "Expected: %v, got: %v", test.name, test.expectedUTXODiff, utxoDiff) + } + } +}