dcrd/blockchain/chainio_test.go
Aaron Campbell 8be96a8729 multi: Correct typos.
Correct typos found by reading code, ispell, and creative grepping.
2019-08-22 10:20:03 -05:00

1515 lines
52 KiB
Go

// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2015-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package blockchain
import (
"bytes"
"encoding/hex"
"errors"
"math/big"
"reflect"
"testing"
"time"
"github.com/decred/dcrd/blockchain/stake/v2"
"github.com/decred/dcrd/blockchain/standalone"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/database/v2"
"github.com/decred/dcrd/wire"
)
// newHashFromStr converts the passed big-endian hex string into a
// chainhash.Hash. It only differs from the one available in chainhash in that
// it ignores the error since it will only (and must only) be called with
// hard-coded, and therefore known good, hashes.
func newHashFromStr(hexStr string) *chainhash.Hash {
hash, _ := chainhash.NewHashFromStr(hexStr)
return hash
}
// hexToFinalState converts the passed hex string into an array of 6 bytes and
// will panic if there is an error. This is only provided for the hard-coded
// constants so errors in the source code can be detected. It will only (and
// must only) be called with hard-coded values.
func hexToFinalState(s string) [6]byte {
b, err := hex.DecodeString(s)
if err != nil {
panic("invalid hex in source file: " + s)
}
var finalState [6]byte
if len(b) != len(finalState) {
panic("invalid hex in source file: " + s)
}
copy(finalState[:], b)
return finalState
}
// hexToExtraData converts the passed hex string into an array of 32 bytes and
// will panic if there is an error. This is only provided for the hard-coded
// constants so errors in the source code can be detected. It will only (and
// must only) be called with hard-coded values.
func hexToExtraData(s string) [32]byte {
b, err := hex.DecodeString(s)
if err != nil {
panic("invalid hex in source file: " + s)
}
var extraData [32]byte
if len(b) != len(extraData) {
panic("invalid hex in source file: " + s)
}
copy(extraData[:], b)
return extraData
}
// isNotInMainChainErr returns whether or not the passed error is an
// errNotInMainChain error.
func isNotInMainChainErr(err error) bool {
_, ok := err.(errNotInMainChain)
return ok
}
// 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 returned 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")
}
}
// TestBlockIndexSerialization ensures serializing and deserializing block index
// entries works as expected.
func TestBlockIndexSerialization(t *testing.T) {
t.Parallel()
// base data is based on block 150287 on mainnet and serves as a template
// for the various tests below.
baseHeader := wire.BlockHeader{
Version: 4,
PrevBlock: *newHashFromStr("000000000000016916671ae225343a5ee131c999d5cadb6348805db25737731f"),
MerkleRoot: *newHashFromStr("5ef2bb79795d7503c0ccc5cb6e0d4731992fc8c8c5b332c1c0e2c687d864c666"),
StakeRoot: *newHashFromStr("022965059b7527dc2bc18daaa533f806eda1f96fd0b04bbda2381f5552d7c2de"),
VoteBits: 0x0001,
FinalState: hexToFinalState("313e16e64c0b"),
Voters: 4,
FreshStake: 3,
Revocations: 2,
PoolSize: 41332,
Bits: 0x1a016f98,
SBits: 7473162478,
Height: 150287,
Size: 11295,
Timestamp: time.Unix(1499907127, 0),
Nonce: 4116576260,
ExtraData: hexToExtraData("8f01ed92645e0a6b11ee3b3c0000000000000000000000000000000000000000"),
StakeVersion: 4,
}
baseTicketsVoted := []chainhash.Hash{
*newHashFromStr("8b62a877544753ea80a822142a48ec066170e9381d21a9e8a84bc7373f0f9b2e"),
*newHashFromStr("4427a003a7aceb1404ffd9072e9aff1e128a24333a543332030e91668a389db7"),
*newHashFromStr("4415b88ac74881d7b6b15d41df465257cd1cc92d55e95f1b648434aef3a2110b"),
*newHashFromStr("9d2621b57352088809d3a069b04b76c832f30a76da14e56aece72208b3e5b87a"),
}
baseTicketsRevoked := []chainhash.Hash{
*newHashFromStr("8146f01b8ffca8008ebc80293d2978d63b1dffa5c456a73e7b39a9b1e695e8eb"),
*newHashFromStr("2292ff2461e725c58cc6e2051eac2a10e6ee6d1f62327ed676b7a196fb94be0c"),
}
baseVoteInfo := []stake.VoteVersionTuple{
{Version: 4, Bits: 0x0001},
{Version: 4, Bits: 0x0015},
{Version: 4, Bits: 0x0015},
{Version: 4, Bits: 0x0001},
}
tests := []struct {
name string
entry blockIndexEntry
serialized []byte
}{
{
name: "no votes, no revokes",
entry: blockIndexEntry{
header: baseHeader,
status: statusDataStored | statusValid,
voteInfo: nil,
ticketsVoted: nil,
ticketsRevoked: nil,
},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030000"),
},
{
name: "1 vote, no revokes",
entry: blockIndexEntry{
header: baseHeader,
status: statusDataStored | statusValid,
voteInfo: baseVoteInfo[:1],
ticketsVoted: baseTicketsVoted[:1],
ticketsRevoked: nil,
},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b040100"),
},
{
name: "no votes, 1 revoke",
entry: blockIndexEntry{
header: baseHeader,
status: statusDataStored | statusValid,
voteInfo: nil,
ticketsVoted: nil,
ticketsRevoked: baseTicketsRevoked[:1],
},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030001ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8e00a8fc8f1bf04681"),
},
{
name: "4 votes, same vote versions, different vote bits, 2 revokes",
entry: blockIndexEntry{
header: baseHeader,
status: statusDataStored | statusValid,
voteInfo: baseVoteInfo,
ticketsVoted: baseTicketsVoted,
ticketsRevoked: baseTicketsRevoked,
},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003042e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0401b79d388a66910e033233543a33248a121eff9a2e07d9ff0414ebaca703a0274404150b11a2f3ae3484641b5fe9552dc91ccd575246df415db1b6d78148c78ab8154404157ab8e5b30822e7ec6ae514da760af332c8764bb069a0d30988085273b521269d040102ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8e00a8fc8f1bf046810cbe94fb96a1b776d67e32621f6deee6102aac1e05e2c68cc525e76124ff9222"),
},
}
for _, test := range tests {
// Ensure the function to calculate the serialized size without
// actually serializing it is calculated properly.
gotSize := blockIndexEntrySerializeSize(&test.entry)
if gotSize != len(test.serialized) {
t.Errorf("blockIndexEntrySerializeSize (%s): did not "+
"get expected size - got %d, want %d", test.name,
gotSize, len(test.serialized))
}
// Ensure the block index entry serializes to the expected value.
gotSerialized, err := serializeBlockIndexEntry(&test.entry)
if err != nil {
t.Errorf("serializeBlockIndexEntry (%s): unexpected "+
"error: %v", test.name, err)
continue
}
if !bytes.Equal(gotSerialized, test.serialized) {
t.Errorf("serializeBlockIndexEntry (%s): did not get "+
"expected bytes - got %x, want %x", test.name,
gotSerialized, test.serialized)
continue
}
// Ensure the block index entry serializes to the expected value
// and produces the expected number of bytes written via a
// direct put.
gotSerialized2 := make([]byte, gotSize)
gotBytesWritten, err := putBlockIndexEntry(gotSerialized2,
&test.entry)
if err != nil {
t.Errorf("putBlockIndexEntry (%s): unexpected error: %v",
test.name, err)
continue
}
if !bytes.Equal(gotSerialized2, test.serialized) {
t.Errorf("putBlockIndexEntry (%s): did not get "+
"expected bytes - got %x, want %x", test.name,
gotSerialized2, test.serialized)
continue
}
if gotBytesWritten != len(test.serialized) {
t.Errorf("putBlockIndexEntry (%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
// block index entry.
gotEntry, err := deserializeBlockIndexEntry(test.serialized)
if err != nil {
t.Errorf("deserializeBlockIndexEntry (%s): unexpected "+
"error: %v", test.name, err)
continue
}
if !reflect.DeepEqual(*gotEntry, test.entry) {
t.Errorf("deserializeBlockIndexEntry (%s): mismatched "+
"entries\ngot %+v\nwant %+v", test.name,
gotEntry, test.entry)
continue
}
// Ensure the serialized bytes are decoded back to the expected
// block index entry.
var gotEntry2 blockIndexEntry
bytesRead, err := decodeBlockIndexEntry(test.serialized, &gotEntry2)
if err != nil {
t.Errorf("decodeBlockIndexEntry (%s): unexpected "+
"error: %v", test.name, err)
continue
}
if !reflect.DeepEqual(gotEntry2, test.entry) {
t.Errorf("decodeBlockIndexEntry (%s): mismatched "+
"entries\ngot %+v\nwant %+v", test.name,
gotEntry2, test.entry)
continue
}
if bytesRead != len(test.serialized) {
t.Errorf("decodeBlockIndexEntry (%s): did not get "+
"expected number of bytes read - got %d, "+
"want %d", test.name, bytesRead,
len(test.serialized))
continue
}
}
}
// TestBlockIndexDecodeErrors performs negative tests against decoding block
// index entries to ensure error paths work as expected.
func TestBlockIndexDecodeErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
entry blockIndexEntry
serialized []byte
bytesRead int // Expected number of bytes read.
errType error
}{
{
name: "nothing serialized",
entry: blockIndexEntry{},
serialized: hexToBytes(""),
errType: errDeserialize(""),
bytesRead: 0,
},
{
name: "no data after block header",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000"),
errType: errDeserialize(""),
bytesRead: 180,
},
{
name: "no data after status",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003"),
errType: errDeserialize(""),
bytesRead: 181,
},
{
name: "no data after num votes with no votes",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c0000000000000000000000000000000000000000040000000300"),
errType: errDeserialize(""),
bytesRead: 182,
},
{
name: "no data after num votes with votes",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c0000000000000000000000000000000000000000040000000301"),
errType: errDeserialize(""),
bytesRead: 182,
},
{
name: "short data in vote ticket hash",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a862"),
errType: errDeserialize(""),
bytesRead: 182,
},
{
name: "no data after vote ticket hash",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b"),
errType: errDeserialize(""),
bytesRead: 214,
},
{
name: "no data after vote version",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b04"),
errType: errDeserialize(""),
bytesRead: 215,
},
{
name: "no data after votes",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0401"),
errType: errDeserialize(""),
bytesRead: 216,
},
{
name: "no data after num revokes with revokes",
entry: blockIndexEntry{},
serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030001"),
errType: errDeserialize(""),
bytesRead: 183,
},
}
for _, test := range tests {
// Ensure the expected error type is returned.
gotBytesRead, err := decodeBlockIndexEntry(test.serialized,
&test.entry)
if reflect.TypeOf(err) != reflect.TypeOf(test.errType) {
t.Errorf("decodeBlockIndexEntry (%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("decodeBlockIndexEntry (%s): unexpected "+
"number of bytes read - got %d, want %d",
test.name, gotBytesRead, test.bytesRead)
continue
}
}
}
// 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
}{
{
name: "Spends last output of coinbase",
stxo: spentTxOut{
amount: 9999,
scriptVersion: 0,
pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"),
height: 12345,
index: 54321,
isCoinBase: true,
hasExpiry: false,
txFullySpent: true,
txType: 0,
txVersion: 1,
},
serialized: hexToBytes("1100006edbc6c4d31bae9f1ccc38538a114bf42de65e8601"),
},
{
name: "Spends last output of non coinbase and is a ticket",
stxo: spentTxOut{
amount: 9999,
scriptVersion: 0,
pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"),
height: 12345,
index: 54321,
isCoinBase: false,
hasExpiry: false,
txFullySpent: true,
txType: 1,
txVersion: 1,
stakeExtra: []byte{0x00},
},
serialized: hexToBytes("1400006edbc6c4d31bae9f1ccc38538a114bf42de65e860100"),
},
{
name: "Does not spend last output",
stxo: spentTxOut{
amount: 34405000000,
pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"),
scriptVersion: 0,
},
txVersion: 1,
serialized: hexToBytes("0000006edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
},
}
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))
}
// 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
offset, err := decodeSpentTxOut(test.serialized, &gotStxo,
test.stxo.amount, test.stxo.height, test.stxo.index)
if err != nil {
t.Errorf("decodeSpentTxOut (%s): unexpected error: %v",
test.name, err)
continue
}
if offset != len(test.serialized) {
t.Errorf("decodeSpentTxOut (%s): did not get expected "+
"number of bytes read - got %d, want %d",
test.name, offset, 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
errType error
bytesRead int // Expected number of bytes read.
}{
{
// [EOF]
name: "nothing serialized (no flags)",
stxo: spentTxOut{},
serialized: hexToBytes(""),
errType: errDeserialize(""),
bytesRead: 0,
},
{
// [<flags 00> EOF]
name: "no compressed txout script version",
stxo: spentTxOut{},
serialized: hexToBytes("00"),
errType: errDeserialize(""),
bytesRead: 1,
},
{
// [<flags 10> <script version 00> EOF]
name: "no tx version data after empty script for a fully spent regular stxo",
stxo: spentTxOut{},
serialized: hexToBytes("1000"),
errType: errDeserialize(""),
bytesRead: 2,
},
{
// [<flags 10> <script version 00> <compressed pk script 01 6e ...> EOF]
name: "no tx version data after a pay-to-script-hash script for a fully spent regular stxo",
stxo: spentTxOut{},
serialized: hexToBytes("1000016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
errType: errDeserialize(""),
bytesRead: 23,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> EOF]
name: "no stakeextra data after script for a fully spent ticket stxo",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601"),
errType: errDeserialize(""),
bytesRead: 24,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (num outputs only)",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e860101"),
errType: errDeserialize(""),
bytesRead: 25,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (num outputs and amount only)",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f"),
errType: errDeserialize(""),
bytesRead: 26,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f} {script version 00}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (num outputs, amount, and script version only)",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f00"),
errType: errDeserialize(""),
bytesRead: 27,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f} {script version 00} {script size 1a} {25 bytes of script instead of 26}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (script size specified as 0x1a, but only 0x19 bytes provided)",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588"),
errType: errDeserialize(""),
bytesRead: 28,
},
}
for _, test := range tests {
// Ensure the expected error type is returned.
gotBytesRead, err := decodeSpentTxOut(test.serialized,
&test.stxo, test.stxo.amount, test.stxo.height, test.stxo.index)
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
}{
{
name: "No spends",
entry: nil,
blockTxns: nil,
utxoView: NewUtxoViewpoint(),
serialized: nil,
},
{
name: "One tx with one input spends last output of coinbase",
entry: []spentTxOut{{
amount: 5000000000,
scriptVersion: 0,
pkScript: hexToBytes("0511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"),
compressed: true,
height: 9,
index: 0,
isCoinBase: true,
hasExpiry: false,
txFullySpent: true,
txType: 0,
txVersion: 1,
stakeExtra: nil,
}},
blockTxns: []*wire.MsgTx{{ // Coinbase omitted.
SerType: wire.TxSerializeFull,
Version: 1,
TxIn: []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"),
Index: 0,
Tree: 0,
},
SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"),
Sequence: 0xffffffff,
BlockHeight: 9,
BlockIndex: 0,
ValueIn: 5000000000,
}},
TxOut: []*wire.TxOut{{
Value: 1000000000,
Version: 0,
PkScript: hexToBytes("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac"),
}, {
Value: 4000000000,
Version: 0,
PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"),
}},
LockTime: 0,
Expiry: 0,
}},
utxoView: NewUtxoViewpoint(),
serialized: hexToBytes("11000511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c01"),
},
{
name: "Two txns when one spends last output, one doesn't",
entry: []spentTxOut{{
amount: 34405000000,
height: 321,
index: 123,
scriptVersion: 0,
pkScript: hexToBytes("016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
compressed: true,
}, {
amount: 13761000000,
scriptVersion: 0,
pkScript: hexToBytes("01b2fb57eadf61e106a100a7445a8c3f67898841ec"),
compressed: true,
height: 3214,
index: 1234,
isCoinBase: false,
hasExpiry: false,
txFullySpent: true,
txType: 0,
txVersion: 1,
stakeExtra: nil,
}},
blockTxns: []*wire.MsgTx{{ // Coinbase omitted.
SerType: wire.TxSerializeFull,
Version: 1,
TxIn: []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"),
Index: 1,
Tree: 0,
},
SignatureScript: hexToBytes("493046022100c167eead9840da4a033c9a56470d7794a9bb1605b377ebe5688499b39f94be59022100fb6345cab4324f9ea0b9ee9169337534834638d818129778370f7d378ee4a325014104d962cac5390f12ddb7539507065d0def320d68c040f2e73337c3a1aaaab7195cb5c4d02e0959624d534f3c10c3cf3d73ca5065ebd62ae986b04c6d090d32627c"),
Sequence: 0xffffffff,
BlockHeight: 321,
BlockIndex: 123,
ValueIn: 34405000000,
}},
TxOut: []*wire.TxOut{{
Value: 5000000,
Version: 0,
PkScript: hexToBytes("76a914f419b8db4ba65f3b6fcc233acb762ca6f51c23d488ac"),
}, {
Value: 34400000000,
Version: 0,
PkScript: hexToBytes("76a914cadf4fc336ab3c6a4610b75f31ba0676b7f663d288ac"),
}},
LockTime: 0,
Expiry: 0,
}, {
SerType: wire.TxSerializeFull,
Version: 1,
TxIn: []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *newHashFromStr("92fbe1d4be82f765dfabc9559d4620864b05cc897c4db0e29adac92d294e52b7"),
Index: 0,
Tree: 0,
},
SignatureScript: hexToBytes("483045022100e256743154c097465cf13e89955e1c9ff2e55c46051b627751dee0144183157e02201d8d4f02cde8496aae66768f94d35ce54465bd4ae8836004992d3216a93a13f00141049d23ce8686fe9b802a7a938e8952174d35dd2c2089d4112001ed8089023ab4f93a3c9fcd5bfeaa9727858bf640dc1b1c05ec3b434bb59837f8640e8810e87742"),
Sequence: 0xffffffff,
BlockHeight: 3214,
BlockIndex: 1234,
ValueIn: 13761000000,
}},
TxOut: []*wire.TxOut{{
Value: 5000000,
Version: 0,
PkScript: hexToBytes("76a914a983ad7c92c38fc0e2025212e9f972204c6e687088ac"),
}, {
Value: 13756000000,
Version: 0,
PkScript: hexToBytes("76a914a6ebd69952ab486a7a300bfffdcb395dc7d47c2388ac"),
}},
LockTime: 0,
Expiry: 0,
}},
utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{
*newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): {
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
height: 321,
index: 123,
sparseOutputs: map[uint32]*utxoOutput{
1: {
amount: 34405000000,
scriptVersion: 0,
pkScript: hexToBytes("76a9142084541c3931677527a7eafe56fd90207c344eb088ac"),
compressed: false,
},
},
},
}},
serialized: hexToBytes("100001b2fb57eadf61e106a100a7445a8c3f67898841ec010000016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
},
{
name: "One tx, two inputs from same tx, neither spend last output",
entry: []spentTxOut{{
amount: 159747816,
height: 1111,
index: 2222,
scriptVersion: 0,
pkScript: hexToBytes("4151"),
compressed: true,
}, {
amount: 159747816,
height: 3333,
index: 4444,
scriptVersion: 0,
pkScript: hexToBytes("4151"),
compressed: true,
}},
blockTxns: []*wire.MsgTx{{ // Coinbase omitted.
SerType: wire.TxSerializeFull,
Version: 1,
TxIn: []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"),
Index: 1,
},
SignatureScript: hexToBytes(""),
Sequence: 0xffffffff,
BlockHeight: 1111,
BlockIndex: 2222,
ValueIn: 159747816,
}, {
PreviousOutPoint: wire.OutPoint{
Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"),
Index: 2,
},
SignatureScript: hexToBytes(""),
Sequence: 0xffffffff,
BlockHeight: 3333,
BlockIndex: 4444,
ValueIn: 159747816,
}},
TxOut: []*wire.TxOut{{
Value: 165125632,
Version: 0,
PkScript: hexToBytes("51"),
}, {
Value: 154370000,
Version: 0,
PkScript: hexToBytes("51"),
}},
LockTime: 0,
Expiry: 0,
}},
utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{
*newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): {
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
height: 100000,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 165712179,
pkScript: hexToBytes("51"),
},
},
},
}},
serialized: hexToBytes("0000415100004151"),
},
}
for i, test := range tests {
// Ensure the journal entry serializes to the expected value.
gotBytes, err := serializeSpendJournalEntry(test.entry)
if err != nil {
t.Errorf("serializeSpendJournalEntry #%d (%s) "+
"unexpected error: %v", i, test.name, err)
continue
}
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.
for j := range gotEntry {
if !reflect.DeepEqual(gotEntry[j], test.entry[j]) {
t.Errorf("deserializeSpendJournalEntry #%d (%s) "+
"mismatched entries in idx %v - got %v, want %v",
i, test.name, j, gotEntry[j], test.entry[j])
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.
SerType: wire.TxSerializeFull,
Version: 1,
TxIn: []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"),
Index: 0,
},
SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"),
Sequence: 0xffffffff,
}},
LockTime: 0,
}},
serialized: hexToBytes(""),
errType: AssertError(""),
},
{
name: "Force deserialization error in stxos",
blockTxns: []*wire.MsgTx{{ // Coinbase omitted.
SerType: wire.TxSerializeFull,
Version: 1,
TxIn: []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"),
Index: 0,
},
SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"),
Sequence: 0xffffffff,
}},
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
// transaction output entries works as expected.
func TestUtxoSerialization(t *testing.T) {
t.Parallel()
tests := []struct {
name string
entry *UtxoEntry
serialized []byte
}{
{
name: "Only output 0, coinbase, even uncomp pubkey",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: true,
hasExpiry: false,
txType: 0,
height: 12345,
index: 54321,
stakeExtra: nil,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 5000000000,
scriptVersion: 0,
pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"),
compressed: false,
},
},
},
serialized: hexToBytes("01df3982a731010132000496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"),
},
{
name: "Only output 0, coinbase, odd uncomp pubkey",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: true,
hasExpiry: false,
txType: 0,
height: 12345,
index: 54321,
stakeExtra: nil,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 5000000000,
scriptVersion: 0,
pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52258a76c86aea2b1f59fb07ebe87e19dd6b8dee99409de18c57d340dbbd37a341ac"),
compressed: false,
},
},
},
serialized: hexToBytes("01df3982a731010132000596b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"),
},
{
name: "Only output 1, not coinbase",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 0,
height: 55555,
index: 1,
stakeExtra: nil,
sparseOutputs: map[uint32]*utxoOutput{
1: {
amount: 1000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"),
compressed: false,
},
},
},
serialized: hexToBytes("0182b103010002070000ee8bd501094a7d5ca318da2506de35e1cb025ddc"),
},
{
name: "Ticket with one output",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 1,
height: 55555,
index: 1,
stakeExtra: hexToBytes("030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588ac0000206a1e1a221182c26bbae681e4d96d452794e1951e70a208520000000000000054b5f466001abd76a9146c4f8b15918566534d134be7d7004b7f481bf36988ac"),
sparseOutputs: map[uint32]*utxoOutput{
1: {
amount: 1000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"),
compressed: false,
},
},
},
serialized: hexToBytes("0182b103010402070000ee8bd501094a7d5ca318da2506de35e1cb025ddc030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588ac0000206a1e1a221182c26bbae681e4d96d452794e1951e70a208520000000000000054b5f466001abd76a9146c4f8b15918566534d134be7d7004b7f481bf36988ac"),
},
{
name: "Only output 2, coinbase, non-zero script version",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: true,
hasExpiry: false,
txType: 0,
height: 12345,
index: 1,
sparseOutputs: map[uint32]*utxoOutput{
2: {
amount: 100937281,
scriptVersion: 0xffff,
pkScript: hexToBytes("76a914da33f77cee27c2a975ed5124d7e4f7f97513510188ac"),
compressed: false,
},
},
},
serialized: hexToBytes("01df390101000182b095bf4182fe7f00da33f77cee27c2a975ed5124d7e4f7f975135101"),
},
{
name: "outputs 0 and 2 not coinbase",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 0,
height: 99999,
index: 3,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 20000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
compressed: false,
},
2: {
amount: 15000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"),
compressed: false,
},
},
},
serialized: hexToBytes("01858c1f03000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
},
{
name: "outputs 0 and 2 not coinbase, has expiry",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: true,
txType: 0,
height: 99999,
index: 3,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 20000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
compressed: false,
},
2: {
amount: 15000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"),
compressed: false,
},
},
},
serialized: hexToBytes("01858c1f03020501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
},
{
name: "outputs 0 and 2, not coinbase, 1 marked spent",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 0,
height: 12345,
index: 1,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 20000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
compressed: false,
},
1: { // This won't be serialized.
spent: true,
amount: 1000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914e43031c3e46f20bf1ccee9553ce815de5a48467588ac"),
compressed: false,
},
2: {
amount: 15000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"),
compressed: false,
},
},
},
serialized: hexToBytes("01df3901000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
},
{
name: "outputs 0 and 2, not coinbase, output 2 compressed",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 0,
height: 12345,
index: 1,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 20000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
compressed: false,
},
2: {
// Uncompressed Amount: 15000000
// Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac
amount: 137,
pkScript: hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
compressed: true,
},
},
},
serialized: hexToBytes("01df3901000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a884f0000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
},
{
name: "outputs 0 and 2, not coinbase, output 2 compressed, packed indexes reversed",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 0,
height: 33333,
index: 21,
sparseOutputs: map[uint32]*utxoOutput{
0: {
amount: 20000000,
scriptVersion: 0,
pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
compressed: false,
},
2: {
// Uncompressed Amount: 15000000
// Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac
amount: 137,
scriptVersion: 0,
pkScript: hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
compressed: true,
},
},
},
serialized: hexToBytes("0181833515000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a884f0000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
},
{
name: "Only output 0, coinbase, fully spent",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 0,
height: 33333,
index: 231,
sparseOutputs: map[uint32]*utxoOutput{
0: {
spent: true,
amount: 5000000000,
scriptVersion: 0,
pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"),
compressed: false,
},
},
},
serialized: nil,
},
{
name: "Only output 22, not coinbase",
entry: &UtxoEntry{
txVersion: 1,
isCoinBase: false,
hasExpiry: false,
txType: 0,
height: 3221,
index: 211,
sparseOutputs: map[uint32]*utxoOutput{
22: {
spent: false,
amount: 366875659,
scriptVersion: 0,
pkScript: hexToBytes("a9141dd46a006572d820e448e12d2bbb38640bc718e687"),
compressed: false,
},
},
},
serialized: hexToBytes("019815805300080000108ba5b9e76300011dd46a006572d820e448e12d2bbb38640bc718e6"),
},
}
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.TxVersion() != test.entry.TxVersion() {
t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+
" txVersion: got %d, want %d", i, test.name,
utxoEntry.TxVersion(), test.entry.TxVersion())
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
}
}
}
}
// 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,
totalSubsidy: 0,
workSum: func() *big.Int {
workSum.Add(workSum, standalone.CalcWork(486604799))
return new(big.Int).Set(workSum)
}(), // 0x0100010001
},
serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000001000000000000000000000000000000050000000100010001"),
},
{
name: "block 1",
state: bestChainState{
hash: *newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"),
height: 1,
totalTxns: 2,
totalSubsidy: 123456789,
workSum: func() *big.Int {
workSum.Add(workSum, standalone.CalcWork(486604799))
return new(big.Int).Set(workSum)
}(), // 0x0200020002,
},
serialized: hexToBytes("4860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a830000000001000000020000000000000015cd5b0700000000050000000200020002"),
},
}
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
}
}
}
}