Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9a8ba84
Add v4 keystone height hash index
marcopeereboom Jun 10, 2025
93c50e9
decode all keystones
marcopeereboom Jun 10, 2025
6725cbd
Decode the various databases
marcopeereboom Jun 10, 2025
33b0add
encode height to keystone hash
marcopeereboom Jun 10, 2025
cf9ff92
Use abbreviated hash
marcopeereboom Jun 10, 2025
7b14ab6
correct spot
marcopeereboom Jun 10, 2025
19f441c
64 entire bits
marcopeereboom Jun 10, 2025
9f8e579
test this shit real quick
marcopeereboom Jun 10, 2025
33d1de3
remove test code and put results in database
marcopeereboom Jun 10, 2025
93b7a33
Let's start putting this together
marcopeereboom Jun 10, 2025
d0b1651
Show antonio
marcopeereboom Jun 11, 2025
bf14da2
remove fixed XXX
marcopeereboom Jun 11, 2025
f6c6d6f
return height as well with a shortcut
marcopeereboom Jun 11, 2025
efb37e6
add height hash index encoding test
AL-CT Jun 11, 2025
953d45a
fix block missing height error and linter
AL-CT Jun 11, 2025
f73790a
Do we want this antonio?
marcopeereboom Jun 11, 2025
2b36211
oops remove panic
marcopeereboom Jun 11, 2025
73c165d
Here is a better version antonio
marcopeereboom Jun 11, 2025
9a9b734
oops, fix error
marcopeereboom Jun 11, 2025
1d09dc4
minor test fixes for db upgrade
AL-CT Jun 11, 2025
3ba098e
add keystone by height and tests
AL-CT Jun 11, 2025
bcb0fc2
Don't use CloneBytes
marcopeereboom Jun 12, 2025
430fa1b
Cleanup
marcopeereboom Jun 12, 2025
211f1fb
verify height and use found height
marcopeereboom Jun 12, 2025
b236647
Attempt at depth
marcopeereboom Jun 12, 2025
1a2f959
fix height seeking on keystone by hash
AL-CT Jun 12, 2025
8cd2b60
off by one fix and tests
AL-CT Jun 12, 2025
def77d6
add keystonesbyheight to hemictl
AL-CT Jun 12, 2025
ac36984
add keystonesbyheight rpc call
AL-CT Jun 12, 2025
c467c79
try to please codeql for all the unnecessary reasons
marcopeereboom Jun 13, 2025
eaa20aa
Just stop being clever to save 4 bytes per keystone
marcopeereboom Jun 13, 2025
adc9c70
fix upgrade and add testing
AL-CT Jun 13, 2025
e3bff87
Revert "fix upgrade and add testing"
AL-CT Jun 16, 2025
5acc2e6
fix v4 upgrade
AL-CT Jun 16, 2025
6acc0fb
add v4 upgrade test and new v3 test database
AL-CT Jun 16, 2025
668434f
Cleanup a little
marcopeereboom Jun 17, 2025
5cf28d2
fix keystones by height
AL-CT Jun 17, 2025
825ac2c
add extra test
AL-CT Jun 19, 2025
5e9552b
forgot one
AL-CT Jun 19, 2025
4e9684f
fix comments and suggestions
AL-CT Jun 19, 2025
b7ce764
cleanup
AL-CT Jun 24, 2025
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
16 changes: 16 additions & 0 deletions api/tbcapi/tbcapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const (

CmdBlockKeystoneByL2KeystoneAbrevHashRequest = "tbcapi-l2-keystone-abrev-by-abrev-hash-request"
CmdBlockKeystoneByL2KeystoneAbrevHashResponse = "tbcapi-l2-keystone-abrev-by-abrev-hash-response"

CmdKeystonesByHeightRequest = "tbcapi-keystones-by-height-request"
CmdKeystonesByHeightResponse = "tbcapi-keystones-by-height-response"
)

var (
Expand Down Expand Up @@ -298,6 +301,17 @@ type BlockKeystoneByL2KeystoneAbrevHashResponse struct {
Error *protocol.Error `json:"error,omitempty"`
}

type KeystonesByHeightRequest struct {
Height uint32 `json:"height"`
Depth int `json:"depth"`
}

type KeystonesByHeightResponse struct {
L2KeystoneAbrevs []*hemi.L2KeystoneAbrev `json:"l2_keystone_abrevs"`
BTCTipHeight uint64 `json:"btc_tip_height"`
Error *protocol.Error `json:"error,omitempty"`
}

type BlockInsertRawResponse struct {
BlockHash *chainhash.Hash `json:"block_hash"`
Error *protocol.Error `json:"error,omitempty"`
Expand Down Expand Up @@ -367,6 +381,8 @@ var commands = map[protocol.Command]reflect.Type{
CmdBlockDownloadAsyncRawResponse: reflect.TypeOf(BlockDownloadAsyncRawResponse{}),
CmdBlockKeystoneByL2KeystoneAbrevHashRequest: reflect.TypeOf(BlockKeystoneByL2KeystoneAbrevHashRequest{}),
CmdBlockKeystoneByL2KeystoneAbrevHashResponse: reflect.TypeOf(BlockKeystoneByL2KeystoneAbrevHashResponse{}),
CmdKeystonesByHeightRequest: reflect.TypeOf(KeystonesByHeightRequest{}),
CmdKeystonesByHeightResponse: reflect.TypeOf(KeystonesByHeightResponse{}),
}

type tbcAPI struct{}
Expand Down
30 changes: 30 additions & 0 deletions cmd/hemictl/hemictl.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ func tbcdb(pctx context.Context, flags []string) error {
fmt.Println("\tdeletemetadata")
fmt.Println("\tdumpmetadata")
fmt.Println("\tdumpoutputs <prefix>")
fmt.Println("\tkeystonesbyheight [height] [depth]")
fmt.Println("\tmetadataget [key]")
fmt.Println("\tmetadatadel [key]")
fmt.Println("\tmetadataput [key] [value]")
Expand Down Expand Up @@ -755,6 +756,34 @@ func tbcdb(pctx context.Context, flags []string) error {
}
spew.Dump(value)

case "keystonesbyheight":
height := args["height"]
if height == "" {
return errors.New("height: must be set")
}

h, err := strconv.ParseUint(height, 10, 32)
if err != nil {
return fmt.Errorf("parse height: %w", err)
}

depth := args["depth"]
if depth == "" {
return errors.New("depth: must be set")
}

d, err := strconv.ParseInt(depth, 10, 0)
if err != nil {
return fmt.Errorf("parse depth: %w", err)
}

kssList, err := s.KeystonesByHeight(ctx, uint32(h), int(d))
if err != nil {
return fmt.Errorf("retrieve keystones: %w", err)
}

spew.Dump(kssList)

case "blockheaderbyutxoindex":
bh, err := s.BlockHeaderByUtxoIndex(ctx)
if err != nil {
Expand All @@ -774,6 +803,7 @@ func tbcdb(pctx context.Context, flags []string) error {
if err != nil {
return err
}
spew.Config.DisableMethods = true
spew.Dump(bh)

case "blockkeystonebyl2keystoneabrevhash":
Expand Down
2 changes: 2 additions & 0 deletions database/tbcd/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,12 @@ type Database interface {
BlockKeystoneUpdate(ctx context.Context, direction int, keystones map[chainhash.Hash]Keystone, keystoneIndexHash chainhash.Hash) error
BlockKeystoneByL2KeystoneAbrevHash(ctx context.Context, abrevhash chainhash.Hash) (*Keystone, error)
BlockHeaderByKeystoneIndex(ctx context.Context) (*BlockHeader, error)
KeystonesByHeight(ctx context.Context, height uint32, depth int) ([]Keystone, error)
}

type Keystone struct {
BlockHash chainhash.Hash // Block that contains abbreviated keystone
BlockHeight uint32 // Block height
AbbreviatedKeystone [hemi.L2KeystoneAbrevSize]byte // Abbreviated keystone
}

Expand Down
137 changes: 125 additions & 12 deletions database/tbcd/level/level.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,17 @@ import (
// UTXOs

const (
ldbVersion = 3
ldbVersion = 4

logLevel = "INFO"
verbose = false

bhsCanonicalTipKey = "canonicaltip"

heighthashSize = 8 + 1 + chainhash.HashSize
blockheaderSize = 120
keystoneSize = chainhash.HashSize + hemi.L2KeystoneAbrevSize
heighthashSize = 8 + 1 + chainhash.HashSize
blockheaderSize = 120
keystoneSize = 4 + chainhash.HashSize + hemi.L2KeystoneAbrevSize
keystoneHeightHashSize = 1 + 4 + chainhash.HashSize // h uint32(height) block_hash
)

type IteratorError error
Expand Down Expand Up @@ -137,12 +138,17 @@ type Config struct {
blockCacheSize int // parsed size of block cache
blockheaderCacheSize int // parsed size of block header cache
nonInteractive bool // Set to true to prevent user interaction
upgradeOpen bool // Set to true when doing an open during upgrade
}

func (cfg *Config) SetNoninteractive(x bool) {
cfg.nonInteractive = x
}

func (cfg *Config) SetUpgradeOpen(x bool) {
cfg.upgradeOpen = x
}

func NewConfig(network, home, blockheaderCacheSizeS, blockCacheSizeS string) (*Config, error) {
if blockheaderCacheSizeS == "" {
blockheaderCacheSizeS = "0"
Expand Down Expand Up @@ -265,6 +271,12 @@ func New(ctx context.Context, cfg *Config) (*ldb, error) {
return nil, err
}
}

// Skip upgrades to prevent re-entrancy.
if cfg.upgradeOpen {
return l, nil
}

switch dbVersion {
case 1:
// Upgrade to v2
Expand All @@ -273,6 +285,9 @@ func New(ctx context.Context, cfg *Config) (*ldb, error) {
// Upgrade to v3, database is closed in the process.
reopen = true
err = l.v3(ctx)
case 3:
// Upgrade to v4
err = l.v4(ctx)
default:
if ldbVersion == dbVersion {
if Welcome {
Expand Down Expand Up @@ -439,14 +454,13 @@ func (l *ldb) BlockKeystoneByL2KeystoneAbrevHash(ctx context.Context, abrevhash
defer log.Tracef("BlockKeystoneByL2KeystoneAbrevHash exit")

kssDB := l.pool[level.KeystonesDB]
eks, err := kssDB.Get(abrevhash.CloneBytes(), nil)
eks, err := kssDB.Get(abrevhash[:], nil)
if err != nil {
if errors.Is(err, leveldb.ErrNotFound) {
return nil, database.NotFoundError(fmt.Sprintf("l2 keystone not found: %v", abrevhash))
}
return nil, fmt.Errorf("l2 keystone get: %w", err)
return nil, fmt.Errorf("l2 keystone: %w", err)
}

ks := decodeKeystone(eks)
return &ks, nil
}
Expand Down Expand Up @@ -1926,8 +1940,11 @@ func (l *ldb) BlockTxUpdate(ctx context.Context, direction int, txs map[tbcd.TxK
// [blockhash,abbreviated keystone] or [32+76] bytes. The abbreviated keystone
// hash is the leveldb table key.
func encodeKeystone(ks tbcd.Keystone) (eks [keystoneSize]byte) {
copy(eks[0:32], ks.BlockHash[:])
copy(eks[32:], ks.AbbreviatedKeystone[:])
var h [4]byte
binary.BigEndian.PutUint32(h[:], ks.BlockHeight)
copy(eks[0:4], h[:])
copy(eks[4:4+32], ks.BlockHash[:])
copy(eks[36:], ks.AbbreviatedKeystone[:])
return
}

Expand All @@ -1938,16 +1955,110 @@ func encodeKeystoneToSlice(ks tbcd.Keystone) []byte {

// decodeKeystone reverse the process of encodeKeystone.
func decodeKeystone(eks []byte) (ks tbcd.Keystone) {
bh, err := chainhash.NewHash(eks[0:32])
ks.BlockHeight = binary.BigEndian.Uint32(eks[0:4])
bh, err := chainhash.NewHash(eks[4 : 4+32])
if err != nil {
panic(err) // Can't happen
}
ks.BlockHash = *bh
// copy the values to prevent slicing reentrancy problems.
copy(ks.AbbreviatedKeystone[:], eks[32:])
copy(ks.AbbreviatedKeystone[:], eks[36:])
return ks
}

func encodeKeystoneHeightHash(height uint32, hash chainhash.Hash) (e [keystoneHeightHashSize]byte) {
var h [4]byte
binary.BigEndian.PutUint32(h[:], height)
e[0] = 'h'
copy(e[1:1+4], h[:])
copy(e[5:5+32], hash[:])
return
}

func encodeKeystoneHeightHashSlice(height uint32, hash chainhash.Hash) []byte {
e := encodeKeystoneHeightHash(height, hash)
return e[:]
}

func decodeKeystoneHeightHash(v []byte) (uint32, chainhash.Hash) {
if len(v) != keystoneHeightHashSize {
panic(fmt.Errorf("invalid height hash size: %x", v))
}
if v[0] != 'h' {
panic(fmt.Errorf("not a keystone height hash index: %x", v))
}
var hash chainhash.Hash
if err := hash.SetBytes(v[5:]); err != nil {
panic(err)
}
return binary.BigEndian.Uint32(v[1 : 1+4]), hash
}

func keystoneHeightRange(height int64, depth int64) *util.Range {
// Casting is a bit awkward here but I am not sure if we can make this
// look better somehow.
start := height + 1
end := start + depth
if depth < 0 {
start = height + depth
end = height
}
return &util.Range{
Start: encodeKeystoneHeightHashSlice(uint32(start), chainhash.Hash{}),
Limit: encodeKeystoneHeightHashSlice(uint32(end), chainhash.Hash{}),
}
}

// Searches for the first occurance of keystones within the given
// height + range, excluding the height itself.
func (l *ldb) KeystonesByHeight(ctx context.Context, height uint32, depth int) ([]tbcd.Keystone, error) {
log.Tracef("KeystonesByHeight")
defer log.Tracef("KeystonesByHeight exit")

d := int64(depth)
if d == 0 {
return nil, errors.New("depth must not be 0")
}
start := int64(height)
end := start + d
if depth > 0 {
end += 1
}
if end > math.MaxUint32 {
return nil, errors.New("the overflow that matters")
}
if end <= 0 {
return nil, errors.New("underflow")
}

kssDB := l.pool[level.KeystonesDB]
i := kssDB.NewIterator(keystoneHeightRange(start, d), nil)
defer i.Release()

kssList := make([]tbcd.Keystone, 0, 16)
for i.Next() {
_, hash := decodeKeystoneHeightHash(i.Key())
eks, err := kssDB.Get(hash[:], nil)
Comment thread
ClaytonNorthey92 marked this conversation as resolved.
if err != nil {
// mismatch between heighthash and hash indexes
panic(fmt.Errorf("data corruption: %w", err))
}
deks := decodeKeystone(eks)
kssList = append(kssList, deks)
}
if i.Error() != nil {
return nil, fmt.Errorf("keystones iterator: %w", i.Error())
}

if len(kssList) == 0 {
return nil, database.NotFoundError(fmt.Sprintf("no first occurrence "+
"keystones range: %v < %v",
min(start+1, end), max(start, end)-1))
}

return kssList, nil
}

func (l *ldb) BlockKeystoneUpdate(ctx context.Context, direction int, keystones map[chainhash.Hash]tbcd.Keystone, keystoneIndexHash chainhash.Hash) error {
log.Tracef("BlockKeystoneUpdate")
defer log.Tracef("BlockKeystoneUpdate exit")
Expand Down Expand Up @@ -1976,13 +2087,15 @@ func (l *ldb) BlockKeystoneUpdate(ctx context.Context, direction int, keystones
// previously found block.
if ks.BlockHash.IsEqual(&v.BlockHash) {
kssBatch.Delete(k[:])
kssBatch.Delete(encodeKeystoneHeightHashSlice(v.BlockHeight, k))
}
}
case 1:
has, _ := kssTx.Has(k[:], nil)
if !has {
// Only store unknown keystones
// Only store unknown keystones and indexes
kssBatch.Put(k[:], encodeKeystoneToSlice(v))
kssBatch.Put(encodeKeystoneHeightHashSlice(v.BlockHeight, k), nil)
}
}

Expand Down
Loading
Loading