Skip to content

Latest commit

 

History

History
 
 

blockstore

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

BlockStore

BlockStore lib/blockstore is a bcoin module intended to be used as a backend for storing block and undo coin data. It includes a backend that uses flat files for storage. Its key benefit is performance improvements across the board in disk I/O, which is the major bottleneck for the initial block sync.

Blocks are stored in wire format directly to the disk, while some additional metadata is stored in a key-value store, i.e. LevelDB, to help with the data management. Both the flat files and the metadata db, are exposed through a unified interace so that the users can simply read and write blocks without having to worry about managing data layout on the disk.

In addition to blocks, undo coin data, which is used to revert the changes applied by a block (in case of a re-org), is also stored on disk, in a similar fashion.

Interface

The AbstractBlockStore interface defines the following abstract methods to be defined by concrete implementations:

Basic housekeeping

  • ensure()
  • open()
  • close()

Block I/O

  • read(hash, offset, size)
  • write(hash, data)
  • prune(hash)
  • has(hash)

Undo Coins I/O

  • readUndo(hash)
  • writeUndo(hash, data)
  • pruneUndo(hash)
  • hasUndo(hash)

The interface is implemented by FileBlockStore and LevelBlockStore, backed by flat files and LevelDB respectively. We will focus here on the FileBlockStore, which is the backend that implements a flat file based storage.

FileBlockStore

FileBlockStore implements the flat file backend for AbstractBlockStore. As the name suggests, it uses flat files for block/undo data and LevelDB for metadata.

Let's create a file blockstore, write a block and walk-through the disk storage:

// nodejs
const store = blockstore.create({
  network: 'regtest',
  prefix: '/tmp/blockstore'
});
await store.ensure();
await store.open();
await store.write(hash, block);
// shell
tree /tmp/blockstore/
/tmp/blockstore/
└── blocks
    ├── blk00000.dat
    └── index
        ├── LOG
        ...

As we can see, the store writes to the file blk00000.dat in /tmp/blockstore/blocks/, and the metadata is written to /tmp/blockstore/index.

Raw blocks are written to the disk in flat files named blkXXXXX.dat, where XXXXX is the number of file being currently written, starting at blk00000.dat. We store the file number as an integer in the metadata db, expanding the digits to five places.

The metadata db key layout.F tracks the last file used for writing. Each file in turn tracks the number of blocks in it, the number of bytes used and its max length. This data is stored in the db key layout.f.

f['block'][0] => [1, 5, 128]  // blk00000.dat: 1 block written, 5 bytes used, 128 bytes length
F['block'] => 0   // writing to file blk00000.dat

Each raw block data is preceded by a magic marker defined as follows, to help identify data written by us:

magic (8 bytes) = network.magic (4 bytes) + block data length (4 bytes)

For raw undo block data, the hash of the block is also included:

magic (40 bytes) = network.magic (4 bytes) + length (4 bytes) + hash (32 bytes)

But a marker alone is not sufficient to track the data we write to the files. For each block we write, we need to store a pointer to the position in the file where to start reading, and the size of the data we need to seek. This data is stored in the metadata db using the key layout.b:

b['block']['hash'] => [0, 8, 285] // 'hash' points to file blk00000.dat, position 8, size 285

Using this we know that our block is in blk00000.dat, bytes 8 through 293 and its size is 285 bytes.

Note that the position indicates that the block data is preceded by 8 bytes of the magic marker.

Examples:

store.write('hash', 'block')

blk00000:
    0xfabfb5da05000000 block

index:
    b['block']['hash'] => [0, 8, 5]
    f['block'][0] => [1, 13, 128]
    F['block'] => 0

store.write('hash1', 'block1')

blk00000:
    0xfabfb5da05000000 block 0xfabfb5da06000000 block1

index:
    b['block']['hash'] => [0, 8, 5]
    b['block']['hash1'] => [0, 13, 6]
    f['block'][0] => [2, 19, 128]
    F['block'] => 0

store.prune('hash1', 'block1')

blk00000:
    0xfabfb5da05000000 block 0xfabfb5da06000000 block1

index:
    b['block']['hash'] => [0, 8, 5]
    f['block'][0] => [1, 19, 128]
    F['block'] => 0