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.
This commit is contained in:
Dave Collins 2020-08-02 21:27:55 -05:00
parent f97951dc7d
commit d6afeac8b0
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
4 changed files with 811 additions and 199 deletions

View File

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

View File

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

View File

@ -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:
//
// <block height><block hash>
//
// Field Type Size
// block height uint32 4 bytes
// block hash chainhash.Hash chainhash.HashSize
//
// The serialized value format is:
//
// <block header><status><num votes><votes info><num revoked><revoked tickets>
//
// 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
}

345
blockchain/upgrade_test.go Normal file
View File

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