// Copyright (c) 2017 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package indexers import ( "errors" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/gcs/builder" "github.com/btcsuite/fastsha256" ) const ( // cfIndexName is the human-readable name for the index. cfIndexName = "committed filter index" ) // Committed filters come in two flavours: basic and extended. They are // generated and dropped in pairs, and both are indexed by a block's hash. // Besides holding different content, they also live in different buckets. var ( // cfBasicIndexKey is the name of the db bucket used to house the // block hash -> basic cf index (cf#0). cfBasicIndexKey = []byte("cf0byhashidx") // cfBasicHeaderKey is the name of the db bucket used to house the // block hash -> basic cf header index (cf#0). cfBasicHeaderKey = []byte("cf0headerbyhashidx") // cfExtendedIndexKey is the name of the db bucket used to house the // block hash -> extended cf index (cf#1). cfExtendedIndexKey = []byte("cf1byhashidx") // cfExtendedHeaderKey is the name of the db bucket used to house the // block hash -> extended cf header index (cf#1). cfExtendedHeaderKey = []byte("cf1headerbyhashidx") ) // dbFetchFilter retrieves a block's basic or extended filter. A filter's // absence is not considered an error. func dbFetchFilter(dbTx database.Tx, key []byte, h *chainhash.Hash) ([]byte, error) { idx := dbTx.Metadata().Bucket(key) return idx.Get(h[:]), nil } // dbFetchFilterHeader retrieves a block's basic or extended filter header. // A filter's absence is not considered an error. func dbFetchFilterHeader(dbTx database.Tx, key []byte, h *chainhash.Hash) ([]byte, error) { idx := dbTx.Metadata().Bucket(key) fh := idx.Get(h[:]) if len(fh) != fastsha256.Size { return nil, errors.New("invalid filter header length") } return fh, nil } // dbStoreFilter stores a block's basic or extended filter. func dbStoreFilter(dbTx database.Tx, key []byte, h *chainhash.Hash, f []byte) error { idx := dbTx.Metadata().Bucket(key) return idx.Put(h[:], f) } // dbStoreFilterHeader stores a block's basic or extended filter header. func dbStoreFilterHeader(dbTx database.Tx, key []byte, h *chainhash.Hash, fh []byte) error { if len(fh) != fastsha256.Size { return errors.New("invalid filter header length") } idx := dbTx.Metadata().Bucket(key) return idx.Put(h[:], fh) } // dbDeleteFilter deletes a filter's basic or extended filter. func dbDeleteFilter(dbTx database.Tx, key []byte, h *chainhash.Hash) error { idx := dbTx.Metadata().Bucket(key) return idx.Delete(h[:]) } // dbDeleteFilterHeader deletes a filter's basic or extended filter header. func dbDeleteFilterHeader(dbTx database.Tx, key []byte, h *chainhash.Hash) error { idx := dbTx.Metadata().Bucket(key) return idx.Delete(h[:]) } // CfIndex implements a committed filter (cf) by hash index. type CfIndex struct { db database.DB chainParams *chaincfg.Params } // Ensure the CfIndex type implements the Indexer interface. var _ Indexer = (*CfIndex)(nil) // Init initializes the hash-based cf index. This is part of the Indexer // interface. func (idx *CfIndex) Init() error { return nil // Nothing to do. } // Key returns the database key to use for the index as a byte slice. This is // part of the Indexer interface. func (idx *CfIndex) Key() []byte { return cfBasicIndexKey } // Name returns the human-readable name of the index. This is part of the // Indexer interface. func (idx *CfIndex) Name() string { return cfIndexName } // Create is invoked when the indexer manager determines the index needs to // be created for the first time. It creates buckets for the two hash-based cf // indexes (simple, extended). func (idx *CfIndex) Create(dbTx database.Tx) error { meta := dbTx.Metadata() _, err := meta.CreateBucket(cfBasicIndexKey) if err != nil { return err } _, err = meta.CreateBucket(cfBasicHeaderKey) if err != nil { return err } _, err = meta.CreateBucket(cfExtendedIndexKey) if err != nil { return err } _, err = meta.CreateBucket(cfExtendedHeaderKey) if err != nil { return err } firstHeader := make([]byte, chainhash.HashSize) err = dbStoreFilterHeader( dbTx, cfBasicHeaderKey, &idx.chainParams.GenesisBlock.Header.PrevBlock, firstHeader, ) if err != nil { return err } err = dbStoreFilterHeader( dbTx, cfExtendedHeaderKey, &idx.chainParams.GenesisBlock.Header.PrevBlock, firstHeader, ) return err } // makeBasicFilterForBlock builds a block's basic filter, which consists of // all outpoints and pkscript data pushes referenced by transactions within the // block. func makeBasicFilterForBlock(block *btcutil.Block) ([]byte, error) { b := builder.WithKeyHash(block.Hash()) _, err := b.Key() if err != nil { return nil, err } for _, tx := range block.Transactions() { for _, txIn := range tx.MsgTx().TxIn { b.AddOutPoint(txIn.PreviousOutPoint) } for _, txOut := range tx.MsgTx().TxOut { b.AddScript(txOut.PkScript) } } f, err := b.Build() if err != nil { return nil, err } return f.Bytes(), nil } // makeExtendedFilterForBlock builds a block's extended filter, which consists // of all tx hashes and sigscript data pushes contained in the block. func makeExtendedFilterForBlock(block *btcutil.Block) ([]byte, error) { b := builder.WithKeyHash(block.Hash()) _, err := b.Key() if err != nil { return nil, err } for _, tx := range block.Transactions() { b.AddHash(tx.Hash()) for _, txIn := range tx.MsgTx().TxIn { b.AddScript(txIn.SignatureScript) } } f, err := b.Build() if err != nil { return nil, err } return f.Bytes(), nil } // makeHeaderForFilter implements the chaining logic between filters, where // a filter's header is defined as sha256(sha256(filter) + previousFilterHeader). func makeHeaderForFilter(f, pfh []byte) []byte { fhash := fastsha256.Sum256(f) chain := make([]byte, 0, 2*fastsha256.Size) chain = append(chain, fhash[:]...) chain = append(chain, pfh...) fh := fastsha256.Sum256(chain) return fh[:] } // storeFilter stores a given filter, and performs the steps needed to // generate the filter's header. func storeFilter(dbTx database.Tx, block *btcutil.Block, f []byte, extended bool) error { // Figure out which buckets to use. fkey := cfBasicIndexKey hkey := cfBasicHeaderKey if extended { fkey = cfExtendedIndexKey hkey = cfExtendedHeaderKey } // Start by storing the filter. h := block.Hash() err := dbStoreFilter(dbTx, fkey, h, f) if err != nil { return err } // Then fetch the previous block's filter header. ph := &block.MsgBlock().Header.PrevBlock pfh, err := dbFetchFilterHeader(dbTx, hkey, ph) if err != nil { return err } // Construct the new block's filter header, and store it. fh := makeHeaderForFilter(f, pfh) return dbStoreFilterHeader(dbTx, hkey, h, fh) } // ConnectBlock is invoked by the index manager when a new block has been // connected to the main chain. This indexer adds a hash-to-cf mapping for // every passed block. This is part of the Indexer interface. func (idx *CfIndex) ConnectBlock(dbTx database.Tx, block *btcutil.Block, view *blockchain.UtxoViewpoint) error { f, err := makeBasicFilterForBlock(block) if err != nil { return err } err = storeFilter(dbTx, block, f, false) if err != nil { return err } f, err = makeExtendedFilterForBlock(block) if err != nil { return err } return storeFilter(dbTx, block, f, true) } // DisconnectBlock is invoked by the index manager when a block has been // disconnected from the main chain. This indexer removes the hash-to-cf // mapping for every passed block. This is part of the Indexer interface. func (idx *CfIndex) DisconnectBlock(dbTx database.Tx, block *btcutil.Block, view *blockchain.UtxoViewpoint) error { err := dbDeleteFilter(dbTx, cfBasicIndexKey, block.Hash()) if err != nil { return err } return dbDeleteFilter(dbTx, cfExtendedIndexKey, block.Hash()) } // FilterByBlockHash returns the serialized contents of a block's basic or // extended committed filter. func (idx *CfIndex) FilterByBlockHash(h *chainhash.Hash, extended bool) ([]byte, error) { var f []byte err := idx.db.View(func(dbTx database.Tx) error { var err error key := cfBasicIndexKey if extended { key = cfExtendedIndexKey } f, err = dbFetchFilter(dbTx, key, h) return err }) return f, err } // FilterHeaderByBlockHash returns the serialized contents of a block's basic // or extended committed filter header. func (idx *CfIndex) FilterHeaderByBlockHash(h *chainhash.Hash, extended bool) ([]byte, error) { var fh []byte err := idx.db.View(func(dbTx database.Tx) error { var err error key := cfBasicHeaderKey if extended { key = cfExtendedHeaderKey } fh, err = dbFetchFilterHeader(dbTx, key, h) return err }) return fh, err } // NewCfIndex returns a new instance of an indexer that is used to create a // mapping of the hashes of all blocks in the blockchain to their respective // committed filters. // // It implements the Indexer interface which plugs into the IndexManager that in // turn is used by the blockchain package. This allows the index to be // seamlessly maintained along with the chain. func NewCfIndex(db database.DB, chainParams *chaincfg.Params) *CfIndex { return &CfIndex{db: db, chainParams: chainParams} } // DropCfIndex drops the CF index from the provided database if exists. func DropCfIndex(db database.DB) error { err := dropIndex(db, cfBasicIndexKey, cfIndexName) if err != nil { return err } err = dropIndex(db, cfBasicHeaderKey, cfIndexName) if err != nil { return err } err = dropIndex(db, cfExtendedIndexKey, cfIndexName) if err != nil { return err } err = dropIndex(db, cfExtendedHeaderKey, cfIndexName) return err }