Files
node/x/blockdb/writeblock_test.go
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

336 lines
8.6 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package blockdb
import (
"math"
"os"
"strings"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/compress"
safemath "github.com/luxfi/math"
)
func TestWriteBlock_Basic(t *testing.T) {
customConfig := DefaultConfig().WithMinimumHeight(10)
tests := []struct {
name string
blockHeights []uint64 // block heights to write, in order
config DatabaseConfig
expectedMCH uint64 // expected max contiguous height
expectedMaxHeight uint64
syncToDisk bool
checkpointInterval uint64
}{
{
name: "no blocks to write",
expectedMCH: unsetHeight,
expectedMaxHeight: unsetHeight,
},
{
name: "single block at min height",
blockHeights: []uint64{0},
expectedMCH: 0,
expectedMaxHeight: 0,
},
{
name: "sequential blocks from min",
blockHeights: []uint64{0, 1, 2, 3},
expectedMCH: 3,
expectedMaxHeight: 3,
},
{
name: "out of order with no gaps",
blockHeights: []uint64{3, 1, 2, 0, 4},
expectedMCH: 4,
expectedMaxHeight: 4,
},
{
name: "blocks with gaps",
blockHeights: []uint64{0, 1, 3, 5, 6},
expectedMCH: 1,
expectedMaxHeight: 6,
},
{
name: "start with gap",
blockHeights: []uint64{5, 6},
expectedMCH: unsetHeight,
expectedMaxHeight: 6,
},
{
name: "overwrite same height",
blockHeights: []uint64{0, 1, 0}, // Write to height 0 twice
expectedMCH: 1,
expectedMaxHeight: 1,
},
{
name: "custom min height single block",
blockHeights: []uint64{10},
config: customConfig,
expectedMCH: 10,
expectedMaxHeight: 10,
},
{
name: "custom min height out of order",
blockHeights: []uint64{13, 11, 10, 12},
config: customConfig,
expectedMCH: 13,
expectedMaxHeight: 13,
},
{
name: "custom min height with gaps",
blockHeights: []uint64{10, 11, 13, 15},
config: customConfig,
expectedMCH: 11,
expectedMaxHeight: 15,
},
{
name: "custom min height start with gap",
blockHeights: []uint64{11, 12},
config: customConfig,
expectedMCH: unsetHeight,
expectedMaxHeight: 12,
},
{
name: "with sync to disk",
blockHeights: []uint64{0, 1, 2, 5},
syncToDisk: true,
expectedMCH: 2,
expectedMaxHeight: 5,
},
{
name: "custom checkpoint interval",
blockHeights: []uint64{0, 1, 2, 3, 4},
checkpointInterval: 2,
expectedMCH: 4,
expectedMaxHeight: 4,
},
{
name: "complicated gaps",
blockHeights: []uint64{
10, 3, 2, 9, 35, 34, 30, 1, 9, 88, 83, 4, 43, 5, 0,
},
expectedMCH: 5,
expectedMaxHeight: 88,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := tt.config
if config.CheckpointInterval == 0 {
config = DefaultConfig()
}
store, cleanup := newTestDatabase(t, config)
defer cleanup()
blocksWritten := make(map[uint64][]byte)
for _, h := range tt.blockHeights {
block := randomBlock(t)
err := store.WriteBlock(h, block)
require.NoError(t, err, "unexpected error at height %d", h)
blocksWritten[h] = block
}
// Verify all written blocks are readable and data is correct
for h, expectedBlock := range blocksWritten {
readBlock, err := store.ReadBlock(h)
require.NoError(t, err, "ReadBlock failed at height %d", h)
require.Equal(t, expectedBlock, readBlock)
}
checkDatabaseState(t, store, tt.expectedMaxHeight, tt.expectedMCH)
})
}
}
func TestWriteBlock_Concurrency(t *testing.T) {
store, cleanup := newTestDatabase(t, DefaultConfig())
defer cleanup()
var wg sync.WaitGroup
var errors atomic.Int32
// Pre-generate blocks for reuse
blocks := make([][]byte, 20)
for i := range 20 {
blocks[i] = randomBlock(t)
wg.Add(1)
go func(i int) {
defer wg.Done()
var height uint64
block := blocks[i]
// create gaps at heights 5 and 10 and rewrite last block
if i == 5 || i == 10 {
height = uint64(i - 1)
block = blocks[i-1]
} else {
height = uint64(i)
}
err := store.WriteBlock(height, block)
if err != nil {
errors.Add(1)
}
}(i)
}
wg.Wait()
require.Zero(t, errors.Load(), "concurrent writes had errors")
// Verify that all expected heights have blocks (except 5, 10)
for i := range 20 {
height := uint64(i)
block, err := store.ReadBlock(height)
if i == 5 || i == 10 {
require.ErrorIs(t, err, ErrBlockNotFound, "expected ErrBlockNotFound at gap height %d", height)
} else {
require.NoError(t, err)
require.Equal(t, blocks[i], block, "block mismatch at height %d", height)
}
}
checkDatabaseState(t, store, 19, 4)
}
func TestWriteBlock_Errors(t *testing.T) {
tests := []struct {
name string
height uint64
block []byte
setup func(db *Database)
config DatabaseConfig
disableCompression bool
wantErr error
wantErrMsg string
// needsUnprivileged marks a case whose failure is provoked by FILE
// PERMISSIONS. root ignores the permission bits, so the operation
// succeeds there and the case cannot prove anything.
needsUnprivileged bool
}{
{
name: "empty block nil",
height: 0,
block: nil,
wantErr: ErrBlockEmpty,
},
{
name: "empty block zero length",
height: 0,
block: []byte{},
wantErr: ErrBlockEmpty,
},
{
name: "height below custom minimum",
height: 5,
block: randomBlock(t),
config: DefaultConfig().WithMinimumHeight(10),
wantErr: ErrInvalidBlockHeight,
},
{
name: "height causes overflow",
height: math.MaxUint64,
block: randomBlock(t),
wantErr: ErrInvalidBlockHeight,
},
{
name: "database closed",
height: 0,
block: randomBlock(t),
setup: func(db *Database) {
db.Close()
},
wantErr: ErrDatabaseClosed,
},
{
name: "exceed max data file size",
height: 0,
disableCompression: true,
block: make([]byte, 1003), // Block + header will exceed 1024 limit (1003 + 26 = 1029 > 1024)
config: DefaultConfig().WithMaxDataFileSize(1024),
wantErr: ErrBlockTooLarge,
},
{
name: "data file offset overflow",
height: 0,
block: make([]byte, 100),
disableCompression: true,
config: DefaultConfig(),
setup: func(db *Database) {
// Set the next write offset to near max to trigger overflow
db.nextDataWriteOffset.Store(math.MaxUint64 - 50)
},
wantErr: safemath.ErrOverflow,
},
{
name: "writeBlockAt - failed to get data file",
height: 0,
block: make([]byte, 100),
setup: func(db *Database) {
// Change file permissions to read-only
file, err := db.getOrOpenDataFile(0)
require.NoError(t, err)
filePath := file.Name()
file.Close()
require.NoError(t, os.Chmod(filePath, 0o444))
},
wantErrMsg: "failed to get data file for writing block",
needsUnprivileged: true,
},
{
name: "writeIndexEntryAt - index file write failure",
height: 0,
block: make([]byte, 100),
setup: func(db *Database) {
db.indexFile.Close()
},
wantErrMsg: "failed to write index entry",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := tt.config
if config.CheckpointInterval == 0 {
config = DefaultConfig()
}
if tt.needsUnprivileged && os.Geteuid() == 0 {
t.Skip("runs as root, which bypasses the permission bits this case relies on")
}
store, cleanup := newTestDatabase(t, config)
if tt.disableCompression {
store.compressor = compress.NewNoCompressor()
}
defer cleanup()
if tt.setup != nil {
tt.setup(store)
}
err := store.WriteBlock(tt.height, tt.block)
if tt.wantErrMsg != "" {
// require.Error FIRST: err.Error() on a nil err panics, and a
// panic in a subtest aborts the entire package -- which is how
// one wrong case took down all of x/blockdb and hid its own name.
require.Error(t, err, "expected an error starting with %s", tt.wantErrMsg)
require.True(t, strings.HasPrefix(err.Error(), tt.wantErrMsg), "expected error message to start with %s, got %s", tt.wantErrMsg, err.Error())
} else {
require.ErrorIs(t, err, tt.wantErr)
}
checkDatabaseState(t, store, unsetHeight, unsetHeight)
})
}
}