multi: allow force channel closures while not connected to peer

This commit adds a much needed feature to the daemon, namely the
ability to force close a channel while the source daemon doesn’t have
an active connection to the counter party. Previously this wasn’t
possible as ALL channel closures were routed through the htlcSwitch
which is only able to trigger a channel closure if the peer is online.

To remedy this, if the closure type is “force” then, we now handle the
channel closure and related RPC streaming updates from the call handler
site of the RPC itself. As a result, there are now only two htlcSwitch
channel closure types: breach, and regular. The logic that’s now in the
rpcSever should likely be refactored into a distinct sub-system, but
getting the initial functionality in is important.

Finally, the channel breach integration test has been modified to skip
connection the peers before attempting the forceful channel closure of
a revoked state as the remote peer no longer needs to be online.
This commit is contained in:
Olaoluwa Osuntokun 2017-01-03 16:02:51 -08:00
parent 57c0f8f005
commit b991cd3d78
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
4 changed files with 158 additions and 69 deletions

View File

@ -611,7 +611,7 @@ type unregisterLinkMsg struct {
done chan struct{}
}
// UnregisterLink requets the htlcSwitch to unregiser the new active link. An
// UnregisterLink requets the htlcSwitch to register the new active link. An
// unregistered link will no longer be considered a candidate to forward
// HTLC's.
func (h *htlcSwitch) UnregisterLink(remotePub *btcec.PublicKey, chanPoint *wire.OutPoint) {
@ -633,14 +633,10 @@ func (h *htlcSwitch) UnregisterLink(remotePub *btcec.PublicKey, chanPoint *wire.
type LinkCloseType uint8
const (
// CloseRegular indicates a regular cooperative channel closure should be attempted.
// CloseRegular indicates a regular cooperative channel closure should
// be attempted.
CloseRegular LinkCloseType = iota
// CloseForce indicates that the channel should be forcefully closed.
// This entails the broadcast of the commitment transaction directly on
// chain unilaterally.
CloseForce
// CloseBreach indicates that a channel breach has been dtected, and
// the link should immediately be marked as unavailable.
CloseBreach

View File

@ -7,7 +7,6 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -1235,16 +1234,10 @@ func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) {
t.Fatalf("db copy failed: %v", bobChan.NumUpdates)
}
err = net.ConnectNodes(ctxb, net.Alice, net.Bob)
if err != nil && !strings.Contains(err.Error(), "already connected") {
t.Fatalf("unable to connect bob and alice: %v", err)
}
// Now force Bob to execute a *force* channel closure by unilaterally
// broadcasting his current channel state. This is actually the
// commitment transaction of a prior *revoked* state, so he'll soon
// feel the wrath of Alice's retribution.
time.Sleep(time.Second * 2)
breachTXID := closeChannelAndAssert(t, net, ctxb, net.Bob, chanPoint,
true)
@ -1340,7 +1333,7 @@ var testsCases = []*testCase{
test: testSingleHopInvoice,
},
{
name: "list of outgoing payments",
name: "list outgoing payments",
test: testListPayments,
},
{

40
peer.go
View File

@ -690,39 +690,6 @@ out:
p.wg.Done()
}
// executeForceClose executes a unilateral close of the target channel by
// broadcasting the current commitment state directly on-chain. Once the
// commitment transaction has been broadcast, a struct describing the final
// state of the channel is sent to the utxoNursery in order to ultimately sweep
// the immature outputs.
func (p *peer) executeForceClose(channel *lnwallet.LightningChannel) (*wire.ShaHash, error) {
// Execute a unilateral close shutting down all further channel
// operation.
closeSummary, err := channel.ForceClose()
if err != nil {
return nil, err
}
closeTx := closeSummary.CloseTx
txid := closeTx.TxSha()
// With the close transaction in hand, broadcast the transaction to the
// network, thereby entering the psot channel resolution state.
peerLog.Infof("Broadcasting force close transaction, ChannelPoint(%v): %v",
channel.ChannelPoint(), newLogClosure(func() string {
return spew.Sdump(closeTx)
}))
if err := p.server.lnwallet.PublishTransaction(closeTx); err != nil {
return nil, err
}
// Send the closed channel summary over to the utxoNursery in order to
// have its outputs swept back into the wallet once they're mature.
p.server.utxoNursery.incubateOutputs(closeSummary)
return &txid, nil
}
// executeCooperativeClose executes the initial phase of a user-executed
// cooperative channel close. The channel state machine is transitioned to the
// closing phase, then our half of the closing witness is sent over to the
@ -771,13 +738,6 @@ func (p *peer) handleLocalClose(req *closeLinkReq) {
p.activeChanMtx.RUnlock()
switch req.CloseType {
// A type of CloseForce indicates that the user has opted for
// unilaterally close the channel on-chain.
case CloseForce:
closingTxid, err = p.executeForceClose(channel)
peerLog.Infof("Force closing ChannelPoint(%v) with txid: %v",
req.chanPoint, closingTxid)
// A type of CloseRegular indicates that the user has opted to close
// out this channel on-chian, so we execute the cooperative channel
// closre workflow.

View File

@ -359,29 +359,106 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
rpcsLog.Errorf("[closechannel] invalid txid: %v", err)
return err
}
targetChannelPoint := wire.NewOutPoint(txid, index)
chanPoint := wire.NewOutPoint(txid, index)
rpcsLog.Tracef("[closechannel] request for ChannelPoint(%v)",
targetChannelPoint)
chanPoint)
var closeType LinkCloseType
switch force {
case true:
// TODO(roasbeef): should be able to force close w/o connection
// to peer
closeType = CloseForce
case false:
closeType = CloseRegular
var (
updateChan chan *lnrpc.CloseStatusUpdate
errChan chan error
)
// If a force closure was requested, then we'll handle all the details
// around the creation and broadcast of the unilateral closure
// transaction here rather than going to the switch as we don't require
// interaction from the peer.
if force {
// As the first part of the force closure, we first fetch the
// channel from the database, then execute a direct force
// closure broadcasting our current commitment transaction.
channel, err := r.fetchActiveChannel(*chanPoint)
if err != nil {
return err
}
closingTxid, err := r.forceCloseChan(channel)
if err != nil {
return err
}
updateChan = make(chan *lnrpc.CloseStatusUpdate)
errChan = make(chan error)
go func() {
// With the transaction broadcast, we send our first
// update to the client.
updateChan <- &lnrpc.CloseStatusUpdate{
Update: &lnrpc.CloseStatusUpdate_ClosePending{
ClosePending: &lnrpc.PendingUpdate{
Txid: closingTxid[:],
},
},
}
// Next, we enter the second phase, waiting for the
// channel to be confirmed before we finalize the force
// closure.
notifier := r.server.chainNotifier
confNtfn, err := notifier.RegisterConfirmationsNtfn(closingTxid, 1)
if err != nil {
errChan <- err
return
}
select {
case txConf, ok := <-confNtfn.Confirmed:
if !ok {
return
}
// As the channel has been closed, we can now
// delete it's state from the database.
rpcsLog.Infof("ChannelPoint(%v) is now "+
"closed at height %v", chanPoint,
txConf.BlockHeight)
if err := channel.DeleteState(); err != nil {
errChan <- err
return
}
case <-r.quit:
return
}
// Respond to the local sub-system which requested the
// channel closure.
updateChan <- &lnrpc.CloseStatusUpdate{
Update: &lnrpc.CloseStatusUpdate_ChanClose{
ChanClose: &lnrpc.ChannelCloseUpdate{
ClosingTxid: closingTxid[:],
Success: true,
},
},
}
// Finally, signal to the breachArbiter that it no
// longer needs to watch the channel as it's been
// closed.
r.server.breachArbiter.settledContracts <- chanPoint
}()
} else {
// Otherwise, the caller has requested a regular interactive
// cooperative channel closure. So we'll forward the request to
// the htlc switch which will handle the negotiation and
// broadcast details.
updateChan, errChan = r.server.htlcSwitch.CloseLink(chanPoint,
CloseRegular)
}
updateChan, errChan := r.server.htlcSwitch.CloseLink(targetChannelPoint, closeType)
out:
for {
select {
case err := <-errChan:
rpcsLog.Errorf("[closechannel] unable to close "+
"ChannelPoint(%v): %v", targetChannelPoint, err)
"ChannelPoint(%v): %v", chanPoint, err)
return err
case closingUpdate := <-updateChan:
rpcsLog.Tracef("[closechannel] sending update: %v",
@ -408,6 +485,69 @@ out:
return nil
}
// fetchActiveChannel attempts to locate a channel identified by it's channel
// point from the database's set of all currently opened channels.
func (r *rpcServer) fetchActiveChannel(chanPoint wire.OutPoint) (*lnwallet.LightningChannel, error) {
dbChannels, err := r.server.chanDB.FetchAllChannels()
if err != nil {
return nil, err
}
// With the channels fetched, attempt to locate the target channel
// according to its channel point.
var dbChan *channeldb.OpenChannel
for _, dbChannel := range dbChannels {
if *dbChannel.ChanID == chanPoint {
dbChan = dbChannel
break
}
}
// If the channel cannot be located, then we exit with an error to the
// caller.
if dbChan == nil {
return nil, fmt.Errorf("unable to find channel")
}
// Otherwise, we create a fully populated channel state machine which
// uses the db channel as backing storage.
return lnwallet.NewLightningChannel(r.server.lnwallet.Signer,
r.server.bio, r.server.chainNotifier, dbChan)
}
// forceCloseChan executes a unilateral close of the target channel by
// broadcasting the current commitment state directly on-chain. Once the
// commitment transaction has been broadcast, a struct describing the final
// state of the channel is sent to the utxoNursery in order to ultimately sweep
// the immature outputs.
func (r *rpcServer) forceCloseChan(channel *lnwallet.LightningChannel) (*wire.ShaHash, error) {
// Execute a unilateral close shutting down all further channel
// operation.
closeSummary, err := channel.ForceClose()
if err != nil {
return nil, err
}
closeTx := closeSummary.CloseTx
txid := closeTx.TxSha()
// With the close transaction in hand, broadcast the transaction to the
// network, thereby entering the psot channel resolution state.
rpcsLog.Infof("Broadcasting force close transaction, ChannelPoint(%v): %v",
channel.ChannelPoint(), newLogClosure(func() string {
return spew.Sdump(closeTx)
}))
if err := r.server.lnwallet.PublishTransaction(closeTx); err != nil {
return nil, err
}
// Send the closed channel summary over to the utxoNursery in order to
// have its outputs swept back into the wallet once they're mature.
r.server.utxoNursery.incubateOutputs(closeSummary)
return &txid, nil
}
// GetInfo serves a request to the "getinfo" RPC call. This call returns
// general information concerning the lightning node including it's LN ID,
// identity address, and information concerning the number of open+pending