Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tbcd: move block header cache from items to size #360

Merged
merged 18 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions cmd/tbcd/tbcd.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2024 Hemi Labs, Inc.
// Copyright (c) 2024-2025 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

Expand All @@ -25,7 +25,8 @@ const (
defaultLogLevel = daemonName + "=INFO;tbc=INFO;level=INFO"
defaultNetwork = "testnet3" // XXX make this mainnet
defaultHome = "~/." + daemonName
bhsDefault = int(1e6) // enough for mainnet
bDefaultSize = "1gb" // ~640 blocks on mainnet
bhsDefaultSize = "128mb" // enough for mainnet
)

var (
Expand All @@ -46,16 +47,16 @@ var (
Help: "enable auto utxo and tx indexes",
Print: config.PrintAll,
},
"TBC_BLOCK_CACHE": config.Config{
Value: &cfg.BlockCache,
DefaultValue: 250,
Help: "number of cached blocks",
"TBC_BLOCK_CACHE_SIZE": config.Config{
Value: &cfg.BlockCacheSize,
DefaultValue: bDefaultSize,
Help: "size of block cache",
Print: config.PrintAll,
},
"TBC_BLOCKHEADER_CACHE": config.Config{
Value: &cfg.BlockheaderCache,
DefaultValue: bhsDefault,
Help: "number of cached blockheaders",
"TBC_BLOCKHEADER_CACHE_SIZE": config.Config{
Value: &cfg.BlockheaderCacheSize,
DefaultValue: bhsDefaultSize,
Help: "size of blockheader cache",
Print: config.PrintAll,
},
"TBC_BLOCK_SANITY": config.Config{
Expand Down
13 changes: 12 additions & 1 deletion database/tbcd/database.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2024 Hemi Labs, Inc.
// Copyright (c) 2024-2025 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

Expand Down Expand Up @@ -91,6 +91,7 @@ type Database interface {
BlockHeaderBest(ctx context.Context) (*BlockHeader, error) // return canonical
BlockHeaderByHash(ctx context.Context, hash *chainhash.Hash) (*BlockHeader, error)
BlockHeaderGenesisInsert(ctx context.Context, wbh *wire.BlockHeader, height uint64, diff *big.Int) error
BlockHeaderCacheStats() CacheStats

// Block headers
BlockHeadersByHeight(ctx context.Context, height uint64) ([]BlockHeader, error)
Expand All @@ -103,6 +104,7 @@ type Database interface {
BlockInsert(ctx context.Context, b *btcutil.Block) (int64, error)
// BlocksInsert(ctx context.Context, bs []*btcutil.Block) (int64, error)
BlockByHash(ctx context.Context, hash *chainhash.Hash) (*btcutil.Block, error)
BlockCacheStats() CacheStats

// Transactions
BlockUtxoUpdate(ctx context.Context, direction int, utxos map[Outpoint]CacheOutput) error
Expand Down Expand Up @@ -413,3 +415,12 @@ func TxIdBlockHashFromTxKey(txKey TxKey) (*chainhash.Hash, *chainhash.Hash, erro
}
return txId, blockHash, nil
}

// Cache
type CacheStats struct {
Hits int
Misses int
Purges int
Size int
marcopeereboom marked this conversation as resolved.
Show resolved Hide resolved
Items int
}
117 changes: 117 additions & 0 deletions database/tbcd/level/blockcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2025 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

package level

import (
"container/list"
"fmt"
"sync"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"

"github.com/hemilabs/heminetwork/database/tbcd"
)

var blockSize = 1677721 // ~1.6MB rough size of a mainnet block as of Jan 2025

type blockElement struct {
element *list.Element
block []byte
}

type lowIQLRU struct {
mtx sync.Mutex

size int // this is the approximate max size

m map[chainhash.Hash]blockElement
totalSize int

// lru list, when used move to back of the list
l *list.List

// stats
c tbcd.CacheStats
}

func (l *lowIQLRU) Put(v *btcutil.Block) {
l.mtx.Lock()
defer l.mtx.Unlock()

hash := v.Hash()
if _, ok := l.m[*hash]; ok {
return
}

block, err := v.Bytes()
if err != nil {
// data corruption, panic
panic(err)
}

// evict first element in list
if l.totalSize+len(block) > l.size {
// LET THEM EAT PANIC
re := l.l.Front()
rha := l.l.Remove(re)
rh := rha.(*chainhash.Hash)
l.totalSize -= len(l.m[*rh].block)
delete(l.m, *rh)
l.c.Purges++
}

// block lookup and lru append
l.m[*hash] = blockElement{element: l.l.PushBack(hash), block: block}
l.totalSize += len(block)

l.c.Size = l.totalSize
}

func (l *lowIQLRU) Get(k *chainhash.Hash) (*btcutil.Block, bool) {
l.mtx.Lock()
defer l.mtx.Unlock()

be, ok := l.m[*k]
if !ok {
l.c.Misses++
return nil, false
}
b, err := btcutil.NewBlockFromBytes(be.block)
if err != nil {
// panic for diagnostics at this time
panic(err)
}

// update access
l.l.MoveToBack(be.element)

l.c.Hits++

return b, true
}

func (l *lowIQLRU) Stats() tbcd.CacheStats {
l.mtx.Lock()
defer l.mtx.Unlock()
l.c.Items = len(l.m)
return l.c
}

func lowIQLRUNewSize(size int) (*lowIQLRU, error) {
marcopeereboom marked this conversation as resolved.
Show resolved Hide resolved
if size <= 0 {
return nil, fmt.Errorf("invalid size: %v", size)
}
// approximate number of blocks
count := size / blockSize
if count <= 0 {
return nil, fmt.Errorf("invalid count: %v", count)
}
return &lowIQLRU{
size: size,
m: make(map[chainhash.Hash]blockElement, count),
l: list.New(),
}, nil
}
87 changes: 87 additions & 0 deletions database/tbcd/level/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package level

import (
"testing"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
)

func newBlock(prevHash *chainhash.Hash, nonce uint32) (chainhash.Hash, *btcutil.Block) {
bh := wire.NewBlockHeader(0, prevHash, &chainhash.Hash{}, 0, uint32(nonce))
b := wire.NewMsgBlock(bh)
return bh.BlockHash(), btcutil.NewBlock(b)
}

func TestLRUCache(t *testing.T) {
maxCache := 10
blockSize = 81 // we'll use empty blocks
l, err := lowIQLRUNewSize(blockSize * maxCache)
if err != nil {
t.Fatal(err)
}

prevHash := chainhash.Hash{} // genesis
blocks := make([]chainhash.Hash, 0, maxCache*2)
for i := 0; i < maxCache; i++ {
h, b := newBlock(&prevHash, uint32(i))
t.Logf("%v: %v", i, h)
blocks = append(blocks, h)
l.Put(b)
prevHash = h
}

// verify stats are 0
s := l.Stats()
if s.Hits != 0 && s.Misses != 0 && s.Purges != 0 {
t.Fatal(spew.Sdump(s))
}

// retrieve all blocks
for k := range blocks {
if _, ok := l.Get(&blocks[k]); !ok {
t.Fatalf("block not found: %v", blocks[k])
}
}

// verify hits are maxBlocks
s = l.Stats()
if s.Hits != 10 && s.Misses != 0 && s.Purges != 0 {
t.Fatal(spew.Sdump(s))
}

// purge oldest cache entries
for i := maxCache; i < maxCache*2; i++ {
h, b := newBlock(&prevHash, uint32(i))
t.Logf("%v: %v", i, h)
blocks = append(blocks, h)
l.Put(b)
prevHash = h
}

// verify purges are maxBlocks
s = l.Stats()
if s.Hits != 10 && s.Misses != 0 && s.Purges != 10 {
t.Fatal(spew.Sdump(s))
}

// retrieve purged blocks
for k := range blocks {
if k >= maxCache {
break
}
if _, ok := l.Get(&blocks[k]); ok {
t.Fatalf("block found: %v", blocks[k])
}
}

// verify misses are maxBlocks
s = l.Stats()
if s.Hits != 10 && s.Misses != 10 && s.Purges != 10 {
t.Fatal(spew.Sdump(s))
}

t.Logf(spew.Sdump(s))
}
59 changes: 52 additions & 7 deletions database/tbcd/level/headercache.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
// Copyright (c) 2024 Hemi Labs, Inc.
// Copyright (c) 2024-2025 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

package level

import (
"fmt"
"sync"

"github.com/btcsuite/btcd/chaincfg/chainhash"

"github.com/hemilabs/heminetwork/database/tbcd"
)

const blockHeaderSize = 8 + 32 + 80 + 8 // rough size of tbcd.BlockHeader

type lowIQMap struct {
mtx sync.RWMutex
mtx sync.Mutex

count int

m map[chainhash.Hash]*tbcd.BlockHeader
m map[chainhash.Hash]*tbcd.BlockHeader // 32+8+80+len([]Word ~ 8)

// stats
c tbcd.CacheStats
}

func (l *lowIQMap) Put(v *tbcd.BlockHeader) {
Expand All @@ -29,9 +35,10 @@ func (l *lowIQMap) Put(v *tbcd.BlockHeader) {
}

if len(l.m) >= l.count {
// evict entry
// evict random entry
for k := range l.m {
delete(l.m, k)
l.c.Purges++
break
}
}
Expand All @@ -40,16 +47,54 @@ func (l *lowIQMap) Put(v *tbcd.BlockHeader) {
}

func (l *lowIQMap) Get(k *chainhash.Hash) (*tbcd.BlockHeader, bool) {
l.mtx.RLock()
defer l.mtx.RUnlock()
l.mtx.Lock()
defer l.mtx.Unlock()

bh, ok := l.m[*k]
if ok {
l.c.Hits++
} else {
l.c.Misses++
}
return bh, ok
}

func lowIQMapNew(count int) *lowIQMap {
func (l *lowIQMap) PurgeBatch(ks []*chainhash.Hash) {
l.mtx.Lock()
defer l.mtx.Unlock()

for v := range ks {
delete(l.m, *ks[v])
l.c.Purges++
marcopeereboom marked this conversation as resolved.
Show resolved Hide resolved
}
}

func (l *lowIQMap) Stats() tbcd.CacheStats {
l.mtx.Lock()
defer l.mtx.Unlock()
l.c.Items = len(l.m)
l.c.Size = len(l.m) * blockHeaderSize // rough size
return l.c
}

func lowIQMapNewCount(count int) (*lowIQMap, error) {
if count <= 0 {
return nil, fmt.Errorf("invalid count: %v", count)
}
return &lowIQMap{
count: count,
m: make(map[chainhash.Hash]*tbcd.BlockHeader, count),
}, nil
}

// lowIQMapNewSize does a bit of math to estimate the number of cache items.
// Since it is an estimate it will overflow if Difficulty becomes bigger than
// 64 bits. This is not an issue since 100MB caches all of mainnet in Jan 2025
// (~819200 items).
func lowIQMapNewSize(size int) (*lowIQMap, error) {
if size <= 0 {
return nil, fmt.Errorf("invalid size: %v", size)
}
// approximate number of headers
return lowIQMapNewCount(size / blockHeaderSize)
}
Loading
Loading