diff --git a/blockchain/chainio.go b/blockchain/chainio.go index 86985cc0..6484700a 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -32,7 +32,7 @@ const ( // currentBlockIndexVersion indicates what the current block index // database version. - currentBlockIndexVersion = 2 + currentBlockIndexVersion = 3 // blockHdrSize is the size of a block header. This is simply the // constant from wire and is only provided here for convenience since @@ -83,7 +83,7 @@ var ( // blockIndexBucketName is the name of the db bucket used to house the block // index which consists of metadata for all known blocks both in the main // chain and on side chains. - blockIndexBucketName = []byte("blockidx") + blockIndexBucketName = []byte("blockidxv3") // gcsFilterBucketName is the name of the db bucket used to house GCS // filters. @@ -239,7 +239,7 @@ func ConvertUtxosToMinimalOutputs(entry *UtxoEntry) []*stake.MinimalOutput { // ----------------------------------------------------------------------------- // The block index consists of an entry for every known block. It consists of -// information such as the block header and hashes of tickets voted and revoked. +// information such as the block header and information about votes. // // The serialized key format is: // @@ -258,21 +258,15 @@ func ConvertUtxosToMinimalOutputs(entry *UtxoEntry) []*stake.MinimalOutput { // status blockStatus 1 byte // num votes VLQ variable // vote info -// ticket hash chainhash.Hash chainhash.HashSize // vote version VLQ variable // vote bits VLQ variable -// num revoked VLQ variable -// revoked tickets -// ticket hash chainhash.Hash chainhash.HashSize // ----------------------------------------------------------------------------- // blockIndexEntry represents a block index database entry. type blockIndexEntry struct { - header wire.BlockHeader - status blockStatus - voteInfo []stake.VoteVersionTuple - ticketsVoted []chainhash.Hash - ticketsRevoked []chainhash.Hash + header wire.BlockHeader + status blockStatus + voteInfo []stake.VoteVersionTuple } // blockIndexKey generates the binary key for an entry in the block index @@ -292,14 +286,12 @@ func blockIndexKey(blockHash *chainhash.Hash, blockHeight uint32) []byte { func blockIndexEntrySerializeSize(entry *blockIndexEntry) int { voteInfoSize := 0 for i := range entry.voteInfo { - voteInfoSize += chainhash.HashSize + - serializeSizeVLQ(uint64(entry.voteInfo[i].Version)) + + voteInfoSize += serializeSizeVLQ(uint64(entry.voteInfo[i].Version)) + serializeSizeVLQ(uint64(entry.voteInfo[i].Bits)) } return blockHdrSize + 1 + serializeSizeVLQ(uint64(len(entry.voteInfo))) + - voteInfoSize + serializeSizeVLQ(uint64(len(entry.ticketsRevoked))) + - chainhash.HashSize*len(entry.ticketsRevoked) + voteInfoSize } // putBlockIndexEntry serializes the passed block index entry according to the @@ -307,11 +299,6 @@ func blockIndexEntrySerializeSize(entry *blockIndexEntry) int { // target byte slice must be at least large enough to handle the number of bytes // returned by the blockIndexEntrySerializeSize function or it will panic. func putBlockIndexEntry(target []byte, entry *blockIndexEntry) (int, error) { - if len(entry.voteInfo) != len(entry.ticketsVoted) { - return 0, AssertError("putBlockIndexEntry called with " + - "mismatched number of tickets voted and vote info") - } - // Serialize the entire block header. w := bytes.NewBuffer(target[0:0]) if err := entry.header.Serialize(w); err != nil { @@ -326,18 +313,10 @@ func putBlockIndexEntry(target []byte, entry *blockIndexEntry) (int, error) { // Serialize the number of votes and associated vote information. offset += putVLQ(target[offset:], uint64(len(entry.voteInfo))) for i := range entry.voteInfo { - offset += copy(target[offset:], entry.ticketsVoted[i][:]) offset += putVLQ(target[offset:], uint64(entry.voteInfo[i].Version)) offset += putVLQ(target[offset:], uint64(entry.voteInfo[i].Bits)) } - // Serialize the number of revocations and associated revocation - // information. - offset += putVLQ(target[offset:], uint64(len(entry.ticketsRevoked))) - for i := range entry.ticketsRevoked { - offset += copy(target[offset:], entry.ticketsRevoked[i][:]) - } - return offset, nil } @@ -376,7 +355,6 @@ func decodeBlockIndexEntry(serialized []byte, entry *blockIndexEntry) (int, erro offset++ // Deserialize the number of tickets spent. - var ticketsVoted []chainhash.Hash var votes []stake.VoteVersionTuple numVotes, bytesRead := deserializeVLQ(serialized[offset:]) if bytesRead == 0 { @@ -385,18 +363,8 @@ func decodeBlockIndexEntry(serialized []byte, entry *blockIndexEntry) (int, erro } offset += bytesRead if numVotes > 0 { - ticketsVoted = make([]chainhash.Hash, numVotes) votes = make([]stake.VoteVersionTuple, numVotes) for i := uint64(0); i < numVotes; i++ { - // Deserialize the ticket hash associated with the vote. - if offset+chainhash.HashSize > len(serialized) { - return offset, errDeserialize(fmt.Sprintf("unexpected "+ - "end of data while reading vote #%d hash", - i)) - } - copy(ticketsVoted[i][:], serialized[offset:]) - offset += chainhash.HashSize - // Deserialize the vote version. version, bytesRead := deserializeVLQ(serialized[offset:]) if bytesRead == 0 { @@ -420,34 +388,9 @@ func decodeBlockIndexEntry(serialized []byte, entry *blockIndexEntry) (int, erro } } - // Deserialize the number of tickets revoked. - var ticketsRevoked []chainhash.Hash - numTicketsRevoked, bytesRead := deserializeVLQ(serialized[offset:]) - if bytesRead == 0 { - return offset, errDeserialize("unexpected end of data while " + - "reading num tickets revoked") - } - offset += bytesRead - if numTicketsRevoked > 0 { - ticketsRevoked = make([]chainhash.Hash, numTicketsRevoked) - for i := uint64(0); i < numTicketsRevoked; i++ { - // Deserialize the ticket hash associated with the - // revocation. - if offset+chainhash.HashSize > len(serialized) { - return offset, errDeserialize(fmt.Sprintf("unexpected "+ - "end of data while reading revocation "+ - "#%d", i)) - } - copy(ticketsRevoked[i][:], serialized[offset:]) - offset += chainhash.HashSize - } - } - entry.header = header entry.status = status entry.voteInfo = votes - entry.ticketsVoted = ticketsVoted - entry.ticketsRevoked = ticketsRevoked return offset, nil } @@ -465,11 +408,9 @@ func deserializeBlockIndexEntry(serialized []byte) (*blockIndexEntry, error) { // block node in the block index according to the format described above. func dbPutBlockNode(dbTx database.Tx, node *blockNode) error { serialized, err := serializeBlockIndexEntry(&blockIndexEntry{ - header: node.Header(), - status: node.status, - voteInfo: node.votes, - ticketsVoted: node.ticketsVoted, - ticketsRevoked: node.ticketsRevoked, + header: node.Header(), + status: node.status, + voteInfo: node.votes, }) if err != nil { return err diff --git a/blockchain/chainio_test.go b/blockchain/chainio_test.go index b9e0dd3b..48a96020 100644 --- a/blockchain/chainio_test.go +++ b/blockchain/chainio_test.go @@ -122,16 +122,6 @@ func TestBlockIndexSerialization(t *testing.T) { 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}, @@ -143,52 +133,48 @@ func TestBlockIndexSerialization(t *testing.T) { name string entry blockIndexEntry serialized []byte - }{ - { - name: "no votes, no revokes", - entry: blockIndexEntry{ - header: baseHeader, - status: statusDataStored | statusValidated, - voteInfo: nil, - ticketsVoted: nil, - ticketsRevoked: nil, - }, - serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030000"), + }{{ + name: "no votes", + entry: blockIndexEntry{ + header: baseHeader, + status: statusDataStored | statusValidated, + voteInfo: nil, }, - { - name: "1 vote, no revokes", - entry: blockIndexEntry{ - header: baseHeader, - status: statusDataStored | statusValidated, - voteInfo: baseVoteInfo[:1], - ticketsVoted: baseTicketsVoted[:1], - ticketsRevoked: nil, - }, - serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b040100"), + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c0000000000000000000000000000000000000000040000000300"), + }, { + name: "1 vote", + entry: blockIndexEntry{ + header: baseHeader, + status: statusDataStored | statusValidated, + voteInfo: baseVoteInfo[:1], }, - { - name: "no votes, 1 revoke", - entry: blockIndexEntry{ - header: baseHeader, - status: statusDataStored | statusValidated, - voteInfo: nil, - ticketsVoted: nil, - ticketsRevoked: baseTicketsRevoked[:1], - }, - serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030001ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8e00a8fc8f1bf04681"), + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a" + + "3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f99314" + + "70d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06" + + "f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a100009" + + "86f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92" + + "645e0a6b11ee3b3c00000000000000000000000000000000000000000400000" + + "003010401"), + }, { + name: "4 votes, same vote versions, different vote bits", + entry: blockIndexEntry{ + header: baseHeader, + status: statusDataStored | statusValidated, + voteInfo: baseVoteInfo, }, - { - name: "4 votes, same vote versions, different vote bits, 2 revokes", - entry: blockIndexEntry{ - header: baseHeader, - status: statusDataStored | statusValidated, - voteInfo: baseVoteInfo, - ticketsVoted: baseTicketsVoted, - ticketsRevoked: baseTicketsRevoked, - }, - serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003042e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0401b79d388a66910e033233543a33248a121eff9a2e07d9ff0414ebaca703a0274404150b11a2f3ae3484641b5fe9552dc91ccd575246df415db1b6d78148c78ab8154404157ab8e5b30822e7ec6ae514da760af332c8764bb069a0d30988085273b521269d040102ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8e00a8fc8f1bf046810cbe94fb96a1b776d67e32621f6deee6102aac1e05e2c68cc525e76124ff9222"), - }, - } + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003040" + + "401041504150401"), + }} for _, test := range tests { // Ensure the function to calculate the serialized size without @@ -289,78 +275,57 @@ func TestBlockIndexDecodeErrors(t *testing.T) { 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, - }, - } + }{{ + name: "nothing serialized", + entry: blockIndexEntry{}, + serialized: hexToBytes(""), + errType: errDeserialize(""), + bytesRead: 0, + }, { + name: "no data after block header", + entry: blockIndexEntry{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c000000000000000000000000000000000000000004000000"), + errType: errDeserialize(""), + bytesRead: 180, + }, { + name: "no data after status", + entry: blockIndexEntry{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003"), + errType: errDeserialize(""), + bytesRead: 181, + }, { + name: "no data after num votes with votes", + entry: blockIndexEntry{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c0000000000000000000000000000000000000000040000000301"), + errType: errDeserialize(""), + bytesRead: 182, + }, { + name: "no data after vote version", + entry: blockIndexEntry{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c000000000000000000000000000000000000000004000000030104"), + errType: errDeserialize(""), + bytesRead: 183, + }} for _, test := range tests { // Ensure the expected error type is returned. diff --git a/blockchain/upgrade.go b/blockchain/upgrade.go index 92193a46..80228ccb 100644 --- a/blockchain/upgrade.go +++ b/blockchain/upgrade.go @@ -233,11 +233,205 @@ func upgradeToVersion2(db database.DB, chainParams *chaincfg.Params, dbInfo *dat return err } - log.Infof("Upgrade to new stake database was successful!") + log.Info("Upgrade to new stake database was successful!") return nil } +// ----------------------------------------------------------------------------- +// The legacy version 2 block index consists of an entry for every known block. +// which includes information such as the block header and hashes of tickets +// voted and revoked. +// +// The serialized key format is: +// +// +// +// Field Type Size +// block height uint32 4 bytes +// block hash chainhash.Hash chainhash.HashSize +// +// The serialized value format is: +// +// +// +// Field Type Size +// block header wire.BlockHeader 180 bytes +// status blockStatus 1 byte +// num votes VLQ variable +// vote info +// ticket hash chainhash.Hash chainhash.HashSize +// vote version VLQ variable +// vote bits VLQ variable +// num revoked VLQ variable +// revoked tickets +// ticket hash chainhash.Hash chainhash.HashSize +// ----------------------------------------------------------------------------- +type blockIndexEntryV2 struct { + header wire.BlockHeader + status blockStatus + voteInfo []stake.VoteVersionTuple + ticketsVoted []chainhash.Hash + ticketsRevoked []chainhash.Hash +} + +// blockIndexEntrySerializeSizV2e returns the number of bytes it would take to +// serialize the passed block index entry according to the legacy version 2 +// format described above. +func blockIndexEntrySerializeSizeV2(entry *blockIndexEntryV2) int { + voteInfoSize := 0 + for i := range entry.voteInfo { + voteInfoSize += chainhash.HashSize + + serializeSizeVLQ(uint64(entry.voteInfo[i].Version)) + + serializeSizeVLQ(uint64(entry.voteInfo[i].Bits)) + } + + return blockHdrSize + 1 + serializeSizeVLQ(uint64(len(entry.voteInfo))) + + voteInfoSize + serializeSizeVLQ(uint64(len(entry.ticketsRevoked))) + + chainhash.HashSize*len(entry.ticketsRevoked) +} + +// putBlockIndexEntryV2 serializes the passed block index entry according to the +// legacy version 2 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 blockIndexEntrySerializeSizeV2 function or it +// will panic. +func putBlockIndexEntryV2(target []byte, entry *blockIndexEntryV2) (int, error) { + if len(entry.voteInfo) != len(entry.ticketsVoted) { + return 0, AssertError("putBlockIndexEntry called with " + + "mismatched number of tickets voted and vote info") + } + + // Serialize the entire block header. + w := bytes.NewBuffer(target[0:0]) + if err := entry.header.Serialize(w); err != nil { + return 0, err + } + + // Serialize the status. + offset := blockHdrSize + target[offset] = byte(entry.status) + offset++ + + // Serialize the number of votes and associated vote information. + offset += putVLQ(target[offset:], uint64(len(entry.voteInfo))) + for i := range entry.voteInfo { + offset += copy(target[offset:], entry.ticketsVoted[i][:]) + offset += putVLQ(target[offset:], uint64(entry.voteInfo[i].Version)) + offset += putVLQ(target[offset:], uint64(entry.voteInfo[i].Bits)) + } + + // Serialize the number of revocations and associated revocation + // information. + offset += putVLQ(target[offset:], uint64(len(entry.ticketsRevoked))) + for i := range entry.ticketsRevoked { + offset += copy(target[offset:], entry.ticketsRevoked[i][:]) + } + + return offset, nil +} + +// decodeBlockIndexEntryV2 decodes the passed serialized block index entry into +// the passed struct according to the legacy version 2 format described above. +// It returns the number of bytes read. +func decodeBlockIndexEntryV2(serialized []byte, entry *blockIndexEntryV2) (int, error) { + // Hardcoded value so updates do not affect old upgrades. + const blockHdrSize = 180 + + // Ensure there are enough bytes to decode header. + if len(serialized) < blockHdrSize { + return 0, errDeserialize("unexpected end of data while reading block " + + "header") + } + hB := serialized[0:blockHdrSize] + + // Deserialize the header. + var header wire.BlockHeader + if err := header.Deserialize(bytes.NewReader(hB)); err != nil { + return 0, err + } + offset := blockHdrSize + + // Deserialize the status. + if offset+1 > len(serialized) { + return offset, errDeserialize("unexpected end of data while reading " + + "status") + } + status := blockStatus(serialized[offset]) + offset++ + + // Deserialize the number of tickets spent. + var ticketsVoted []chainhash.Hash + var votes []stake.VoteVersionTuple + numVotes, bytesRead := deserializeVLQ(serialized[offset:]) + if bytesRead == 0 { + return offset, errDeserialize("unexpected end of data while reading " + + "num votes") + } + offset += bytesRead + if numVotes > 0 { + ticketsVoted = make([]chainhash.Hash, numVotes) + votes = make([]stake.VoteVersionTuple, numVotes) + for i := uint64(0); i < numVotes; i++ { + // Deserialize the ticket hash associated with the vote. + if offset+chainhash.HashSize > len(serialized) { + return offset, errDeserialize(fmt.Sprintf("unexpected end of "+ + "data while reading vote #%d hash", i)) + } + copy(ticketsVoted[i][:], serialized[offset:]) + offset += chainhash.HashSize + + // Deserialize the vote version. + version, bytesRead := deserializeVLQ(serialized[offset:]) + if bytesRead == 0 { + return offset, errDeserialize(fmt.Sprintf("unexpected end of "+ + "data while reading vote #%d version", i)) + } + offset += bytesRead + + // Deserialize the vote bits. + voteBits, bytesRead := deserializeVLQ(serialized[offset:]) + if bytesRead == 0 { + return offset, errDeserialize(fmt.Sprintf("unexpected end of "+ + "data while reading vote #%d bits", i)) + } + offset += bytesRead + + votes[i].Version = uint32(version) + votes[i].Bits = uint16(voteBits) + } + } + + // Deserialize the number of tickets revoked. + var ticketsRevoked []chainhash.Hash + numTicketsRevoked, bytesRead := deserializeVLQ(serialized[offset:]) + if bytesRead == 0 { + return offset, errDeserialize("unexpected end of data while reading " + + "num tickets revoked") + } + offset += bytesRead + if numTicketsRevoked > 0 { + ticketsRevoked = make([]chainhash.Hash, numTicketsRevoked) + for i := uint64(0); i < numTicketsRevoked; i++ { + // Deserialize the ticket hash associated with the + // revocation. + if offset+chainhash.HashSize > len(serialized) { + return offset, errDeserialize(fmt.Sprintf("unexpected end of "+ + "data while reading revocation #%d", i)) + } + copy(ticketsRevoked[i][:], serialized[offset:]) + offset += chainhash.HashSize + } + } + + entry.header = header + entry.status = status + entry.voteInfo = votes + entry.ticketsVoted = ticketsVoted + entry.ticketsRevoked = ticketsRevoked + return offset, nil +} + // migrateBlockIndex migrates all block entries from the v1 block index bucket // managed by ffldb to the v2 bucket managed by this package. The v1 bucket // stored all block entries keyed by block hash, whereas the v2 bucket stores @@ -357,14 +551,15 @@ func migrateBlockIndex(ctx context.Context, db database.DB) error { // Write the serialized block index entry to the new bucket keyed by // its hash and height. ticketInfo := stake.FindSpentTicketsInBlock(&block) - serialized, err := serializeBlockIndexEntry(&blockIndexEntry{ + entry := &blockIndexEntryV2{ header: block.Header, status: status, voteInfo: ticketInfo.Votes, ticketsVoted: ticketInfo.VotedTickets, ticketsRevoked: ticketInfo.RevokedTickets, - }) - if err != nil { + } + serialized := make([]byte, blockIndexEntrySerializeSizeV2(entry)) + if _, err = putBlockIndexEntryV2(serialized, entry); err != nil { return err } err = v2BlockIdxBucket.Put(key, serialized) @@ -962,6 +1157,164 @@ func upgradeToVersion6(ctx context.Context, db database.DB, chainParams *chaincf return nil } +// migrateBlockIndexVersion2To3 migrates all block entries from the v2 block +// index bucket to a v3 bucket and removes the old v2 bucket. As compared to +// the v2 block index, the v3 index removes the ticket hashes associated with +// vote info and revocations. +// +// The new block index is guaranteed to be fully updated if this returns without +// failure. +func migrateBlockIndexVersion2To3(ctx context.Context, db database.DB, dbInfo *databaseInfo) error { + // Hardcoded bucket names so updates do not affect old upgrades. + v2BucketName := []byte("blockidx") + v3BucketName := []byte("blockidxv3") + + log.Info("Reindexing block information in the database. This may take a " + + "while...") + start := time.Now() + + // Create the new block index bucket as needed. + err := db.Update(func(dbTx database.Tx) error { + _, err := dbTx.Metadata().CreateBucketIfNotExists(v3BucketName) + return err + }) + if err != nil { + return err + } + + // doBatch contains the primary logic for upgrading the block index from + // version 2 to 3 in batches. This is done because attempting to migrate in + // a single database transaction could result in massive memory usage and + // could potentially crash on many systems due to ulimits. + // + // It returns the number of entries processed. + const maxEntries = 20000 + var resumeOffset uint32 + doBatch := func(dbTx database.Tx) (uint32, error) { + meta := dbTx.Metadata() + v2BlockIdxBucket := meta.Bucket(v2BucketName) + if v2BlockIdxBucket == nil { + return 0, fmt.Errorf("bucket %s does not exist", v2BucketName) + } + + v3BlockIdxBucket := meta.Bucket(v3BucketName) + if v3BlockIdxBucket == nil { + return 0, fmt.Errorf("bucket %s does not exist", v3BucketName) + } + + // Migrate block index entries so long as the max number of entries for + // this batch has not been exceeded. + var numMigrated, numIterated uint32 + err := v2BlockIdxBucket.ForEach(func(key, oldSerialized []byte) error { + if numMigrated >= maxEntries { + return errBatchFinished + } + + // Skip entries that have already been migrated in previous batches. + numIterated++ + if numIterated-1 < resumeOffset { + return nil + } + resumeOffset++ + + // Skip entries that have already been migrated in previous + // interrupted upgrades. + if v3BlockIdxBucket.Get(key) != nil { + return nil + } + + // Decode the old block index entry. + var entry blockIndexEntryV2 + _, err := decodeBlockIndexEntryV2(oldSerialized, &entry) + if err != nil { + return err + } + + // Write the block index entry seriliazed with the new format to the + // new bucket. + serialized, err := serializeBlockIndexEntry(&blockIndexEntry{ + header: entry.header, + status: entry.status, + voteInfo: entry.voteInfo, + }) + if err != nil { + return err + } + err = v3BlockIdxBucket.Put(key, serialized) + if err != nil { + return err + } + + numMigrated++ + + if interruptRequested(ctx) { + return errInterruptRequested + } + + return nil + }) + return numMigrated, err + } + + // Migrate all entries in batches for the reasons mentioned above. + var totalMigrated uint64 + for { + var numMigrated uint32 + err := db.Update(func(dbTx database.Tx) error { + var err error + numMigrated, err = doBatch(dbTx) + if errors.Is(err, errInterruptRequested) || + errors.Is(err, errBatchFinished) { + // No error here so the database transaction is not cancelled + // and therefore outstanding work is written to disk. The outer + // function will exit with an interrupted error below due to + // another interrupted check. + err = nil + } + return err + }) + if err != nil { + return err + } + + if interruptRequested(ctx) { + return errInterruptRequested + } + + if numMigrated == 0 { + break + } + + totalMigrated += uint64(numMigrated) + log.Infof("Migrated %d entries (%d total)", numMigrated, totalMigrated) + } + + elapsed := time.Since(start).Round(time.Millisecond) + log.Infof("Done migrating block index. Total entries: %d in %v", + totalMigrated, elapsed) + + if interruptRequested(ctx) { + return errInterruptRequested + } + + // Drop version 2 block index. + log.Info("Removing old block index entries...") + start = time.Now() + err = incrementalFlatDrop(ctx, db, v2BucketName, "old block index") + if err != nil { + return err + } + elapsed = time.Since(start).Round(time.Millisecond) + log.Infof("Done removing old block index entries in %v", elapsed) + + // Update and persist the database versions. + err = db.Update(func(dbTx database.Tx) error { + dbInfo.bidxVer = 3 + return dbPutDatabaseInfo(dbTx, dbInfo) + }) + return err +} + // upgradeDB upgrades old database versions to the newest version by applying // all possible upgrades iteratively. // @@ -1009,5 +1362,13 @@ func upgradeDB(ctx context.Context, db database.DB, chainParams *chaincfg.Params } } + // Update to the version 3 block index format if needed. + if dbInfo.version == 6 && dbInfo.bidxVer == 2 { + err := migrateBlockIndexVersion2To3(ctx, db, dbInfo) + if err != nil { + return err + } + } + return nil } diff --git a/blockchain/upgrade_test.go b/blockchain/upgrade_test.go new file mode 100644 index 00000000..f176ec57 --- /dev/null +++ b/blockchain/upgrade_test.go @@ -0,0 +1,345 @@ +// Copyright (c) 2020 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" + "errors" + "reflect" + "testing" + "time" + + "github.com/decred/dcrd/blockchain/stake/v3" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/wire" +) + +// TestBlockIndexDecodeErrorsV2 performs negative tests against decoding block +// index entries from the legacy version 2 format to ensure error paths work as +// expected. +func TestBlockIndexDecodeErrorsV2(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry blockIndexEntryV2 + serialized []byte + bytesRead int // Expected number of bytes read. + errType error + }{{ + name: "nothing serialized", + entry: blockIndexEntryV2{}, + serialized: hexToBytes(""), + errType: errDeserialize(""), + bytesRead: 0, + }, { + name: "no data after block header", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c000000000000000000000000000000000000000004000000"), + errType: errDeserialize(""), + bytesRead: 180, + }, { + name: "no data after status", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003"), + errType: errDeserialize(""), + bytesRead: 181, + }, { + name: "no data after num votes with no votes", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c0000000000000000000000000000000000000000040000000300"), + errType: errDeserialize(""), + bytesRead: 182, + }, { + name: "no data after num votes with votes", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c0000000000000000000000000000000000000000040000000301"), + errType: errDeserialize(""), + bytesRead: 182, + }, { + name: "short data in vote ticket hash", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003012" + + "e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a862"), + errType: errDeserialize(""), + bytesRead: 182, + }, { + name: "no data after vote ticket hash", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003012" + + "e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b"), + errType: errDeserialize(""), + bytesRead: 214, + }, { + name: "no data after vote version", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003012" + + "e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b04"), + errType: errDeserialize(""), + bytesRead: 215, + }, { + name: "no data after votes", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003012" + + "e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0" + + "401"), + errType: errDeserialize(""), + bytesRead: 216, + }, { + name: "no data after num revokes with revokes", + entry: blockIndexEntryV2{}, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c000000000000000000000000000000000000000004000000030001"), + errType: errDeserialize(""), + bytesRead: 183, + }} + + for _, test := range tests { + // Ensure the expected error type is returned. + gotBytesRead, err := decodeBlockIndexEntryV2(test.serialized, + &test.entry) + if !errors.As(err, &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 + } + } +} + +// TestBlockIndexSerializationV2 ensures serializing and deserializing block +// index entries from the legacy version 2 format works as expected. +func TestBlockIndexSerializationV2(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 blockIndexEntryV2 + serialized []byte + }{{ + name: "no votes, no revokes", + entry: blockIndexEntryV2{ + header: baseHeader, + status: statusDataStored | statusValidated, + voteInfo: nil, + ticketsVoted: nil, + ticketsRevoked: nil, + }, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c000000000000000000000000000000000000000004000000030000"), + }, { + name: "1 vote, no revokes", + entry: blockIndexEntryV2{ + header: baseHeader, + status: statusDataStored | statusValidated, + voteInfo: baseVoteInfo[:1], + ticketsVoted: baseTicketsVoted[:1], + ticketsRevoked: nil, + }, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003012" + + "e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0" + + "40100"), + }, { + name: "no votes, 1 revoke", + entry: blockIndexEntryV2{ + header: baseHeader, + status: statusDataStored | statusValidated, + voteInfo: nil, + ticketsVoted: nil, + ticketsRevoked: baseTicketsRevoked[:1], + }, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003000" + + "1ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8e00a8fc8f1bf04681"), + }, { + name: "4 votes, same vote versions, different vote bits, 2 revokes", + entry: blockIndexEntryV2{ + header: baseHeader, + status: statusDataStored | statusValidated, + voteInfo: baseVoteInfo, + ticketsVoted: baseTicketsVoted, + ticketsRevoked: baseTicketsRevoked, + }, + serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3" + + "425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470" + + "d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f83" + + "3a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f0" + + "11aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0" + + "a6b11ee3b3c00000000000000000000000000000000000000000400000003042" + + "e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0" + + "401b79d388a66910e033233543a33248a121eff9a2e07d9ff0414ebaca703a02" + + "74404150b11a2f3ae3484641b5fe9552dc91ccd575246df415db1b6d78148c78" + + "ab8154404157ab8e5b30822e7ec6ae514da760af332c8764bb069a0d30988085" + + "273b521269d040102ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8" + + "e00a8fc8f1bf046810cbe94fb96a1b776d67e32621f6deee6102aac1e05e2c68" + + "cc525e76124ff9222"), + }} + + for _, test := range tests { + // Ensure the function to calculate the serialized size without + // actually serializing it is calculated properly. + gotSize := blockIndexEntrySerializeSizeV2(&test.entry) + if gotSize != len(test.serialized) { + t.Errorf("%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 := make([]byte, blockIndexEntrySerializeSizeV2(&test.entry)) + _, err := putBlockIndexEntryV2(gotSerialized, &test.entry) + if err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + continue + } + if !bytes.Equal(gotSerialized, test.serialized) { + t.Errorf("%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 := putBlockIndexEntryV2(gotSerialized2, + &test.entry) + if err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + continue + } + if !bytes.Equal(gotSerialized2, test.serialized) { + t.Errorf("%s: did not get expected bytes - got %x, want %x", + test.name, gotSerialized2, test.serialized) + continue + } + if gotBytesWritten != len(test.serialized) { + t.Errorf("%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. + var gotEntry blockIndexEntryV2 + bytesRead, err := decodeBlockIndexEntryV2(test.serialized, &gotEntry) + if err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + continue + } + if !reflect.DeepEqual(gotEntry, test.entry) { + t.Errorf("%s: mismatched entries\ngot %+v\nwant %+v", test.name, + gotEntry, test.entry) + continue + } + if bytesRead != len(test.serialized) { + t.Errorf("%s: did not get expected number of bytes read - got %d, "+ + "want %d", test.name, bytesRead, len(test.serialized)) + continue + } + } +}