blockdb

package
v1.14.1-db-metrics-fix Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2025 License: BSD-3-Clause Imports: 18 Imported by: 0

README

BlockDB

BlockDB is a specialized database optimized for blockchain blocks.

Key Functionalities

  • O(1) Performance: Both reads and writes complete in constant time
  • Parallel Operations: Multiple threads can read and write blocks concurrently without blocking
  • Flexible Write Ordering: Supports out-of-order block writes for bootstrapping
  • Configurable Durability: Optional syncToDisk mode guarantees immediate recoverability
  • Automatic Recovery: Detects and recovers unindexed blocks after unclean shutdowns
  • Block Compression: zstd compression for block data

Design

BlockDB uses a single index file and multiple data files. The index file maps block heights to locations in the data files, while data files store the actual block content. Data storage can be split across multiple data files based on the maximum data file size.

┌─────────────────┐         ┌─────────────────┐
│   Index File    │         │  Data File 1    │
│   (.idx)        │         │   (.dat)        │
├─────────────────┤         ├─────────────────┤
│ Header          │         │ Block 0         │
│ - Version       │  ┌─────>│ - Header        │
│ - Min Height    │  │      │ - Data          │
│ - Max Height    │  │      ├─────────────────┤
│ - Data Size     │  │      │ Block 1         │
│ - ...           │  │  ┌──>│ - Header        │
├─────────────────┤  │  │   │ - Data          │
│ Entry[0]        │  │  │   ├─────────────────┤
│ - Offset ───────┼──┘  │   │     ...         │
│ - Size          │     │   └─────────────────┘
│ - Header Size   │     │
├─────────────────┤     │
│ Entry[1]        │     │
│ - Offset ───────┼─────┘
│ - Size          │
│ - Header Size   │
├─────────────────┤
│     ...         │
└─────────────────┘
File Formats
Index File Structure

The index file consists of a fixed-size header followed by fixed-size entries:

Index File Header (64 bytes):
┌────────────────────────────────┬─────────┐
│ Field                          │ Size    │
├────────────────────────────────┼─────────┤
│ Version                        │ 8 bytes │
│ Max Data File Size             │ 8 bytes │
│ Min Block Height               │ 8 bytes │
│ Max Block Height               │ 8 bytes │
│ Next Write Offset              │ 8 bytes │
│ Reserved                       │ 24 bytes│
└────────────────────────────────┴─────────┘

Index Entry (16 bytes):
┌────────────────────────────────┬─────────┐
│ Field                          │ Size    │
├────────────────────────────────┼─────────┤
│ Data File Offset               │ 8 bytes │
│ Block Data Size                │ 4 bytes │
│ Reserved                       │ 4 bytes │
└────────────────────────────────┴─────────┘
Data File Structure

Each block in the data file is stored with a block entry header followed by the raw block data:

Block Entry Header (22 bytes):
┌────────────────────────────────┬─────────┐
│ Field                          │ Size    │
├────────────────────────────────┼─────────┤
│ Height                         │ 8 bytes │
│ Size                           │ 4 bytes │
│ Checksum                       │ 8 bytes │
│ Version                        │ 2 bytes │
└────────────────────────────────┴─────────┘
Block Overwrites

BlockDB allows overwriting blocks at existing heights. When a block is overwritten, the new block is appended to the data file and the index entry is updated to point to the new location, leaving the old block data as unreferenced "dead" space. However, since blocks are immutable and rarely overwritten (e.g., during reorgs), this trade-off should have minimal impact in practice.

Fixed-Size Index Entries

Each index entry is exactly 16 bytes on disk, containing the offset, size, and reserved bytes for future use. This fixed size enables direct calculation of where each block's index entry is located, providing O(1) lookups. For blockchains with high block heights, the index remains efficient, even at height 1 billion, the index file would only be ~16GB.

Durability and Fsync Behavior

BlockDB provides configurable durability through the syncToDisk parameter:

Data File Behavior:

  • When syncToDisk=true: The data file is fsync'd after every block write, guaranteeing durability against both process failures and kernel/machine failures.
  • When syncToDisk=false: Data file writes are buffered, providing durability against process failures but not against kernel or machine failures.

Index File Behavior:

  • When syncToDisk=true: The index file is fsync'd every CheckpointInterval blocks (when the header is written).
  • When syncToDisk=false: The index file relies on OS buffering and is not explicitly fsync'd.
Recovery Mechanism

On startup, BlockDB checks for signs of an unclean shutdown by comparing the data file size on disk with the indexed data size stored in the index file header. If the data files are larger than what the index claims, it indicates that blocks were written but the index wasn't properly updated before shutdown.

Recovery Process:

  1. Starts scanning from where the index left off (NextWriteOffset)
  2. For each unindexed block found:
    • Validates the block entry header and checksum
    • Writes the corresponding index entry
  3. Calculates the max block height
  4. Updates the index header with the updated max block height and next write offset

Usage

Creating a Database
import (
    "errors"
    "github.com/ava-labs/avalanchego/x/blockdb"
)

config := blockdb.DefaultConfig().
    WithDir("/path/to/blockdb")
db, err := blockdb.New(config, logging.NoLog{})
if err != nil {
    fmt.Println("Error creating database:", err)
    return
}
defer db.Close()
Writing and Reading Blocks
// Write a block
height := uint64(100)
blockData := []byte("block data")
err := db.Put(height, blockData)
if err != nil {
    fmt.Println("Error writing block:", err)
    return
}

// Read a block
blockData, err := db.Get(height)
if err != nil {
    if errors.Is(err, database.ErrNotFound) {
        fmt.Println("Block doesn't exist at this height")
        return
    }
    fmt.Println("Error reading block:", err)
    return
}

TODO

  • Implement a block cache for recently accessed blocks
  • Use a buffered pool to avoid allocations on reads and writes
  • Add performance benchmarks
  • Consider supporting missing data files (currently we error if any data files are missing)

Documentation

Index

Constants

View Source
const (

	// IndexFileVersion is the version of the index file format.
	IndexFileVersion uint64 = 1

	// BlockEntryVersion is the version of the block entry.
	BlockEntryVersion uint16 = 1
)
View Source
const DefaultMaxDataFileSize = 500 * 1024 * 1024 * 1024

DefaultMaxDataFileSize is the default maximum size of the data block file in bytes (500GB).

View Source
const DefaultMaxDataFiles = 10

DefaultMaxDataFiles is the default maximum number of data files descriptors cached.

Variables

View Source
var (
	ErrInvalidBlockHeight = errors.New("blockdb: invalid block height")
	ErrCorrupted          = errors.New("blockdb: unrecoverable corruption detected")
	ErrBlockTooLarge      = errors.New("blockdb: block size too large")
)

Functions

This section is empty.

Types

type BlockData

type BlockData = []byte

BlockData defines the type for block data.

type BlockHeight

type BlockHeight = uint64

BlockHeight defines the type for block heights.

type Database

type Database struct {
	// contains filtered or unexported fields
}

Database stores blockchain blocks on disk and provides methods to read and write blocks.

func New

func New(config DatabaseConfig, log logging.Logger) (*Database, error)

New creates a block database. Parameters:

  • config: Configuration parameters
  • log: Logger instance for structured logging

func (*Database) Close

func (s *Database) Close() error

Close flushes pending writes and closes the store files.

func (*Database) Get

func (s *Database) Get(height BlockHeight) (BlockData, error)

Get retrieves a block by its height. Returns database.ErrNotFound if the block is not found.

func (*Database) Has

func (s *Database) Has(height BlockHeight) (bool, error)

Has checks if a block exists at the given height.

func (*Database) Put

func (s *Database) Put(height BlockHeight, block BlockData) error

Put inserts a block into the store at the given height.

type DatabaseConfig

type DatabaseConfig struct {
	// IndexDir is the directory where the index file is stored.
	IndexDir string

	// DataDir is the directory where the data files are stored.
	DataDir string

	// MinimumHeight is the lowest block height tracked by the database.
	MinimumHeight uint64

	// MaxDataFileSize sets the maximum size of the data block file in bytes.
	MaxDataFileSize uint64

	// MaxDataFiles is the maximum number of data files descriptors cached.
	MaxDataFiles int

	// CheckpointInterval defines how frequently (in blocks) the index file header is updated (default: 1024).
	CheckpointInterval uint64

	// SyncToDisk determines if fsync is called after each write for durability.
	SyncToDisk bool
}

DatabaseConfig contains configuration parameters for BlockDB.

func DefaultConfig

func DefaultConfig() DatabaseConfig

DefaultConfig returns the default options for BlockDB.

func (DatabaseConfig) Validate

func (c DatabaseConfig) Validate() error

Validate checks if the store options are valid.

func (DatabaseConfig) WithCheckpointInterval

func (c DatabaseConfig) WithCheckpointInterval(interval uint64) DatabaseConfig

WithCheckpointInterval returns a copy of the config with CheckpointInterval set to the given value.

func (DatabaseConfig) WithDataDir

func (c DatabaseConfig) WithDataDir(dataDir string) DatabaseConfig

WithDataDir returns a copy of the config with DataDir set to the given value.

func (DatabaseConfig) WithDir

func (c DatabaseConfig) WithDir(directory string) DatabaseConfig

WithDir sets both IndexDir and DataDir to the given value.

func (DatabaseConfig) WithIndexDir

func (c DatabaseConfig) WithIndexDir(indexDir string) DatabaseConfig

WithIndexDir returns a copy of the config with IndexDir set to the given value.

func (DatabaseConfig) WithMaxDataFileSize

func (c DatabaseConfig) WithMaxDataFileSize(maxSize uint64) DatabaseConfig

WithMaxDataFileSize returns a copy of the config with MaxDataFileSize set to the given value.

func (DatabaseConfig) WithMaxDataFiles

func (c DatabaseConfig) WithMaxDataFiles(maxFiles int) DatabaseConfig

WithMaxDataFiles returns a copy of the config with MaxDataFiles set to the given value.

func (DatabaseConfig) WithMinimumHeight

func (c DatabaseConfig) WithMinimumHeight(minHeight uint64) DatabaseConfig

WithMinimumHeight returns a copy of the config with MinimumHeight set to the given value.

func (DatabaseConfig) WithSyncToDisk

func (c DatabaseConfig) WithSyncToDisk(syncToDisk bool) DatabaseConfig

WithSyncToDisk returns a copy of the config with SyncToDisk set to the given value.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL