Skip to content

Commit 15ae1d1

Browse files
committed
feat: add tests and update blockdb to have separate methods to read header and body
1 parent cf35473 commit 15ae1d1

File tree

11 files changed

+1308
-352
lines changed

11 files changed

+1308
-352
lines changed

x/blockdb/README.md

Lines changed: 52 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# BlockDB
22

3-
BlockDB is a specialized storage system designed for blockchain blocks. It provides O(1) write performance with support for parallel operations. Unlike general-purpose key-value stores like LevelDB that require periodic compaction, BlockDB's append-only design ensures consistently fast writes without the overhead of background maintenance operations.
3+
BlockDB is a specialized database optimized for blockchain blocks.
44

55
## Key Functionalities
66

@@ -9,51 +9,47 @@ BlockDB is a specialized storage system designed for blockchain blocks. It provi
99
- **Flexible Write Ordering**: Supports out-of-order block writes for bootstrapping
1010
- **Configurable Durability**: Optional `syncToDisk` mode guarantees immediate recoverability
1111
- **Automatic Recovery**: Detects and recovers unindexed blocks after unclean shutdowns
12-
- **Data Integrity**: Checksums verify block data on reads
1312

14-
## Architecture
13+
## Design
1514

16-
BlockDB uses two file types: index files and data files. The index file maps block heights to locations in data files, while data files store the actual block content. Data storage can be split across multiple files based on size limits.
15+
BlockDB uses two file types: index files and data files. The index file maps block heights to locations in data files, while data files store the actual block content. Data storage can be split across multiple files based on the maximum data file size.
1716

1817
```
1918
┌─────────────────┐ ┌─────────────────┐
2019
│ Index File │ │ Data File 1 │
2120
│ (.idx) │ │ (.dat) │
2221
├─────────────────┤ ├─────────────────┤
23-
│ Header │ │ Block 1
22+
│ Header │ │ Block 0
2423
│ - Version │ ┌─────>│ - Header │
2524
│ - Min Height │ │ │ - Data │
26-
│ - MCH │ │ ├─────────────────┤
27-
│ - Data Size │ │ │ Block 2 │
28-
│ - ... │ │ │ │
29-
├─────────────────┤ │ ┌──>│ - Header │
30-
│ Entry[0] │ │ │ │ - Data │
31-
│ - Offset ───────┼──┘ │ ├─────────────────┤
32-
│ - Size │ │ │ ... │
33-
├─────────────────┤ │ └─────────────────┘
34-
│ Entry[1] │ │
35-
│ - Offset ───────┼─────┘ ┌─────────────────┐
36-
│ - Size │ │ Data File 2 │
37-
├─────────────────┤ │ (.dat) │
38-
│ ... │ ├─────────────────┤
39-
└─────────────────┘ │ Block N │
40-
│ - Header │
41-
│ - Data │
42-
├─────────────────┤
25+
│ - Max Height │ │ ├─────────────────┤
26+
│ - Data Size │ │ │ Block 1 │
27+
│ - ... │ │ │ - Header │
28+
├─────────────────┤ │ ┌──>│ - Data │
29+
│ Entry[0] │ │ │ ├─────────────────┤
30+
│ - Offset ───────┼──┘ │ │ ... │
31+
│ - Size │ │ └─────────────────┘
32+
│ - Header Size │ │
33+
├─────────────────┤ │ ┌─────────────────┐
34+
│ Entry[1] │ │ │ Data File 2 │
35+
│ - Offset ───────┼─────┘ │ (.dat) │
36+
│ - Size │ ├─────────────────┤
37+
│ - Header Size │ │ Block N │
38+
├─────────────────┤ │ - Header │
39+
│ ... │ │ - Data │
40+
└─────────────────┘ ├─────────────────┤
4341
│ ... │
4442
└─────────────────┘
4543
```
4644

47-
## Implementation Details
48-
4945
### File Formats
5046

5147
#### Index File Structure
5248

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

5551
```
56-
Index File Header (48 bytes):
52+
Index File Header (72 bytes):
5753
┌────────────────────────────────┬─────────┐
5854
│ Field │ Size │
5955
├────────────────────────────────┼─────────┤
@@ -63,14 +59,16 @@ Index File Header (48 bytes):
6359
│ Min Block Height │ 8 bytes │
6460
│ Max Contiguous Height │ 8 bytes │
6561
│ Data File Size │ 8 bytes │
62+
│ Reserved │ 24 bytes│
6663
└────────────────────────────────┴─────────┘
6764
68-
Index Entry (16 bytes):
65+
Index Entry (18 bytes):
6966
┌────────────────────────────────┬─────────┐
7067
│ Field │ Size │
7168
├────────────────────────────────┼─────────┤
7269
│ Data File Offset │ 8 bytes │
7370
│ Block Data Size │ 8 bytes │
71+
│ Header Size │ 2 bytes │
7472
└────────────────────────────────┴─────────┘
7573
```
7674

@@ -79,88 +77,45 @@ Index Entry (16 bytes):
7977
Each block in the data file is stored with a header followed by the raw block data:
8078

8179
```
82-
Block Header (24 bytes):
80+
Block Header (26 bytes):
8381
┌────────────────────────────────┬─────────┐
8482
│ Field │ Size │
8583
├────────────────────────────────┼─────────┤
8684
│ Height │ 8 bytes │
8785
│ Size │ 8 bytes │
86+
│ Header Size │ 2 bytes │
8887
│ Checksum │ 8 bytes │
8988
└────────────────────────────────┴─────────┘
9089
```
9190

92-
### Design Decisions
93-
94-
#### Append-Only Architecture
95-
96-
BlockDB is strictly append-only with no support for deletions. This aligns with blockchain's immutable nature and provides:
97-
98-
- Simplified concurrency model
99-
- Predictable write performance
100-
- Straightforward recovery logic
101-
- No compaction overhead
102-
103-
**Trade-off**: Overwriting a block leaves the old data as unreferenced "dead" space. However, since blocks are immutable and rarely overwritten (only during reorgs), this trade-off has minimal impact in practice.
104-
105-
#### Fixed-Size Index Entries
106-
107-
Each index entry is exactly 16 bytes, containing the offset and size. 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.
108-
109-
#### Two File Type Separation
110-
111-
Separating index and data provides several benefits:
91+
### Block Overwrites
11292

113-
- Index files remain relatively small and can benefit from SSD storage
114-
- Data files can use cheaper storage and be backed up independently
115-
- Sequential append-only writes to data files minimize fragmentation
116-
- Index can be rebuilt by scanning data files if needed
93+
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.
11794

118-
#### Out-of-Order Block Writing
95+
### Fixed-Size Index Entries
11996

120-
Blocks can be written at any height regardless of arrival order. This is essential for blockchain nodes that may receive blocks out of sequence during syncing operations.
97+
Each index entry is exactly 18 bytes on disk, containing the offset, size, and header size. 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 ~18GB.
12198

122-
#### Durability and Fsync Behavior
99+
### Durability and Fsync Behavior
123100

124101
BlockDB provides configurable durability through the `syncToDisk` parameter:
125102

126103
- When enabled, the data file is fsync'd after every block write, guaranteeing immediate durability
127104
- The index file is fsync'd periodically (every `CheckpointInterval` blocks) to balance performance and recovery time
128105
- When disabled, writes rely on OS buffering, trading durability for significantly better performance
129106

130-
### Key Operations
131-
132-
#### Write Performance
133-
134-
- **Time Complexity**: O(1) to write a block
135-
- **I/O Pattern**: Sequential append to data file + single index entry write
136-
- **Block Size Impact**: While index operations are O(1), total write time depends on block size. With a maximum block size enforced, write time remains bounded, maintaining effectively O(1) performance.
137-
138-
#### Read Performance
139-
140-
- **Time Complexity**: O(1) to read a block
141-
- **I/O Pattern**: One index read + one data read
142-
- **Concurrency**: Multiple blocks can be read in parallel
143-
144-
#### Recovery Mechanism
107+
### Recovery Mechanism
145108

146109
On startup, BlockDB checks for signs of an unclean shutdown. If detected, it performs recovery:
147110

148-
1. Compares the data file size with the indexed data size (stored in index header)
149-
2. If data file is larger, starts scanning from where the index left off
111+
1. Compares the data file size with the indexed data size (stored in the index header)
112+
2. If the data file is larger, it starts scanning from where the index left off
150113
3. For each unindexed block found:
151-
- Validates block header and checksum
114+
- Validates the block header and checksum
152115
- Writes the corresponding index entry
153-
4. Updates maximum contiguous height
116+
4. Updates the max contiguous height and max block height
154117
5. Persists the updated index header
155118

156-
### Concurrency Model
157-
158-
BlockDB uses a reader-writer lock for overall thread safety, with atomic operations for write coordination:
159-
160-
- Multiple threads can read different blocks simultaneously without blocking
161-
- Multiple threads can write concurrently - they use atomic operations to allocate unique space in the data file
162-
- The reader-writer lock ensures consistency between reads and writes
163-
164119
## Usage
165120

166121
### Creating a Database
@@ -169,8 +124,6 @@ BlockDB uses a reader-writer lock for overall thread safety, with atomic operati
169124
import "github.com/ava-labs/avalanchego/x/blockdb"
170125

171126
config := blockdb.DefaultDatabaseOptions()
172-
config.MinimumHeight = 1
173-
174127
db, err := blockdb.New(
175128
"/path/to/index", // Index directory
176129
"/path/to/data", // Data directory
@@ -180,35 +133,37 @@ db, err := blockdb.New(
180133
logger,
181134
)
182135
if err != nil {
183-
return err
136+
fmt.Println("Error creating database:", err)
137+
return
184138
}
185139
defer db.Close()
186140
```
187141

188142
### Writing and Reading Blocks
189143

190144
```go
191-
// Write a block
145+
// Write a block with header size
192146
height := uint64(100)
193147
blockData := []byte("block data...")
194-
err := db.WriteBlock(height, blockData)
148+
headerSize := uint16(500) // First 500 bytes are the header
149+
err := db.WriteBlock(height, blockData, headerSize)
195150

196-
// Read a block
151+
// Read a complete block
197152
blockData, err := db.ReadBlock(height)
198-
if err == blockdb.ErrBlockNotFound {
153+
if blockData == nil {
199154
// Block doesn't exist at this height
200155
}
201156

202-
// Query database state
203-
maxContiguous := db.MaxContiguousHeight()
204-
minHeight := db.MinHeight()
157+
// Read block components separately
158+
headerData, err := db.ReadHeader(height)
159+
bodyData, err := db.ReadBody(height)
205160
```
206161

207162
## TODO
208163

209-
- [ ] **Multiple Data Files**: Split data across multiple files when MaxDataFileSize is reached
210-
- [ ] **Block Cache**: Implement circular buffer cache for recently accessed blocks
211-
- [ ] **Enforced In-Order Writes**: Optional mode to require blocks be written sequentially, preventing gaps
212-
- [ ] **User buffered pool**: Use a buffered pool for fetch index entries and block headers to avoid allocations
213-
- [ ] **Unit Tests**: Add comprehensive test coverage for all core functionality
214-
- [ ] **Benchmarks**: Add performance benchmarks for all major operations
164+
- [ ] Compress data files to reduce storage size
165+
- [ ] Split data across multiple files when `MaxDataFileSize` is reached
166+
- [ ] Implement a block cache for recently accessed blocks
167+
- [ ] Use a buffered pool to avoid allocations on reads and writes
168+
- [ ] Add tests for core functionality
169+
- [ ] Add performance benchmarks

0 commit comments

Comments
 (0)