diff --git a/chainntnfs/height_hint_cache.go b/chainntnfs/height_hint_cache.go new file mode 100644 index 000000000..ba3c22244 --- /dev/null +++ b/chainntnfs/height_hint_cache.go @@ -0,0 +1,297 @@ +package chainntnfs + +import ( + "bytes" + "errors" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + bolt "github.com/coreos/bbolt" + "github.com/lightningnetwork/lnd/channeldb" +) + +const ( + // dbName is the default name of the database storing the height hints. + dbName = "heighthint.db" + + // dbFilePermission is the default permission of the database file + // storing the height hints. + dbFilePermission = 0600 +) + +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") + + // ErrCorruptedHeightHintCache indicates that the on-disk bucketing + // structure has altered since the height hint cache instance was + // initialized. + ErrCorruptedHeightHintCache = errors.New("height hint cache has been " + + "corrupted") + + // ErrSpendHintNotFound is an error returned when a spend hint for an + // outpoint was not found. + ErrSpendHintNotFound = errors.New("spend hint not found") + + // ErrConfirmHintNotFound is an error returned when a confirm hint for a + // transaction was not found. + ErrConfirmHintNotFound = errors.New("confirm hint not found") +) + +// SpendHintCache is an interface whose duty is to cache spend hints for +// outpoints. A spend hint is defined as the earliest height in the chain at +// which an outpoint could have been spent within. +type SpendHintCache interface { + // CommitSpendHint commits a spend hint for the outpoints to the cache. + CommitSpendHint(height uint32, ops ...wire.OutPoint) error + + // 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. + QuerySpendHint(op wire.OutPoint) (uint32, error) + + // PurgeSpendHint removes the spend hint for the outpoints from the + // cache. + PurgeSpendHint(ops ...wire.OutPoint) error +} + +// ConfirmHintCache is an interface whose duty is to cache confirm hints for +// transactions. A confirm hint is defined as the earliest height in the chain +// at which a transaction could have been included in a block. +type ConfirmHintCache interface { + // CommitConfirmHint commits a confirm hint for the transactions to the + // cache. + CommitConfirmHint(height uint32, txids ...chainhash.Hash) error + + // 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. + QueryConfirmHint(txid chainhash.Hash) (uint32, error) + + // PurgeConfirmHint removes the confirm hint for the transactions from + // the cache. + PurgeConfirmHint(txids ...chainhash.Hash) error +} + +// 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 { + db *channeldb.DB +} + +// Compile-time checks to ensure HeightHintCache satisfies the SpendHintCache +// and ConfirmHintCache interfaces. +var _ SpendHintCache = (*HeightHintCache)(nil) +var _ ConfirmHintCache = (*HeightHintCache)(nil) + +// NewHeightHintCache returns a new height hint cache backed by a database. +func NewHeightHintCache(db *channeldb.DB) (*HeightHintCache, error) { + cache := &HeightHintCache{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 c.db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(spendHintBucket) + if err != nil { + return err + } + + _, err = tx.CreateBucketIfNotExists(confirmHintBucket) + return err + }) +} + +// CommitSpendHint commits a spend hint for the outpoints to the cache. +func (c *HeightHintCache) CommitSpendHint(height uint32, ops ...wire.OutPoint) error { + Log.Tracef("Updating spend hint to height %d for %v", height, ops) + + return c.db.Batch(func(tx *bolt.Tx) error { + spendHints := tx.Bucket(spendHintBucket) + if spendHints == nil { + return ErrCorruptedHeightHintCache + } + + var hint bytes.Buffer + if err := channeldb.WriteElement(&hint, height); err != nil { + return err + } + + for _, op := range ops { + var outpoint bytes.Buffer + err := channeldb.WriteElement(&outpoint, op) + if err != nil { + return err + } + + err = spendHints.Put(outpoint.Bytes(), 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(op wire.OutPoint) (uint32, error) { + var hint uint32 + err := c.db.View(func(tx *bolt.Tx) error { + spendHints := tx.Bucket(spendHintBucket) + if spendHints == nil { + return ErrCorruptedHeightHintCache + } + + var outpoint bytes.Buffer + if err := channeldb.WriteElement(&outpoint, op); err != nil { + return err + } + + spendHint := spendHints.Get(outpoint.Bytes()) + if spendHint == nil { + return ErrSpendHintNotFound + } + + return channeldb.ReadElement(bytes.NewReader(spendHint), &hint) + }) + if err != nil { + return 0, err + } + + return hint, nil +} + +// PurgeSpendHint removes the spend hint for the outpoints from the cache. +func (c *HeightHintCache) PurgeSpendHint(ops ...wire.OutPoint) error { + Log.Tracef("Removing spend hints for %v", ops) + + return c.db.Batch(func(tx *bolt.Tx) error { + spendHints := tx.Bucket(spendHintBucket) + if spendHints == nil { + return ErrCorruptedHeightHintCache + } + + for _, op := range ops { + var outpoint bytes.Buffer + err := channeldb.WriteElement(&outpoint, op) + if err != nil { + return err + } + + err = spendHints.Delete(outpoint.Bytes()) + if err != nil { + return err + } + } + + return nil + }) +} + +// CommitConfirmHint commits a confirm hint for the transactions to the cache. +func (c *HeightHintCache) CommitConfirmHint(height uint32, txids ...chainhash.Hash) error { + Log.Tracef("Updating confirm hints to height %d for %v", height, txids) + + return c.db.Batch(func(tx *bolt.Tx) error { + confirmHints := tx.Bucket(confirmHintBucket) + if confirmHints == nil { + return ErrCorruptedHeightHintCache + } + + var hint bytes.Buffer + if err := channeldb.WriteElement(&hint, height); err != nil { + return err + } + + for _, txid := range txids { + var txHash bytes.Buffer + err := channeldb.WriteElement(&txHash, txid) + if err != nil { + return err + } + + err = confirmHints.Put(txHash.Bytes(), 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(txid chainhash.Hash) (uint32, error) { + var hint uint32 + err := c.db.View(func(tx *bolt.Tx) error { + confirmHints := tx.Bucket(confirmHintBucket) + if confirmHints == nil { + return ErrCorruptedHeightHintCache + } + + var txHash bytes.Buffer + if err := channeldb.WriteElement(&txHash, txid); err != nil { + return err + } + + confirmHint := confirmHints.Get(txHash.Bytes()) + if confirmHint == nil { + return ErrConfirmHintNotFound + } + + return channeldb.ReadElement(bytes.NewReader(confirmHint), &hint) + }) + if err != nil { + return 0, err + } + + return hint, nil +} + +// PurgeConfirmHint removes the confirm hint for the transactions from the +// cache. +func (c *HeightHintCache) PurgeConfirmHint(txids ...chainhash.Hash) error { + Log.Tracef("Removing confirm hints for %v", txids) + + return c.db.Batch(func(tx *bolt.Tx) error { + confirmHints := tx.Bucket(confirmHintBucket) + if confirmHints == nil { + return ErrCorruptedHeightHintCache + } + + for _, txid := range txids { + var txHash bytes.Buffer + err := channeldb.WriteElement(&txHash, txid) + if err != nil { + return err + } + + err = confirmHints.Delete(txHash.Bytes()) + if err != nil { + return err + } + } + + return nil + }) +} diff --git a/chainntnfs/height_hint_cache_test.go b/chainntnfs/height_hint_cache_test.go new file mode 100644 index 000000000..f444b18d3 --- /dev/null +++ b/chainntnfs/height_hint_cache_test.go @@ -0,0 +1,148 @@ +package chainntnfs + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" +) + +func initHintCache(t *testing.T) *HeightHintCache { + t.Helper() + + tempDir, err := ioutil.TempDir("", "kek") + if err != nil { + t.Fatalf("unable to create temp dir: %v", err) + } + db, err := channeldb.Open(tempDir) + if err != nil { + t.Fatalf("unable to create db: %v", err) + } + hintCache, err := NewHeightHintCache(db) + if err != nil { + t.Fatalf("unable to create hint cache: %v", err) + } + + 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 + _, err := hintCache.QueryConfirmHint(unknownHash) + if err != ErrConfirmHintNotFound { + t.Fatalf("expected ErrConfirmHintNotFound, got: %v", err) + } + + // Now, we'll create some transaction hashes and commit them to the + // cache with the same confirm hint. + const height = 100 + const numHashes = 5 + txHashes := make([]chainhash.Hash, numHashes) + for i := 0; i < numHashes; i++ { + var txHash chainhash.Hash + copy(txHash[:], bytes.Repeat([]byte{byte(i)}, 32)) + txHashes[i] = txHash + } + + if err := hintCache.CommitConfirmHint(height, txHashes...); err != nil { + t.Fatalf("unable to add entries to cache: %v", err) + } + + // With the hashes committed, we'll now query the cache to ensure that + // we're able to properly retrieve the confirm hints. + for _, txHash := range txHashes { + confirmHint, err := hintCache.QueryConfirmHint(txHash) + if err != nil { + t.Fatalf("unable to query for hint: %v", err) + } + if confirmHint != height { + t.Fatalf("expected confirm hint %d, got %d", height, + confirmHint) + } + } + + // We'll also attempt to purge all of them in a single database + // transaction. + if err := hintCache.PurgeConfirmHint(txHashes...); err != nil { + t.Fatalf("unable to remove confirm hints: %v", err) + } + + // Finally, we'll attempt to query for each hash. We should expect not + // to find a hint for any of them. + for _, txHash := range txHashes { + _, err := hintCache.QueryConfirmHint(txHash) + if err != ErrConfirmHintNotFound { + t.Fatalf("expected ErrConfirmHintNotFound, got :%v", err) + } + } +} + +// 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. + var unknownOutPoint wire.OutPoint + _, err := hintCache.QuerySpendHint(unknownOutPoint) + if err != ErrSpendHintNotFound { + t.Fatalf("expected ErrSpendHintNotFound, got: %v", err) + } + + // Now, we'll create some outpoints and commit them to the cache with + // the same spend hint. + const height = 100 + const numOutpoints = 5 + var txHash chainhash.Hash + copy(txHash[:], bytes.Repeat([]byte{0xFF}, 32)) + outpoints := make([]wire.OutPoint, numOutpoints) + for i := uint32(0); i < numOutpoints; i++ { + outpoints[i] = wire.OutPoint{Hash: txHash, Index: i} + } + + if err := hintCache.CommitSpendHint(height, outpoints...); err != nil { + t.Fatalf("unable to add entry to cache: %v", err) + } + + // With the outpoints committed, we'll now query the cache to ensure + // that we're able to properly retrieve the confirm hints. + for _, op := range outpoints { + spendHint, err := hintCache.QuerySpendHint(op) + if err != nil { + t.Fatalf("unable to query for hint: %v", err) + } + if spendHint != height { + t.Fatalf("expected spend hint %d, got %d", height, + spendHint) + } + } + + // We'll also attempt to purge all of them in a single database + // transaction. + if err := hintCache.PurgeSpendHint(outpoints...); err != nil { + t.Fatalf("unable to remove spend hint: %v", err) + } + + // Finally, we'll attempt to query for each outpoint. We should expect + // not to find a hint for any of them. + for _, op := range outpoints { + _, err = hintCache.QuerySpendHint(op) + if err != ErrSpendHintNotFound { + t.Fatalf("expected ErrSpendHintNotFound, got: %v", err) + } + } +}