multi: remove spend pruner.
Since the address index has been removed there is no longer a need for a spend pruner component to keep track of spend journal entries. This removes the spend pruner components and its integrations. DropConsumerDepsBucket has been added to remove persisted spend pruner consumer dependencies which are now unneeded.
This commit is contained in:
parent
f0efcc2226
commit
dfbd978579
@ -257,10 +257,6 @@ type BlockChain struct {
|
||||
calcVoterVersionIntervalCache map[[chainhash.HashSize]byte]uint32
|
||||
calcStakeVersionCache map[[chainhash.HashSize]byte]uint32
|
||||
|
||||
// spendPruner prunes spend journal data for disconnected blocks
|
||||
// if there are no consumers left for it.
|
||||
spendPruner *spendpruner.SpendJournalPruner
|
||||
|
||||
// bulkImportMode provides a mechanism to indicate that several validation
|
||||
// checks can be avoided when bulk importing blocks already known to be valid.
|
||||
// It is protected by the chain lock.
|
||||
@ -813,9 +809,6 @@ func (b *BlockChain) connectBlock(node *blockNode, block, parent *dcrutil.Block,
|
||||
log.Debugf("New target %08x (%064x)", node.bits, newDiff)
|
||||
}
|
||||
|
||||
// Notify the spend pruner of the connected block.
|
||||
go b.spendPruner.NotifyConnectedBlock(block.Hash())
|
||||
|
||||
// Notify the caller that the block was connected to the main chain.
|
||||
// The caller would typically want to react with actions such as
|
||||
// updating wallets.
|
||||
@ -971,13 +964,6 @@ func (b *BlockChain) disconnectBlock(node *blockNode, block, parent *dcrutil.Blo
|
||||
return err
|
||||
}
|
||||
|
||||
// Prune the associated spend data for the provided block hash if there
|
||||
// are no spend dependencies for it.
|
||||
err = b.spendPruner.MaybePruneSpendData(block.Hash(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This node's parent is now the end of the best chain.
|
||||
b.bestChain.SetTip(node.parent)
|
||||
|
||||
@ -2231,45 +2217,11 @@ func (q *ChainQueryerAdapter) Best() (int64, *chainhash.Hash) {
|
||||
return snapshot.Height, &snapshot.Hash
|
||||
}
|
||||
|
||||
// Ancestor returns the ancestor of the provided block at the provided height.
|
||||
//
|
||||
// This function is safe for concurrent access and is part of the
|
||||
// indexers.ChainQueryer interface.
|
||||
func (q *ChainQueryerAdapter) Ancestor(block *chainhash.Hash, height int64) *chainhash.Hash {
|
||||
node := q.index.LookupNode(block)
|
||||
ancestor := node.Ancestor(height)
|
||||
return &ancestor.hash
|
||||
}
|
||||
|
||||
// AddSpendConsumer adds the provided spend consumer to the spend pruner.
|
||||
func (q *ChainQueryerAdapter) AddSpendConsumer(consumer spendpruner.SpendConsumer) {
|
||||
q.spendPruner.AddConsumer(consumer)
|
||||
}
|
||||
|
||||
// RemoveSpendConsumerDependency removes the provided spend consumer dependency
|
||||
// associated with the provided block hash from the spend pruner.
|
||||
func (q *ChainQueryerAdapter) RemoveSpendConsumerDependency(dbTx database.Tx, blockHash *chainhash.Hash, consumerID string) error {
|
||||
return q.spendPruner.RemoveSpendConsumerDependency(dbTx, blockHash, consumerID)
|
||||
}
|
||||
|
||||
// FetchSpendConsumer returns the spend journal consumer associated with the
|
||||
// provided id.
|
||||
func (q *ChainQueryerAdapter) FetchSpendConsumer(id string) (spendpruner.SpendConsumer, error) {
|
||||
return q.spendPruner.FetchConsumer(id)
|
||||
}
|
||||
|
||||
// BlockHeaderByHash returns the block header identified by the given hash.
|
||||
func (q *ChainQueryerAdapter) BlockHeaderByHash(hash *chainhash.Hash) (wire.BlockHeader, error) {
|
||||
return q.HeaderByHash(hash)
|
||||
}
|
||||
|
||||
// SpendPrunerHandler processes incoming spending pruner signals.
|
||||
//
|
||||
// This must be run as a goroutine.
|
||||
func (b *BlockChain) SpendPrunerHandler(ctx context.Context) {
|
||||
b.spendPruner.HandleSignals(ctx)
|
||||
}
|
||||
|
||||
// Config is a descriptor which specifies the blockchain instance configuration.
|
||||
type Config struct {
|
||||
// DB defines the database which houses the blocks and will be used to
|
||||
@ -2441,8 +2393,8 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.spendPruner, err = spendpruner.NewSpendJournalPruner(b.db, b.RemoveSpendEntry)
|
||||
if err != nil {
|
||||
// Drop the spendpruner consumer dependencies bucket if it exists.
|
||||
if err := spendpruner.DropConsumerDepsBucket(b.db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@ import (
|
||||
"github.com/decred/dcrd/database/v3"
|
||||
"github.com/decred/dcrd/dcrutil/v4"
|
||||
"github.com/decred/dcrd/internal/blockchain/progresslog"
|
||||
"github.com/decred/dcrd/internal/blockchain/spendpruner"
|
||||
"github.com/decred/dcrd/wire"
|
||||
)
|
||||
|
||||
@ -80,21 +79,6 @@ type ChainQueryer interface {
|
||||
// BlockByHash returns the block of the provided hash.
|
||||
BlockByHash(*chainhash.Hash) (*dcrutil.Block, error)
|
||||
|
||||
// Ancestor returns the ancestor of the provided block at the
|
||||
// provided height.
|
||||
Ancestor(block *chainhash.Hash, height int64) *chainhash.Hash
|
||||
|
||||
// AddSpendConsumer adds the provided spend consumer.
|
||||
AddSpendConsumer(consumer spendpruner.SpendConsumer)
|
||||
|
||||
// RemoveSpendConsumerDependency removes the provided spend consumer
|
||||
// dependency associated with the provided block hash.
|
||||
RemoveSpendConsumerDependency(dbTx database.Tx, blockHash *chainhash.Hash, consumerID string) error
|
||||
|
||||
// FetchSpendConsumer returns the spend journal consumer associated with
|
||||
// the provided id.
|
||||
FetchSpendConsumer(id string) (spendpruner.SpendConsumer, error)
|
||||
|
||||
// PrevScripts returns a source of previous transaction scripts and their
|
||||
// associated versions spent by the given block.
|
||||
PrevScripts(database.Tx, *dcrutil.Block) (PrevScripter, error)
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
// Copyright (c) 2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package indexers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
)
|
||||
|
||||
// SpendConsumer represents an indexer dependent on
|
||||
// spend journal entries.
|
||||
type SpendConsumer struct {
|
||||
id string
|
||||
queryer ChainQueryer
|
||||
tipHash *chainhash.Hash
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
// NewSpendConsumer initializes a spend consumer.
|
||||
func NewSpendConsumer(id string, tipHash *chainhash.Hash, queryer ChainQueryer) *SpendConsumer {
|
||||
return &SpendConsumer{
|
||||
id: id,
|
||||
queryer: queryer,
|
||||
tipHash: tipHash,
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the identifier of the consumer.
|
||||
func (s *SpendConsumer) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// UpdateTip sets the tip of the consumer to the provided hash.
|
||||
func (s *SpendConsumer) UpdateTip(hash *chainhash.Hash) {
|
||||
s.mtx.Lock()
|
||||
s.tipHash = hash
|
||||
s.mtx.Unlock()
|
||||
}
|
||||
|
||||
// NeedSpendData checks whether the associated spend journal entry
|
||||
// for the provided block hash will be needed by the indexer.
|
||||
func (s *SpendConsumer) NeedSpendData(hash *chainhash.Hash) (bool, error) {
|
||||
// The indexer does not need spend journal data if it has not
|
||||
// been initialized.
|
||||
s.mtx.Lock()
|
||||
tipHash := s.tipHash
|
||||
s.mtx.Unlock()
|
||||
if tipHash == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
tipHeader, err := s.queryer.BlockHeaderByHash(tipHash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
header, err := s.queryer.BlockHeaderByHash(hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// The spend consumer does not need the spend journal
|
||||
// data associated with the provided block hash if
|
||||
// its current tip is below the provided hash.
|
||||
if header.Height > tipHeader.Height {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// The spend consumer does not need the spend data associated
|
||||
// with the provided block hash if the block is not an ancestor
|
||||
// of the current tip.
|
||||
blockHash := s.queryer.Ancestor(tipHash, int64(header.Height))
|
||||
if !blockHash.IsEqual(hash) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
// Copyright (c) 2021-2022 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
package indexers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/decred/dcrd/blockchain/v5/chaingen"
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
"github.com/decred/dcrd/database/v3"
|
||||
"github.com/decred/dcrd/wire"
|
||||
)
|
||||
|
||||
// TestSpendConsumer ensures the spend consumer behaves as expected.
|
||||
func TestSpendConsumer(t *testing.T) {
|
||||
dbPath := t.TempDir()
|
||||
|
||||
db, err := database.Create("ffldb", dbPath, wire.SimNet)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
chain, err := newTestChain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
g, err := chaingen.MakeGenerator(chaincfg.SimNetParams())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
// Add four blocks to the chain.
|
||||
bk1 := addBlock(t, chain, &g, "bk1")
|
||||
bk2 := addBlock(t, chain, &g, "bk2")
|
||||
bk3 := addBlock(t, chain, &g, "bk3")
|
||||
bk4 := addBlock(t, chain, &g, "bk4")
|
||||
|
||||
id := "testConsumer"
|
||||
consumer := NewSpendConsumer(id, bk3.Hash(), chain)
|
||||
|
||||
// Ensure the consumer is correctly identified.
|
||||
if id != consumer.ID() {
|
||||
t.Fatalf("expected consumer id to be %v, got %v", id, consumer.ID())
|
||||
}
|
||||
|
||||
// Ensure bk2 is needed by the consumer since it is an ancestor
|
||||
// of the current tip, bk3.
|
||||
needed, err := consumer.NeedSpendData(bk2.Hash())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected NeedSpendData error: %v", err)
|
||||
}
|
||||
|
||||
if !needed {
|
||||
t.Fatalf("expected the spend data for block bk2 to be needed")
|
||||
}
|
||||
|
||||
// Ensure bk4 is not needed by the address index since it is an ancestor
|
||||
// of the current tip, bk4.
|
||||
needed, err = consumer.NeedSpendData(bk4.Hash())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected NeedSpendData error: %v", err)
|
||||
}
|
||||
|
||||
if needed {
|
||||
t.Fatalf("expected the spend data for block bk4 to not be needed")
|
||||
}
|
||||
|
||||
// Ensure finding the ancestor of an unknown block hash fails.
|
||||
_, err = consumer.NeedSpendData(&chainhash.Hash{0})
|
||||
if err == nil {
|
||||
t.Fatal("expected an unknown block error")
|
||||
}
|
||||
|
||||
// Update the consumer tip to bk1.
|
||||
consumer.UpdateTip(bk1.Hash())
|
||||
|
||||
// Ensure bk2 is now no longer needed by the consumer since it is
|
||||
// not an ancestor of the current tip, bk1.
|
||||
needed, err = consumer.NeedSpendData(bk2.Hash())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected NeedSpendData error: %v", err)
|
||||
}
|
||||
|
||||
if needed {
|
||||
t.Fatalf("expected the spend data for block bk2 to not be needed")
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,6 @@ import (
|
||||
"github.com/decred/dcrd/database/v3"
|
||||
_ "github.com/decred/dcrd/database/v3/ffldb"
|
||||
"github.com/decred/dcrd/dcrutil/v4"
|
||||
"github.com/decred/dcrd/internal/blockchain/spendpruner"
|
||||
"github.com/decred/dcrd/wire"
|
||||
)
|
||||
|
||||
@ -30,7 +29,6 @@ type testChain struct {
|
||||
keyedByHeight map[int64]*dcrutil.Block
|
||||
keyedByHash map[chainhash.Hash]*dcrutil.Block
|
||||
orphans map[chainhash.Hash]*dcrutil.Block
|
||||
consumers map[string]spendpruner.SpendConsumer
|
||||
removedSpendDeps map[chainhash.Hash][]string
|
||||
mtx sync.Mutex
|
||||
}
|
||||
@ -41,7 +39,6 @@ func newTestChain() (*testChain, error) {
|
||||
keyedByHeight: make(map[int64]*dcrutil.Block),
|
||||
keyedByHash: make(map[chainhash.Hash]*dcrutil.Block),
|
||||
orphans: make(map[chainhash.Hash]*dcrutil.Block),
|
||||
consumers: make(map[string]spendpruner.SpendConsumer),
|
||||
removedSpendDeps: make(map[chainhash.Hash][]string),
|
||||
}
|
||||
genesis := dcrutil.NewBlock(chaincfg.SimNetParams().GenesisBlock)
|
||||
@ -167,13 +164,6 @@ func (tc *testChain) Ancestor(block *chainhash.Hash, height int64) *chainhash.Ha
|
||||
}
|
||||
}
|
||||
|
||||
// AddSpendConsumer adds the provided spend consumer.
|
||||
func (tc *testChain) AddSpendConsumer(consumer spendpruner.SpendConsumer) {
|
||||
tc.mtx.Lock()
|
||||
tc.consumers[consumer.ID()] = consumer
|
||||
tc.mtx.Unlock()
|
||||
}
|
||||
|
||||
// RemoveSpendConsumerDependency removes the provided spend consumer dependency
|
||||
// associated with the provided block hash.
|
||||
func (tc *testChain) RemoveSpendConsumerDependency(_ database.Tx, blockHash *chainhash.Hash, consumerID string) error {
|
||||
@ -211,19 +201,6 @@ func (tc *testChain) IsRemovedSpendConsumerDependency(blockHash *chainhash.Hash,
|
||||
return false
|
||||
}
|
||||
|
||||
// FetchSpendConsumer returns the spend journal consumer associated with
|
||||
// the provided id.
|
||||
func (tc *testChain) FetchSpendConsumer(id string) (spendpruner.SpendConsumer, error) {
|
||||
tc.mtx.Lock()
|
||||
defer tc.mtx.Unlock()
|
||||
consumer, ok := tc.consumers[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no spend consumer found with id %s", id)
|
||||
}
|
||||
|
||||
return consumer, nil
|
||||
}
|
||||
|
||||
// ChainParams returns the parameters of the chain.
|
||||
func (tc *testChain) ChainParams() *chaincfg.Params {
|
||||
return chaincfg.SimNetParams()
|
||||
|
||||
@ -4,93 +4,31 @@
|
||||
|
||||
package spendpruner
|
||||
|
||||
// NB: This package is scehduled for removal once DropConsumerDepsBucket no
|
||||
// longer needs to be called to remove unneeded persisted data.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/database/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
// depsSeparator is the character used in separating spend journal consumer
|
||||
// dependencies when serializing.
|
||||
depsSeparator = ','
|
||||
)
|
||||
|
||||
var (
|
||||
// spendConsumerDepsBucketName is the name of the bucket used in storing
|
||||
// spend journal consumer dependencies.
|
||||
spendConsumerDepsBucketName = []byte("spendconsumerdeps")
|
||||
)
|
||||
|
||||
// initConsumerDepsBucket creates the spend consumer dependencies bucket if it
|
||||
// does not exist.
|
||||
func initConsumerDepsBucket(db database.DB) error {
|
||||
// Create the spend consumer dependencies bucket if it does not exist yet.
|
||||
// DropConsumerDepsBucket removes the spend pruner consumer dependencies bucket
|
||||
// if it exists.
|
||||
func DropConsumerDepsBucket(db database.DB) error {
|
||||
return db.Update(func(dbTx database.Tx) error {
|
||||
meta := dbTx.Metadata()
|
||||
_, err := meta.CreateBucketIfNotExists(spendConsumerDepsBucketName)
|
||||
return err
|
||||
err := meta.DeleteBucket(spendConsumerDepsBucketName)
|
||||
if !errors.Is(err, database.ErrBucketNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// serializeSpendConsumerDeps returns serialized bytes of the provided spend
|
||||
// journal consumer dependencies.
|
||||
func serializeSpendConsumerDeps(deps []string) []byte {
|
||||
var buf bytes.Buffer
|
||||
for idx := 0; idx < len(deps); idx++ {
|
||||
buf.WriteString(deps[idx])
|
||||
if idx < len(deps)-1 {
|
||||
buf.WriteByte(depsSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// deserializeSpendConsumerDeps returns deserialized spend consumer
|
||||
// dependencies from the provided serialized bytes.
|
||||
func deserializeSpendConsumerDeps(serializedBytes []byte) []string {
|
||||
depBytes := bytes.Split(serializedBytes, []byte{depsSeparator})
|
||||
deps := make([]string, len(depBytes))
|
||||
for idx := 0; idx < len(depBytes); idx++ {
|
||||
deps[idx] = string(depBytes[idx])
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
// dbUpdateSpendConsumerDeps uses an existing database transaction to update
|
||||
// the spend consumer dependency entry for the provided block hash.
|
||||
func dbUpdateSpendConsumerDeps(dbTx database.Tx, blockHash chainhash.Hash, consumerDeps []string) error {
|
||||
depsBucket := dbTx.Metadata().Bucket(spendConsumerDepsBucketName)
|
||||
|
||||
// Remove the dependency entry if there are no spend consumer dependencies
|
||||
// left for the block hash.
|
||||
if len(consumerDeps) == 0 {
|
||||
return depsBucket.Delete(blockHash[:])
|
||||
}
|
||||
|
||||
// Update the dependency entry.
|
||||
serialized := serializeSpendConsumerDeps(consumerDeps)
|
||||
return depsBucket.Put(blockHash[:], serialized)
|
||||
}
|
||||
|
||||
// dbFetchSpendConsumerDeps uses an existing database transaction to fetch all
|
||||
// spend consumer dependency entries in the database.
|
||||
func dbFetchSpendConsumerDeps(dbTx database.Tx) (map[chainhash.Hash][]string, error) {
|
||||
depsBucket := dbTx.Metadata().Bucket(spendConsumerDepsBucketName)
|
||||
consumerDeps := make(map[chainhash.Hash][]string)
|
||||
cursor := depsBucket.Cursor()
|
||||
for ok := cursor.First(); ok; ok = cursor.Next() {
|
||||
hash, err := chainhash.NewHash(cursor.Key())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deps := deserializeSpendConsumerDeps(cursor.Value())
|
||||
consumerDeps[*hash] = deps
|
||||
}
|
||||
|
||||
return consumerDeps, nil
|
||||
}
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
// Copyright (c) 2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package spendpruner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/decred/dcrd/database/v3"
|
||||
_ "github.com/decred/dcrd/database/v3/ffldb"
|
||||
"github.com/decred/dcrd/wire"
|
||||
)
|
||||
|
||||
// TestSerializeConsumerDeps ensures consumer dependencies serialize as
|
||||
// intended.
|
||||
func TestSerializeConsumerDeps(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
deps []string
|
||||
expected []byte
|
||||
}{{
|
||||
name: "no dependencies",
|
||||
deps: []string{},
|
||||
expected: []byte{},
|
||||
}, {
|
||||
name: "one dependency",
|
||||
deps: []string{"alpha"},
|
||||
expected: []byte("alpha"),
|
||||
}, {
|
||||
name: "odd number of dependencies",
|
||||
deps: []string{"alpha", "bravo", "charlie"},
|
||||
expected: []byte("alpha,bravo,charlie"),
|
||||
}, {
|
||||
name: "even number of dependencies",
|
||||
deps: []string{"alpha", "bravo", "charlie", "echo"},
|
||||
expected: []byte("alpha,bravo,charlie,echo"),
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
serialized := serializeSpendConsumerDeps(test.deps)
|
||||
if !bytes.Equal(serialized, test.expected) {
|
||||
t.Errorf("%q: unexpected serialized mismatch, "+
|
||||
"expected %q, got %q", test.name, test.expected, serialized)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeserializeConsumerDeps ensures consumer dependencies deserialize as
|
||||
// intended.
|
||||
func TestDeserializeConsumerDeps(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serialized []byte
|
||||
expected []string
|
||||
}{{
|
||||
name: "no dependencies",
|
||||
serialized: []byte{},
|
||||
expected: []string{},
|
||||
}, {
|
||||
name: "one dependency",
|
||||
serialized: []byte("alpha"),
|
||||
expected: []string{"alpha"},
|
||||
}, {
|
||||
name: "odd number of dependencies",
|
||||
serialized: []byte("alpha,bravo,charlie"),
|
||||
expected: []string{"alpha", "bravo", "charlie"},
|
||||
}, {
|
||||
name: "even number of dependencies",
|
||||
serialized: []byte("alpha,bravo,charlie,echo"),
|
||||
expected: []string{"alpha", "bravo", "charlie", "echo"},
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
deserialized := deserializeSpendConsumerDeps(test.serialized)
|
||||
for idx := range test.expected {
|
||||
if deserialized[idx] != test.expected[idx] {
|
||||
t.Errorf("%q: unexpected dependency mismatch at index %d, "+
|
||||
"expected %q, got %q", test.name, idx,
|
||||
test.expected[idx], deserialized[idx])
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createdDB creates the test database. This is intended to be used for testing
|
||||
// purposes only.
|
||||
func createDB() (database.DB, func(), error) {
|
||||
dbPath := filepath.Join(os.TempDir(), "spdb")
|
||||
|
||||
err := os.MkdirAll(dbPath, 0700)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
db, err := database.Create("ffldb", dbPath, wire.SimNet)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = initConsumerDepsBucket(db)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
teardown := func() {
|
||||
db.Close()
|
||||
os.RemoveAll(dbPath)
|
||||
}
|
||||
|
||||
return db, teardown, nil
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package spendpruner
|
||||
|
||||
// ErrorKind identifies a kind of error. It has full support for errors.Is and
|
||||
// errors.As, so the caller can directly check against an error kind when
|
||||
// determining the reason for an error.
|
||||
type ErrorKind string
|
||||
|
||||
// These constants are used to identify a specific PruneError.
|
||||
const (
|
||||
// ErrNoConsumer indicates a spend journal consumer hash does not exist.
|
||||
ErrNoConsumer = ErrorKind("ErrNoConsumer")
|
||||
|
||||
// ErrLoadSpendDeps indicates an error loading consumer
|
||||
// spend dependencies.
|
||||
ErrLoadSpendDeps = ErrorKind("ErrLoadSpendDeps")
|
||||
|
||||
// ErrNeedSpendData indicates an error asserting a spend data dependency.
|
||||
ErrNeedSpendData = ErrorKind("ErrNeedSpendData")
|
||||
|
||||
// ErrUpdateConsumerDeps indicates an error updating consumer spend
|
||||
// dependencies.
|
||||
ErrUpdateConsumerDeps = ErrorKind("ErrUpdateConsumerDeps")
|
||||
)
|
||||
|
||||
// Error satisfies the error interface and prints human-readable errors.
|
||||
func (e ErrorKind) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// PruneError identifies a spend journal pruner error. It has full support for
|
||||
// errors.Is and errors.As, so the caller can ascertain the specific reason
|
||||
// for the error by checking the underlying error.
|
||||
type PruneError struct {
|
||||
Description string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error satisfies the error interface and prints human-readable errors.
|
||||
func (e PruneError) Error() string {
|
||||
return e.Description
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying wrapped error.
|
||||
func (e PruneError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// pruneError creates a PruneError given a set of arguments.
|
||||
func pruneError(kind ErrorKind, desc string) PruneError {
|
||||
return PruneError{Err: kind, Description: desc}
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
// Copyright (c) 2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package spendpruner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestErrorKindStringer tests the stringized output for the ErrorKind type.
|
||||
func TestErrorKindStringer(t *testing.T) {
|
||||
tests := []struct {
|
||||
in ErrorKind
|
||||
want string
|
||||
}{
|
||||
{ErrNoConsumer, "ErrNoConsumer"},
|
||||
{ErrLoadSpendDeps, "ErrLoadSpendDeps"},
|
||||
{ErrNeedSpendData, "ErrNeedSpendData"},
|
||||
{ErrUpdateConsumerDeps, "ErrUpdateConsumerDeps"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
result := test.in.Error()
|
||||
if result != test.want {
|
||||
t.Errorf("%d: got: %s want: %s", i, result, test.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPruneError tests the error output for the PruneError type.
|
||||
func TestPruneError(t *testing.T) {
|
||||
tests := []struct {
|
||||
in PruneError
|
||||
want string
|
||||
}{{
|
||||
PruneError{Description: "duplicate block"},
|
||||
"duplicate block",
|
||||
}, {
|
||||
PruneError{Description: "human-readable error"},
|
||||
"human-readable error",
|
||||
}}
|
||||
|
||||
for i, test := range tests {
|
||||
result := test.in.Error()
|
||||
if result != test.want {
|
||||
t.Errorf("#%d: got: %s want: %s", i, result, test.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPruneErrorKindIsAs ensures both ErrorKind and PruneError can be
|
||||
// identified as being a specific error kind via errors.Is and unwrapped
|
||||
// via errors.As.
|
||||
func TestPruneErrorKindIsAs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
target error
|
||||
wantMatch bool
|
||||
wantAs ErrorKind
|
||||
}{{
|
||||
name: "ErrNoConsumer == ErrNoConsumer",
|
||||
err: ErrNoConsumer,
|
||||
target: ErrNoConsumer,
|
||||
wantMatch: true,
|
||||
wantAs: ErrNoConsumer,
|
||||
}, {
|
||||
name: "PruneError.ErrNoConsumer == ErrNoConsumer",
|
||||
err: pruneError(ErrNoConsumer, ""),
|
||||
target: ErrNoConsumer,
|
||||
wantMatch: true,
|
||||
wantAs: ErrNoConsumer,
|
||||
}, {
|
||||
name: "PruneError.ErrNoConsumer == PruneError.ErrNoConsumer",
|
||||
err: pruneError(ErrNoConsumer, ""),
|
||||
target: pruneError(ErrNoConsumer, ""),
|
||||
wantMatch: true,
|
||||
wantAs: ErrNoConsumer,
|
||||
}, {
|
||||
name: "ErrNoConsumer != ErrNeedSpendData",
|
||||
err: ErrNoConsumer,
|
||||
target: ErrNeedSpendData,
|
||||
wantMatch: false,
|
||||
wantAs: ErrNoConsumer,
|
||||
}, {
|
||||
name: "PruneError.ErrNoConsumer != ErrNeedSpendData",
|
||||
err: pruneError(ErrNoConsumer, ""),
|
||||
target: ErrNeedSpendData,
|
||||
wantMatch: false,
|
||||
wantAs: ErrNoConsumer,
|
||||
}, {
|
||||
name: "ErrNoConsumer != PruneError.ErrNeedSpendData",
|
||||
err: ErrNoConsumer,
|
||||
target: pruneError(ErrNeedSpendData, ""),
|
||||
wantMatch: false,
|
||||
wantAs: ErrNoConsumer,
|
||||
}, {
|
||||
name: "PruneError.ErrNoConsumer != PruneError.ErrNeedSpendData",
|
||||
err: pruneError(ErrNoConsumer, ""),
|
||||
target: pruneError(ErrNeedSpendData, ""),
|
||||
wantMatch: false,
|
||||
wantAs: ErrNoConsumer,
|
||||
}, {
|
||||
name: "PruneError.ErrNoConsumer != io.EOF",
|
||||
err: pruneError(ErrNoConsumer, ""),
|
||||
target: io.EOF,
|
||||
wantMatch: false,
|
||||
wantAs: ErrNoConsumer,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
// Ensure the error matches or not depending on the expected result.
|
||||
result := errors.Is(test.err, test.target)
|
||||
if result != test.wantMatch {
|
||||
t.Errorf("%s: incorrect error identification -- got %v, want %v",
|
||||
test.name, result, test.wantMatch)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the underlying error kind can be unwrapped and is the
|
||||
// expected kind.
|
||||
var kind ErrorKind
|
||||
if !errors.As(test.err, &kind) {
|
||||
t.Errorf("%s: unable to unwrap to error kind", test.name)
|
||||
continue
|
||||
}
|
||||
if kind != test.wantAs {
|
||||
t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v",
|
||||
test.name, kind, test.wantAs)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
// Copyright (c) 2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package spendpruner
|
||||
|
||||
import (
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
)
|
||||
|
||||
// SpendConsumer describes the requirements for implementing a spend journal
|
||||
// consumer.
|
||||
//
|
||||
// All functions MUST be safe for concurrent access.
|
||||
type SpendConsumer interface {
|
||||
// ID returns the identifier of the consumer.
|
||||
ID() string
|
||||
|
||||
// NeedSpendData checks whether the associated spend journal entry for the
|
||||
// provided block hash will be needed by the consumer.
|
||||
NeedSpendData(hash *chainhash.Hash) (bool, error)
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
// Copyright (c) 2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package spendpruner
|
||||
|
||||
import (
|
||||
"github.com/decred/slog"
|
||||
)
|
||||
|
||||
// log is a logger that is initialized with no output filters. This means
|
||||
// the package will not perform any logging by default until the caller
|
||||
// requests it. The default amount of logging is none.
|
||||
var (
|
||||
log = slog.Disabled
|
||||
)
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
func UseLogger(logger slog.Logger) {
|
||||
log = logger
|
||||
}
|
||||
@ -1,364 +0,0 @@
|
||||
// Copyright (c) 2021-2022 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package spendpruner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/database/v3"
|
||||
)
|
||||
|
||||
// spendPrunerEventType represents a spend pruner event message.
|
||||
type spendPrunerEventType int
|
||||
|
||||
const (
|
||||
// spBlockConnected indicates a new block was connected to the chain.
|
||||
spBlockConnected spendPrunerEventType = iota
|
||||
|
||||
// spBlockDisconnected indicates there are no spend dependencies for the
|
||||
// block disconnected from the chain.
|
||||
spBlockDisconnected
|
||||
)
|
||||
|
||||
// SpendJournalNotification represents the notification received by the pruner
|
||||
// on block connections and disconnections.
|
||||
type SpendJournalNotification struct {
|
||||
BlockHash *chainhash.Hash
|
||||
Event spendPrunerEventType
|
||||
Done chan bool
|
||||
}
|
||||
|
||||
// SpendJournalPruner represents a spend journal pruner that ensures spend
|
||||
// journal entries needed by consumers are retained until no longer needed.
|
||||
type SpendJournalPruner struct {
|
||||
// This removes the spend journal entry of the provided block hash if it
|
||||
// is not part of the main chain.
|
||||
removeSpendEntry func(hash *chainhash.Hash) error
|
||||
|
||||
// These fields track spend consumers and their spend journal dependencies.
|
||||
dependents map[chainhash.Hash][]string
|
||||
dependentsMtx sync.RWMutex
|
||||
consumers map[string]SpendConsumer
|
||||
consumersMtx sync.RWMutex
|
||||
|
||||
// This field relays block connection and disconnection signals for
|
||||
// processing.
|
||||
ch chan SpendJournalNotification
|
||||
|
||||
// This field provides access to the database.
|
||||
db database.DB
|
||||
}
|
||||
|
||||
// NewSpendJournalPruner initializes a spend journal pruner.
|
||||
func NewSpendJournalPruner(db database.DB, removeSpendEntry func(hash *chainhash.Hash) error) (*SpendJournalPruner, error) {
|
||||
err := initConsumerDepsBucket(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spendPruner := &SpendJournalPruner{
|
||||
db: db,
|
||||
removeSpendEntry: removeSpendEntry,
|
||||
dependents: make(map[chainhash.Hash][]string),
|
||||
consumers: make(map[string]SpendConsumer),
|
||||
ch: make(chan SpendJournalNotification),
|
||||
}
|
||||
|
||||
err = spendPruner.loadSpendConsumerDeps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spendPruner, nil
|
||||
}
|
||||
|
||||
// AddConsumer adds a spend journal consumer to the pruner.
|
||||
func (s *SpendJournalPruner) AddConsumer(consumer SpendConsumer) {
|
||||
s.consumersMtx.Lock()
|
||||
s.consumers[consumer.ID()] = consumer
|
||||
s.consumersMtx.Unlock()
|
||||
}
|
||||
|
||||
// FetchConsumer returns the spend journal consumer associated with the
|
||||
// provided id.
|
||||
func (s *SpendJournalPruner) FetchConsumer(id string) (SpendConsumer, error) {
|
||||
s.consumersMtx.RLock()
|
||||
defer s.consumersMtx.RUnlock()
|
||||
consumer, ok := s.consumers[id]
|
||||
if !ok {
|
||||
msg := fmt.Sprintf("no spend consumer found with id %s", id)
|
||||
return nil, pruneError(ErrNoConsumer, msg)
|
||||
}
|
||||
|
||||
return consumer, nil
|
||||
}
|
||||
|
||||
// DependencyExists determines whether there are spend consumer dependencies
|
||||
// for the provided block hash.
|
||||
func (s *SpendJournalPruner) DependencyExists(blockHash *chainhash.Hash) bool {
|
||||
s.dependentsMtx.RLock()
|
||||
defer s.dependentsMtx.RUnlock()
|
||||
|
||||
_, ok := s.dependents[*blockHash]
|
||||
return ok
|
||||
}
|
||||
|
||||
// NotifyConnectedBlock signals the spend pruner of the provided
|
||||
// connected block hash.
|
||||
func (s *SpendJournalPruner) NotifyConnectedBlock(blockHash *chainhash.Hash) {
|
||||
s.ch <- SpendJournalNotification{
|
||||
BlockHash: blockHash,
|
||||
Event: spBlockConnected,
|
||||
}
|
||||
}
|
||||
|
||||
// dependencyExistsInternal determines whether a spend consumer depends on
|
||||
// the spend data of the provided block hash.
|
||||
func (s *SpendJournalPruner) dependencyExistsInternal(blockHash *chainhash.Hash, consumerID string) bool {
|
||||
s.dependentsMtx.RLock()
|
||||
dependents, ok := s.dependents[*blockHash]
|
||||
s.dependentsMtx.RUnlock()
|
||||
if !ok {
|
||||
// The dependency does not exist if the block hash is not
|
||||
// a key for dependents.
|
||||
return false
|
||||
}
|
||||
|
||||
for _, id := range dependents {
|
||||
if consumerID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// addSpendConsumerDeps adds an entry for each spend consumer dependent on
|
||||
// journal data for the provided block hash.
|
||||
func (s *SpendJournalPruner) addSpendConsumerDeps(blockHash *chainhash.Hash) error {
|
||||
s.consumersMtx.RLock()
|
||||
defer s.consumersMtx.RUnlock()
|
||||
|
||||
for _, consumer := range s.consumers {
|
||||
if s.dependencyExistsInternal(blockHash, consumer.ID()) {
|
||||
// Dependency already created, skip.
|
||||
continue
|
||||
}
|
||||
|
||||
needSpendData, err := consumer.NeedSpendData(blockHash)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("unable to assert dependency: %s", err)
|
||||
return pruneError(ErrNeedSpendData, msg)
|
||||
}
|
||||
|
||||
if !needSpendData {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add a spend dependency entry of the block hash for the consumer.
|
||||
s.dependentsMtx.Lock()
|
||||
dependents, ok := s.dependents[*blockHash]
|
||||
if !ok {
|
||||
dependents = []string{consumer.ID()}
|
||||
s.dependents[*blockHash] = dependents
|
||||
s.dependentsMtx.Unlock()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
dependents = append(dependents, consumer.ID())
|
||||
s.dependents[*blockHash] = dependents
|
||||
s.dependentsMtx.Unlock()
|
||||
}
|
||||
|
||||
s.dependentsMtx.Lock()
|
||||
dependents := s.dependents[*blockHash]
|
||||
s.dependentsMtx.Unlock()
|
||||
|
||||
// Update the persisted spend consumer deps entry for
|
||||
// the provided block hash.
|
||||
err := s.db.Update(func(tx database.Tx) error {
|
||||
err := dbUpdateSpendConsumerDeps(tx, *blockHash, dependents)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("unable to update persisted consumer "+
|
||||
"dependencies for block hash %v: %v", blockHash, err)
|
||||
return pruneError(ErrUpdateConsumerDeps, msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSpendConsumerDependency removes the provided spend consumer dependency
|
||||
// associated with the provided block hash from the spend pruner. The block
|
||||
// hash is removed as a key of the dependents map once all its dependency
|
||||
// entries are removed.
|
||||
func (s *SpendJournalPruner) RemoveSpendConsumerDependency(dbTx database.Tx, blockHash *chainhash.Hash, consumerID string) error {
|
||||
s.dependentsMtx.Lock()
|
||||
dependents, ok := s.dependents[*blockHash]
|
||||
if !ok {
|
||||
s.dependentsMtx.Unlock()
|
||||
// No entry for block hash found, do nothing.
|
||||
return nil
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(dependents); idx++ {
|
||||
if dependents[idx] == consumerID {
|
||||
dependents = append(dependents[:idx], dependents[idx+1:]...)
|
||||
s.dependents[*blockHash] = dependents
|
||||
break
|
||||
}
|
||||
}
|
||||
s.dependentsMtx.Unlock()
|
||||
|
||||
if len(dependents) == 0 {
|
||||
s.dependentsMtx.Lock()
|
||||
delete(s.dependents, *blockHash)
|
||||
s.dependentsMtx.Unlock()
|
||||
go func() {
|
||||
s.ch <- SpendJournalNotification{
|
||||
BlockHash: blockHash,
|
||||
Event: spBlockDisconnected,
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Update the tracked spend journal entry for the provided
|
||||
// block hash.
|
||||
err := dbUpdateSpendConsumerDeps(dbTx, *blockHash, dependents)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("unable to update consumer dependencies "+
|
||||
"entry for block hash %v: %v", blockHash, err)
|
||||
return pruneError(ErrUpdateConsumerDeps, msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeSpendConsumerDeps removes the key/value pair of spend consumer
|
||||
// dependencies and the provided block hash from the the prune set as well
|
||||
// as the database.
|
||||
func (s *SpendJournalPruner) removeSpendConsumerDeps(blockHash *chainhash.Hash) error {
|
||||
s.dependentsMtx.Lock()
|
||||
delete(s.dependents, *blockHash)
|
||||
s.dependentsMtx.Unlock()
|
||||
|
||||
// Remove the tracked spend journal entry for the provided
|
||||
// block hash.
|
||||
err := s.db.Update(func(tx database.Tx) error {
|
||||
return dbUpdateSpendConsumerDeps(tx, *blockHash, nil)
|
||||
})
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("unable to remove persisted consumer dependencies "+
|
||||
"entry for block hash %v: %v", blockHash, err)
|
||||
return pruneError(ErrUpdateConsumerDeps, msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSpendConsumerDeps loads persisted consumer spend dependencies from
|
||||
// the database.
|
||||
func (s *SpendJournalPruner) loadSpendConsumerDeps() error {
|
||||
return s.db.View(func(tx database.Tx) error {
|
||||
consumerDeps, err := dbFetchSpendConsumerDeps(tx)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("unable to load spend consumer "+
|
||||
"dependencies: %v", err)
|
||||
return pruneError(ErrLoadSpendDeps, msg)
|
||||
}
|
||||
|
||||
s.dependentsMtx.Lock()
|
||||
for k, v := range consumerDeps {
|
||||
s.dependents[k] = v
|
||||
}
|
||||
s.dependentsMtx.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// HandleSignals processes incoming signals to the spend pruner.
|
||||
func (s *SpendJournalPruner) HandleSignals(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case ntfn := <-s.ch:
|
||||
signalDone := func() {
|
||||
if ntfn.Done != nil {
|
||||
close(ntfn.Done)
|
||||
}
|
||||
}
|
||||
|
||||
switch ntfn.Event {
|
||||
case spBlockDisconnected:
|
||||
err := s.removeSpendEntry(ntfn.BlockHash)
|
||||
if err != nil {
|
||||
log.Errorf("unable to prune spend data for "+
|
||||
"block hash (%s): %v", ntfn.BlockHash, err)
|
||||
|
||||
}
|
||||
|
||||
signalDone()
|
||||
|
||||
case spBlockConnected:
|
||||
if !s.DependencyExists(ntfn.BlockHash) {
|
||||
// Do nothing if there are no spend journal dependencies
|
||||
// for the the connected block.
|
||||
signalDone()
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove the key/value pair of persisted spend consumer
|
||||
// dependencies and the provided connected block hash from
|
||||
// the prune set.
|
||||
err := s.removeSpendConsumerDeps(ntfn.BlockHash)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
signalDone()
|
||||
|
||||
default:
|
||||
log.Errorf("unknown spend journal notification type: %d",
|
||||
ntfn.Event)
|
||||
|
||||
signalDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MaybePruneSpendData firsts adds consumer spend dependencies for the provided
|
||||
// blockhash if any. If there are no dependencies the spend journal entry
|
||||
// associated with the provided block hash is pruned.
|
||||
func (s *SpendJournalPruner) MaybePruneSpendData(blockHash *chainhash.Hash, done chan bool) error {
|
||||
err := s.addSpendConsumerDeps(blockHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.DependencyExists(blockHash) {
|
||||
// Do nothing if there are spend dependencies for the provided block
|
||||
// hash.
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.ch <- SpendJournalNotification{
|
||||
BlockHash: blockHash,
|
||||
Event: spBlockDisconnected,
|
||||
Done: done,
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,369 +0,0 @@
|
||||
// Copyright (c) 2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package spendpruner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/database/v3"
|
||||
)
|
||||
|
||||
// testSpendConsumer represents a mock spend consumer for testing purposes only.
|
||||
type testSpendConsumer struct {
|
||||
id string
|
||||
queryer *testChain
|
||||
tip *chainhash.Hash
|
||||
needSpendData bool
|
||||
needSpendDataErr error
|
||||
}
|
||||
|
||||
// ID returns the identifier of the consumer.
|
||||
func (t *testSpendConsumer) ID() string {
|
||||
return t.id
|
||||
}
|
||||
|
||||
// NeedSpendData checks whether the associated spend journal entry
|
||||
// for the provided block hash will be needed by the consumer.
|
||||
func (t *testSpendConsumer) NeedSpendData(hash *chainhash.Hash) (bool, error) {
|
||||
return t.needSpendData, t.needSpendDataErr
|
||||
}
|
||||
|
||||
// testChain represents a mock chain for testing purposes only.
|
||||
type testChain struct {
|
||||
removedSpendEntries map[chainhash.Hash]struct{}
|
||||
removeSpendEntryErr error
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
// RemoveSpendEntry purges the associated spend journal entry of the provided
|
||||
// block hash.
|
||||
func (t *testChain) RemoveSpendEntry(hash *chainhash.Hash) error {
|
||||
if t.removeSpendEntryErr != nil {
|
||||
return t.removeSpendEntryErr
|
||||
}
|
||||
|
||||
t.mtx.Lock()
|
||||
t.removedSpendEntries[*hash] = struct{}{}
|
||||
t.mtx.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestSpendPruner ensures the spend pruner works as intended.
|
||||
func TestSpendPruner(t *testing.T) {
|
||||
chain := &testChain{
|
||||
removedSpendEntries: make(map[chainhash.Hash]struct{}),
|
||||
}
|
||||
|
||||
consumerA := &testSpendConsumer{
|
||||
id: "consumer_a",
|
||||
queryer: chain,
|
||||
tip: &chainhash.Hash{0},
|
||||
needSpendData: true,
|
||||
}
|
||||
|
||||
consumerB := &testSpendConsumer{
|
||||
id: "consumer_b",
|
||||
queryer: chain,
|
||||
tip: &chainhash.Hash{0},
|
||||
needSpendData: false,
|
||||
needSpendDataErr: fmt.Errorf("unable to confirm spend data need"),
|
||||
}
|
||||
|
||||
db, teardown, err := createDB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer teardown()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
pruner, err := NewSpendJournalPruner(db, chain.RemoveSpendEntry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go pruner.HandleSignals(ctx)
|
||||
|
||||
pruner.AddConsumer(consumerA)
|
||||
pruner.AddConsumer(consumerB)
|
||||
|
||||
// Ensure adding the same consumer does not create a duplicate.
|
||||
pruner.AddConsumer(consumerB)
|
||||
|
||||
pruner.consumersMtx.Lock()
|
||||
consumers := len(pruner.consumers)
|
||||
pruner.consumersMtx.Unlock()
|
||||
|
||||
if consumers != 2 {
|
||||
t.Fatalf("expected 2 consumers, got %d", consumers)
|
||||
}
|
||||
|
||||
// Ensure adding dependents fails if a consumer errors checking if the
|
||||
// data is needed.
|
||||
hashA := &chainhash.Hash{'a'}
|
||||
err = pruner.addSpendConsumerDeps(hashA)
|
||||
if !errors.Is(err, ErrNeedSpendData) {
|
||||
t.Fatalf("expected a spend data error, got %v", err)
|
||||
}
|
||||
|
||||
fetchHashSpendDependents := func(pruner *SpendJournalPruner, hash *chainhash.Hash) ([]string, bool) {
|
||||
pruner.dependentsMtx.Lock()
|
||||
dependents, ok := pruner.dependents[*hash]
|
||||
pruner.dependentsMtx.Unlock()
|
||||
|
||||
return dependents, ok
|
||||
}
|
||||
|
||||
isRemovedSpendEntry := func(chain *testChain, hash *chainhash.Hash) bool {
|
||||
chain.mtx.Lock()
|
||||
_, ok := chain.removedSpendEntries[*hash]
|
||||
chain.mtx.Unlock()
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
consumerB.needSpendDataErr = nil
|
||||
|
||||
// Ensure adding dependents creates entries for consumers that need
|
||||
// the spend data of the provided block hash.
|
||||
err = pruner.addSpendConsumerDeps(hashA)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding consumer dependencies: %v", err)
|
||||
}
|
||||
|
||||
// Ensure there is only one dependent entry for hashA, belonging to
|
||||
// consumerA.
|
||||
dependents, ok := fetchHashSpendDependents(pruner, hashA)
|
||||
if !ok {
|
||||
t.Fatalf("expected dependents to have an entry for %s", hashA)
|
||||
}
|
||||
|
||||
if len(dependents) != 1 {
|
||||
t.Fatalf("expected one dependent entry for hashA, got %d",
|
||||
len(dependents))
|
||||
}
|
||||
|
||||
if dependents[0] != consumerA.ID() {
|
||||
t.Fatalf("expected hashA's dependent to be %s, got %s",
|
||||
consumerA.ID(), dependents[0])
|
||||
}
|
||||
|
||||
// Ensure there is no existing dependency for consumerB on hashA.
|
||||
exists := pruner.dependencyExistsInternal(hashA, consumerB.ID())
|
||||
if exists {
|
||||
t.Fatal("unexpected dependency found for consumerB on hashA")
|
||||
}
|
||||
|
||||
// Ensure there is an existing dependency for consumerA on hashA.
|
||||
exists = pruner.dependencyExistsInternal(hashA, consumerA.ID())
|
||||
if !exists {
|
||||
t.Fatal("expected dependency found for consumerA on hashA")
|
||||
}
|
||||
|
||||
// Ensure consumerC and consumerA now have dependencies on hashA.
|
||||
consumerC := &testSpendConsumer{
|
||||
id: "consumer_c",
|
||||
tip: &chainhash.Hash{0},
|
||||
needSpendData: true,
|
||||
}
|
||||
|
||||
pruner.AddConsumer(consumerC)
|
||||
|
||||
setConsumerNeedSpendData := func(state bool) {
|
||||
consumerA.needSpendData = state
|
||||
consumerB.needSpendData = state
|
||||
consumerC.needSpendData = state
|
||||
}
|
||||
|
||||
removeSpendConsumerDep := func(pruner *SpendJournalPruner, blockHash *chainhash.Hash, consumerID string) error {
|
||||
return pruner.db.Update(func(tx database.Tx) error {
|
||||
return pruner.RemoveSpendConsumerDependency(tx, blockHash, consumerID)
|
||||
})
|
||||
}
|
||||
|
||||
err = pruner.addSpendConsumerDeps(hashA)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding spend data dependents "+
|
||||
"for hashA: %v", err)
|
||||
}
|
||||
|
||||
exists = pruner.DependencyExists(hashA)
|
||||
if !exists {
|
||||
t.Fatal("expected existing dependencies for hashA")
|
||||
}
|
||||
|
||||
exists = pruner.dependencyExistsInternal(hashA, consumerC.ID())
|
||||
if !exists {
|
||||
t.Fatal("expected dependency found for consumerC on hashA")
|
||||
}
|
||||
|
||||
exists = pruner.dependencyExistsInternal(hashA, consumerA.ID())
|
||||
if !exists {
|
||||
t.Fatal("expected dependency found for consumerA on hashA")
|
||||
}
|
||||
|
||||
// Ensure there are now two dependent entries for hashA.
|
||||
dependents, _ = fetchHashSpendDependents(pruner, hashA)
|
||||
if len(dependents) != 2 {
|
||||
t.Fatalf("expected two dependent entry for hashA, got %d",
|
||||
len(dependents))
|
||||
}
|
||||
|
||||
// Ensure removing a non-existent dependency does nothing.
|
||||
err = removeSpendConsumerDep(pruner, hashA, "consumer_x")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error removing non-existent spend "+
|
||||
"consumer dependency %v", err)
|
||||
}
|
||||
|
||||
// Ensure removing dependencies for a non-existent block hash does nothing.
|
||||
hashT := &chainhash.Hash{'T'}
|
||||
err = removeSpendConsumerDep(pruner, hashT, "consumer_x")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error removing spend "+
|
||||
"consumer dependency for non-existent block hash %v", err)
|
||||
}
|
||||
|
||||
dependents, _ = fetchHashSpendDependents(pruner, hashA)
|
||||
if len(dependents) != 2 {
|
||||
t.Fatalf("expected two dependent entry for hashA, got %d",
|
||||
len(dependents))
|
||||
}
|
||||
|
||||
// Update spend consumers to not need spend data for upcoming tests.
|
||||
setConsumerNeedSpendData(false)
|
||||
|
||||
// Ensure the spend pruner does not remove the spend entries if
|
||||
// there are existing dependencies for it.
|
||||
err = pruner.MaybePruneSpendData(hashA, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("[MaybePruneSpendData] unexpected error: %v", err)
|
||||
}
|
||||
|
||||
ok = isRemovedSpendEntry(chain, hashA)
|
||||
if ok {
|
||||
t.Fatal("unexpected hashA spend data pruned")
|
||||
}
|
||||
|
||||
// Ensure the spend pruner does remove spend entries for
|
||||
// a hash if there are no existing spend dependencies for it.
|
||||
hashX := &chainhash.Hash{'x'}
|
||||
done := make(chan bool)
|
||||
err = pruner.MaybePruneSpendData(hashX, done)
|
||||
if err != nil {
|
||||
t.Fatalf("[MaybePruneSpendData] unexpected error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Do nothing.
|
||||
case <-time.After(time.Second * 3):
|
||||
t.Fatal("timeout waiting for remove spend journal done signal")
|
||||
}
|
||||
|
||||
ok = isRemovedSpendEntry(chain, hashX)
|
||||
if !ok {
|
||||
t.Fatalf("expected hashX spend data to be pruned")
|
||||
}
|
||||
|
||||
// Remove the HashA dependency for consumerC.
|
||||
removeSpendConsumerDep(pruner, hashA, consumerC.ID())
|
||||
|
||||
dependents, _ = fetchHashSpendDependents(pruner, hashA)
|
||||
if len(dependents) != 1 {
|
||||
t.Fatalf("expected no dependent entry for hashA, got %d",
|
||||
len(dependents))
|
||||
}
|
||||
|
||||
// Trigger a spend journal prune for HashA's entry by removing the
|
||||
// last dependency for it.
|
||||
removeSpendConsumerDep(pruner, hashA, consumerA.ID())
|
||||
|
||||
// Ensure the pruner no longer has a dependent entry for HashA.
|
||||
_, ok = pruner.dependents[*hashA]
|
||||
if ok {
|
||||
t.Fatal("expected no dependent entry for hashA")
|
||||
}
|
||||
|
||||
ok = isRemovedSpendEntry(chain, hashX)
|
||||
if !ok {
|
||||
t.Fatal("expected hashA spend data to be pruned")
|
||||
}
|
||||
|
||||
// Update spend consumers to need spend data for upcoming tests.
|
||||
setConsumerNeedSpendData(true)
|
||||
|
||||
// Create consumer dependencies for hashB and hashC.
|
||||
hashB := &chainhash.Hash{'b'}
|
||||
err = pruner.addSpendConsumerDeps(hashB)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding consumer dependencies "+
|
||||
"for hashB: %v", err)
|
||||
}
|
||||
|
||||
hashC := &chainhash.Hash{'c'}
|
||||
err = pruner.addSpendConsumerDeps(hashC)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding consumer dependencies "+
|
||||
"for hashC: %v", err)
|
||||
}
|
||||
|
||||
if !pruner.DependencyExists(hashB) {
|
||||
t.Fatal("expected consumer dependencies for hashB")
|
||||
}
|
||||
|
||||
// Trigger a consumer dependencies purge by signalling hashB as a
|
||||
// connected block.
|
||||
go pruner.NotifyConnectedBlock(hashB)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
if pruner.DependencyExists(hashB) {
|
||||
t.Fatal("expected no consumer dependencies entry for hashB")
|
||||
}
|
||||
|
||||
// Ensure the spend pruner does nothing if the provided connected block
|
||||
// does not have any spend consumer dependencies.
|
||||
go pruner.NotifyConnectedBlock(hashT)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
// Ensure there are consumer dependencies for hashC before shutting down.
|
||||
if !pruner.DependencyExists(hashC) {
|
||||
t.Fatal("expected consumer dependencies for hashC")
|
||||
}
|
||||
|
||||
pruner.dependentsMtx.Lock()
|
||||
expected := len(pruner.dependents[*hashC])
|
||||
pruner.dependentsMtx.Unlock()
|
||||
|
||||
cancel()
|
||||
|
||||
// Load the spend pruner from the database.
|
||||
pruner, err = NewSpendJournalPruner(db, chain.RemoveSpendEntry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure the consumer dependencies for hashC were loaded from
|
||||
// the database.
|
||||
if !pruner.DependencyExists(hashC) {
|
||||
t.Fatal("expected consumer dependencies for hashC")
|
||||
}
|
||||
|
||||
// Ensure the expected number of dependents for hashC are identical
|
||||
// after loading from the database.
|
||||
deps, _ := fetchHashSpendDependents(pruner, hashC)
|
||||
|
||||
if len(deps) != expected {
|
||||
t.Fatalf("dependencies mismatch, expected count of %d, got %d",
|
||||
expected, len(deps))
|
||||
}
|
||||
}
|
||||
@ -3072,14 +3072,6 @@ func (s *server) Run(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Start the chain's spend pruner handler which processes spend journal
|
||||
// prune signals.
|
||||
s.wg.Add(1)
|
||||
go func(ctx context.Context, s *server) {
|
||||
s.chain.SpendPrunerHandler(ctx)
|
||||
s.wg.Done()
|
||||
}(ctx, s)
|
||||
|
||||
// Start the chain's index subscriber.
|
||||
s.wg.Add(1)
|
||||
go func(ctx context.Context, s *server) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user