From d0353b2864bb4467c8648233ddd3e320f36a131e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 12 Sep 2016 12:33:22 -0700 Subject: [PATCH] lnwallet: add ability to trigger a force closure within channel state machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the concept of a manually initiated “force” closer within the channel state machine. A force closure is a closure initiated by a local subsystem which broadcasts the current commitment state directly on-chain rather than attempting to cooperatively negotiate a closure with the remote party. A force closure returns a ForceCloseSummary which includes all the details required for claiming all rightfully owned outputs within the broadcast commitment transaction. Additionally two new publicly exported channels are introduced, one which is closed due a locally initiated force closure, and the other which is closed once we detect that the remote party has executed a unilateral closure by broadcasting their version of the commitment transaction. --- channeldb/channel.go | 2 + lnd.go | 2 +- lnwallet/channel.go | 234 ++++++++++++++++++++++++++++++++++++------ lnwallet/interface.go | 1 + lnwallet/wallet.go | 4 +- networktest.go | 4 +- peer.go | 2 +- server.go | 13 ++- 8 files changed, 218 insertions(+), 44 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 0d7e6cd68..784478b79 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -282,6 +282,8 @@ type HTLC struct { // sweep the funds on-chain in the case of a unilateral channel // closure. RevocationDelay uint32 + + // TODO(roasbeef): add output index? } // Copy returns a full copy of the target HTLC. diff --git a/lnd.go b/lnd.go index 74b801cf1..9ea8f0c72 100644 --- a/lnd.go +++ b/lnd.go @@ -154,7 +154,7 @@ func lndMain() error { defaultListenAddrs := []string{ net.JoinHostPort("", strconv.Itoa(loadedConfig.PeerPort)), } - server, err := newServer(defaultListenAddrs, notifier, wallet, chanDB) + server, err := newServer(defaultListenAddrs, notifier, bio, wallet, chanDB) if err != nil { srvrLog.Errorf("unable to create server: %v\n", err) return err diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 524b9a37c..87344091e 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -310,14 +310,11 @@ func (s *commitmentChain) tail() *commitment { // // See the individual comments within the above methods for further details. type LightningChannel struct { - // TODO(roasbeef): temporarily replace wallet with either goroutine or - // BlockChainIO interface? Later once all signing is done through - // wallet can add back. - lnwallet *LightningWallet - signer Signer signDesc *SignDescriptor + bio BlockChainIO + channelEvents chainntnfs.ChainNotifier sync.RWMutex @@ -389,6 +386,16 @@ type LightningChannel struct { fundingTxIn *wire.TxIn fundingP2WSH []byte + // ForceCloseSignal is a channel that is closed to indicate that a + // local system has initiated a force close by broadcasting the current + // commitment transaction directly on-chain. + ForceCloseSignal chan struct{} + + // UnilateralCloseSignal is a channel that is closed to indicate that + // the remote party has performed a unilateral close by broadcasting + // their version of the commitment transaction on-chain. + UnilateralCloseSignal chan struct{} + started int32 shutdown int32 @@ -397,38 +404,40 @@ type LightningChannel struct { } // NewLightningChannel creates a new, active payment channel given an -// implementation of the wallet controller, chain notifier, channel database, -// and the current settled channel state. Throughout state transitions, then -// channel will automatically persist pertinent state to the database in an -// efficient manner. -func NewLightningChannel(signer Signer, wallet *LightningWallet, +// implementation of the chain notifier, channel database, and the current +// settled channel state. Throughout state transitions, then channel will +// automatically persist pertinent state to the database in an efficient +// manner. +func NewLightningChannel(signer Signer, bio BlockChainIO, events chainntnfs.ChainNotifier, state *channeldb.OpenChannel) (*LightningChannel, error) { // TODO(roasbeef): remove events+wallet lc := &LightningChannel{ - signer: signer, - lnwallet: wallet, - channelEvents: events, - currentHeight: state.NumUpdates, - remoteCommitChain: newCommitmentChain(state.NumUpdates), - localCommitChain: newCommitmentChain(state.NumUpdates), - channelState: state, - revocationWindowEdge: state.NumUpdates, - ourUpdateLog: list.New(), - theirUpdateLog: list.New(), - ourLogIndex: make(map[uint32]*list.Element), - theirLogIndex: make(map[uint32]*list.Element), - Capacity: state.Capacity, - LocalDeliveryScript: state.OurDeliveryScript, - RemoteDeliveryScript: state.TheirDeliveryScript, - FundingRedeemScript: state.FundingRedeemScript, + signer: signer, + bio: bio, + channelEvents: events, + currentHeight: state.NumUpdates, + remoteCommitChain: newCommitmentChain(state.NumUpdates), + localCommitChain: newCommitmentChain(state.NumUpdates), + channelState: state, + revocationWindowEdge: state.NumUpdates, + ourUpdateLog: list.New(), + theirUpdateLog: list.New(), + ourLogIndex: make(map[uint32]*list.Element), + theirLogIndex: make(map[uint32]*list.Element), + Capacity: state.Capacity, + LocalDeliveryScript: state.OurDeliveryScript, + RemoteDeliveryScript: state.TheirDeliveryScript, + FundingRedeemScript: state.FundingRedeemScript, + ForceCloseSignal: make(chan struct{}), + UnilateralCloseSignal: make(chan struct{}), } // Initialize both of our chains the current un-revoked commitment for // each side. // TODO(roasbeef): add chnneldb.RevocationLogTail method, then init - // their commitment from that + // their commitment from that as we may be de-synced initialCommitment := &commitment{ height: lc.currentHeight, ourBalance: state.OurBalance, @@ -445,16 +454,15 @@ func NewLightningChannel(signer Signer, wallet *LightningWallet, lc.restoreStateLogs() } - // TODO(roasbeef): do a NotifySpent for the funding input, and - // NotifyReceived for all commitment outputs. - + // Create the sign descriptor which we'll be using very frequently to + // request a signature for the 2-of-2 multi-sig from the signer in + // order to complete channel state transitions. fundingPkScript, err := witnessScriptHash(state.FundingRedeemScript) if err != nil { return nil, err } lc.fundingTxIn = wire.NewTxIn(state.FundingOutpoint, nil, nil) lc.fundingP2WSH = fundingPkScript - lc.signDesc = &SignDescriptor{ PubKey: lc.channelState.OurMultiSigKey, RedeemScript: lc.channelState.FundingRedeemScript, @@ -466,6 +474,38 @@ func NewLightningChannel(signer Signer, wallet *LightningWallet, InputIndex: 0, } + // Register for a notification to be dispatched if the funding outpoint + // has been spent. This indicates that either us or the remote party + // has broadcasted a commitment transaction on-chain. + fundingOut := &lc.fundingTxIn.PreviousOutPoint + channelCloseNtfn, err := lc.channelEvents.RegisterSpendNtfn(fundingOut) + if err != nil { + return nil, err + } + + // TODO(roasbeef) move into the peer's htlcManager? + // * if not, send the SpendDetail over the channel instead of just + // closing it + go func() { + // If the daemon is shutting down, then this notification channel + // will be closed, so check the second read-value to avoid a false + // positive. + if _, ok := <-channelCloseNtfn.Spend; !ok { + return + } + + // If the channel's doesn't already indicate that a commitment + // transaction has been broadcast on-chain, then this means the + // remote party broadcasted their commitment transaction. + // TODO(roasbeef): wait for a conf? + lc.Lock() + if lc.status != channelDispute { + close(lc.UnilateralCloseSignal) + lc.status = channelDispute + } + lc.Unlock() + }() + return lc, nil } @@ -1379,9 +1419,135 @@ func (lc *LightningChannel) addHTLC(commitTx *wire.MsgTx, ourCommit bool, return nil } -// ForceClose... -func (lc *LightningChannel) ForceClose() error { - return nil +// ForceCloseSummary describes the final commitment state before the channel is +// locked-down to initiate a force closure by broadcasting the latest state +// on-chain. The summary includes all the information required to claim all +// rightfully owned outputs. +// TODO(roasbeef): generalize, add HTLC info, revocatio info, etc. +type ForceCloseSummary struct { + // CloseTx is the transaction which closed the channel on-chain. If we + // initiate the force close, then this'll be our latest commitment + // state. Otherwise, this'll be the state that the remote peer + // broadcasted on-chain. + CloseTx *wire.MsgTx + + // SelfOutpoint is the output created by the above close tx which is + // spendable by us after a relative time delay. + SelfOutpoint wire.OutPoint + + // SelfOutputMaturity is the relative maturity period before the above + // output can be claimed. + SelfOutputMaturity uint32 + + // SelfOutputSignDesc is a fully populated sign descriptor capable of + // generating a valid signature to swee the self output. + SelfOutputSignDesc *SignDescriptor +} + +// ForceClose executes a unilateral closure of the transaction at the current +// lowest commitment height of the channel. Following a force closure, all +// state transitions, or modifications to the state update logs will be +// rejected. Additionally, this function also returns a ForceCloseSummary which +// includes the necessary details required to sweep all the time-locked within +// the commitment transaction. +// +// TODO(roasbeef): all methods need to abort if in dispute state +// TODO(roasbeef): method to generate CloseSummaries for when the remote peer +// does a unilateral close +func (lc *LightningChannel) ForceClose() (*ForceCloseSummary, error) { + lc.Lock() + defer lc.Unlock() + + // Set the channel state to indicate that the channel is now in a + // contested state. + lc.status = channelDispute + + // Fetch the current commitment transaction, along with their signature + // for the transaction. + commitTx := lc.channelState.OurCommitTx + theirSig := append(lc.channelState.OurCommitSig, byte(txscript.SigHashAll)) + + // With this, we then generate the full witness so the caller can + // broadcast a fully signed transaction. + lc.signDesc.SigHashes = txscript.NewTxSigHashes(commitTx) + ourSigRaw, err := lc.signer.SignOutputRaw(commitTx, lc.signDesc) + if err != nil { + return nil, err + } + + ourSig := append(ourSigRaw, byte(txscript.SigHashAll)) + + // With the final signature generated, create the witness stack + // required to spend from the multi-sig output. + ourKey := lc.channelState.OurMultiSigKey.SerializeCompressed() + theirKey := lc.channelState.TheirMultiSigKey.SerializeCompressed() + witness := SpendMultiSig(lc.FundingRedeemScript, ourKey, ourSig, + theirKey, theirSig) + commitTx.TxIn[0].Witness = witness + + // Locate the output index of the delayed commitment output back to us. + // We'll return the details of this output to the caller so they can + // sweep it once it's mature. + // TODO(roasbeef): also return HTLC info, assumes only p2wsh is commit + // tx + var delayIndex uint32 + var delayScript []byte + for i, txOut := range commitTx.TxOut { + if !txscript.IsPayToWitnessScriptHash(txOut.PkScript) { + continue + } + + delayIndex = uint32(i) + delayScript = txOut.PkScript + } + + csvTimeout := lc.channelState.LocalCsvDelay + selfKey := lc.channelState.OurCommitKey + + // Re-derive the original pkScript for out to-self output within the + // commitment transaction. We'll need this for the created sign + // descriptor. + elkrem := lc.channelState.LocalElkrem + unusedRevocation, err := elkrem.AtIndex(lc.currentHeight) + if err != nil { + return nil, err + } + revokeKey := DeriveRevocationPubkey(lc.channelState.TheirCommitKey, + unusedRevocation[:]) + selfScript, err := commitScriptToSelf(csvTimeout, selfKey, revokeKey) + if err != nil { + return nil, err + } + + // With the necessary information gatehred above, create a new sign + // descriptor which is capable of generating the signature the caller + // needs to sweep this output. The hash cache, and input index are not + // set as the caller will decide these values once sweeping the output. + selfSignDesc := &SignDescriptor{ + PubKey: selfKey, + RedeemScript: selfScript, + Output: &wire.TxOut{ + PkScript: delayScript, + Value: int64(lc.channelState.OurBalance), + }, + HashType: txscript.SigHashAll, + } + + // Finally, close the channel force close signal which notifies any + // subscribers that the channel has now been forcibly closed. This + // allows callers to begin to carry out any post channel closure + // activities. + close(lc.ForceCloseSignal) + + return &ForceCloseSummary{ + CloseTx: commitTx, + SelfOutpoint: wire.OutPoint{ + Hash: commitTx.TxSha(), + Index: delayIndex, + }, + SelfOutputMaturity: csvTimeout, + SelfOutputSignDesc: selfSignDesc, + }, nil } // InitCooperativeClose initiates a cooperative closure of an active lightning diff --git a/lnwallet/interface.go b/lnwallet/interface.go index a29dffcc4..3e563dcc2 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -130,6 +130,7 @@ type WalletController interface { // to date data possible. // // TODO(roasbeef): move to diff package perhaps? +// TODO(roasbeef): move publish txn here? type BlockChainIO interface { // GetCurrentHeight returns the current height of the valid most-work // chain the implementation is aware of. diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 41c1894ef..529f4dac0 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1142,7 +1142,7 @@ func (l *LightningWallet) handleChannelOpen(req *channelOpenMsg) { // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier, res.partialState) + channel, _ := NewLightningChannel(l.Signer, l.chainIO, l.chainNotifier, res.partialState) res.chanOpen <- channel req.err <- nil @@ -1183,7 +1183,7 @@ out: // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier, + channel, _ := NewLightningChannel(l.Signer, l.chainIO, l.chainNotifier, res.partialState) res.chanOpen <- channel } diff --git a/networktest.go b/networktest.go index 59ae5c424..36c406b26 100644 --- a/networktest.go +++ b/networktest.go @@ -545,8 +545,8 @@ func (n *networkHarness) CloseChannel(ctx context.Context, force bool) (lnrpc.Lightning_CloseChannelClient, error) { closeReq := &lnrpc.CloseChannelRequest{ - ChannelPoint: cp, - AllowForceClose: force, + ChannelPoint: cp, + Force: force, } closeRespStream, err := lnNode.CloseChannel(ctx, closeReq) if err != nil { diff --git a/peer.go b/peer.go index db8b21067..938d9ccc6 100644 --- a/peer.go +++ b/peer.go @@ -219,7 +219,7 @@ func (p *peer) loadActiveChannels(chans []*channeldb.OpenChannel) error { for _, dbChan := range chans { chanID := dbChan.ChanID lnChan, err := lnwallet.NewLightningChannel(p.server.lnwallet.Signer, - p.server.lnwallet, p.server.chainNotifier, dbChan) + p.server.bio, p.server.chainNotifier, dbChan) if err != nil { return err } diff --git a/server.go b/server.go index 654205758..381a74492 100644 --- a/server.go +++ b/server.go @@ -39,9 +39,12 @@ type server struct { listeners []net.Listener peers map[int32]*peer - rpcServer *rpcServer + rpcServer *rpcServer + chainNotifier chainntnfs.ChainNotifier - lnwallet *lnwallet.LightningWallet + + bio lnwallet.BlockChainIO + lnwallet *lnwallet.LightningWallet // TODO(roasbeef): add to constructor fundingMgr *fundingManager @@ -63,7 +66,8 @@ type server struct { // newServer creates a new instance of the server which is to listen using the // passed listener address. func newServer(listenAddrs []string, notifier chainntnfs.ChainNotifier, - wallet *lnwallet.LightningWallet, chanDB *channeldb.DB) (*server, error) { + bio lnwallet.BlockChainIO, wallet *lnwallet.LightningWallet, + chanDB *channeldb.DB) (*server, error) { privKey, err := wallet.GetIdentitykey() if err != nil { @@ -80,6 +84,7 @@ func newServer(listenAddrs []string, notifier chainntnfs.ChainNotifier, serializedPubKey := privKey.PubKey().SerializeCompressed() s := &server{ + bio: bio, chainNotifier: notifier, chanDB: chanDB, fundingMgr: newFundingManager(wallet), @@ -283,7 +288,7 @@ out: } case msg := <-s.routingMgr.ChOut: msg1 := msg.(*routing.RoutingMessage) - if msg1.ReceiverID == nil{ + if msg1.ReceiverID == nil { peerLog.Critical("msg1.GetReceiverID() == nil") } receiverID := msg1.ReceiverID.ToByte32()