{"action":"created","comment":{"url":"https://api.github.com/repos/btcsuite/btcd/pulls/comments/29283017","id":29283017,"diff_hunk":"@@ -0,0 +1,745 @@\n+// Copyright (c) 2015 Conformal Systems LLC.\n+// Use of this source code is governed by an ISC\n+// license that can be found in the LICENSE file.\n+\n+// This file contains the implementation functions for reading, writing, and\n+// otherwise working with the flat files that house the actual blocks.\n+\n+package ffboltdb\n+\n+import (\n+\t\"container/list\"\n+\t\"encoding/binary\"\n+\t\"fmt\"\n+\t\"hash/crc32\"\n+\t\"io\"\n+\t\"os\"\n+\t\"path/filepath\"\n+\t\"sync\"\n+\n+\t\"github.com/btcsuite/btcd/database2\"\n+\t\"github.com/btcsuite/btcd/wire\"\n+)\n+\n+const (\n+\t// The Bitcoin protocol encodes block height as int32, so max number of\n+\t// blocks is 2^31.  Max block size per the protocol is 32MiB per block.\n+\t// So the theoretical max at the time this comment was written is 64PiB\n+\t// (pebibytes).  With files @ 512MiB each, this would require a maximum\n+\t// of 134,217,728 files.  Thus, choose 9 digits of precision for the\n+\t// filenames.  An additional benefit is 9 digits provides 10^9 files @\n+\t// 512MiB each for a total of ~476.84PiB (roughly 7.4 times the current\n+\t// theoretical max), so there is room for the max block size to grow in\n+\t// the future.\n+\tblockFilenameTemplate = \"%09d.fdb\"\n+\n+\t// maxOpenFiles is the max number of open files to maintain in the\n+\t// open blocks cache.  Note that this does not include the current\n+\t// write file, so there will typically be one more than this value open.\n+\tmaxOpenFiles = 25\n+\n+\t// maxBlockFileSize is the maximum size for each file used to store\n+\t// blocks.\n+\t//\n+\t// NOTE: The current code uses uint32 for all offsets, so this value\n+\t// must be less than 2^32 (4 GiB).  This is also why it's a typed\n+\t// constant.\n+\tmaxBlockFileSize uint32 = 512 * 1024 * 1024 // 512 MiB\n+\n+\t// blockLocSize is the number of bytes the serialized block location\n+\t// data that is stored in the block index.\n+\t//\n+\t// The serialized block location format is:\n+\t//\n+\t//  [0:4]  Block file (4 bytes)\n+\t//  [4:8]  File offset (4 bytes)\n+\t//  [8:12] Block length (4 bytes)\n+\tblockLocSize = 12\n+)\n+\n+var (\n+\t// castagnoli houses the Catagnoli polynomial used for CRC-32 checksums.\n+\tcastagnoli = crc32.MakeTable(crc32.Castagnoli)\n+)\n+\n+// filer is an interface which acts very similar to a *os.File and is typically\n+// implemented by it.  It exists so the test code can provide mock files for\n+// properly testing corruption and file system issues.\n+type filer interface {\n+\tio.Closer\n+\tio.WriterAt\n+\tio.ReaderAt\n+\tTruncate(size int64) error\n+\tSync() error\n+}\n+\n+// lockableFile represents a block file on disk that has been opened for either\n+// read or read/write access.  It also contains a read-write mutex to support\n+// multiple concurrent readers.\n+type lockableFile struct {\n+\tsync.RWMutex\n+\tfile filer\n+}\n+\n+// writeCursor represents the current file and offset of the block file on disk\n+// for performing all writes. It also contains a read-write mutex to support\n+// multiple concurrent readers which can reuse the file handle.\n+type writeCursor struct {\n+\tsync.RWMutex\n+\n+\t// curFile is the current block file that will be appended to when\n+\t// writing new blocks.\n+\tcurFile *lockableFile\n+\n+\t// curFileNum is the current block file number and is used to allow\n+\t// readers to use the same open file handle.\n+\tcurFileNum uint32\n+\n+\t// curOffset is the offset in the current write block file where the\n+\t// next new block will be written.\n+\tcurOffset uint32\n+}\n+\n+// blockStore houses information used to handle reading and writing blocks (and\n+// part of blocks) into flat files with support for multiple concurrent readers.\n+type blockStore struct {\n+\t// network is the specific network to use in the flat files for each\n+\t// block.\n+\tnetwork wire.BitcoinNet\n+\n+\t// basePath is the base path used for the flat block files and metadata.\n+\tbasePath string\n+\n+\t// maxBlockFileSize is the maximum size for each file used to store\n+\t// blocks.  It is defined on the store so the whitebox tests can\n+\t// override the value.\n+\tmaxBlockFileSize uint32\n+\n+\t// The following fields are related to the flat files which hold the\n+\t// actual blocks.   The number of open files is limited by maxOpenFiles.\n+\t//\n+\t// obfMutex protects concurrent access to the openBlockFiles map.  It is\n+\t// a RWMutex so multiple readers can simultaneously access open files.\n+\t//\n+\t// openBlockFiles houses the open file handles for existing block files\n+\t// which have been opened read-only along with an individual RWMutex.\n+\t// This scheme allows multiple concurrent readers to the same file while\n+\t// preventing the file from being closed out from under them.\n+\t//\n+\t// lruMutex protects concurrent access to the least recently used list\n+\t// and lookup map.\n+\t//\n+\t// openBlocksLRU tracks how the open files are refenced by pushing the\n+\t// most recently used files ot the front of the list thereby trickling\n+\t// the least recently used files to end of the list.  When a file needs\n+\t// to be closed due to exceeding the the max number of allowed open\n+\t// files, the one at the back of the list is closed.\n+\t//\n+\t// fileNumToLRUElem is a mapping between a specific block file number\n+\t// and the associated list element on the least recently used list.\n+\t//\n+\t// Thus, with the combination of these fields, the database supports\n+\t// concurrent non-blocking reads across multiple and individual files\n+\t// along with intelligently limiting the number of open file handles by\n+\t// closing the least recently used files as needed.\n+\t//\n+\t// NOTE: The locking order used throughout is well-defined and MUST be\n+\t// followed.  Failure to do so could lead to deadlocks.  In particular,\n+\t// the locking order is as follows:\n+\t//   1) obfMutex\n+\t//   2) lruMutex\n+\t//   3) writeCursor mutex\n+\t//   4) specific file mutexes\n+\t//\n+\t// None of the mutexes are required to be locked at the same time, and\n+\t// often aren't.  However, if they are to be locked simultaneously, they\n+\t// MUST be locked in the order previously specified.\n+\t//\n+\t// Due to the high performance and multi-read concurrency requirements,\n+\t// write locks should only be held for the minimum time necessary.\n+\tobfMutex         sync.RWMutex\n+\tlruMutex         sync.Mutex\n+\topenBlocksLRU    *list.List // Contains uint32 block file numbers.\n+\tfileNumToLRUElem map[uint32]*list.Element\n+\topenBlockFiles   map[uint32]*lockableFile\n+\n+\t// writeCursor houses the state for the current file and location that\n+\t// new blocks are written to.\n+\twriteCursor *writeCursor\n+\n+\t// These functions are set to openFile, openWriteFile, and deleteFile by\n+\t// default, but are exposed here to allow the whitebox tests to replace\n+\t// them when working with mock files.\n+\topenFileFunc      func(fileNum uint32) (*lockableFile, error)\n+\topenWriteFileFunc func(fileNum uint32) (filer, error)\n+\tdeleteFileFunc    func(fileNum uint32) error\n+}\n+\n+// blockLocation identifies a particular block file and location.\n+type blockLocation struct {\n+\tblockFileNum uint32\n+\tfileOffset   uint32\n+\tblockLen     uint32\n+}\n+\n+// deserializeBlockLoc deserializes the passed serialized block location\n+// information.  This is data stored into the block index metadata for each\n+// block.  The serialized data passed to this function MUST be at least\n+// blockLocSize bytes or it will panic.  There error check is avoided here\n+// because this information will always be coming from the block index which\n+// includes a checksum to detect corruption.  Thus it is safe to use this\n+// unchecked here.\n+func deserializeBlockLoc(serializedLoc []byte) blockLocation {\n+\t// The serialized block location format is:\n+\t//\n+\t//  [0:4]  Block file (4 bytes)\n+\t//  [4:8]  File offset (4 bytes)\n+\t//  [8:12] Block length (4 bytes)\n+\treturn blockLocation{\n+\t\tblockFileNum: byteOrder.Uint32(serializedLoc[0:4]),\n+\t\tfileOffset:   byteOrder.Uint32(serializedLoc[4:8]),\n+\t\tblockLen:     byteOrder.Uint32(serializedLoc[8:12]),\n+\t}\n+}\n+\n+// serializeBlockLoc returns the serialization of the passed block location.\n+// This is data to be stored into the block index metadata for each block.\n+func serializeBlockLoc(loc blockLocation) []byte {\n+\t// The serialized block location format is:\n+\t//\n+\t//  [0:4]  Block file (4 bytes)\n+\t//  [4:8]  File offset (4 bytes)\n+\t//  [8:12] Block length (4 bytes)\n+\tvar serializedData [12]byte\n+\tbyteOrder.PutUint32(serializedData[0:4], loc.blockFileNum)\n+\tbyteOrder.PutUint32(serializedData[4:8], loc.fileOffset)\n+\tbyteOrder.PutUint32(serializedData[8:12], loc.blockLen)\n+\treturn serializedData[:]\n+}\n+\n+// blockFilePath return the file path for the provided block file number.\n+func blockFilePath(dbPath string, fileNum uint32) string {\n+\tfileName := fmt.Sprintf(blockFilenameTemplate, fileNum)\n+\treturn filepath.Join(dbPath, fileName)\n+}\n+\n+// openWriteFile returns a file handle for the passed flat file number in\n+// read/write mode.  The file will be created if needed.  It is typically used\n+// for the current file that will have all new data appended.  Unlike openFile,\n+// this function does not keep track the open file and it is not subject to the\n+// maxOpenFiles limit.\n+func (s *blockStore) openWriteFile(fileNum uint32) (filer, error) {\n+\t// The current block file needs to be read-write so it is possible to\n+\t// append to it.  Also, it shouldn't be part of the least recently used\n+\t// file.\n+\tfilePath := blockFilePath(s.basePath, fileNum)\n+\tfile, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0666)\n+\tif err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to open file %q: %v\", filePath, err)\n+\t\treturn nil, makeDbErr(database.ErrDriverSpecific, str, err)\n+\t}\n+\n+\treturn file, nil\n+}\n+\n+// openFile returns a read-only file handle for the passed flat file number.\n+// The function also keeps track of the open files, performs least recently\n+// used tracking, and limits the number of open files to maxOpenFiles by closing\n+// the least recently used file as needed.\n+//\n+// This function MUST be called with the overall files mutex (s.obfMutex) locked\n+// for WRITES.\n+func (s *blockStore) openFile(fileNum uint32) (*lockableFile, error) {\n+\t// Open the appropriate file as read-only.\n+\tfilePath := blockFilePath(s.basePath, fileNum)\n+\tfile, err := os.Open(filePath)\n+\tif err != nil {\n+\t\treturn nil, makeDbErr(database.ErrDriverSpecific, err.Error(),\n+\t\t\terr)\n+\t}\n+\tblockFile := &lockableFile{file: file}\n+\n+\t// Close the least recently used file if the file exceeds the max\n+\t// allowed open files.  This is not done until after the file open in\n+\t// case the file fails to open, there is no need to close any files.\n+\t//\n+\t// A write lock is required on the LRU list here to protect against\n+\t// modifications happening as already open files are read from and\n+\t// shuffled to the front of the list.\n+\t//\n+\t// Also, add the file that was just opened to the front of the least\n+\t// recently used list to indicate it is the most recently used file and\n+\t// therefore should be closed last.\n+\ts.lruMutex.Lock()\n+\tlruList := s.openBlocksLRU\n+\tif lruList.Len() >= maxOpenFiles {\n+\t\tlruFileNum := lruList.Remove(lruList.Back()).(uint32)\n+\t\toldBlockFile := s.openBlockFiles[lruFileNum]\n+\n+\t\t// Close the old file under the write lock for the file in case\n+\t\t// any readers are currently reading from it so it's not closed\n+\t\t// out from under them.\n+\t\toldBlockFile.Lock()\n+\t\t_ = oldBlockFile.file.Close()\n+\t\toldBlockFile.Unlock()\n+\n+\t\tdelete(s.openBlockFiles, lruFileNum)\n+\t\tdelete(s.fileNumToLRUElem, lruFileNum)\n+\t}\n+\ts.fileNumToLRUElem[fileNum] = lruList.PushFront(fileNum)\n+\ts.lruMutex.Unlock()\n+\n+\t// Store a reference to it the open block files map.\n+\ts.openBlockFiles[fileNum] = blockFile\n+\n+\treturn blockFile, nil\n+}\n+\n+// deleteFile remove the block file for the passed flat file number.  The file\n+// must already be closed and it is the responsibility of the caller to do any\n+// other state cleanup necessary.\n+func (s *blockStore) deleteFile(fileNum uint32) error {\n+\tfilePath := blockFilePath(s.basePath, fileNum)\n+\tif err := os.Remove(filePath); err != nil {\n+\t\treturn makeDbErr(database.ErrDriverSpecific, err.Error(), err)\n+\t}\n+\n+\treturn nil\n+}\n+\n+// blockFile attempts to return an existing file handle for the passed flat file\n+// number if it is already open as well as marking it as most recently used.  It\n+// will also open the file when it's not already open subject to the rules\n+// described in openFile.\n+//\n+// NOTE: The returned block file will already have the read lock acquired and\n+// the caller MUST call .RUnlock() to release it once it has finished all read\n+// operations.  This is necessary because otherwise it would be possible for a\n+// separate goroutine to close the file after it is returned from here, but\n+// before the caller has acquired a read lock.\n+func (s *blockStore) blockFile(fileNum uint32) (*lockableFile, error) {\n+\t// When the requested block file is open for writes, return it.\n+\twc := s.writeCursor\n+\twc.RLock()\n+\tif fileNum == wc.curFileNum && wc.curFile.file != nil {\n+\t\tobf := wc.curFile\n+\t\tobf.RLock()\n+\t\twc.RUnlock()\n+\t\treturn obf, nil\n+\t}\n+\twc.RUnlock()\n+\n+\t// Try to return an open file under the overall files read lock.\n+\ts.obfMutex.RLock()\n+\tif obf, ok := s.openBlockFiles[fileNum]; ok {\n+\t\ts.lruMutex.Lock()\n+\t\ts.openBlocksLRU.MoveToFront(s.fileNumToLRUElem[fileNum])\n+\t\ts.lruMutex.Unlock()\n+\n+\t\tobf.RLock()\n+\t\ts.obfMutex.RUnlock()\n+\t\treturn obf, nil\n+\t}\n+\ts.obfMutex.RUnlock()\n+\n+\t// Since the file isn't open already, need to check the open block files\n+\t// map again under write lock in case multiple readers got here and a\n+\t// separate one is already opening the file.\n+\ts.obfMutex.Lock()\n+\tif obf, ok := s.openBlockFiles[fileNum]; ok {\n+\t\tobf.RLock()\n+\t\ts.obfMutex.Unlock()\n+\t\treturn obf, nil\n+\t}\n+\n+\t// The file isn't open, so open it while closing the least recently used\n+\t// one.  The called function grabs the overall files write lock and\n+\t// checks the opened block files map again in case multiple readers get\n+\t// here.\n+\tobf, err := s.openFileFunc(fileNum)\n+\tif err != nil {\n+\t\ts.obfMutex.Unlock()\n+\t\treturn nil, err\n+\t}\n+\tobf.RLock()\n+\ts.obfMutex.Unlock()\n+\treturn obf, nil\n+}\n+\n+// writeBlock appends the specified raw block bytes to the store's write cursor\n+// location and increments it accordingly.  When the block would exceed the max\n+// file size for the current flat file, this function will close the current\n+// file, create the next file, update the write cursor, and write the block to\n+// the new file.\n+//\n+// The write cursor will also be advanced the number of bytes actually written\n+// in the event of failure.\n+//\n+// Format: <network><block length><serialized block><checksum>\n+func (s *blockStore) writeBlock(rawBlock []byte) (blockLocation, error) {\n+\t// Compute how many bytes will be written.\n+\t// 4 bytes each for block network + 4 bytes for block length +\n+\t// length of raw block + 4 bytes for checksum.\n+\tblockLen := uint32(len(rawBlock))\n+\tfullLen := blockLen + 12\n+\n+\t// Move to the next block file if adding the new block would exceed the\n+\t// max allowed size for the current block file.  Also detect overflow\n+\t// to be paranoid, even though it isn't possible currently, numbers\n+\t// might change in the future to make possible.\n+\t//\n+\t// NOTE: The writeCursor.offset field isn't protected by the mutex\n+\t// since it's only read/changed in this function which can only be\n+\t// called during a write transaction, of which there can be only one at\n+\t// a time.\n+\twc := s.writeCursor\n+\tfinalOffset := wc.curOffset + fullLen\n+\tif finalOffset < wc.curOffset || finalOffset > s.maxBlockFileSize {\n+\t\t// This is done under the write cursor lock since the fileNum\n+\t\t// field is accessed elsewhere by readers.\n+\t\t//\n+\t\t// Close the current write file to force a read-only reopen\n+\t\t// with LRU tracking.  The close is done under the write lock\n+\t\t// for the file to prevent it from being closed out from under\n+\t\t// any readers currently reading from it.\n+\t\twc.Lock()\n+\t\twc.curFile.Lock()\n+\t\tif wc.curFile.file != nil {\n+\t\t\t_ = wc.curFile.file.Close()\n+\t\t\twc.curFile.file = nil\n+\t\t}\n+\t\twc.curFile.Unlock()\n+\n+\t\t// Start writes into next file.\n+\t\twc.curFileNum++\n+\t\twc.curOffset = 0\n+\t\twc.Unlock()\n+\t}\n+\n+\t// All writes are done under the write lock for the file to ensure any\n+\t// readers are finished and blocked first.\n+\twc.curFile.Lock()\n+\tdefer wc.curFile.Unlock()\n+\n+\t// Open the current file if needed.  This will typically only be the\n+\t// case when moving to the next file to write to or on initial database\n+\t// load.  However, it might also be the case if rollbacks happened after\n+\t// file writes started during a transaction commit.\n+\tif wc.curFile.file == nil {\n+\t\tfile, err := s.openWriteFileFunc(wc.curFileNum)\n+\t\tif err != nil {\n+\t\t\treturn blockLocation{}, err\n+\t\t}\n+\t\twc.curFile.file = file\n+\t}\n+\n+\t// Bitcoin network.\n+\torigOffset := wc.curOffset\n+\thasher := crc32.New(castagnoli)\n+\tvar scratch [4]byte\n+\tbyteOrder.PutUint32(scratch[:], uint32(s.network))\n+\tn, err := wc.curFile.file.WriteAt(scratch[:], int64(wc.curOffset))\n+\twc.curOffset += uint32(n)\n+\tif err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to write network to file %d at \"+\n+\t\t\t\"offset %d: %v\", wc.curFileNum, wc.curOffset, err)\n+\t\treturn blockLocation{}, makeDbErr(database.ErrDriverSpecific,\n+\t\t\tstr, err)\n+\t}\n+\t_, _ = hasher.Write(scratch[:])\n+\n+\t// Block length.\n+\tbyteOrder.PutUint32(scratch[:], blockLen)\n+\tn, err = wc.curFile.file.WriteAt(scratch[:], int64(wc.curOffset))\n+\twc.curOffset += uint32(n)\n+\tif err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to write block length to file %d \"+\n+\t\t\t\"at offset %d: %v\", wc.curFileNum, wc.curOffset, err)\n+\t\treturn blockLocation{}, makeDbErr(database.ErrDriverSpecific,\n+\t\t\tstr, err)\n+\t}\n+\t_, _ = hasher.Write(scratch[:])\n+\n+\t// Serialized block.\n+\tn, err = wc.curFile.file.WriteAt(rawBlock, int64(wc.curOffset))\n+\twc.curOffset += uint32(n)\n+\tif err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to write block to file %d at \"+\n+\t\t\t\"offset %d: %v\", wc.curFileNum, wc.curOffset, err)\n+\t\treturn blockLocation{}, makeDbErr(database.ErrDriverSpecific,\n+\t\t\tstr, err)\n+\t}\n+\t_, _ = hasher.Write(rawBlock)\n+\n+\t// Castagnoli CRC-32 as a checksum of all the previous.\n+\tn, err = wc.curFile.file.WriteAt(hasher.Sum(nil), int64(wc.curOffset))\n+\twc.curOffset += uint32(n)\n+\tif err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to write checksum to file %d at \"+\n+\t\t\t\"offset %d: %v\", wc.curFileNum, wc.curOffset, err)\n+\t\treturn blockLocation{}, makeDbErr(database.ErrDriverSpecific,\n+\t\t\tstr, err)\n+\t}\n+\n+\t// Sync the file to disk.\n+\tif err := wc.curFile.file.Sync(); err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to sync file %d: %v\", wc.curFileNum,\n+\t\t\terr)\n+\t\treturn blockLocation{}, makeDbErr(database.ErrDriverSpecific,\n+\t\t\tstr, err)\n+\t}\n+\n+\tloc := blockLocation{\n+\t\tblockFileNum: wc.curFileNum,\n+\t\tfileOffset:   origOffset,\n+\t\tblockLen:     fullLen,\n+\t}\n+\treturn loc, nil\n+}\n+\n+// readBlock reads the specified block record and returns the serialized block.\n+// It ensures the integrity of the block data by checking that the serialized\n+// network matches the current network associated with the block store and\n+// comparing the calculated checksum against the one stored in the flat file.\n+// This function also automatically handles all file management such as opening\n+// and closing files as necessary to stay within the maximum allowed open files\n+// limit.\n+//\n+// Returns ErrDriverSpecific if the data fails to read for any reason and\n+// ErrCorruption if the checksum of the read data doesn't match the checksum\n+// read from the file.\n+//\n+// Format: <network><block length><serialized block><checksum>\n+func (s *blockStore) readBlock(hash *wire.ShaHash, loc blockLocation) ([]byte, error) {\n+\t// Get the referenced block file handle opening the file as needed.  The\n+\t// function also handles closing files as needed to avoid going over the\n+\t// max allowed open files.\n+\tblockFile, err := s.blockFile(loc.blockFileNum)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tserializedData := make([]byte, loc.blockLen)\n+\tn, err := blockFile.file.ReadAt(serializedData, int64(loc.fileOffset))\n+\tblockFile.RUnlock()\n+\tif err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to read block %s from file %d, \"+\n+\t\t\t\"offset %d: %v\", hash, loc.blockFileNum, loc.fileOffset,\n+\t\t\terr)\n+\t\treturn nil, makeDbErr(database.ErrDriverSpecific, str, err)\n+\t}\n+\n+\t// Calculate the checksum of the read data and ensure it matches the\n+\t// serialized checksum.  This will detect any data corruption in the\n+\t// flat file without having to do much more expensive merkle root\n+\t// calculations on the loaded block.\n+\tserializedChecksum := binary.BigEndian.Uint32(serializedData[n-4:])\n+\tcalculatedChecksum := crc32.Checksum(serializedData[:n-4], castagnoli)\n+\tif serializedChecksum != calculatedChecksum {\n+\t\tstr := fmt.Sprintf(\"block data for block %s checksum \"+\n+\t\t\t\"does not match - got %x, want %x\", hash,\n+\t\t\tcalculatedChecksum, serializedChecksum)\n+\t\treturn nil, makeDbErr(database.ErrCorruption, str, nil)\n+\t}\n+\n+\t// The network associated with the block must match the current active\n+\t// network, otherwise somebody probably put the block files for the\n+\t// wrong network in the directory.\n+\tserializedNet := byteOrder.Uint32(serializedData[:4])\n+\tif serializedNet != uint32(s.network) {\n+\t\tstr := fmt.Sprintf(\"block data for block %s is for the \"+\n+\t\t\t\"wrong network - got %d, want %d\", hash, serializedNet,\n+\t\t\tuint32(s.network))\n+\t\treturn nil, makeDbErr(database.ErrDriverSpecific, str, nil)\n+\t}\n+\n+\t// The raw block excludes the network, length of the block, and\n+\t// checksum.\n+\treturn serializedData[8 : n-4], nil\n+}\n+\n+// readBlockRegion reads the specified amount of data at the provided offset for\n+// a given block location.  The offset is relative to the start of the\n+// serialized block (as opposed to the beginning of the block record).  This\n+// function automatically handles all file management such as opening and\n+// closing files as necessary to stay within the maximum allowed open files\n+// limit.\n+//\n+// Returns ErrDriverSpecific if the data fails to read for any reason.\n+func (s *blockStore) readBlockRegion(loc blockLocation, offset, numBytes uint32) ([]byte, error) {\n+\t// Get the referenced block file handle opening the file as needed.  The\n+\t// function also handles closing files as needed to avoid going over the\n+\t// max allowed open files.\n+\tblockFile, err := s.blockFile(loc.blockFileNum)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\t// Regions are offsets into the actual block, however the serialized\n+\t// data for a block includes an initial 4 bytes for network + 4 bytes\n+\t// for block length.  Thus, add 8 bytes to adjust.\n+\treadOffset := loc.fileOffset + 8 + offset\n+\tserializedData := make([]byte, numBytes)\n+\t_, err = blockFile.file.ReadAt(serializedData, int64(readOffset))\n+\tblockFile.RUnlock()\n+\tif err != nil {\n+\t\tstr := fmt.Sprintf(\"failed to read region from block file %d, \"+\n+\t\t\t\"offset %d, len %d: %v\", loc.blockFileNum, readOffset,\n+\t\t\tnumBytes, err)\n+\t\treturn nil, makeDbErr(database.ErrDriverSpecific, str, err)\n+\t}\n+\n+\treturn serializedData, nil\n+}\n+\n+// handleRollback rolls the block files on disk back to the provided file number\n+// and offset.  This involves potentially deleting and truncating the files that\n+// were partially written.\n+//\n+// There are effectively two scenarios to consider here:\n+//   1) Transient write failures from which recovery is possible\n+//   2) More permanant failures such as hard disk death and/or removal\n+//\n+// In either case, the write cursor will be repositioned to the old block file\n+// offset regardless of any other errors that occur while attempting to undo\n+// writes.\n+//\n+// For the first scenario, this will lead to any data which failed to be undone\n+// being overwritten and thus behaves as desired as the system continues to run.\n+//\n+// For the second scenario, the metadata which stores the current write cursor\n+// position within the block files will not have been updated yet and thus if\n+// the system eventually recovers (perhaps the hard drive is reconnected), it\n+// will also lead to any data which failed to be undone being overwritten and\n+// thus behaves as desired.\n+//\n+// Therefore, any errors are simply logged at a warning level rather than being\n+// returned since there is nothing more that could be done about it anyways.\n+func (s *blockStore) handleRollback(oldBlockFileNum, oldBlockOffset uint32) {\n+\t// Grab the write cursor mutex since it is modified throughout this\n+\t// function.\n+\twc := s.writeCursor\n+\twc.Lock()\n+\tdefer wc.Unlock()\n+\n+\t// Nothing to do if the rollback point is the same as the current write\n+\t// cursor.\n+\tif wc.curFileNum == oldBlockFileNum && wc.curOffset == oldBlockOffset {\n+\t\treturn\n+\t}\n+\n+\t// Regardless of any failures that happen below, reposition the write\n+\t// cursor to the old block file and offset.\n+\tdefer func() {\n+\t\twc.curFileNum = oldBlockFileNum\n+\t\twc.curOffset = oldBlockOffset\n+\t}()\n+\n+\tlog.Debugf(\"ROLLBACK: Rolling back to file %d, offset %d\",\n+\t\toldBlockFileNum, oldBlockOffset)\n+\n+\t// Close the current write file if it needs to be deleted.  Then delete\n+\t// all files that are newer than the provided rollback file while\n+\t// also moving the write cursor file backwards accordingly.\n+\tif wc.curFileNum > oldBlockFileNum {\n+\t\twc.curFile.Lock()\n+\t\tif wc.curFile.file != nil {\n+\t\t\t_ = wc.curFile.file.Close()\n+\t\t\twc.curFile.file = nil\n+\t\t}\n+\t\twc.curFile.Unlock()\n+\t}\n+\tfor ; wc.curFileNum > oldBlockFileNum; wc.curFileNum-- {\n+\t\tif err := s.deleteFileFunc(wc.curFileNum); err != nil {\n+\t\t\t_ = log.Warnf(\"ROLLBACK: Failed to delete block file \"+","path":"database2/ffboltdb/blockio.go","position":654,"original_position":654,"commit_id":"3ef15eda645d82c0eef1986c85f41a0162c625ff","original_commit_id":"3ef15eda645d82c0eef1986c85f41a0162c625ff","user":{"login":"jrick","id":1420313,"avatar_url":"https://avatars.githubusercontent.com/u/1420313?v=3","gravatar_id":"","url":"https://api.github.com/users/jrick","html_url":"https://github.com/jrick","followers_url":"https://api.github.com/users/jrick/followers","following_url":"https://api.github.com/users/jrick/following{/other_user}","gists_url":"https://api.github.com/users/jrick/gists{/gist_id}","starred_url":"https://api.github.com/users/jrick/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jrick/subscriptions","organizations_url":"https://api.github.com/users/jrick/orgs","repos_url":"https://api.github.com/users/jrick/repos","events_url":"https://api.github.com/users/jrick/events{/privacy}","received_events_url":"https://api.github.com/users/jrick/received_events","type":"User","site_admin":false},"body":"Maybe we should use `Critical`/`Criticalf` for logging rollback errors.","created_at":"2015-04-28T20:11:29Z","updated_at":"2015-04-28T20:11:29Z","html_url":"https://github.com/btcsuite/btcd/pull/380#discussion_r29283017","pull_request_url":"https://api.github.com/repos/btcsuite/btcd/pulls/380","_links":{"self":{"href":"https://api.github.com/repos/btcsuite/btcd/pulls/comments/29283017"},"html":{"href":"https://github.com/btcsuite/btcd/pull/380#discussion_r29283017"},"pull_request":{"href":"https://api.github.com/repos/btcsuite/btcd/pulls/380"}}},"pull_request":{"url":"https://api.github.com/repos/btcsuite/btcd/pulls/380","id":33496029,"html_url":"https://github.com/btcsuite/btcd/pull/380","diff_url":"https://github.com/btcsuite/btcd/pull/380.diff","patch_url":"https://github.com/btcsuite/btcd/pull/380.patch","issue_url":"https://api.github.com/repos/btcsuite/btcd/issues/380","number":380,"state":"open","locked":false,"title":"WIP: database: Major redesign of database package.","user":{"login":"davecgh","id":2115102,"avatar_url":"https://avatars.githubusercontent.com/u/2115102?v=3","gravatar_id":"","url":"https://api.github.com/users/davecgh","html_url":"https://github.com/davecgh","followers_url":"https://api.github.com/users/davecgh/followers","following_url":"https://api.github.com/users/davecgh/following{/other_user}","gists_url":"https://api.github.com/users/davecgh/gists{/gist_id}","starred_url":"https://api.github.com/users/davecgh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/davecgh/subscriptions","organizations_url":"https://api.github.com/users/davecgh/orgs","repos_url":"https://api.github.com/users/davecgh/repos","events_url":"https://api.github.com/users/davecgh/events{/privacy}","received_events_url":"https://api.github.com/users/davecgh/received_events","type":"User","site_admin":false},"body":"This pull request contains a complete redesign and rewrite of the database package that approaches things in a vastly different manner than the previous version.  This is the first part of several stages that will be needed to ultimately make use of this new package.\r\n\r\nSome of the reasons for this were discussed in #255, however a quick summary is as follows:\r\n\r\n- The previous database could only contain blocks on the main chain and reorgs required deleting the blocks from the database.  This made it impossible to store orphans and could make external RPC calls for information about blocks during the middle of a reorg fail.\r\n- The previous database interface forced a high level of bitcoin-specific intelligence such as spend tracking into each backend driver.\r\n- The aforementioned point led to making it difficult to implement new backend drivers due to the need to repeat a lot of non-trivial logic which is better handled at a higher layer, such as the blockchain package.\r\n- The old database stored all blocks in leveldb.  This made it extremely inefficient to do things such as lookup headers and individual transactions since the entire block had to be loaded from leveldb (which entails it doing data copies) to get access.\r\n- The vast majority of database activity after the initial block download is read activity, however leveldb, as its name implies, is optimized for leveled write performance at the expense of read performance.\r\n\r\nIn order to address all of these concerns, and others not mentioned, the database interface has been redesigned as follows:\r\n\r\n- Two main categories of functionality are provided: block storage and metadata storage\r\n- All block storage and metadata storage are done via read-only and read-write MVCC transactions with both manual and managed modes\r\n  - Support for multiple concurrent readers and a single writer\r\n  - Readers use a snapshot and therefore are not blocked by the writer\r\n- Some key properties of the block storage and retrieval API:\r\n  - It is generic and does NOT contain additional bitcoin logic such spend tracking and block linking\r\n  - Provides access to the raw serialized bytes so deserialization is not forced for callers that don't need it\r\n  - Support for fetching headers via independent functions which allows implementations to provide significant optimizations\r\n  - Ability to efficiently retrieve arbitrary regions of blocks (transactions, scripts, etc)\r\n- A rich metadata storage API is provided:\r\n  - Key/value with arbitrary data\r\n  - Support for buckets and nested buckets\r\n  - Bucket iteration through a couple of different mechanisms\r\n  - Cursors for efficient and direct key seeking\r\n- Supports registration of backend database implementations\r\n- Comprehensive test coverage\r\n- Provides strong documentation with example usage\r\n\r\nThis pull request also contains an implementation of the previously discussed interface named ffboltdb (flat file plus boltdb metadata backend).  Here is a quick overview:\r\n\r\n- Highly optimized for read performance\r\n- All blocks are stored in flat files on the file system\r\n- Bulk block region fetching is optimized to perform linear reads which improves performance on spindle disks\r\n- The metadata storage uses boltdb under the hood which provides fast memory-mapped access\r\n  - NOTE: Due to the fact the database can get quite large and due to how the OS does memory-mapped files this can result in the process showing a huge amount of memory used.  This is typical for memory-mapped files, but the key thing to remember is the reported memory is not _really_ used because the OS will relinquish it as soon as other processes need it\r\n- Anti-corruption mechanisms:\r\n  - Flat files contain full block checksums to quickly an easily detect database corruption without needing to do expensive merkle root calculations\r\n  - Metadata checksums\r\n  - Open reconciliation\r\n- Extensive test coverage:\r\n  - Comprehensive blackbox interface testing\r\n  - Whitebox testing which uses intimate knowledge to exercise uncommon failure paths such as deleting files out from under the database\r\n  - Corruption tests (replacing random data in the files)\r\n\r\nIn addition, there is a new tool provided under the new database directory named dbtool which provides a few basic commands for testing the database.  It is designed around commands, so it could be useful to expand on in the future.\r\n\r\nFinally, this commit addresses the following issues:\r\n\r\n- Adds support for and therefore closes #255\r\n- Fixes #199\r\n- Fixes #201\r\n- Implements and closes #256\r\n- Obsoletes and closes #257\r\n- Closes #247 once the required chain and btcd modifications are in place\r\n  to make use of this new code\r\n\r\nThere are several things that need to happen before this PR can be merged and ultimately used:\r\n- [ ] More whitebox tests to top off the test coverage for ffboltdb (~93% at the time of PR submission)\r\n- [ ] Implement a WAL and/or coalescer to improve write performance\r\n- [ ] Provide a memory database backend\r\n- [ ] The blockchain package needs to be redesigned to pick up the functionality the new database is losing (spend tracking, chain linking, transaction index)\r\n- [ ] The blockchain package needs to be made safe for concurrency\r\n- [ ] btcd needs to be updated to use the redesigned blockchain and database packages","created_at":"2015-04-16T21:46:33Z","updated_at":"2015-04-28T20:11:29Z","closed_at":null,"merged_at":null,"merge_commit_sha":"5d4d236bbe6f00b2578580c31c947f00bcd6104c","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/btcsuite/btcd/pulls/380/commits","review_comments_url":"https://api.github.com/repos/btcsuite/btcd/pulls/380/comments","review_comment_url":"https://api.github.com/repos/btcsuite/btcd/pulls/comments{/number}","comments_url":"https://api.github.com/repos/btcsuite/btcd/issues/380/comments","statuses_url":"https://api.github.com/repos/btcsuite/btcd/statuses/3ef15eda645d82c0eef1986c85f41a0162c625ff","head":{"label":"davecgh:database2","ref":"database2","sha":"3ef15eda645d82c0eef1986c85f41a0162c625ff","user":{"login":"davecgh","id":2115102,"avatar_url":"https://avatars.githubusercontent.com/u/2115102?v=3","gravatar_id":"","url":"https://api.github.com/users/davecgh","html_url":"https://github.com/davecgh","followers_url":"https://api.github.com/users/davecgh/followers","following_url":"https://api.github.com/users/davecgh/following{/other_user}","gists_url":"https://api.github.com/users/davecgh/gists{/gist_id}","starred_url":"https://api.github.com/users/davecgh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/davecgh/subscriptions","organizations_url":"https://api.github.com/users/davecgh/orgs","repos_url":"https://api.github.com/users/davecgh/repos","events_url":"https://api.github.com/users/davecgh/events{/privacy}","received_events_url":"https://api.github.com/users/davecgh/received_events","type":"User","site_admin":false},"repo":{"id":16972952,"name":"btcd","full_name":"davecgh/btcd","owner":{"login":"davecgh","id":2115102,"avatar_url":"https://avatars.githubusercontent.com/u/2115102?v=3","gravatar_id":"","url":"https://api.github.com/users/davecgh","html_url":"https://github.com/davecgh","followers_url":"https://api.github.com/users/davecgh/followers","following_url":"https://api.github.com/users/davecgh/following{/other_user}","gists_url":"https://api.github.com/users/davecgh/gists{/gist_id}","starred_url":"https://api.github.com/users/davecgh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/davecgh/subscriptions","organizations_url":"https://api.github.com/users/davecgh/orgs","repos_url":"https://api.github.com/users/davecgh/repos","events_url":"https://api.github.com/users/davecgh/events{/privacy}","received_events_url":"https://api.github.com/users/davecgh/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/davecgh/btcd","description":"An alternative full node bitcoin implementation written in Go (golang)","fork":true,"url":"https://api.github.com/repos/davecgh/btcd","forks_url":"https://api.github.com/repos/davecgh/btcd/forks","keys_url":"https://api.github.com/repos/davecgh/btcd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/davecgh/btcd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/davecgh/btcd/teams","hooks_url":"https://api.github.com/repos/davecgh/btcd/hooks","issue_events_url":"https://api.github.com/repos/davecgh/btcd/issues/events{/number}","events_url":"https://api.github.com/repos/davecgh/btcd/events","assignees_url":"https://api.github.com/repos/davecgh/btcd/assignees{/user}","branches_url":"https://api.github.com/repos/davecgh/btcd/branches{/branch}","tags_url":"https://api.github.com/repos/davecgh/btcd/tags","blobs_url":"https://api.github.com/repos/davecgh/btcd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/davecgh/btcd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/davecgh/btcd/git/refs{/sha}","trees_url":"https://api.github.com/repos/davecgh/btcd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/davecgh/btcd/statuses/{sha}","languages_url":"https://api.github.com/repos/davecgh/btcd/languages","stargazers_url":"https://api.github.com/repos/davecgh/btcd/stargazers","contributors_url":"https://api.github.com/repos/davecgh/btcd/contributors","subscribers_url":"https://api.github.com/repos/davecgh/btcd/subscribers","subscription_url":"https://api.github.com/repos/davecgh/btcd/subscription","commits_url":"https://api.github.com/repos/davecgh/btcd/commits{/sha}","git_commits_url":"https://api.github.com/repos/davecgh/btcd/git/commits{/sha}","comments_url":"https://api.github.com/repos/davecgh/btcd/comments{/number}","issue_comment_url":"https://api.github.com/repos/davecgh/btcd/issues/comments{/number}","contents_url":"https://api.github.com/repos/davecgh/btcd/contents/{+path}","compare_url":"https://api.github.com/repos/davecgh/btcd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/davecgh/btcd/merges","archive_url":"https://api.github.com/repos/davecgh/btcd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/davecgh/btcd/downloads","issues_url":"https://api.github.com/repos/davecgh/btcd/issues{/number}","pulls_url":"https://api.github.com/repos/davecgh/btcd/pulls{/number}","milestones_url":"https://api.github.com/repos/davecgh/btcd/milestones{/number}","notifications_url":"https://api.github.com/repos/davecgh/btcd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/davecgh/btcd/labels{/name}","releases_url":"https://api.github.com/repos/davecgh/btcd/releases{/id}","created_at":"2014-02-19T03:45:51Z","updated_at":"2015-04-21T20:13:13Z","pushed_at":"2015-04-28T20:06:56Z","git_url":"git://github.com/davecgh/btcd.git","ssh_url":"git@github.com:davecgh/btcd.git","clone_url":"https://github.com/davecgh/btcd.git","svn_url":"https://github.com/davecgh/btcd","homepage":null,"size":9219,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"btcsuite:master","ref":"master","sha":"a4a52ae24f2244a23f13f829aa3632dd0de52d6c","user":{"login":"btcsuite","id":10235229,"avatar_url":"https://avatars.githubusercontent.com/u/10235229?v=3","gravatar_id":"","url":"https://api.github.com/users/btcsuite","html_url":"https://github.com/btcsuite","followers_url":"https://api.github.com/users/btcsuite/followers","following_url":"https://api.github.com/users/btcsuite/following{/other_user}","gists_url":"https://api.github.com/users/btcsuite/gists{/gist_id}","starred_url":"https://api.github.com/users/btcsuite/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/btcsuite/subscriptions","organizations_url":"https://api.github.com/users/btcsuite/orgs","repos_url":"https://api.github.com/users/btcsuite/repos","events_url":"https://api.github.com/users/btcsuite/events{/privacy}","received_events_url":"https://api.github.com/users/btcsuite/received_events","type":"Organization","site_admin":false},"repo":{"id":11931032,"name":"btcd","full_name":"btcsuite/btcd","owner":{"login":"btcsuite","id":10235229,"avatar_url":"https://avatars.githubusercontent.com/u/10235229?v=3","gravatar_id":"","url":"https://api.github.com/users/btcsuite","html_url":"https://github.com/btcsuite","followers_url":"https://api.github.com/users/btcsuite/followers","following_url":"https://api.github.com/users/btcsuite/following{/other_user}","gists_url":"https://api.github.com/users/btcsuite/gists{/gist_id}","starred_url":"https://api.github.com/users/btcsuite/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/btcsuite/subscriptions","organizations_url":"https://api.github.com/users/btcsuite/orgs","repos_url":"https://api.github.com/users/btcsuite/repos","events_url":"https://api.github.com/users/btcsuite/events{/privacy}","received_events_url":"https://api.github.com/users/btcsuite/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/btcsuite/btcd","description":"An alternative full node bitcoin implementation written in Go (golang)","fork":false,"url":"https://api.github.com/repos/btcsuite/btcd","forks_url":"https://api.github.com/repos/btcsuite/btcd/forks","keys_url":"https://api.github.com/repos/btcsuite/btcd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/btcsuite/btcd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/btcsuite/btcd/teams","hooks_url":"https://api.github.com/repos/btcsuite/btcd/hooks","issue_events_url":"https://api.github.com/repos/btcsuite/btcd/issues/events{/number}","events_url":"https://api.github.com/repos/btcsuite/btcd/events","assignees_url":"https://api.github.com/repos/btcsuite/btcd/assignees{/user}","branches_url":"https://api.github.com/repos/btcsuite/btcd/branches{/branch}","tags_url":"https://api.github.com/repos/btcsuite/btcd/tags","blobs_url":"https://api.github.com/repos/btcsuite/btcd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/btcsuite/btcd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/btcsuite/btcd/git/refs{/sha}","trees_url":"https://api.github.com/repos/btcsuite/btcd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/btcsuite/btcd/statuses/{sha}","languages_url":"https://api.github.com/repos/btcsuite/btcd/languages","stargazers_url":"https://api.github.com/repos/btcsuite/btcd/stargazers","contributors_url":"https://api.github.com/repos/btcsuite/btcd/contributors","subscribers_url":"https://api.github.com/repos/btcsuite/btcd/subscribers","subscription_url":"https://api.github.com/repos/btcsuite/btcd/subscription","commits_url":"https://api.github.com/repos/btcsuite/btcd/commits{/sha}","git_commits_url":"https://api.github.com/repos/btcsuite/btcd/git/commits{/sha}","comments_url":"https://api.github.com/repos/btcsuite/btcd/comments{/number}","issue_comment_url":"https://api.github.com/repos/btcsuite/btcd/issues/comments{/number}","contents_url":"https://api.github.com/repos/btcsuite/btcd/contents/{+path}","compare_url":"https://api.github.com/repos/btcsuite/btcd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/btcsuite/btcd/merges","archive_url":"https://api.github.com/repos/btcsuite/btcd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/btcsuite/btcd/downloads","issues_url":"https://api.github.com/repos/btcsuite/btcd/issues{/number}","pulls_url":"https://api.github.com/repos/btcsuite/btcd/pulls{/number}","milestones_url":"https://api.github.com/repos/btcsuite/btcd/milestones{/number}","notifications_url":"https://api.github.com/repos/btcsuite/btcd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/btcsuite/btcd/labels{/name}","releases_url":"https://api.github.com/repos/btcsuite/btcd/releases{/id}","created_at":"2013-08-06T18:10:52Z","updated_at":"2015-04-28T15:36:49Z","pushed_at":"2015-04-28T20:06:56Z","git_url":"git://github.com/btcsuite/btcd.git","ssh_url":"git@github.com:btcsuite/btcd.git","clone_url":"https://github.com/btcsuite/btcd.git","svn_url":"https://github.com/btcsuite/btcd","homepage":null,"size":18863,"stargazers_count":603,"watchers_count":603,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":123,"mirror_url":null,"open_issues_count":43,"forks":123,"open_issues":43,"watchers":603,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/btcsuite/btcd/pulls/380"},"html":{"href":"https://github.com/btcsuite/btcd/pull/380"},"issue":{"href":"https://api.github.com/repos/btcsuite/btcd/issues/380"},"comments":{"href":"https://api.github.com/repos/btcsuite/btcd/issues/380/comments"},"review_comments":{"href":"https://api.github.com/repos/btcsuite/btcd/pulls/380/comments"},"review_comment":{"href":"https://api.github.com/repos/btcsuite/btcd/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/btcsuite/btcd/pulls/380/commits"},"statuses":{"href":"https://api.github.com/repos/btcsuite/btcd/statuses/3ef15eda645d82c0eef1986c85f41a0162c625ff"}}}}