Skip to content

Commit

Permalink
Merge pull request #8183 from starius/close-tx-in-static-backup
Browse files Browse the repository at this point in the history
chanbackup, server, rpcserver: put close unsigned tx, remote signature and commit height to SCB
  • Loading branch information
guggero authored Oct 14, 2024
2 parents 136cb42 + 65df996 commit 1080230
Show file tree
Hide file tree
Showing 16 changed files with 737 additions and 49 deletions.
43 changes: 43 additions & 0 deletions chanbackup/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/kvdb"
)

Expand Down Expand Up @@ -53,6 +54,48 @@ func assembleChanBackup(addrSource AddressSource,
return &single, nil
}

// buildCloseTxInputs generates inputs needed to force close a channel from
// an open channel. Anyone having these inputs and the signer, can sign the
// force closure transaction. Warning! If the channel state updates, an attempt
// to close the channel using this method with outdated CloseTxInputs can result
// in loss of funds! This may happen if an outdated channel backup is attempted
// to be used to force close the channel.
func buildCloseTxInputs(
targetChan *channeldb.OpenChannel) fn.Option[CloseTxInputs] {

log.Debugf("Crafting CloseTxInputs for ChannelPoint(%v)",
targetChan.FundingOutpoint)

localCommit := targetChan.LocalCommitment

if localCommit.CommitTx == nil {
log.Infof("CommitTx is nil for ChannelPoint(%v), "+
"skipping CloseTxInputs. This is possible when "+
"DLP is active.", targetChan.FundingOutpoint)

return fn.None[CloseTxInputs]()
}

// We need unsigned force close tx and the counterparty's signature.
inputs := CloseTxInputs{
CommitTx: localCommit.CommitTx,
CommitSig: localCommit.CommitSig,
}

// In case of a taproot channel, commit height is needed as well to
// produce verification nonce for the taproot channel using shachain.
if targetChan.ChanType.IsTaproot() {
inputs.CommitHeight = localCommit.CommitHeight
}

// In case of a custom taproot channel, TapscriptRoot is needed as well.
if targetChan.ChanType.HasTapscriptRoot() {
inputs.TapscriptRoot = targetChan.TapscriptRoot
}

return fn.Some(inputs)
}

// FetchBackupForChan attempts to create a plaintext static channel backup for
// the target channel identified by its channel point. If we're unable to find
// the target channel, then an error will be returned.
Expand Down
100 changes: 93 additions & 7 deletions chanbackup/pubsub.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ type ChannelEvent struct {
NewChans []ChannelWithAddrs
}

// manualUpdate holds a group of channel state updates and an error channel
// to send back an error happened upon update processing or file updating.
type manualUpdate struct {
// singles hold channels backups. They can be either new or known
// channels in the Swapper.
singles []Single

// errChan is the channel to send an error back. If the update handling
// and the subsequent file updating succeeds, nil is sent.
// The channel must have capacity of 1 to prevent Swapper blocking.
errChan chan error
}

// ChannelSubscription represents an intent to be notified of any updates to
// the primary channel state.
type ChannelSubscription struct {
Expand Down Expand Up @@ -90,6 +103,8 @@ type SubSwapper struct {
// over.
chanEvents *ChannelSubscription

manualUpdates chan manualUpdate

// keyRing is the main key ring that will allow us to pack the new
// multi backup.
keyRing keychain.KeyRing
Expand Down Expand Up @@ -126,11 +141,12 @@ func NewSubSwapper(startingChans []Single, chanNotifier ChannelNotifier,
}

return &SubSwapper{
backupState: backupState,
chanEvents: chanEvents,
keyRing: keyRing,
Swapper: backupSwapper,
quit: make(chan struct{}),
backupState: backupState,
chanEvents: chanEvents,
keyRing: keyRing,
Swapper: backupSwapper,
quit: make(chan struct{}),
manualUpdates: make(chan manualUpdate),
}, nil
}

Expand Down Expand Up @@ -168,6 +184,43 @@ func (s *SubSwapper) Stop() error {
return nil
}

// ManualUpdate inserts/updates channel states into the swapper. The updates
// are processed in another goroutine. The method waits for the updates to be
// fully processed and the file to be updated on-disk before returning.
func (s *SubSwapper) ManualUpdate(singles []Single) error {
// Create the channel to send an error back. If the update handling
// and the subsequent file updating succeeds, nil is sent.
// The channel must have capacity of 1 to prevent Swapper blocking.
errChan := make(chan error, 1)

// Create the update object to insert into the processing loop.
update := manualUpdate{
singles: singles,
errChan: errChan,
}

select {
case s.manualUpdates <- update:
case <-s.quit:
return fmt.Errorf("swapper stopped when sending manual update")
}

// Wait for processing, block on errChan.
select {
case err := <-errChan:
if err != nil {
return fmt.Errorf("processing of manual update "+
"failed: %w", err)
}

case <-s.quit:
return fmt.Errorf("swapper stopped when waiting for outcome")
}

// Success.
return nil
}

// updateBackupFile updates the backup file in place given the current state of
// the SubSwapper. We accept the set of channels that were closed between this
// update and the last to make sure we leave them out of our backup set union.
Expand Down Expand Up @@ -267,9 +320,10 @@ func (s *SubSwapper) backupUpdater() {
log.Debugf("Adding channel %v to backup state",
newChan.FundingOutpoint)

s.backupState[newChan.FundingOutpoint] = NewSingle(
single := NewSingle(
newChan.OpenChannel, newChan.Addrs,
)
s.backupState[newChan.FundingOutpoint] = single
}

// For all closed channels, we'll remove the prior
Expand All @@ -293,13 +347,45 @@ func (s *SubSwapper) backupUpdater() {
"num_old_chans=%v, num_new_chans=%v",
oldStateSize, newStateSize)

// With out new state constructed, we'll, atomically
// Without new state constructed, we'll, atomically
// update the on-disk backup state.
if err := s.updateBackupFile(closedChans...); err != nil {
log.Errorf("unable to update backup file: %v",
err)
}

// We received a manual update. Handle it and update the file.
case manualUpdate := <-s.manualUpdates:
oldStateSize := len(s.backupState)

// For all open channels, we'll create a new SCB given
// the required information.
for _, single := range manualUpdate.singles {
log.Debugf("Manual update of channel %v",
single.FundingOutpoint)

s.backupState[single.FundingOutpoint] = single
}

newStateSize := len(s.backupState)

log.Infof("Updating on-disk multi SCB backup: "+
"num_old_chans=%v, num_new_chans=%v",
oldStateSize, newStateSize)

// Without new state constructed, we'll, atomically
// update the on-disk backup state.
err := s.updateBackupFile()
if err != nil {
log.Errorf("unable to update backup file: %v",
err)
}

// Send the error (or nil) to the caller of
// ManualUpdate. The error channel must have capacity of
// 1 not to block here.
manualUpdate.errChan <- err

// TODO(roasbeef): refresh periodically on a time basis due to
// possible addr changes from node

Expand Down
14 changes: 14 additions & 0 deletions chanbackup/pubsub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,18 @@ func TestSubSwapperUpdater(t *testing.T) {
// Verify that the new set of backups, now has one less after the
// sub-swapper switches the new set with the old.
assertExpectedBackupSwap(t, swapper, subSwapper, keyRing, backupSet)

// Check ManualUpdate method.
channel, err := genRandomOpenChannelShell()
require.NoError(t, err)
single := NewSingle(channel, nil)
backupSet[channel.FundingOutpoint] = single
require.NoError(t, subSwapper.ManualUpdate([]Single{single}))

// Verify that the state of the backup is as expected.
assertExpectedBackupSwap(t, swapper, subSwapper, keyRing, backupSet)

// Check the case ManualUpdate returns an error.
swapper.fail = true
require.Error(t, subSwapper.ManualUpdate([]Single{single}))
}
Loading

0 comments on commit 1080230

Please sign in to comment.