package htlcswitch import ( "bytes" "encoding/binary" "errors" "fmt" "sync" "sync/atomic" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb/kvdb" ) const ( // defaultDbDirectory is the default directory where our decayed log // will store our (sharedHash, CLTV) key-value pairs. defaultDbDirectory = "sharedhashes" // dbPermissions sets the database permissions to user write-and-readable. dbPermissions = 0600 ) var ( // sharedHashBucket is a bucket which houses the first HashPrefixSize // bytes of a received HTLC's hashed shared secret as the key and the HTLC's // CLTV expiry as the value. sharedHashBucket = []byte("shared-hash") // batchReplayBucket is a bucket that maps batch identifiers to // serialized ReplaySets. This is used to give idempotency in the event // that a batch is processed more than once. batchReplayBucket = []byte("batch-replay") ) var ( // ErrDecayedLogInit is used to indicate a decayed log failed to create // the proper bucketing structure on startup. ErrDecayedLogInit = errors.New("unable to initialize decayed log") // ErrDecayedLogCorrupted signals that the anticipated bucketing // structure has diverged since initialization. ErrDecayedLogCorrupted = errors.New("decayed log structure corrupted") ) // DecayedLog implements the PersistLog interface. It stores the first // HashPrefixSize bytes of a sha256-hashed shared secret along with a node's // CLTV value. It is a decaying log meaning there will be a garbage collector // to collect entries which are expired according to their stored CLTV value // and the current block height. DecayedLog wraps boltdb for simplicity and // batches writes to the database to decrease write contention. type DecayedLog struct { started int32 // To be used atomically. stopped int32 // To be used atomically. dbPath string db kvdb.Backend notifier chainntnfs.ChainNotifier wg sync.WaitGroup quit chan struct{} } // NewDecayedLog creates a new DecayedLog, which caches recently seen hash // shared secrets. Entries are evicted as their cltv expires using block epochs // from the given notifier. func NewDecayedLog(dbPath string, notifier chainntnfs.ChainNotifier) *DecayedLog { // Use default path for log database if dbPath == "" { dbPath = defaultDbDirectory } return &DecayedLog{ dbPath: dbPath, notifier: notifier, quit: make(chan struct{}), } } // Start opens the database we will be using to store hashed shared secrets. // It also starts the garbage collector in a goroutine to remove stale // database entries. func (d *DecayedLog) Start() error { if !atomic.CompareAndSwapInt32(&d.started, 0, 1) { return nil } // Open the boltdb for use. var err error d.db, err = kvdb.Create( kvdb.BoltBackendName, d.dbPath, true, ) if err != nil { return fmt.Errorf("Could not open boltdb: %v", err) } // Initialize the primary buckets used by the decayed log. if err := d.initBuckets(); err != nil { return err } // Start garbage collector. if d.notifier != nil { epochClient, err := d.notifier.RegisterBlockEpochNtfn(nil) if err != nil { return fmt.Errorf("Unable to register for epoch "+ "notifications: %v", err) } d.wg.Add(1) go d.garbageCollector(epochClient) } return nil } // initBuckets initializes the primary buckets used by the decayed log, namely // the shared hash bucket, and batch replay func (d *DecayedLog) initBuckets() error { return kvdb.Update(d.db, func(tx kvdb.RwTx) error { _, err := tx.CreateTopLevelBucket(sharedHashBucket) if err != nil { return ErrDecayedLogInit } _, err = tx.CreateTopLevelBucket(batchReplayBucket) if err != nil { return ErrDecayedLogInit } return nil }) } // Stop halts the garbage collector and closes boltdb. func (d *DecayedLog) Stop() error { if !atomic.CompareAndSwapInt32(&d.stopped, 0, 1) { return nil } // Stop garbage collector. close(d.quit) d.wg.Wait() // Close boltdb. d.db.Close() return nil } // garbageCollector deletes entries from sharedHashBucket whose expiry height // has already past. This function MUST be run as a goroutine. func (d *DecayedLog) garbageCollector(epochClient *chainntnfs.BlockEpochEvent) { defer d.wg.Done() defer epochClient.Cancel() for { select { case epoch, ok := <-epochClient.Epochs: if !ok { // Block epoch was canceled, shutting down. log.Infof("Block epoch canceled, " + "decaying hash log shutting down") return } // Perform a bout of garbage collection using the // epoch's block height. height := uint32(epoch.Height) numExpired, err := d.gcExpiredHashes(height) if err != nil { log.Errorf("unable to expire hashes at "+ "height=%d", height) } if numExpired > 0 { log.Infof("Garbage collected %v shared "+ "secret hashes at height=%v", numExpired, height) } case <-d.quit: // Received shutdown request. log.Infof("Decaying hash log received " + "shutdown request") return } } } // gcExpiredHashes purges the decaying log of all entries whose CLTV expires // below the provided height. func (d *DecayedLog) gcExpiredHashes(height uint32) (uint32, error) { var numExpiredHashes uint32 err := kvdb.Batch(d.db, func(tx kvdb.RwTx) error { numExpiredHashes = 0 // Grab the shared hash bucket sharedHashes := tx.ReadWriteBucket(sharedHashBucket) if sharedHashes == nil { return fmt.Errorf("sharedHashBucket " + "is nil") } var expiredCltv [][]byte if err := sharedHashes.ForEach(func(k, v []byte) error { // Deserialize the CLTV value for this entry. cltv := uint32(binary.BigEndian.Uint32(v)) if cltv < height { // This CLTV is expired. We must add it to an // array which we'll loop over and delete every // hash contained from the db. expiredCltv = append(expiredCltv, k) numExpiredHashes++ } return nil }); err != nil { return err } // Delete every item in the array. This must // be done explicitly outside of the ForEach // function for safety reasons. for _, hash := range expiredCltv { err := sharedHashes.Delete(hash) if err != nil { return err } } return nil }) if err != nil { return 0, err } return numExpiredHashes, nil } // Delete removes a key-pair from the // sharedHashBucket. func (d *DecayedLog) Delete(hash *sphinx.HashPrefix) error { return kvdb.Batch(d.db, func(tx kvdb.RwTx) error { sharedHashes := tx.ReadWriteBucket(sharedHashBucket) if sharedHashes == nil { return ErrDecayedLogCorrupted } return sharedHashes.Delete(hash[:]) }) } // Get retrieves the CLTV of a processed HTLC given the first 20 bytes of the // Sha-256 hash of the shared secret. func (d *DecayedLog) Get(hash *sphinx.HashPrefix) (uint32, error) { var value uint32 err := kvdb.View(d.db, func(tx kvdb.ReadTx) error { // Grab the shared hash bucket which stores the mapping from // truncated sha-256 hashes of shared secrets to CLTV's. sharedHashes := tx.ReadBucket(sharedHashBucket) if sharedHashes == nil { return fmt.Errorf("sharedHashes is nil, could " + "not retrieve CLTV value") } // Retrieve the bytes which represents the CLTV valueBytes := sharedHashes.Get(hash[:]) if valueBytes == nil { return sphinx.ErrLogEntryNotFound } // The first 4 bytes represent the CLTV, store it in value. value = uint32(binary.BigEndian.Uint32(valueBytes)) return nil }) if err != nil { return value, err } return value, nil } // Put stores a shared secret hash as the key and the CLTV as the value. func (d *DecayedLog) Put(hash *sphinx.HashPrefix, cltv uint32) error { // Optimisitically serialize the cltv value into the scratch buffer. var scratch [4]byte binary.BigEndian.PutUint32(scratch[:], cltv) return kvdb.Batch(d.db, func(tx kvdb.RwTx) error { sharedHashes := tx.ReadWriteBucket(sharedHashBucket) if sharedHashes == nil { return ErrDecayedLogCorrupted } // Check to see if this hash prefix has been recorded before. If // a value is found, this packet is being replayed. valueBytes := sharedHashes.Get(hash[:]) if valueBytes != nil { return sphinx.ErrReplayedPacket } return sharedHashes.Put(hash[:], scratch[:]) }) } // PutBatch accepts a pending batch of hashed secret entries to write to disk. // Each hashed secret is inserted with a corresponding time value, dictating // when the entry will be evicted from the log. // NOTE: This method enforces idempotency by writing the replay set obtained // from the first attempt for a particular batch ID, and decoding the return // value to subsequent calls. For the indices of the replay set to be aligned // properly, the batch MUST be constructed identically to the first attempt, // pruning will cause the indices to become invalid. func (d *DecayedLog) PutBatch(b *sphinx.Batch) (*sphinx.ReplaySet, error) { // Since batched boltdb txns may be executed multiple times before // succeeding, we will create a new replay set for each invocation to // avoid any side-effects. If the txn is successful, this replay set // will be merged with the replay set computed during batch construction // to generate the complete replay set. If this batch was previously // processed, the replay set will be deserialized from disk. var replays *sphinx.ReplaySet if err := kvdb.Batch(d.db, func(tx kvdb.RwTx) error { sharedHashes := tx.ReadWriteBucket(sharedHashBucket) if sharedHashes == nil { return ErrDecayedLogCorrupted } // Load the batch replay bucket, which will be used to either // retrieve the result of previously processing this batch, or // to write the result of this operation. batchReplayBkt := tx.ReadWriteBucket(batchReplayBucket) if batchReplayBkt == nil { return ErrDecayedLogCorrupted } // Check for the existence of this batch's id in the replay // bucket. If a non-nil value is found, this indicates that we // have already processed this batch before. We deserialize the // resulting and return it to ensure calls to put batch are // idempotent. replayBytes := batchReplayBkt.Get(b.ID) if replayBytes != nil { replays = sphinx.NewReplaySet() return replays.Decode(bytes.NewReader(replayBytes)) } // The CLTV will be stored into scratch and then stored into the // sharedHashBucket. var scratch [4]byte replays = sphinx.NewReplaySet() err := b.ForEach(func(seqNum uint16, hashPrefix *sphinx.HashPrefix, cltv uint32) error { // Retrieve the bytes which represents the CLTV valueBytes := sharedHashes.Get(hashPrefix[:]) if valueBytes != nil { replays.Add(seqNum) return nil } // Serialize the cltv value and write an entry keyed by // the hash prefix. binary.BigEndian.PutUint32(scratch[:], cltv) return sharedHashes.Put(hashPrefix[:], scratch[:]) }) if err != nil { return err } // Merge the replay set computed from checking the on-disk // entries with the in-batch replays computed during this // batch's construction. replays.Merge(b.ReplaySet) // Write the replay set under the batch identifier to the batch // replays bucket. This can be used during recovery to test (1) // that a particular batch was successfully processed and (2) // recover the indexes of the adds that were rejected as // replays. var replayBuf bytes.Buffer if err := replays.Encode(&replayBuf); err != nil { return err } return batchReplayBkt.Put(b.ID, replayBuf.Bytes()) }); err != nil { return nil, err } b.ReplaySet = replays b.IsCommitted = true return replays, nil } // A compile time check to see if DecayedLog adheres to the PersistLog // interface. var _ sphinx.ReplayLog = (*DecayedLog)(nil)