[DEV-92] Package blockdag tests (#177)

* [DEV-92] Covered blocknode.go with tests.

* [DEV-92] Added test for blockSet.highest. Fixed a bug in it.

* [DEV-92] Added tests for blockSet.subtract and blockSet.addSet.

* [DEV-92] Covered blockset.go with tests.

* [DEV-92] Got rid of some old stuff related to STXOs.

* [DEV-92] Covered error.go with tests.

* [DEV-92] Covered utxoSet with tests.

* [DEV-92] Fixed formatting.
This commit is contained in:
stasatdaglabs 2019-01-27 18:04:31 +02:00 committed by Ori Newman
parent 100fbbaaa4
commit a82e6ae24a
8 changed files with 423 additions and 649 deletions

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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:
//
// [<header code><reserved><compressed txout>],...
//
// 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

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}
}