Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chanbackup, server, rpcserver: put close unsigned tx, remote signature and commit height to SCB #8183

Merged
merged 13 commits into from
Oct 14, 2024
Merged
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
ziggie1984 marked this conversation as resolved.
Show resolved Hide resolved
// 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() {
ziggie1984 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of avoiding closing channels etc, maybe we just add an errChan in the function call and run this function in a goroutine ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want the method to have convenient synchronous interface, because it is used in method server.Stop, which itself is quite complicated.

// 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 {
ziggie1984 marked this conversation as resolved.
Show resolved Hide resolved
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
yyforyongyu marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading