diff --git a/blockchain/chain.go b/blockchain/chain.go index fd11045837..f8af86e85a 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -119,6 +119,10 @@ type BlockChain struct { // is pruned. pruneTarget uint64 + // pruneSpendJDepth is the block depth of spend journal entries to + // keep. Zero means disabled spend journal pruning. + pruneSpendJDepth int32 + // These fields are related to the memory block index. They both have // their own locks, however they are often also protected by the chain // lock to help prevent logic races when blocks are being processed. @@ -676,6 +680,17 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block, return err } + // Prune the spend journal depth if spend journal pruning is + // enabled. + if b.pruneSpendJDepth != 0 { + err = dbPruneSpendJournalDepth( + dbTx, b.pruneSpendJDepth, block.Height(), + ) + if err != nil { + return err + } + } + // Allow the index manager to call each of the currently active // optional indexes with the block being connected so they can // update themselves accordingly. @@ -2117,6 +2132,43 @@ func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error { return b.reorganizeChain(detachNodes, attachNodes) } +// setupSpendJournalPruning initializes internal journal pruning control +// structures and prunes the spend journal if needed. +func (b *BlockChain) setupSpendJournalPruning(interrupt <-chan struct{}) error { + // If spend journal pruning is disabled, we have nothing to do here. + if b.pruneSpendJDepth == 0 { + return nil + } + + log.Info("Executing the initial spend journal pruning, the first " + + "initialization after enabling or changing the journal prune " + + "depth may take a long time.") + + var stats spendJournalStats + err := b.db.Update(func(tx database.Tx) error { + err := dbInitSpendJournalStats( + tx, interrupt, b.pruneSpendJDepth, + ) + if err != nil { + return err + } + stats, err = dbFetchSpendJournalStats(tx) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + bestHeight := b.bestChain.height() + return dbPruneSpendJournalDepthSlow( + b.db, b.pruneSpendJDepth, stats.nextHeight, bestHeight, + interrupt, + ) +} + // IndexManager provides a generic interface that the is called when blocks are // connected and disconnected to and from the tip of the main chain for the // purpose of supporting optional indexes. @@ -2213,6 +2265,11 @@ type Config struct { // will target for with block files. Prune at 0 specifies that no // blocks will be deleted. Prune uint64 + + // PruneSpendJ specifies the target block depth of spend journal + // entries to keep. A value of zero means that spend journal pruning is + // disabled. + PruneSpendJ int32 } // New returns a BlockChain instance using the provided configuration details. @@ -2271,6 +2328,7 @@ func New(config *Config) (*BlockChain, error) { warningCaches: newThresholdCaches(vbNumBits), deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments), pruneTarget: config.Prune, + pruneSpendJDepth: config.PruneSpendJ, } // Ensure all the deployments are synchronized with our clock if @@ -2323,6 +2381,11 @@ func New(config *Config) (*BlockChain, error) { bestNode.height, bestNode.hash, b.stateSnapshot.TotalTxns, bestNode.workSum) + // Setup spend journal pruning. + if err := b.setupSpendJournalPruning(config.Interrupt); err != nil { + return nil, err + } + return &b, nil } diff --git a/blockchain/chainio.go b/blockchain/chainio.go index be5c9a2009..3878f7ace5 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -7,6 +7,7 @@ package blockchain import ( "bytes" "encoding/binary" + "errors" "fmt" "math/big" "sync" @@ -59,6 +60,8 @@ var ( // the version of the spend journal currently in the database. spendJournalVersionKeyName = []byte("spendjournalversion") + spendJournalStatsKeyName = []byte("spendjournalstats") + // spendJournalBucketName is the name of the db bucket used to house // transactions outputs that are spent in each block. spendJournalBucketName = []byte("spendjournal") @@ -76,6 +79,10 @@ var ( byteOrder = binary.LittleEndian ) +var ( + errMissingSpendJournalStats = errors.New("missing spend journal stats") +) + // errNotInMainChain signifies that a block hash or height that is not in the // main chain was requested. type errNotInMainChain string @@ -513,6 +520,256 @@ func dbPruneSpendJournalEntry(dbTx database.Tx, blockHashes []chainhash.Hash) er return nil } +// spendJournalStats is the internal control structure used for depth pruning of +// the spend journal. +type spendJournalStats struct { + pruneDepth int32 + nextHeight int32 +} + +// deserialize reads data into s. +func (s *spendJournalStats) deserialize(data []byte) error { + if len(data) != 8 { + return fmt.Errorf("bad spend journal stats len") + } + + s.pruneDepth = int32(byteOrder.Uint32(data[:4])) + s.nextHeight = int32(byteOrder.Uint32(data[4:8])) + + return nil +} + +// serialize appends the serialization of s into buf and returns the serialized +// spendJournalStats. +func (s *spendJournalStats) serialize(buf []byte) []byte { + buf = byteOrder.AppendUint32(buf, uint32(s.pruneDepth)) + return byteOrder.AppendUint32(buf, uint32(s.nextHeight)) +} + +// dbFetchSpendJournalStats returns the spendJournalStats from DB, or error in +// case the entry does not exists. +func dbFetchSpendJournalStats(dbTx database.Tx) (spendJournalStats, error) { + var zr spendJournalStats + serialized := dbTx.Metadata().Get(spendJournalStatsKeyName) + if serialized == nil { + return zr, errMissingSpendJournalStats + } + + var stats spendJournalStats + err := stats.deserialize(serialized) + if err != nil { + return zr, err + } + + return stats, nil +} + +// DBSpendJournalPruneDepth returns the previously configured spend journal +// prune depth, or 0 if spend journal pruning was never enabled. +func DBSpendJournalPruneDepth(dbTx database.Tx) (int32, error) { + stats, err := dbFetchSpendJournalStats(dbTx) + if err != nil && errors.Is(err, errMissingSpendJournalStats) { + return 0, nil + } + if err != nil { + return 0, err + } + + return stats.pruneDepth, nil +} + +// dbPutSpendJournalStats stores the spend journal stats into the database. +func dbPutSpendJournalStats(dbTx database.Tx, stats spendJournalStats) error { + return dbTx.Metadata().Put( + spendJournalStatsKeyName, stats.serialize(nil), + ) +} + +// dbInitSpendJournalStats updates the spend journal stats pruneDepth if it +// exists in the DB, otherwise, the spend journal will be scanned to initialize +// spend journal stats nextHeight with the height of the lowest spend journal +// block height and the passed pruneDepth. +func dbInitSpendJournalStats(dbTx database.Tx, + interrupt <-chan struct{}, pruneDepth int32) error { + + initialized := true + stats, err := dbFetchSpendJournalStats(dbTx) + if err != nil && errors.Is(err, errMissingSpendJournalStats) { + initialized = false + } else if err != nil { + return err + } + + // Update the stored prune depth if the node was started with a + // different --prunespendjournal flag. By storing this config in the + // database, --prunespendjournal can be passed only once, which improves + // the UX, as next initializations may omit the flag. + if initialized && pruneDepth != stats.pruneDepth { + stats.pruneDepth = pruneDepth + + return dbPutSpendJournalStats(dbTx, stats) + } + + // Find the lowest block height in the spend journal. + spendCursor := dbTx.Metadata().Bucket(spendJournalBucketName).Cursor() + var height int32 + { + ok := spendCursor.First() + if !ok { + return dbPutSpendJournalStats( + dbTx, spendJournalStats{ + pruneDepth: pruneDepth, + nextHeight: height, + }, + ) + } + hash := spendCursor.Key() + if len(hash) != 32 { + return fmt.Errorf("bad hash length: %d", len(hash)) + } + bHeight, err := dbFetchHeightByHash( + dbTx, (*chainhash.Hash)(hash), + ) + if err != nil { + return err + } + + height = bHeight + } + ok := spendCursor.Next() + for ; ok; ok = spendCursor.Next() { + select { + case <-interrupt: + return errInterruptRequested + default: + } + + hash := spendCursor.Key() + if len(hash) != 32 { + return fmt.Errorf("bad hash length: %d", len(hash)) + } + bHeight, err := dbFetchHeightByHash( + dbTx, (*chainhash.Hash)(hash), + ) + if err != nil { + return err + } + height = min(height, bHeight) + } + + return dbPutSpendJournalStats( + dbTx, spendJournalStats{ + pruneDepth: pruneDepth, nextHeight: height, + }, + ) +} + +// dbPruneSpendJournalDepthSlow prunes the spend journal depth to the passed +// spend journal prune depth. +// +// NOTE: this procedure uses several internal transactions to avoid reaching +// database transaction size limit. +func dbPruneSpendJournalDepthSlow(db database.DB, pruneDepth, + spendJournalHeight, bestHeight int32, interrupt <-chan struct{}) error { + + const maxBatch = 100000 + + totalEntries := bestHeight - spendJournalHeight + if totalEntries <= pruneDepth { + return nil + } + + toDelete := totalEntries - pruneDepth + hSize := min(toDelete, maxBatch) + hashes := make([]chainhash.Hash, 0, hSize) + + // loop while deleted entries is less than entries to delete. + var deleted int32 + for deleted < toDelete { + err := db.Update(func(tx database.Tx) error { + hashes = hashes[:0] + var i int32 + for i < maxBatch && deleted < toDelete { + select { + case <-interrupt: + return errInterruptRequested + default: + } + + hash, err := dbFetchHashByHeight( + tx, spendJournalHeight+i, + ) + if err != nil { + return err + } + hashes = append(hashes, *hash) + + i++ + deleted++ + } + + err := dbPruneSpendJournalEntry(tx, hashes) + if err != nil { + return err + } + + spendJournalHeight += int32(len(hashes)) + err = dbPutSpendJournalStats( + tx, spendJournalStats{ + nextHeight: spendJournalHeight, + }, + ) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + } + + return nil +} + +// dbPruneSpendJournalDepth prunes the spend journal depth to the passed spend +// journal prune depth. +func dbPruneSpendJournalDepth(dbTx database.Tx, pruneDepth, + bestHeight int32) error { + + stats, err := dbFetchSpendJournalStats(dbTx) + if err != nil { + return err + } + totalEntries := bestHeight - stats.nextHeight + if totalEntries <= pruneDepth { + return nil + } + + toDelete := totalEntries - pruneDepth + hashes := make([]chainhash.Hash, 0, toDelete) + for i := range toDelete { + hash, err := dbFetchHashByHeight(dbTx, stats.nextHeight+i) + if err != nil { + return err + } + hashes = append(hashes, *hash) + } + err = dbPruneSpendJournalEntry(dbTx, hashes) + if err != nil { + return err + } + + stats.nextHeight += toDelete + err = dbPutSpendJournalStats(dbTx, stats) + if err != nil { + return err + } + + return nil +} + // ----------------------------------------------------------------------------- // The unspent transaction output (utxo) set consists of an entry for each // unspent output using a format that is optimized to reduce space using domain diff --git a/btcd.go b/btcd.go index 1d9a1e5f6b..9ca915f8a1 100644 --- a/btcd.go +++ b/btcd.go @@ -16,6 +16,7 @@ import ( "runtime/pprof" "runtime/trace" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/blockchain/indexers" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/limits" @@ -37,6 +38,76 @@ var ( // as a service and reacts accordingly. var winServiceMain func() (bool, error) +// checkSpendJournalPruneConstraints verifies spend journal pruning contraints +// if the spend journal pruning is enabled or was enabled in the past. +func checkSpendJournalPruneConstraints(db database.DB) error { + // Spend journal depth pruning will only be enabled with disabled and + // non initialized cfindex, txindex and addrindex, as this simplifies + // the implementation because otherwise we would have to re-create the + // spend journal to rebuild dropped indexes. + var sjPrune bool + if d := cfg.PruneSpendJournal; d != 0 && d < spendJournalPruneMinDepth { + return fmt.Errorf("--prunespendjournal min depth is %d, "+ + "current is %d", spendJournalPruneMinDepth, d) + } + if cfg.PruneSpendJournal != 0 { + sjPrune = true + } + // If prunespendjournal is 0 but previously the spend journal has been + // pruned, load the prune depth from the database, enable the spend + // journal pruning and set the config spend journal prune depth. + if !sjPrune { + err := db.View(func(tx database.Tx) error { + d, err := blockchain.DBSpendJournalPruneDepth(tx) + if err != nil { + return err + } + sjPrune = d != 0 + cfg.PruneSpendJournal = d + + return nil + }) + if err != nil { + return err + } + } + if !sjPrune { + return nil + } + + // Disable cfindex when spend journal pruning is enabled, we do it here + // because otherwise the user would be required to pass --nocfilters + // every time. + cfg.NoCFilters = true + + if cfg.TxIndex { + return fmt.Errorf("spend journal pruning and txindex can " + + "not be enabled at the same time") + } + if cfg.AddrIndex { + return fmt.Errorf("spend journal pruning and addrindex can " + + "not be enabled at the same time") + } + + if indexers.CfIndexInitialized(db) { + return fmt.Errorf("spend journal pruning was enabled but " + + "cfindex is initialized, please drop cfindex with " + + "--dropcfindex") + } + if indexers.TxIndexInitialized(db) { + return fmt.Errorf("spend journal pruning was enabled but " + + "txindex is initialized, please drop txindex with " + + "--droptxindex") + } + if indexers.AddrIndexInitialized(db) { + return fmt.Errorf("spend journal pruning was enabled but " + + "addrindex is initialized, please drop addrindex " + + "with --dropaddrindex") + } + + return nil +} + // btcdMain is the real main function for btcd. It is necessary to work around // the fact that deferred functions do not run when os.Exit() is called. The // optional serverChan parameter is mainly used by the service code to be @@ -170,6 +241,12 @@ func btcdMain(serverChan chan<- *server) error { return nil } + err = checkSpendJournalPruneConstraints(db) + if err != nil { + btcdLog.Errorf("%v", err) + return err + } + // Check if the database had previously been pruned. If it had been, it's // not possible to newly generate the tx index and addr index. var beenPruned bool diff --git a/config.go b/config.go index 54febc8964..8e76fee578 100644 --- a/config.go +++ b/config.go @@ -70,6 +70,7 @@ const ( defaultTxIndex = false defaultAddrIndex = false pruneMinSize = 1536 + spendJournalPruneMinDepth = 288 ) var ( @@ -168,6 +169,7 @@ type config struct { ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` ProxyUser string `long:"proxyuser" description:"Username for proxy server"` Prune uint64 `long:"prune" description:"Prune already validated blocks from the database. Must specify a target size in MiB (minimum value of 1536, default value of 0 will disable pruning)"` + PruneSpendJournal int32 `long:"prunespendjournal" description:"When different than zero, prunespendjournal keeps only the last N blocks of spend journal entries, which also requires disabled and non initialized cfindex, txindex and addrindex. As cfindex is enabled by default, when this config option is used, cfindex is also automatically disabled. Spend journal pruning is useful to keep all the blocks but at the same time significantly reduce storage usage. The minimum value is 288, as a chain reorg of 288 blocks is unlikely to happen."` RegressionTest bool `long:"regtest" description:"Use the regression test network"` RejectNonStd bool `long:"rejectnonstd" description:"Reject non-standard transactions regardless of the default settings for the active network."` RejectReplacement bool `long:"rejectreplacement" description:"Reject transactions that attempt to replace existing transactions within the mempool through the Replace-By-Fee (RBF) signaling policy."` diff --git a/server.go b/server.go index 8ab2fc8ea6..82b9d373ef 100644 --- a/server.go +++ b/server.go @@ -3080,6 +3080,12 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, btcdLog.Infof("Prune set to %d MiB", cfg.Prune) } + // Log that spend journal pruning is enabled. + if cfg.PruneSpendJournal != 0 { + btcdLog.Infof("Spend journal pruning enabled with depth of %d", + cfg.PruneSpendJournal) + } + // Create a new block chain instance with the appropriate configuration. var err error s.chain, err = blockchain.New(&blockchain.Config{ @@ -3092,6 +3098,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, IndexManager: indexManager, HashCache: s.hashCache, Prune: cfg.Prune * 1024 * 1024, + PruneSpendJ: cfg.PruneSpendJournal, UtxoCacheMaxSize: uint64(cfg.UtxoCacheMaxSizeMiB) * 1024 * 1024, }) if err != nil {