hsd
Version:
Cryptocurrency bike-shed
149 lines (108 loc) • 4.48 kB
Markdown
BlockStore `lib/blockstore` is a handshake 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:
```js
// nodejs
const store = blockstore.create({
network: 'regtest',
prefix: '/tmp/blockstore'
});
await store.ensure();
await store.open();
await store.write(hash, block);
```
```sh
// 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