package spvchain import ( "bytes" "encoding/binary" "fmt" "time" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil/gcs" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" ) const ( // LatestDBVersion is the most recent database version. LatestDBVersion = 1 ) var ( // latestDBVersion is the most recent database version as a variable so // the tests can change it to force errors. latestDBVersion uint32 = LatestDBVersion ) // Key names for various database fields. var ( // Bucket names. spvBucketName = []byte("spv") blockHeaderBucketName = []byte("bh") basicHeaderBucketName = []byte("bfh") basicFilterBucketName = []byte("bf") extHeaderBucketName = []byte("efh") extFilterBucketName = []byte("ef") // Db related key names (main bucket). dbVersionName = []byte("dbver") dbCreateDateName = []byte("dbcreated") maxBlockHeightName = []byte("maxblockheight") ) // uint32ToBytes converts a 32 bit unsigned integer into a 4-byte slice in // little-endian order: 1 -> [1 0 0 0]. func uint32ToBytes(number uint32) []byte { buf := make([]byte, 4) binary.LittleEndian.PutUint32(buf, number) return buf } // uint64ToBytes converts a 64 bit unsigned integer into a 8-byte slice in // little-endian order: 1 -> [1 0 0 0 0 0 0 0]. func uint64ToBytes(number uint64) []byte { buf := make([]byte, 8) binary.LittleEndian.PutUint64(buf, number) return buf } // fetchDBVersion fetches the current manager version from the database. func fetchDBVersion(tx walletdb.Tx) (uint32, error) { bucket := tx.RootBucket().Bucket(spvBucketName) verBytes := bucket.Get(dbVersionName) if verBytes == nil { return 0, fmt.Errorf("required version number not stored in " + "database") } version := binary.LittleEndian.Uint32(verBytes) return version, nil } // putDBVersion stores the provided version to the database. func putDBVersion(tx walletdb.Tx, version uint32) error { bucket := tx.RootBucket().Bucket(spvBucketName) verBytes := uint32ToBytes(version) return bucket.Put(dbVersionName, verBytes) } // putMaxBlockHeight stores the max block height to the database. func putMaxBlockHeight(tx walletdb.Tx, maxBlockHeight uint32) error { bucket := tx.RootBucket().Bucket(spvBucketName) maxBlockHeightBytes := uint32ToBytes(maxBlockHeight) err := bucket.Put(maxBlockHeightName, maxBlockHeightBytes) if err != nil { return fmt.Errorf("failed to store max block height: %s", err) } return nil } // putBlock stores the provided block header and height, keyed to the block // hash, in the database. func putBlock(tx walletdb.Tx, header wire.BlockHeader, height uint32) error { var buf bytes.Buffer err := header.Serialize(&buf) if err != nil { return err } _, err = buf.Write(uint32ToBytes(height)) if err != nil { return err } bucket := tx.RootBucket().Bucket(spvBucketName).Bucket(blockHeaderBucketName) blockHash := header.BlockHash() err = bucket.Put(blockHash[:], buf.Bytes()) if err != nil { return fmt.Errorf("failed to store SPV block info: %s", err) } err = bucket.Put(uint32ToBytes(height), blockHash[:]) if err != nil { return fmt.Errorf("failed to store block height info: %s", err) } return nil } // putFilter stores the provided filter, keyed to the block hash, in the // appropriate filter bucket in the database. func putFilter(tx walletdb.Tx, blockHash chainhash.Hash, bucketName []byte, filter *gcs.Filter) error { var buf bytes.Buffer _, err := buf.Write(filter.NBytes()) if err != nil { return err } bucket := tx.RootBucket().Bucket(spvBucketName).Bucket(bucketName) err = bucket.Put(blockHash[:], buf.Bytes()) if err != nil { return fmt.Errorf("failed to store filter: %s", err) } return nil } // putBasicFilter stores the provided filter, keyed to the block hash, in the // basic filter bucket in the database. func putBasicFilter(tx walletdb.Tx, blockHash chainhash.Hash, filter *gcs.Filter) error { return putFilter(tx, blockHash, basicFilterBucketName, filter) } // putExtFilter stores the provided filter, keyed to the block hash, in the // extended filter bucket in the database. func putExtFilter(tx walletdb.Tx, blockHash chainhash.Hash, filter *gcs.Filter) error { return putFilter(tx, blockHash, extFilterBucketName, filter) } // putHeader stores the provided filter, keyed to the block hash, in the // appropriate filter bucket in the database. func putHeader(tx walletdb.Tx, blockHash chainhash.Hash, bucketName []byte, filterTip chainhash.Hash) error { bucket := tx.RootBucket().Bucket(spvBucketName).Bucket(bucketName) err := bucket.Put(blockHash[:], filterTip[:]) if err != nil { return fmt.Errorf("failed to store filter header: %s", err) } return nil } // putBasicHeader stores the provided filter, keyed to the block hash, in the // basic filter bucket in the database. func putBasicHeader(tx walletdb.Tx, blockHash chainhash.Hash, filterTip chainhash.Hash) error { return putHeader(tx, blockHash, basicHeaderBucketName, filterTip) } // putExtHeader stores the provided filter, keyed to the block hash, in the // extended filter bucket in the database. func putExtHeader(tx walletdb.Tx, blockHash chainhash.Hash, filterTip chainhash.Hash) error { return putHeader(tx, blockHash, extHeaderBucketName, filterTip) } // getHeader retrieves the provided filter, keyed to the block hash, from the // appropriate filter bucket in the database. func getHeader(tx walletdb.Tx, blockHash chainhash.Hash, bucketName []byte) (*chainhash.Hash, error) { bucket := tx.RootBucket().Bucket(spvBucketName).Bucket(bucketName) filterTip := bucket.Get(blockHash[:]) if len(filterTip) == 0 { return &chainhash.Hash{}, fmt.Errorf("failed to get filter header") } return chainhash.NewHash(filterTip) } // getBasicHeader retrieves the provided filter, keyed to the block hash, from // the basic filter bucket in the database. func getBasicHeader(tx walletdb.Tx, blockHash chainhash.Hash) (*chainhash.Hash, error) { return getHeader(tx, blockHash, basicHeaderBucketName) } // getExtHeader retrieves the provided filter, keyed to the block hash, from the // extended filter bucket in the database. func getExtHeader(tx walletdb.Tx, blockHash chainhash.Hash) (*chainhash.Hash, error) { return getHeader(tx, blockHash, extHeaderBucketName) } // rollbackLastBlock rolls back the last known block and returns the BlockStamp // representing the new last known block. func rollbackLastBlock(tx walletdb.Tx) (*waddrmgr.BlockStamp, error) { bs, err := syncedTo(tx) if err != nil { return nil, err } bucket := tx.RootBucket().Bucket(spvBucketName).Bucket(blockHeaderBucketName) err = bucket.Delete(bs.Hash[:]) if err != nil { return nil, err } err = bucket.Delete(uint32ToBytes(uint32(bs.Height))) if err != nil { return nil, err } err = putMaxBlockHeight(tx, uint32(bs.Height-1)) if err != nil { return nil, err } return syncedTo(tx) } // getBlockByHash retrieves the block header, filter, and filter tip, based on // the provided block hash, from the database. func getBlockByHash(tx walletdb.Tx, blockHash chainhash.Hash) (wire.BlockHeader, uint32, error) { //chainhash.Hash, chainhash.Hash, bucket := tx.RootBucket().Bucket(spvBucketName).Bucket(blockHeaderBucketName) blockBytes := bucket.Get(blockHash[:]) if len(blockBytes) == 0 { return wire.BlockHeader{}, 0, fmt.Errorf("failed to retrieve block info for hash: %s", blockHash) } buf := bytes.NewReader(blockBytes[:wire.MaxBlockHeaderPayload]) var header wire.BlockHeader err := header.Deserialize(buf) if err != nil { return wire.BlockHeader{}, 0, fmt.Errorf("failed to deserialize block header for "+ "hash: %s", blockHash) } height := binary.LittleEndian.Uint32( blockBytes[wire.MaxBlockHeaderPayload : wire.MaxBlockHeaderPayload+4]) return header, height, nil } // getBlockHashByHeight retrieves the hash of a block by its height. func getBlockHashByHeight(tx walletdb.Tx, height uint32) (chainhash.Hash, error) { bucket := tx.RootBucket().Bucket(spvBucketName).Bucket(blockHeaderBucketName) var hash chainhash.Hash hashBytes := bucket.Get(uint32ToBytes(height)) if hashBytes == nil { return hash, fmt.Errorf("no block hash for height %d", height) } hash.SetBytes(hashBytes) return hash, nil } // getBlockByHeight retrieves a block's information by its height. func getBlockByHeight(tx walletdb.Tx, height uint32) (wire.BlockHeader, uint32, error) { // chainhash.Hash, chainhash.Hash blockHash, err := getBlockHashByHeight(tx, height) if err != nil { return wire.BlockHeader{}, 0, err } return getBlockByHash(tx, blockHash) } // syncedTo retrieves the most recent block's height and hash. func syncedTo(tx walletdb.Tx) (*waddrmgr.BlockStamp, error) { header, height, err := latestBlock(tx) if err != nil { return nil, err } var blockStamp waddrmgr.BlockStamp blockStamp.Hash = header.BlockHash() blockStamp.Height = int32(height) return &blockStamp, nil } // latestBlock retrieves all the info about the latest stored block. func latestBlock(tx walletdb.Tx) (wire.BlockHeader, uint32, error) { bucket := tx.RootBucket().Bucket(spvBucketName) maxBlockHeightBytes := bucket.Get(maxBlockHeightName) if maxBlockHeightBytes == nil { return wire.BlockHeader{}, 0, fmt.Errorf("no max block height stored") } maxBlockHeight := binary.LittleEndian.Uint32(maxBlockHeightBytes) header, height, err := getBlockByHeight(tx, maxBlockHeight) if err != nil { return wire.BlockHeader{}, 0, err } if height != maxBlockHeight { return wire.BlockHeader{}, 0, fmt.Errorf("max block height inconsistent") } return header, height, nil } // CheckConnectivity cycles through all of the block headers, from last to // first, and makes sure they all connect to each other. func CheckConnectivity(tx walletdb.Tx) error { header, height, err := latestBlock(tx) if err != nil { return fmt.Errorf("Couldn't retrieve latest block: %s", err) } for height > 0 { newheader, newheight, err := getBlockByHash(tx, header.PrevBlock) if err != nil { return fmt.Errorf("Couldn't retrieve block %s: %s", header.PrevBlock, err) } if newheader.BlockHash() != header.PrevBlock { return fmt.Errorf("Block %s doesn't match block %s's "+ "PrevBlock (%s)", newheader.BlockHash(), header.BlockHash(), header.PrevBlock) } if newheight != height-1 { return fmt.Errorf("Block %s doesn't have correct "+ "height: want %d, got %d", newheader.BlockHash(), height-1, newheight) } header = newheader height = newheight } return nil } // blockLocatorFromHash returns a block locator based on the provided hash. func blockLocatorFromHash(tx walletdb.Tx, hash chainhash.Hash) blockchain.BlockLocator { locator := make(blockchain.BlockLocator, 0, wire.MaxBlockLocatorsPerMsg) locator = append(locator, &hash) // If hash isn't found in DB or this is the genesis block, return // the locator as is _, height, err := getBlockByHash(tx, hash) if (err != nil) || (height == 0) { return locator } decrement := uint32(1) for (height > 0) && (len(locator) < wire.MaxBlockLocatorsPerMsg) { // Decrement by 1 for the first 10 blocks, then double the // jump until we get to the genesis hash if len(locator) > 10 { decrement *= 2 } if decrement > height { height = 0 } else { height -= decrement } blockHash, err := getBlockHashByHeight(tx, height) if err != nil { return locator } locator = append(locator, &blockHash) } return locator } // createSPVNS creates the initial namespace structure needed for all of the // SPV-related data. This includes things such as all of the buckets as well as // the version and creation date. func createSPVNS(namespace walletdb.Namespace, params *chaincfg.Params) error { err := namespace.Update(func(tx walletdb.Tx) error { rootBucket := tx.RootBucket() spvBucket, err := rootBucket.CreateBucketIfNotExists(spvBucketName) if err != nil { return fmt.Errorf("failed to create main bucket: %s", err) } _, err = spvBucket.CreateBucketIfNotExists(blockHeaderBucketName) if err != nil { return fmt.Errorf("failed to create block header "+ "bucket: %s", err) } _, err = spvBucket.CreateBucketIfNotExists(basicFilterBucketName) if err != nil { return fmt.Errorf("failed to create basic filter "+ "bucket: %s", err) } _, err = spvBucket.CreateBucketIfNotExists(basicHeaderBucketName) if err != nil { return fmt.Errorf("failed to create basic header "+ "bucket: %s", err) } _, err = spvBucket.CreateBucketIfNotExists(extFilterBucketName) if err != nil { return fmt.Errorf("failed to create extended filter "+ "bucket: %s", err) } _, err = spvBucket.CreateBucketIfNotExists(extHeaderBucketName) if err != nil { return fmt.Errorf("failed to create extended header "+ "bucket: %s", err) } createDate := spvBucket.Get(dbCreateDateName) if createDate != nil { log.Info("Wallet SPV namespace already created.") return nil } log.Info("Creating wallet SPV namespace.") basicFilter, err := buildBasicFilter(params.GenesisBlock) if err != nil { return err } basicFilterTip := makeHeaderForFilter(basicFilter, params.GenesisBlock.Header.PrevBlock) extFilter, err := buildExtFilter(params.GenesisBlock) if err != nil { return err } extFilterTip := makeHeaderForFilter(extFilter, params.GenesisBlock.Header.PrevBlock) err = putBlock(tx, params.GenesisBlock.Header, 0) if err != nil { return err } err = putBasicFilter(tx, *params.GenesisHash, basicFilter) if err != nil { return err } err = putBasicHeader(tx, *params.GenesisHash, basicFilterTip) if err != nil { return err } err = putExtFilter(tx, *params.GenesisHash, extFilter) if err != nil { return err } err = putExtHeader(tx, *params.GenesisHash, extFilterTip) if err != nil { return err } err = putDBVersion(tx, latestDBVersion) if err != nil { return err } err = putMaxBlockHeight(tx, 0) if err != nil { return err } err = spvBucket.Put(dbCreateDateName, uint64ToBytes(uint64(time.Now().Unix()))) if err != nil { return fmt.Errorf("failed to store database creation "+ "time: %s", err) } return nil }) if err != nil { return fmt.Errorf("failed to update database: %s", err) } return nil }