Skip to content

Commit 4a29640

Browse files
feat(tbc): store tx byte location in tx index, bump DB to v6
Store TxLoc (offset + length within raw block) in the t entry value instead of nil. This allows callers to jump directly to a tx's bytes in the raw block without scanning — O(1) instead of O(txs_in_block). BlockHashByTxId now returns (*chainhash.Hash, wire.TxLoc, error). All callers updated. No separate method needed — callers that only need the hash use bh, _, err := BlockHashByTxId(...). processTxs calls block.TxLoc() and stores the location via NewTxMappingWithLoc. Errors from TxLoc() are logged at Errorf and the indexer falls back to nil values (legacy format). BlockTxUpdate uses stack-allocated reusable buffers instead of slicing loop variables. The previous code sliced the range variable and passed the slice to leveldb.Batch.Put. appendRec copies immediately, but the interaction between range variable reuse, map deletion, and GC is not guaranteed safe. Stack buffers are zero-alloc and independent per iteration. DB version 5 -> 6. Upgrade path wipes the transactions index for rebuild with TxLoc values. The index is fully derived from block data. Ref: #1050
1 parent 99d768f commit 4a29640

12 files changed

Lines changed: 510 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
### Breaking Changes
1717

18+
- `BlockHashByTxId` now returns `(*chainhash.Hash, wire.TxLoc, error)`;
19+
callers that only need the hash use `bh, _, err :=`
20+
([#1052](https://github.com/hemilabs/heminetwork/pull/1052)).
1821
- Rename `TBC_BLOCKHEADER_CACHE_SIZE` environment variable to
1922
`TBC_HEADER_CACHE_SIZE`
2023
([#1034](https://github.com/hemilabs/heminetwork/pull/1034)).
@@ -30,6 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3033
- Add `BlockRawByHash` to DB interface and `lazyBlock` type for zero-copy
3134
per-tx block access without full deserialization
3235
([#1051](https://github.com/hemilabs/heminetwork/pull/1051)).
36+
- Store tx byte location (`TxLoc`) in tx index `'t'` entry values for
37+
O(1) tx lookup; DB version 5 → 6
38+
([#1052](https://github.com/hemilabs/heminetwork/pull/1052)).
3339
- Add generic `lru` package with cost-based LRU cache (`lru.Cache[K,V]`)
3440
([#1034](https://github.com/hemilabs/heminetwork/pull/1034)).
3541
- Add utxo read LRU cache (`TBC_UTXO_READ_CACHE_SIZE`) to reduce LevelDB
@@ -60,6 +66,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6066

6167
### Changed
6268

69+
- `BlockTxUpdate` uses stack-allocated reusable buffers instead of slicing
70+
loop variables, avoiding potential data integrity issues
71+
([#1052](https://github.com/hemilabs/heminetwork/pull/1052),
72+
[#1050](https://github.com/hemilabs/heminetwork/issues/1050)).
6373
- Replace block and header caches in level package with generic `lru.Cache[K,V]`
6474
([#1034](https://github.com/hemilabs/heminetwork/pull/1034)).
6575

cmd/hemictl/hemictl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ func tbcdb(pctx context.Context, flags []string) error {
619619
return fmt.Errorf("chainhash: %w", err)
620620
}
621621

622-
bh, err := s.BlockHashByTxId(ctx, *chtxid)
622+
bh, _, err := s.BlockHashByTxId(ctx, *chtxid)
623623
if err != nil {
624624
return fmt.Errorf("block by txid: %w", err)
625625
}

database/tbcd/database.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ type Database interface {
121121
BlockHeaderByTxIndex(ctx context.Context) (*BlockHeader, error)
122122
BlockUtxoUpdate(ctx context.Context, direction int, utxos map[Outpoint]CacheOutput, utxoIndexHash chainhash.Hash) error
123123
BlockTxUpdate(ctx context.Context, direction int, txs map[TxKey]*TxValue, txIndexHash chainhash.Hash) error
124-
BlockHashByTxId(ctx context.Context, txId chainhash.Hash) (*chainhash.Hash, error)
124+
BlockHashByTxId(ctx context.Context, txId chainhash.Hash) (*chainhash.Hash, wire.TxLoc, error)
125125
SpentOutputsByTxId(ctx context.Context, txId chainhash.Hash) ([]SpentInfo, error)
126126
// ScriptHash returns the sha256 of PkScript for the provided outpoint.
127127
BalanceByScriptHash(ctx context.Context, sh ScriptHash) (uint64, error)
@@ -481,6 +481,20 @@ func NewTxMapping(txId, blockHash *chainhash.Hash) (txKey TxKey) {
481481
return txKey
482482
}
483483

484+
// NewTxMappingWithLoc returns a TxKey and TxValue that maps a tx id to a block
485+
// hash with the tx byte location (offset + length) within the raw block. This
486+
// allows callers to jump directly to the tx's bytes without scanning the block.
487+
func NewTxMappingWithLoc(txId, blockHash *chainhash.Hash, loc wire.TxLoc) (txKey TxKey, txValue TxValue) {
488+
txKey[0] = 't'
489+
copy(txKey[1:33], txId[:])
490+
copy(txKey[33:], blockHash[:])
491+
492+
binary.BigEndian.PutUint32(txValue[0:4], uint32(loc.TxStart))
493+
binary.BigEndian.PutUint32(txValue[4:8], uint32(loc.TxLen))
494+
495+
return txKey, txValue
496+
}
497+
484498
func TxIdBlockHashFromTxKey(txKey TxKey) (*chainhash.Hash, *chainhash.Hash, error) {
485499
if txKey[0] != 't' {
486500
return nil, nil, fmt.Errorf("invalid magic 0x%02x", txKey[0])

database/tbcd/level/level.go

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import (
4848
// UTXOs
4949

5050
const (
51-
ldbVersion = 5
51+
ldbVersion = 6
5252

5353
logLevel = "INFO"
5454
verbose = false
@@ -312,6 +312,10 @@ func New(ctx context.Context, cfg *Config) (*ldb, error) {
312312
// Upgrade to v5: wipe witness-stripped block
313313
// bodies and rebuild blocksmissing from headers.
314314
err = l.v5(ctx)
315+
case 5:
316+
// Upgrade to v6: wipe tx index so it rebuilds
317+
// with TxLoc values in 't' entries.
318+
err = l.v6(ctx)
315319
default:
316320
if ldbVersion == dbVersion {
317321
if Welcome {
@@ -1642,36 +1646,38 @@ func (l *ldb) BlockExistsByHash(ctx context.Context, hash chainhash.Hash) (bool,
16421646
return ok, nil
16431647
}
16441648

1645-
func (l *ldb) BlockHashByTxId(ctx context.Context, txId chainhash.Hash) (*chainhash.Hash, error) {
1649+
func (l *ldb) BlockHashByTxId(ctx context.Context, txId chainhash.Hash) (*chainhash.Hash, wire.TxLoc, error) {
16461650
log.Tracef("BlockHashByTxId")
16471651
defer log.Tracef("BlockHashByTxId exit")
16481652

1649-
blocks := make([]*chainhash.Hash, 0, 2)
16501653
txDB := l.pool[level.TransactionsDB]
1651-
var txid [33]byte
1652-
txid[0] = 't'
1653-
copy(txid[1:], txId[:])
1654-
it := txDB.NewIterator(util.BytesPrefix(txid[:]), nil)
1654+
var prefix [33]byte
1655+
prefix[0] = 't'
1656+
copy(prefix[1:], txId[:])
1657+
it := txDB.NewIterator(util.BytesPrefix(prefix[:]), nil)
16551658
defer it.Release()
1659+
1660+
var found bool
1661+
var blockHash chainhash.Hash
1662+
var loc wire.TxLoc
16561663
for it.Next() {
1657-
block, err := chainhash.NewHash(it.Key()[33:])
1658-
if err != nil {
1659-
return nil, err
1664+
if found {
1665+
panic(fmt.Sprintf("multiple blocks for tx %v", txId))
1666+
}
1667+
copy(blockHash[:], it.Key()[33:])
1668+
if v := it.Value(); len(v) >= 8 {
1669+
loc.TxStart = int(binary.BigEndian.Uint32(v[0:4]))
1670+
loc.TxLen = int(binary.BigEndian.Uint32(v[4:8]))
16601671
}
1661-
blocks = append(blocks, block)
1672+
found = true
16621673
}
16631674
if err := it.Error(); err != nil {
1664-
return nil, fmt.Errorf("blocks by id iterator: %w", err)
1675+
return nil, wire.TxLoc{}, fmt.Errorf("blocks by id iterator: %w", err)
16651676
}
1666-
switch len(blocks) {
1667-
case 0:
1668-
return nil, database.NotFoundError(fmt.Sprintf("tx not found: %v", txId))
1669-
case 1:
1670-
return blocks[0], nil
1671-
default:
1672-
panic(fmt.Sprintf("invalid blocks count %v: %v",
1673-
len(blocks), spew.Sdump(blocks)))
1677+
if !found {
1678+
return nil, wire.TxLoc{}, database.NotFoundError(fmt.Sprintf("tx not found: %v", txId))
16741679
}
1680+
return &blockHash, loc, nil
16751681
}
16761682

16771683
func (l *ldb) SpentOutputsByTxId(ctx context.Context, txId chainhash.Hash) ([]tbcd.SpentInfo, error) {
@@ -1917,32 +1923,39 @@ func (l *ldb) BlockTxUpdate(ctx context.Context, direction int, txs map[tbcd.TxK
19171923
}
19181924
defer txsDiscard()
19191925

1920-
block := make([]byte, 33)
1921-
block[0] = 'b'
19221926
var blk []byte
19231927
bm := make(map[string]struct{}, len(txs))
19241928
defer clear(bm)
19251929

19261930
txsBatch := new(leveldb.Batch)
1931+
var keyBuf [69]byte
1932+
var valBuf [36]byte
1933+
var blkBuf [33]byte
1934+
blkBuf[0] = 'b'
19271935
for k, v := range txs {
1928-
// cache is being emptied so we can slice it here.
19291936
var key, value []byte
19301937
switch k[0] {
19311938
case 't':
1932-
key = k[0:65]
1933-
value = nil
1939+
copy(keyBuf[:], k[0:65])
1940+
key = keyBuf[:65]
1941+
if v != nil {
1942+
copy(valBuf[:], v[0:8])
1943+
value = valBuf[:8]
1944+
}
19341945

19351946
// insert block hash to determine if it was indexed later
19361947
if _, ok := bm[string(k[33:65])]; !ok {
19371948
bm[string(k[33:65])] = struct{}{}
1938-
copy(block[1:], k[33:65])
1939-
blk = block
1949+
copy(blkBuf[1:], k[33:65])
1950+
blk = blkBuf[:]
19401951
} else {
19411952
blk = nil
19421953
}
19431954
case 's':
1944-
key = k[:]
1945-
value = v[:]
1955+
copy(keyBuf[:], k[:])
1956+
key = keyBuf[:]
1957+
copy(valBuf[:], v[:])
1958+
value = valBuf[:]
19461959

19471960
// don't insert block
19481961
blk = nil

0 commit comments

Comments
 (0)