chainntnfs: move cache implementation to channeldb

This commit moves the `HeightHintCache` implementation to the
`channeldb` package and inverts the dependency relation between
`chainntnfs` and `channeldb`.

Many packages depend on channeldb for type definitions,
interfaces, etc. `chainntnfs` is an example of that. `chainntnfs`
defines the  `SpendHintCache` and `ConfirmHintCache` interfaces but
it also implments them (`HeightHintCache` struct). The implementation
uses logic that should not leak from channeldb (ex: bucket paths).
This makes our code highly coupled + it would not allow us to use any
of these interfaces in a package that is imported by `channeldb`
(circular dependency).
This commit is contained in:
positiveblue committed 2023-01-16 03:13:17 -08:00
1 parent 681da4b2b5
commit c602ac07e7
10 files changed
+169 -173

No files matched your search

+315
View File
@@ -0,0 +1,315 @@
package channeldb
import (
"bytes"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/kvdb"
)
var (
// spendHintBucket is the name of the bucket which houses the height
// hint for outpoints. Each height hint represents the earliest height
// at which its corresponding outpoint could have been spent within.
spendHintBucket = []byte("spend-hints")
// confirmHintBucket is the name of the bucket which houses the height
// hints for transactions. Each height hint represents the earliest
// height at which its corresponding transaction could have been
// confirmed within.
confirmHintBucket = []byte("confirm-hints")
)
// CacheConfig contains the HeightHintCache configuration.
type CacheConfig struct {
// QueryDisable prevents reliance on the Height Hint Cache. This is
// necessary to recover from an edge case when the height recorded in
// the cache is higher than the actual height of a spend, causing a
// channel to become "stuck" in a pending close state.
QueryDisable bool
}
// HeightHintCache is an implementation of the SpendHintCache and
// ConfirmHintCache interfaces backed by a channeldb DB instance where the hints
// will be stored.
type HeightHintCache struct {
cfg CacheConfig
db kvdb.Backend
}
// Compile-time checks to ensure HeightHintCache satisfies the SpendHintCache
// and ConfirmHintCache interfaces.
var _ chainntnfs.SpendHintCache = (*HeightHintCache)(nil)
var _ chainntnfs.ConfirmHintCache = (*HeightHintCache)(nil)
// NewHeightHintCache returns a new height hint cache backed by a database.
func NewHeightHintCache(cfg CacheConfig, db kvdb.Backend) (*HeightHintCache,
error) {
cache := &HeightHintCache{cfg, db}
if err := cache.initBuckets(); err != nil {
return nil, err
}
return cache, nil
}
// initBuckets ensures that the primary buckets used by the circuit are
// initialized so that we can assume their existence after startup.
func (c *HeightHintCache) initBuckets() error {
return kvdb.Batch(c.db, func(tx kvdb.RwTx) error {
_, err := tx.CreateTopLevelBucket(spendHintBucket)
if err != nil {
return err
}
_, err = tx.CreateTopLevelBucket(confirmHintBucket)
return err
})
}
// CommitSpendHint commits a spend hint for the outpoints to the cache.
func (c *HeightHintCache) CommitSpendHint(height uint32,
spendRequests ...chainntnfs.SpendRequest) error {
if len(spendRequests) == 0 {
return nil
}
log.Tracef("Updating spend hint to height %d for %v", height,
spendRequests)
return kvdb.Batch(c.db, func(tx kvdb.RwTx) error {
spendHints := tx.ReadWriteBucket(spendHintBucket)
if spendHints == nil {
return chainntnfs.ErrCorruptedHeightHintCache
}
var hint bytes.Buffer
if err := WriteElement(&hint, height); err != nil {
return err
}
for _, spendRequest := range spendRequests {
spendRequest := spendRequest
spendHintKey, err := spendHintKey(&spendRequest)
if err != nil {
return err
}
err = spendHints.Put(spendHintKey, hint.Bytes())
if err != nil {
return err
}
}
return nil
})
}
// QuerySpendHint returns the latest spend hint for an outpoint.
// ErrSpendHintNotFound is returned if a spend hint does not exist within the
// cache for the outpoint.
func (c *HeightHintCache) QuerySpendHint(
spendRequest chainntnfs.SpendRequest) (uint32, error) {
var hint uint32
if c.cfg.QueryDisable {
log.Debugf("Ignoring spend height hint for %v (height hint "+
"cache query disabled)", spendRequest)
return 0, nil
}
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
spendHints := tx.ReadBucket(spendHintBucket)
if spendHints == nil {
return chainntnfs.ErrCorruptedHeightHintCache
}
spendHintKey, err := spendHintKey(&spendRequest)
if err != nil {
return err
}
spendHint := spendHints.Get(spendHintKey)
if spendHint == nil {
return chainntnfs.ErrSpendHintNotFound
}
return ReadElement(bytes.NewReader(spendHint), &hint)
}, func() {
hint = 0
})
if err != nil {
return 0, err
}
return hint, nil
}
// PurgeSpendHint removes the spend hint for the outpoints from the cache.
func (c *HeightHintCache) PurgeSpendHint(
spendRequests ...chainntnfs.SpendRequest) error {
if len(spendRequests) == 0 {
return nil
}
log.Tracef("Removing spend hints for %v", spendRequests)
return kvdb.Batch(c.db, func(tx kvdb.RwTx) error {
spendHints := tx.ReadWriteBucket(spendHintBucket)
if spendHints == nil {
return chainntnfs.ErrCorruptedHeightHintCache
}
for _, spendRequest := range spendRequests {
spendRequest := spendRequest
spendHintKey, err := spendHintKey(&spendRequest)
if err != nil {
return err
}
if err := spendHints.Delete(spendHintKey); err != nil {
return err
}
}
return nil
})
}
// CommitConfirmHint commits a confirm hint for the transactions to the cache.
func (c *HeightHintCache) CommitConfirmHint(height uint32,
confRequests ...chainntnfs.ConfRequest) error {
if len(confRequests) == 0 {
return nil
}
log.Tracef("Updating confirm hints to height %d for %v", height,
confRequests)
return kvdb.Batch(c.db, func(tx kvdb.RwTx) error {
confirmHints := tx.ReadWriteBucket(confirmHintBucket)
if confirmHints == nil {
return chainntnfs.ErrCorruptedHeightHintCache
}
var hint bytes.Buffer
if err := WriteElement(&hint, height); err != nil {
return err
}
for _, confRequest := range confRequests {
confRequest := confRequest
confHintKey, err := confHintKey(&confRequest)
if err != nil {
return err
}
err = confirmHints.Put(confHintKey, hint.Bytes())
if err != nil {
return err
}
}
return nil
})
}
// QueryConfirmHint returns the latest confirm hint for a transaction hash.
// ErrConfirmHintNotFound is returned if a confirm hint does not exist within
// the cache for the transaction hash.
func (c *HeightHintCache) QueryConfirmHint(
confRequest chainntnfs.ConfRequest) (uint32, error) {
var hint uint32
if c.cfg.QueryDisable {
log.Debugf("Ignoring confirmation height hint for %v (height "+
"hint cache query disabled)", confRequest)
return 0, nil
}
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
confirmHints := tx.ReadBucket(confirmHintBucket)
if confirmHints == nil {
return chainntnfs.ErrCorruptedHeightHintCache
}
confHintKey, err := confHintKey(&confRequest)
if err != nil {
return err
}
confirmHint := confirmHints.Get(confHintKey)
if confirmHint == nil {
return chainntnfs.ErrConfirmHintNotFound
}
return ReadElement(bytes.NewReader(confirmHint), &hint)
}, func() {
hint = 0
})
if err != nil {
return 0, err
}
return hint, nil
}
// PurgeConfirmHint removes the confirm hint for the transactions from the
// cache.
func (c *HeightHintCache) PurgeConfirmHint(
confRequests ...chainntnfs.ConfRequest) error {
if len(confRequests) == 0 {
return nil
}
log.Tracef("Removing confirm hints for %v", confRequests)
return kvdb.Batch(c.db, func(tx kvdb.RwTx) error {
confirmHints := tx.ReadWriteBucket(confirmHintBucket)
if confirmHints == nil {
return chainntnfs.ErrCorruptedHeightHintCache
}
for _, confRequest := range confRequests {
confRequest := confRequest
confHintKey, err := confHintKey(&confRequest)
if err != nil {
return err
}
if err := confirmHints.Delete(confHintKey); err != nil {
return err
}
}
return nil
})
}
// confHintKey returns the key that will be used to index the confirmation
// request's hint within the height hint cache.
func confHintKey(r *chainntnfs.ConfRequest) ([]byte, error) {
if r.TxID == chainntnfs.ZeroHash {
return r.PkScript.Script(), nil
}
var txid bytes.Buffer
if err := WriteElement(&txid, r.TxID); err != nil {
return nil, err
}
return txid.Bytes(), nil
}
// spendHintKey returns the key that will be used to index the spend request's
// hint within the height hint cache.
func spendHintKey(r *chainntnfs.SpendRequest) ([]byte, error) {
if r.OutPoint == chainntnfs.ZeroOutPoint {
return r.PkScript.Script(), nil
}
var outpoint bytes.Buffer
err := WriteElement(&outpoint, r.OutPoint)
if err != nil {
return nil, err
}
return outpoint.Bytes(), nil
}
+176
View File
@@ -0,0 +1,176 @@
package channeldb
import (
"bytes"
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/stretchr/testify/require"
)
func initHintCache(t *testing.T) *HeightHintCache {
t.Helper()
defaultCfg := CacheConfig{
QueryDisable: false,
}
return initHintCacheWithConfig(t, defaultCfg)
}
func initHintCacheWithConfig(t *testing.T, cfg CacheConfig) *HeightHintCache {
t.Helper()
db, err := Open(t.TempDir())
require.NoError(t, err, "unable to create db")
hintCache, err := NewHeightHintCache(cfg, db.Backend)
require.NoError(t, err, "unable to create hint cache")
t.Cleanup(func() {
require.NoError(t, db.Close())
})
return hintCache
}
// TestHeightHintCacheConfirms ensures that the height hint cache properly
// caches confirm hints for transactions.
func TestHeightHintCacheConfirms(t *testing.T) {
t.Parallel()
hintCache := initHintCache(t)
// Querying for a transaction hash not found within the cache should
// return an error indication so.
var unknownHash chainhash.Hash
copy(unknownHash[:], bytes.Repeat([]byte{0x01}, 32))
unknownConfRequest := chainntnfs.ConfRequest{TxID: unknownHash}
_, err := hintCache.QueryConfirmHint(unknownConfRequest)
require.ErrorIs(t, err, chainntnfs.ErrConfirmHintNotFound)
// Now, we'll create some transaction hashes and commit them to the
// cache with the same confirm hint.
const height = 100
const numHashes = 5
confRequests := make([]chainntnfs.ConfRequest, numHashes)
for i := 0; i < numHashes; i++ {
var txHash chainhash.Hash
copy(txHash[:], bytes.Repeat([]byte{byte(i + 1)}, 32))
confRequests[i] = chainntnfs.ConfRequest{TxID: txHash}
}
err = hintCache.CommitConfirmHint(height, confRequests...)
require.NoError(t, err, "unable to add entries to cache")
// With the hashes committed, we'll now query the cache to ensure that
// we're able to properly retrieve the confirm hints.
for _, confRequest := range confRequests {
confirmHint, err := hintCache.QueryConfirmHint(confRequest)
require.NoError(t, err)
require.EqualValues(t, height, confirmHint)
}
// We'll also attempt to purge all of them in a single database
// transaction.
err = hintCache.PurgeConfirmHint(confRequests...)
require.NoError(t, err)
// Finally, we'll attempt to query for each hash. We should expect not
// to find a hint for any of them.
for _, confRequest := range confRequests {
_, err := hintCache.QueryConfirmHint(confRequest)
require.ErrorIs(t, err, chainntnfs.ErrConfirmHintNotFound)
}
}
// TestHeightHintCacheSpends ensures that the height hint cache properly caches
// spend hints for outpoints.
func TestHeightHintCacheSpends(t *testing.T) {
t.Parallel()
hintCache := initHintCache(t)
// Querying for an outpoint not found within the cache should return an
// error indication so.
unknownOutPoint := wire.OutPoint{Index: 1}
unknownSpendRequest := chainntnfs.SpendRequest{
OutPoint: unknownOutPoint,
}
_, err := hintCache.QuerySpendHint(unknownSpendRequest)
require.ErrorIs(t, err, chainntnfs.ErrSpendHintNotFound)
// Now, we'll create some outpoints and commit them to the cache with
// the same spend hint.
const height = 100
const numOutpoints = 5
spendRequests := make([]chainntnfs.SpendRequest, numOutpoints)
for i := uint32(0); i < numOutpoints; i++ {
spendRequests[i] = chainntnfs.SpendRequest{
OutPoint: wire.OutPoint{Index: i + 1},
}
}
err = hintCache.CommitSpendHint(height, spendRequests...)
require.NoError(t, err, "unable to add entries to cache")
// With the outpoints committed, we'll now query the cache to ensure
// that we're able to properly retrieve the confirm hints.
for _, spendRequest := range spendRequests {
spendHint, err := hintCache.QuerySpendHint(spendRequest)
require.NoError(t, err)
require.EqualValues(t, height, spendHint)
}
// We'll also attempt to purge all of them in a single database
// transaction.
err = hintCache.PurgeSpendHint(spendRequests...)
require.NoError(t, err)
// Finally, we'll attempt to query for each outpoint. We should expect
// not to find a hint for any of them.
for _, spendRequest := range spendRequests {
_, err = hintCache.QuerySpendHint(spendRequest)
require.ErrorIs(t, err, chainntnfs.ErrSpendHintNotFound)
}
}
// TestQueryDisable asserts querying for confirmation or spend hints always
// return height zero when QueryDisabled is set to true in the CacheConfig.
func TestQueryDisable(t *testing.T) {
cfg := CacheConfig{
QueryDisable: true,
}
hintCache := initHintCacheWithConfig(t, cfg)
// Insert a new confirmation hint with a non-zero height.
const confHeight = 100
confRequest := chainntnfs.ConfRequest{
TxID: chainhash.Hash{0x01, 0x02, 0x03},
}
err := hintCache.CommitConfirmHint(confHeight, confRequest)
require.Nil(t, err)
// Query for the confirmation hint, which should return zero.
cachedConfHeight, err := hintCache.QueryConfirmHint(confRequest)
require.Nil(t, err)
require.Equal(t, uint32(0), cachedConfHeight)
// Insert a new spend hint with a non-zero height.
const spendHeight = 200
spendRequest := chainntnfs.SpendRequest{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x4, 0x05, 0x06},
Index: 42,
},
}
err = hintCache.CommitSpendHint(spendHeight, spendRequest)
require.Nil(t, err)
// Query for the spend hint, which should return zero.
cachedSpendHeight, err := hintCache.QuerySpendHint(spendRequest)
require.Nil(t, err)
require.Equal(t, uint32(0), cachedSpendHeight)
}