From d6afeac8b0c6fefb1db583caaa6c94afb183f4fd Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 2 Aug 2020 21:27:55 -0500 Subject: [PATCH] blockchain: Migration for v3 block index. This adds the code to migrate the database to a new v3 block index. It involves removing the ticket hashes associated with vote info and revocations that are no longer needed. The process can be interrupted at any point and future invocations will resume from the point it was interrupted. --- blockchain/chainio.go | 81 ++------ blockchain/chainio_test.go | 215 +++++++++------------ blockchain/upgrade.go | 369 ++++++++++++++++++++++++++++++++++++- blockchain/upgrade_test.go | 345 ++++++++++++++++++++++++++++++++++ 4 files changed, 811 insertions(+), 199 deletions(-) create mode 100644 blockchain/upgrade_test.go 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 + } + } +}