diff --git a/blockchain/chain.go b/blockchain/chain.go index fd11045837..cb7ba3f339 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -7,7 +7,10 @@ package blockchain import ( "container/list" + "crypto/sha256" + "encoding/binary" "fmt" + "io" "sync" "time" @@ -94,14 +97,16 @@ type BlockChain struct { // The following fields are set when the instance is created and can't // be changed afterwards, so there is no need to protect them with a // separate mutex. - checkpoints []chaincfg.Checkpoint - checkpointsByHeight map[int32]*chaincfg.Checkpoint - db database.DB - chainParams *chaincfg.Params - timeSource MedianTimeSource - sigCache *txscript.SigCache - indexManager IndexManager - hashCache *txscript.HashCache + checkpoints []chaincfg.Checkpoint + checkpointsByHeight map[int32]*chaincfg.Checkpoint + assumeUTXOCheckpoints []chaincfg.AssumeUTXOCheckpoint + assumeUTXOCheckpointsByHeight map[int32]*chaincfg.AssumeUTXOCheckpoint + db database.DB + chainParams *chaincfg.Params + timeSource MedianTimeSource + sigCache *txscript.SigCache + indexManager IndexManager + hashCache *txscript.HashCache // The following fields are calculated based upon the provided chain // parameters. They are also set when the instance is created and @@ -1215,7 +1220,8 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla // Connect the transactions to the cache. All the txs are considered valid // at this point as they have passed validation or was considered valid already. stxos := make([]SpentTxOut, 0, countSpentOutputs(block)) - err := b.utxoCache.connectTransactions(block, &stxos) + err := b.utxoCache.connectTransactions2(block, &stxos, + cacheOpts{bflags: flags}) if err != nil { return false, err } @@ -2175,6 +2181,14 @@ type Config struct { // checkpoints. Checkpoints []chaincfg.Checkpoint + // AssumeUTXOCheckpoints hold caller-defined assumeutxo checkpoints that + // should be added to the default assumeutxo checkpoints in ChainParams. + // Checkpoints must be sorted by height. + // + // This field can be nil if the caller does not wish to specify any + // checkpoints. + AssumeUTXOCheckpoints []chaincfg.AssumeUTXOCheckpoint + // TimeSource defines the median time source to use for things such as // block processing and determining whether or not the chain is current. // @@ -2246,31 +2260,54 @@ func New(config *Config) (*BlockChain, error) { } } + // Generate a assumeutxo checkpoint by height map from the provided + // checkpoints and assert the provided checkpoints are sorted by height + // as required. + var auCheckpointsByHeight map[int32]*chaincfg.AssumeUTXOCheckpoint + var auPrevCheckpointHeight int32 + if len(config.AssumeUTXOCheckpoints) > 0 { + auCheckpointsByHeight = make( + map[int32]*chaincfg.AssumeUTXOCheckpoint) + + for i := range config.AssumeUTXOCheckpoints { + checkpoint := &config.AssumeUTXOCheckpoints[i] + if checkpoint.Height <= auPrevCheckpointHeight { + return nil, AssertError("blockchain.New " + + "checkpoints are not sorted by height") + } + + auCheckpointsByHeight[checkpoint.Height] = checkpoint + auPrevCheckpointHeight = checkpoint.Height + } + } + params := config.ChainParams targetTimespan := int64(params.TargetTimespan / time.Second) targetTimePerBlock := int64(params.TargetTimePerBlock / time.Second) adjustmentFactor := params.RetargetAdjustmentFactor b := BlockChain{ - checkpoints: config.Checkpoints, - checkpointsByHeight: checkpointsByHeight, - db: config.DB, - chainParams: params, - timeSource: config.TimeSource, - sigCache: config.SigCache, - indexManager: config.IndexManager, - minRetargetTimespan: targetTimespan / adjustmentFactor, - maxRetargetTimespan: targetTimespan * adjustmentFactor, - blocksPerRetarget: int32(targetTimespan / targetTimePerBlock), - index: newBlockIndex(config.DB, params), - utxoCache: newUtxoCache(config.DB, config.UtxoCacheMaxSize), - hashCache: config.HashCache, - bestChain: newChainView(nil), - bestHeader: newChainView(nil), - orphans: make(map[chainhash.Hash]*orphanBlock), - prevOrphans: make(map[chainhash.Hash][]*orphanBlock), - warningCaches: newThresholdCaches(vbNumBits), - deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments), - pruneTarget: config.Prune, + checkpoints: config.Checkpoints, + checkpointsByHeight: checkpointsByHeight, + assumeUTXOCheckpoints: config.AssumeUTXOCheckpoints, + assumeUTXOCheckpointsByHeight: auCheckpointsByHeight, + db: config.DB, + chainParams: params, + timeSource: config.TimeSource, + sigCache: config.SigCache, + indexManager: config.IndexManager, + minRetargetTimespan: targetTimespan / adjustmentFactor, + maxRetargetTimespan: targetTimespan * adjustmentFactor, + blocksPerRetarget: int32(targetTimespan / targetTimePerBlock), + index: newBlockIndex(config.DB, params), + utxoCache: newUtxoCache(config.DB, config.UtxoCacheMaxSize), + hashCache: config.HashCache, + bestChain: newChainView(nil), + bestHeader: newChainView(nil), + orphans: make(map[chainhash.Hash]*orphanBlock), + prevOrphans: make(map[chainhash.Hash][]*orphanBlock), + warningCaches: newThresholdCaches(vbNumBits), + deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments), + pruneTarget: config.Prune, } // Ensure all the deployments are synchronized with our clock if @@ -2333,3 +2370,125 @@ func (b *BlockChain) CachedStateSize() uint64 { defer b.chainLock.Unlock() return b.utxoCache.totalMemoryUsage() } + +// CheckUTXOSetIntegrity verifies that the current UTXO state matches +// expectedHash. +func (b *BlockChain) CheckUTXOSetIntegrity(expectedHash *[32]byte) error { + // Flush the cache to be sure all utxos are on disk + err := b.FlushUtxoCache(FlushRequired) + if err != nil { + return err + } + + writeCompactSize := func(w io.Writer, v uint64) error { + var b [8]byte + return wire.WriteVarIntBuf(w, 0, v, b[:]) + } + + writeUint32 := func(w io.Writer, v uint32) error { + var b [4]byte + binary.LittleEndian.PutUint32(b[:], v) + _, err := w.Write(b[:]) + return err + } + + writeUint64 := func(w io.Writer, v uint64) error { + var b [8]byte + binary.LittleEndian.PutUint64(b[:], v) + _, err := w.Write(b[:]) + return err + } + + writeTxOut := func(w io.Writer, outpoint *wire.OutPoint, + utxo *UtxoEntry) error { + + // txid + _, err := w.Write(outpoint.Hash[:]) + if err != nil { + return err + } + + // vout index + err = writeUint32(w, outpoint.Index) + if err != nil { + return err + } + + // height and coinbase flag + err = writeUint32( + w, + uint32(utxo.blockHeight<<1)| + uint32(utxo.packedFlags&tfCoinBase), + ) + if err != nil { + return err + } + + // amount + err = writeUint64(w, uint64(utxo.amount)) + if err != nil { + return err + } + + // scriptpub size + err = writeCompactSize(w, uint64(len(utxo.pkScript))) + if err != nil { + return err + } + + // scriptpub + _, err = w.Write(utxo.pkScript) + if err != nil { + return err + } + return nil + } + + processUtxo := func(w io.Writer, outpoint *wire.OutPoint, + utxo *UtxoEntry) error { + + err := writeTxOut(w, outpoint, utxo) + if err != nil { + return err + } + return nil + } + + hasher := sha256.New() + + utxoIterator := func(k, v []byte) error { + outpoint := deserializeOutpoint(k) + utxo, err := deserializeUtxoEntry(v) + if err != nil { + return err + } + + return processUtxo(hasher, outpoint, utxo) + } + + err = b.db.View(func(tx database.Tx) error { + return tx. + Metadata(). + Bucket(utxoSetBucketName). + ForEach(utxoIterator) + }) + if err != nil { + return fmt.Errorf("error while computing UTXOSet hash: %w", err) + } + + var hash [32]byte + hasher.Sum(hash[:0]) + hash = sha256.Sum256(hash[:]) + + if hash != *expectedHash { + return fmt.Errorf( + "error while computing UTXOSet hash"+ + " expected: %x"+ + " current: %x", + *expectedHash, + hash, + ) + } + + return nil +} diff --git a/blockchain/chainio.go b/blockchain/chainio.go index 5d2c033c88..5e9588bf32 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -622,6 +622,14 @@ func outpointKey(outpoint wire.OutPoint) *[]byte { return key } +func deserializeOutpoint(data []byte) *wire.OutPoint { + r := &wire.OutPoint{} + copy(r.Hash[:], data[:32]) + v, _ := deserializeVLQ(data[32:]) + r.Index = uint32(v) + return r +} + // recycleOutpointKey puts the provided byte slice, which should have been // obtained via the outpointKey function, back on the free list. func recycleOutpointKey(key *[]byte) { diff --git a/blockchain/checkpoints.go b/blockchain/checkpoints.go index 2ad1784111..f8083d95a0 100644 --- a/blockchain/checkpoints.go +++ b/blockchain/checkpoints.go @@ -36,6 +36,15 @@ func (b *BlockChain) Checkpoints() []chaincfg.Checkpoint { return b.checkpoints } +// AssumeUTXOCheckpoints returns a slice of assumeutxo checkpoints (regardless +// of whether they are already known). When there are no checkpoints for the +// chain, it will return nil. +// +// This function is safe for concurrent access. +func (b *BlockChain) AssumeUTXOCheckpoints() []chaincfg.AssumeUTXOCheckpoint { + return b.assumeUTXOCheckpoints +} + // HasCheckpoints returns whether this BlockChain has checkpoints defined. // // This function is safe for concurrent access. diff --git a/blockchain/process.go b/blockchain/process.go index 87f4deb4d2..d42dc366b3 100644 --- a/blockchain/process.go +++ b/blockchain/process.go @@ -30,6 +30,12 @@ const ( // not be performed. BFNoPoWCheck + // BFAssumeUTXO may be set to indicate that several checks can be + // avoided for the block since it is already known to fit into the chain + // due to already proving it correct links into the chain up to a known + // AssumeUTXO checkpoint. + BFAssumeUTXO + // BFNone is a convenience value to specifically indicate no flags. BFNone BehaviorFlags = 0 ) diff --git a/blockchain/utxocache.go b/blockchain/utxocache.go index a423ad43fd..8f3271a3d3 100644 --- a/blockchain/utxocache.go +++ b/blockchain/utxocache.go @@ -206,6 +206,12 @@ const ( FlushIfNeeded ) +var dummyUtxoEntry = &UtxoEntry{} + +type cacheOpts struct { + bflags BehaviorFlags +} + // utxoCache is a cached utxo view in the chainstate of a BlockChain. type utxoCache struct { db database.DB @@ -266,6 +272,12 @@ func (s *utxoCache) totalMemoryUsage() uint64 { // // The returned entries are NOT safe for concurrent access. func (s *utxoCache) fetchEntries(outpoints []wire.OutPoint) ([]*UtxoEntry, error) { + return s.fetchEntries2(outpoints, cacheOpts{}) +} + +func (s *utxoCache) fetchEntries2(outpoints []wire.OutPoint, + opts cacheOpts) ([]*UtxoEntry, error) { + entries := make([]*UtxoEntry, len(outpoints)) var ( missingOps []wire.OutPoint @@ -288,19 +300,43 @@ func (s *utxoCache) fetchEntries(outpoints []wire.OutPoint) ([]*UtxoEntry, error missingOps = append(missingOps, outpoints[i]) } - // Return early and don't attempt access the database if we don't have any - // missing outpoints. + // Return early and don't attempt access the database if we don't have + // any missing outpoints. if len(missingOps) == 0 { return entries, nil } + // To reduce I/O on cache misses when operating under assumeUTXO, we + // just add a dummy entry to the cache, assuming that we trust the + // checkpoint, we can also safely skip the prevout check. + // + // Also, when we reach an assumeUTXO checkpoint block, the utxo state + // will be hashed and must match the expected hash which is hardcoded. + if opts.bflags&BFAssumeUTXO == BFAssumeUTXO { + // Add each of the entries to the UTXO cache and update their + // memory usage. + for i := range missingOps { + s.cachedEntries.put(missingOps[i], dummyUtxoEntry, + s.totalEntryMemory) + s.totalEntryMemory += dummyUtxoEntry.memoryUsage() + } + + // Fill in the entries with the dummy entry. + for i := range missingOpsIdx { + entries[missingOpsIdx[i]] = dummyUtxoEntry + } + + return entries, nil + } + // Fetch the missing outpoints in the cache from the database. dbEntries := make([]*UtxoEntry, len(missingOps)) err := s.db.View(func(dbTx database.Tx) error { utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName) for i := range missingOps { - entry, err := dbFetchUtxoEntry(dbTx, utxoBucket, missingOps[i]) + entry, err := dbFetchUtxoEntry(dbTx, utxoBucket, + missingOps[i]) if err != nil { return err } @@ -399,9 +435,16 @@ func (s *utxoCache) addTxOuts(tx *btcutil.Tx, blockHeight int32) error { // will be marked as spent and if the utxo is fresh (meaning that the database on disk // never saw it), it will be removed from the cache. func (s *utxoCache) addTxIn(txIn *wire.TxIn, stxos *[]SpentTxOut) error { + return s.addTxIn2(txIn, stxos, cacheOpts{}) +} + +func (s *utxoCache) addTxIn2(txIn *wire.TxIn, stxos *[]SpentTxOut, + opts cacheOpts) error { + // Ensure the referenced utxo exists in the view. This should // never happen unless there is a bug is introduced in the code. - entries, err := s.fetchEntries([]wire.OutPoint{txIn.PreviousOutPoint}) + entries, err := s.fetchEntries2([]wire.OutPoint{txIn.PreviousOutPoint}, + opts) if err != nil { return err } @@ -449,14 +492,16 @@ func (s *utxoCache) addTxIn(txIn *wire.TxIn, stxos *[]SpentTxOut) error { // utxo that is being spent by the input will be marked as spent and if the utxo // is fresh (meaning that the database on disk never saw it), it will be removed // from the cache. -func (s *utxoCache) addTxIns(tx *btcutil.Tx, stxos *[]SpentTxOut) error { +func (s *utxoCache) addTxIns(tx *btcutil.Tx, stxos *[]SpentTxOut, + opts cacheOpts) error { + // Coinbase transactions don't have any inputs to spend. if IsCoinBase(tx) { return nil } for _, txIn := range tx.MsgTx().TxIn { - err := s.addTxIn(txIn, stxos) + err := s.addTxIn2(txIn, stxos, opts) if err != nil { return err } @@ -471,9 +516,10 @@ func (s *utxoCache) addTxIns(tx *btcutil.Tx, stxos *[]SpentTxOut) error { // be updated to append an entry for each spent txout. An error will be returned // if the cache and the database does not contain the required utxos. func (s *utxoCache) connectTransaction( - tx *btcutil.Tx, blockHeight int32, stxos *[]SpentTxOut) error { + tx *btcutil.Tx, blockHeight int32, stxos *[]SpentTxOut, + opts cacheOpts) error { - err := s.addTxIns(tx, stxos) + err := s.addTxIns(tx, stxos, opts) if err != nil { return err } @@ -488,8 +534,14 @@ func (s *utxoCache) connectTransaction( // the passed block. In addition, when the 'stxos' argument is not nil, it will // be updated to append an entry for each spent txout. func (s *utxoCache) connectTransactions(block *btcutil.Block, stxos *[]SpentTxOut) error { + return s.connectTransactions2(block, stxos, cacheOpts{}) +} + +func (s *utxoCache) connectTransactions2(block *btcutil.Block, stxos *[]SpentTxOut, + opts cacheOpts) error { + for _, tx := range block.Transactions() { - err := s.connectTransaction(tx, block.Height(), stxos) + err := s.connectTransaction(tx, block.Height(), stxos, opts) if err != nil { return err } diff --git a/chaincfg/params.go b/chaincfg/params.go index 14056d7137..17d806a460 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -79,6 +79,15 @@ type Checkpoint struct { Hash *chainhash.Hash } +// AssumeUTXOCheckpoint identifies a known hash of the UTXOSet. This enables +// faster IBD by avoiding several validations. The hash is reproducible at the +// height and is compatible with the Bitcoin Core assumeutxo hashes. +type AssumeUTXOCheckpoint struct { + Height int32 + UTXOSetHash [32]byte + BlockHash [32]byte +} + // EffectiveAlwaysActiveHeight returns the effective activation height for the // deployment. If AlwaysActiveHeight is unset (i.e. zero), it returns // the maximum uint32 value to indicate that it does not force activation. @@ -261,6 +270,9 @@ type Params struct { // Checkpoints ordered from oldest to newest. Checkpoints []Checkpoint + // AssumeUTXOCheckpoints from oldest to newest + AssumeUTXOCheckpoints []AssumeUTXOCheckpoint + // These fields are related to voting on consensus rule changes as // defined by BIP0009. // @@ -372,6 +384,16 @@ var MainNetParams = Params{ {781565, newHashFromStr("00000000000000000002b8c04999434c33b8e033f11a977b288f8411766ee61c")}, {800000, newHashFromStr("00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054")}, {810000, newHashFromStr("000000000000000000028028ca82b6aa81ce789e4eb9e0321b74c3cbaf405dd1")}, + {938343, newHashFromStr("00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac")}, + }, + + // AssumeUTXOCheckpoints from oldest to newest + AssumeUTXOCheckpoints: []AssumeUTXOCheckpoint{ + { + Height: 935000, + UTXOSetHash: *newHashFromStr("e4b90ef9eae834f56c4b64d2d50143cee10ad87994c614d7d04125e2a6025050"), + BlockHash: *newHashFromStr("0000000000000000000147034958af1652b2b91bba607beacc5e72a56f0fb5ee"), + }, }, // Consensus rule change deployments. @@ -1028,7 +1050,18 @@ func CustomSignetParams(challenge []byte, dnsSeeds []DNSSeed) Params { GenerateSupported: false, // Checkpoints ordered from oldest to newest. - Checkpoints: nil, + Checkpoints: []Checkpoint{ + {293175, newHashFromStr("00000008414aab61092ef93f1aacc54cf9e9f16af29ddad493b908a01ff5c329")}, + }, + + // AssumeUTXOCheckpoints from oldest to newest + AssumeUTXOCheckpoints: []AssumeUTXOCheckpoint{ + { + Height: 290000, + UTXOSetHash: *newHashFromStr("97267e000b4b876800167e71b9123f1529d13b14308abec2888bbd2160d14545"), + BlockHash: *newHashFromStr("0000000577f2741bb30cd9d39d6d71b023afbeb9764f6260786a97969d5c9ac0"), + }, + }, // Consensus rule change deployments. // diff --git a/netsync/manager.go b/netsync/manager.go index cf9c898b37..b28a5a87fe 100644 --- a/netsync/manager.go +++ b/netsync/manager.go @@ -5,6 +5,7 @@ package netsync import ( + "fmt" "math/rand" "sync" "sync/atomic" @@ -231,6 +232,34 @@ func (sm *SyncManager) findNextHeaderCheckpoint(height int32) *chaincfg.Checkpoi return nextCheckpoint } +// findNextHeaderAssumeUTXOCheckpoint returns the next assumeutxo checkpoint +// after the passed height. It returns nil when there is not one. +func (sm *SyncManager) findNextHeaderAssumeUTXOCheckpoint( + height int32) *chaincfg.AssumeUTXOCheckpoint { + + checkpoints := sm.chain.AssumeUTXOCheckpoints() + if len(checkpoints) == 0 { + return nil + } + + // There is no next checkpoint if the height is already after the final + // checkpoint. + finalCheckpoint := &checkpoints[len(checkpoints)-1] + if height >= finalCheckpoint.Height { + return nil + } + + // Find the next checkpoint. + nextCheckpoint := finalCheckpoint + for i := len(checkpoints) - 2; i >= 0; i-- { + if height >= checkpoints[i].Height { + break + } + nextCheckpoint = &checkpoints[i] + } + return nextCheckpoint +} + // fetchHigherPeers returns all the peers that are at a higher block than the // given height. The peers that are not sync candidates are omitted from the // returned list. @@ -690,6 +719,41 @@ func (sm *SyncManager) checkHeadersList(blockHash *chainhash.Hash) ( return isCheckpointBlock, behaviorFlags } +// checkAssumeUTXO checks if the sync manager is in the initial block download +// mode and returns if the given block hash is a assumeUTXO block and the +// behavior flags for this block. If the block is still under the checkpoint, +// then it's given the assumeUTXO flag. +func (sm *SyncManager) checkAssumeUTXO(utxoSetHashOut *[32]byte, + blockHash *chainhash.Hash, flags blockchain.BehaviorFlags) (bool, + blockchain.BehaviorFlags) { + + if flags&blockchain.BFFastAdd == 0 { + return false, flags + } + + isAssumeUTXOBlock := false + + height, err := sm.chain.HeaderHeightByHash(*blockHash) + if err != nil { + return false, flags + } + + // Since findNextHeaderCheckpoint returns the next checkpoint after the + // passed height, we do a -1 to include the current block. + checkpoint := sm.findNextHeaderAssumeUTXOCheckpoint(height - 1) + if checkpoint == nil { + return false, flags + } + + flags |= blockchain.BFAssumeUTXO + if *blockHash == checkpoint.BlockHash { + isAssumeUTXOBlock = true + copy(utxoSetHashOut[:], checkpoint.UTXOSetHash[:]) + } + + return isAssumeUTXOBlock, flags +} + // handleBlockMsg handles block messages from all peers. func (sm *SyncManager) handleBlockMsg(bmsg *blockMsg) { peer := bmsg.peer @@ -720,6 +784,12 @@ func (sm *SyncManager) handleBlockMsg(bmsg *blockMsg) { // next checkpoint. isCheckpointBlock, behaviorFlags := sm.checkHeadersList(blockHash) + // Check if the block is eligible to assumeUTXO, which significantly + // improves IBD speed by reducing the I/O during block validation + var utxoSetHash [32]byte + isAssumeUTXOBlock, behaviorFlags := sm.checkAssumeUTXO(&utxoSetHash, + blockHash, behaviorFlags) + // Remove block from request maps. Either chain will know about it and // so we shouldn't have any more instances of trying to fetch it, or we // will fail the insert and thus we'll retry next time we get an inv. @@ -836,6 +906,24 @@ func (sm *SyncManager) handleBlockMsg(bmsg *blockMsg) { return } + // If we're on an assumeUTXO block, perform the verification of the + // utxoset + if isAssumeUTXOBlock { + log.Infof("Reached assumeUTXO block, performing utxoset validation") + start := time.Now() + err := sm.chain.CheckUTXOSetIntegrity(&utxoSetHash) + if err != nil { + // we are panicking here because we can't proceed with + // an invalid UTXOSet + err2 := fmt.Errorf( + "error checking UTXOSet integrity: %w", err) + log.Error(err2) + panic(err2) + } + log.Infof("utxoset validation completed in %fs", + time.Since(start).Seconds()) + } + // If we're on a checkpointed block, check if we still have checkpoints // to let the user know if we're switching to normal mode. if isCheckpointBlock { diff --git a/server.go b/server.go index b94abdca66..bc207e4c28 100644 --- a/server.go +++ b/server.go @@ -2981,16 +2981,17 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, // Create a new block chain instance with the appropriate configuration. var err error s.chain, err = blockchain.New(&blockchain.Config{ - DB: s.db, - Interrupt: interrupt, - ChainParams: s.chainParams, - Checkpoints: checkpoints, - TimeSource: s.timeSource, - SigCache: s.sigCache, - IndexManager: indexManager, - HashCache: s.hashCache, - Prune: cfg.Prune * 1024 * 1024, - UtxoCacheMaxSize: uint64(cfg.UtxoCacheMaxSizeMiB) * 1024 * 1024, + DB: s.db, + Interrupt: interrupt, + ChainParams: s.chainParams, + Checkpoints: checkpoints, + AssumeUTXOCheckpoints: s.chainParams.AssumeUTXOCheckpoints, + TimeSource: s.timeSource, + SigCache: s.sigCache, + IndexManager: indexManager, + HashCache: s.hashCache, + Prune: cfg.Prune * 1024 * 1024, + UtxoCacheMaxSize: uint64(cfg.UtxoCacheMaxSizeMiB) * 1024 * 1024, }) if err != nil { return nil, err