7c174620f7
This introduces a new indexing infrastructure for supporting optional indexes using the new database and blockchain infrastructure along with two concrete indexer implementations which provide both a transaction-by-hash and a transaction-by-address index. The new infrastructure is mostly separated into a package named indexers which is housed under the blockchain package. In order to support this, a new interface named IndexManager has been introduced in the blockchain package which provides methods to be notified when the chain has been initialized and when blocks are connected and disconnected from the main chain. A concrete implementation of an index manager is provided by the new indexers package. The new indexers package also provides a new interface named Indexer which allows the index manager to manage concrete index implementations which conform to the interface. The following is high level overview of the main index infrastructure changes: - Define a new IndexManager interface in the blockchain package and modify the package to make use of the interface when specified - Create a new indexers package - Provides an Index interface which allows concrete indexes to plugin to an index manager - Provides a concrete IndexManager implementation - Handles the lifecycle of all indexes it manages - Tracks the index tips - Handles catching up disabled indexes that have been reenabled - Handles reorgs while the index was disabled - Invokes the appropriate methods for all managed indexes to allow them to index and deindex the blocks and transactions - Implement a transaction-by-hash index - Makes use of internal block IDs to save a significant amount of space and indexing costs over the old transaction index format - Implement a transaction-by-address index - Makes use of a leveling scheme in order to provide a good tradeoff between space required and indexing costs - Supports enabling and disabling indexes at will - Support the ability to drop indexes if they are no longer desired The following is an overview of the btcd changes: - Add a new index logging subsystem - Add new options --txindex and --addrindex in order to enable the optional indexes - NOTE: The transaction index will automatically be enabled when the address index is enabled because it depends on it - Add new options --droptxindex and --dropaddrindex to allow the indexes to be removed - NOTE: The address index will also be removed when the transaction index is dropped because it depends on it - Update getrawtransactions RPC to make use of the transaction index - Reimplement the searchrawtransaction RPC that makes use of the address index - Update sample-btcd.conf to include sample usage for the new optional index flags
275 lines
7.6 KiB
Go
275 lines
7.6 KiB
Go
// Copyright (c) 2016 The btcsuite developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package indexers
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
)
|
|
|
|
// addrIndexBucket provides a mock address index database bucket by implementing
|
|
// the internalBucket interface.
|
|
type addrIndexBucket struct {
|
|
levels map[[levelKeySize]byte][]byte
|
|
}
|
|
|
|
// Clone returns a deep copy of the mock adress index bucket.
|
|
func (b *addrIndexBucket) Clone() *addrIndexBucket {
|
|
levels := make(map[[levelKeySize]byte][]byte)
|
|
for k, v := range b.levels {
|
|
vCopy := make([]byte, len(v))
|
|
copy(vCopy, v)
|
|
levels[k] = vCopy
|
|
}
|
|
return &addrIndexBucket{levels: levels}
|
|
}
|
|
|
|
// Get returns the value associated with the key from the mock address index
|
|
// bucket.
|
|
//
|
|
// This is part of the internalBucket interface.
|
|
func (b *addrIndexBucket) Get(key []byte) []byte {
|
|
var levelKey [levelKeySize]byte
|
|
copy(levelKey[:], key)
|
|
return b.levels[levelKey]
|
|
}
|
|
|
|
// Put stores the provided key/value pair to the mock address index bucket.
|
|
//
|
|
// This is part of the internalBucket interface.
|
|
func (b *addrIndexBucket) Put(key []byte, value []byte) error {
|
|
var levelKey [levelKeySize]byte
|
|
copy(levelKey[:], key)
|
|
b.levels[levelKey] = value
|
|
return nil
|
|
}
|
|
|
|
// Delete removes the provided key from the mock address index bucket.
|
|
//
|
|
// This is part of the internalBucket interface.
|
|
func (b *addrIndexBucket) Delete(key []byte) error {
|
|
var levelKey [levelKeySize]byte
|
|
copy(levelKey[:], key)
|
|
delete(b.levels, levelKey)
|
|
return nil
|
|
}
|
|
|
|
// printLevels returns a string with a visual representation of the provided
|
|
// address key taking into account the max size of each level. It is useful
|
|
// when creating and debugging test cases.
|
|
func (b *addrIndexBucket) printLevels(addrKey [addrKeySize]byte) string {
|
|
highestLevel := uint8(0)
|
|
for k := range b.levels {
|
|
if !bytes.Equal(k[:levelOffset], addrKey[:]) {
|
|
continue
|
|
}
|
|
level := uint8(k[levelOffset])
|
|
if level > highestLevel {
|
|
highestLevel = level
|
|
}
|
|
}
|
|
|
|
var levelBuf bytes.Buffer
|
|
_, _ = levelBuf.WriteString("\n")
|
|
maxEntries := level0MaxEntries
|
|
for level := uint8(0); level <= highestLevel; level++ {
|
|
data := b.levels[keyForLevel(addrKey, level)]
|
|
numEntries := len(data) / txEntrySize
|
|
for i := 0; i < numEntries; i++ {
|
|
start := i * txEntrySize
|
|
num := byteOrder.Uint32(data[start:])
|
|
_, _ = levelBuf.WriteString(fmt.Sprintf("%02d ", num))
|
|
}
|
|
for i := numEntries; i < maxEntries; i++ {
|
|
_, _ = levelBuf.WriteString("_ ")
|
|
}
|
|
_, _ = levelBuf.WriteString("\n")
|
|
maxEntries *= 2
|
|
}
|
|
|
|
return levelBuf.String()
|
|
}
|
|
|
|
// sanityCheck ensures that all data stored in the bucket for the given address
|
|
// adheres to the level-based rules described by the address index
|
|
// documentation.
|
|
func (b *addrIndexBucket) sanityCheck(addrKey [addrKeySize]byte, expectedTotal int) error {
|
|
// Find the highest level for the key.
|
|
highestLevel := uint8(0)
|
|
for k := range b.levels {
|
|
if !bytes.Equal(k[:levelOffset], addrKey[:]) {
|
|
continue
|
|
}
|
|
level := uint8(k[levelOffset])
|
|
if level > highestLevel {
|
|
highestLevel = level
|
|
}
|
|
}
|
|
|
|
// Ensure the expected total number of entries are present and that
|
|
// all levels adhere to the rules described in the address index
|
|
// documentation.
|
|
var totalEntries int
|
|
maxEntries := level0MaxEntries
|
|
for level := uint8(0); level <= highestLevel; level++ {
|
|
// Level 0 can'have more entries than the max allowed if the
|
|
// levels after it have data and it can't be empty. All other
|
|
// levels must either be half full or full.
|
|
data := b.levels[keyForLevel(addrKey, level)]
|
|
numEntries := len(data) / txEntrySize
|
|
totalEntries += numEntries
|
|
if level == 0 {
|
|
if (highestLevel != 0 && numEntries == 0) ||
|
|
numEntries > maxEntries {
|
|
|
|
return fmt.Errorf("level %d has %d entries",
|
|
level, numEntries)
|
|
}
|
|
} else if numEntries != maxEntries && numEntries != maxEntries/2 {
|
|
return fmt.Errorf("level %d has %d entries", level,
|
|
numEntries)
|
|
}
|
|
maxEntries *= 2
|
|
}
|
|
if totalEntries != expectedTotal {
|
|
return fmt.Errorf("expected %d entries - got %d", expectedTotal,
|
|
totalEntries)
|
|
}
|
|
|
|
// Ensure all of the numbers are in order starting from the highest
|
|
// level moving to the lowest level.
|
|
expectedNum := uint32(0)
|
|
for level := highestLevel + 1; level > 0; level-- {
|
|
data := b.levels[keyForLevel(addrKey, level)]
|
|
numEntries := len(data) / txEntrySize
|
|
for i := 0; i < numEntries; i++ {
|
|
start := i * txEntrySize
|
|
num := byteOrder.Uint32(data[start:])
|
|
if num != expectedNum {
|
|
return fmt.Errorf("level %d offset %d does "+
|
|
"not contain the expected number of "+
|
|
"%d - got %d", level, i, num,
|
|
expectedNum)
|
|
}
|
|
expectedNum++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TestAddrIndexLevels ensures that adding and deleting entries to the address
|
|
// index creates multiple levels as decribed by the address index documentation.
|
|
func TestAddrIndexLevels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
key [addrKeySize]byte
|
|
numInsert int
|
|
printLevels bool // Set to help debug a specific test.
|
|
}{
|
|
{
|
|
name: "level 0 not full",
|
|
numInsert: level0MaxEntries - 1,
|
|
},
|
|
{
|
|
name: "level 1 half",
|
|
numInsert: level0MaxEntries + 1,
|
|
},
|
|
{
|
|
name: "level 1 full",
|
|
numInsert: level0MaxEntries*2 + 1,
|
|
},
|
|
{
|
|
name: "level 2 half, level 1 half",
|
|
numInsert: level0MaxEntries*3 + 1,
|
|
},
|
|
{
|
|
name: "level 2 half, level 1 full",
|
|
numInsert: level0MaxEntries*4 + 1,
|
|
},
|
|
{
|
|
name: "level 2 full, level 1 half",
|
|
numInsert: level0MaxEntries*5 + 1,
|
|
},
|
|
{
|
|
name: "level 2 full, level 1 full",
|
|
numInsert: level0MaxEntries*6 + 1,
|
|
},
|
|
{
|
|
name: "level 3 half, level 2 half, level 1 half",
|
|
numInsert: level0MaxEntries*7 + 1,
|
|
},
|
|
{
|
|
name: "level 3 full, level 2 half, level 1 full",
|
|
numInsert: level0MaxEntries*12 + 1,
|
|
},
|
|
}
|
|
|
|
nextTest:
|
|
for testNum, test := range tests {
|
|
// Insert entries in order.
|
|
populatedBucket := &addrIndexBucket{
|
|
levels: make(map[[levelKeySize]byte][]byte),
|
|
}
|
|
for i := 0; i < test.numInsert; i++ {
|
|
txLoc := wire.TxLoc{TxStart: i * 2}
|
|
err := dbPutAddrIndexEntry(populatedBucket, test.key,
|
|
uint32(i), txLoc)
|
|
if err != nil {
|
|
t.Errorf("dbPutAddrIndexEntry #%d (%s) - "+
|
|
"unexpected error: %v", testNum,
|
|
test.name, err)
|
|
continue nextTest
|
|
}
|
|
}
|
|
if test.printLevels {
|
|
t.Log(populatedBucket.printLevels(test.key))
|
|
}
|
|
|
|
// Delete entries from the populated bucket until all entries
|
|
// have been deleted. The bucket is reset to the fully
|
|
// populated bucket on each iteration so every combination is
|
|
// tested. Notice the upper limit purposes exceeds the number
|
|
// of entries to ensure attempting to delete more entries than
|
|
// there are works correctly.
|
|
for numDelete := 0; numDelete <= test.numInsert+1; numDelete++ {
|
|
// Clone populated bucket to run each delete against.
|
|
bucket := populatedBucket.Clone()
|
|
|
|
// Remove the number of entries for this iteration.
|
|
err := dbRemoveAddrIndexEntries(bucket, test.key,
|
|
numDelete)
|
|
if err != nil {
|
|
if numDelete <= test.numInsert {
|
|
t.Errorf("dbRemoveAddrIndexEntries (%s) "+
|
|
" delete %d - unexpected error: "+
|
|
"%v", test.name, numDelete, err)
|
|
continue nextTest
|
|
}
|
|
}
|
|
if test.printLevels {
|
|
t.Log(bucket.printLevels(test.key))
|
|
}
|
|
|
|
// Sanity check the levels to ensure the adhere to all
|
|
// rules.
|
|
numExpected := test.numInsert
|
|
if numDelete <= test.numInsert {
|
|
numExpected -= numDelete
|
|
}
|
|
err = bucket.sanityCheck(test.key, numExpected)
|
|
if err != nil {
|
|
t.Errorf("sanity check fail (%s) delete %d: %v",
|
|
test.name, numDelete, err)
|
|
continue nextTest
|
|
}
|
|
}
|
|
}
|
|
}
|