From 408be356fbe7767b181eb8c40bddeaa0e9abc87e Mon Sep 17 00:00:00 2001 From: bryanvu Date: Wed, 17 May 2017 16:51:10 -0700 Subject: [PATCH] lnwallet: update channel close to use fee estimation interface This commit switches the channel close workflow to use the lnwallet fee estimation interface rather than the hardcoded proposedFee. --- lnwallet/channel.go | 54 +++++++++++++++++++++++++--------------- lnwallet/channel_test.go | 43 ++++++++++++++++---------------- peer.go | 31 +++++++++++++++-------- 3 files changed, 76 insertions(+), 52 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 36ebe510..d6629c72 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2532,39 +2532,42 @@ func (lc *LightningChannel) ForceClose() (*ForceCloseSummary, error) { // // TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs, // settle any inflight. -func (lc *LightningChannel) CreateCloseProposal(feeRate uint64) ([]byte, error) { +func (lc *LightningChannel) CreateCloseProposal(feeRate uint64) ([]byte, uint64, + error) { + lc.Lock() defer lc.Unlock() // If we're already closing the channel, then ignore this request. if lc.status == channelClosing || lc.status == channelClosed { // TODO(roasbeef): check to ensure no pending payments - return nil, ErrChanClosing + return nil, 0, ErrChanClosing } - // Calculate the fee for the commitment transaction based on its size. - // For a cooperative close, there should be no HTLCs. - commitFee := (lc.channelState.FeePerKw * commitWeight) / 1000 + // Subtract the proposed fee from the appropriate balance, taking care + // not to persist the adjusted balance, as the feeRate may change + // during the channel closing process. + proposedFee := uint64(btcutil.Amount(feeRate) * commitWeight / 1000) + ourBalance := lc.channelState.OurBalance + theirBalance := lc.channelState.TheirBalance if lc.channelState.IsInitiator { - lc.channelState.OurBalance = lc.channelState.OurBalance - commitFee + ourBalance = ourBalance - btcutil.Amount(proposedFee) } else { - lc.channelState.TheirBalance = lc.channelState.TheirBalance - commitFee + theirBalance = theirBalance - btcutil.Amount(proposedFee) } closeTx := CreateCooperativeCloseTx(lc.fundingTxIn, lc.channelState.OurDustLimit, lc.channelState.TheirDustLimit, - lc.channelState.OurBalance, lc.channelState.TheirBalance, - lc.channelState.OurDeliveryScript, - lc.channelState.TheirDeliveryScript, - lc.channelState.IsInitiator) + ourBalance, theirBalance, lc.channelState.OurDeliveryScript, + lc.channelState.TheirDeliveryScript, lc.channelState.IsInitiator) // Ensure that the transaction doesn't explicitly violate any // consensus rules such as being too big, or having any value with a // negative output. tx := btcutil.NewTx(closeTx) if err := blockchain.CheckTransactionSanity(tx); err != nil { - return nil, err + return nil, 0, err } // Finally, sign the completed cooperative closure transaction. As the @@ -2574,14 +2577,14 @@ func (lc *LightningChannel) CreateCloseProposal(feeRate uint64) ([]byte, error) lc.signDesc.SigHashes = txscript.NewTxSigHashes(closeTx) sig, err := lc.signer.SignOutputRaw(closeTx, lc.signDesc) if err != nil { - return nil, err + return nil, 0, err } // As everything checks out, indicate in the channel status that a // channel closure has been initiated. lc.status = channelClosing - return sig, nil + return sig, proposedFee, nil } // CompleteCooperativeClose completes the cooperative closure of the target @@ -2590,8 +2593,8 @@ func (lc *LightningChannel) CreateCloseProposal(feeRate uint64) ([]byte, error) // // NOTE: The passed local and remote sigs are expected to be fully complete // signatures including the proper sighash byte. -func (lc *LightningChannel) CompleteCooperativeClose(localSig, - remoteSig []byte, proposedFee uint64) (*wire.MsgTx, error) { +func (lc *LightningChannel) CompleteCooperativeClose(localSig, remoteSig []byte, + feeRate uint64) (*wire.MsgTx, error) { lc.Lock() defer lc.Unlock() @@ -2601,15 +2604,26 @@ func (lc *LightningChannel) CompleteCooperativeClose(localSig, return nil, ErrChanClosing } + // Subtract the proposed fee from the appropriate balance, taking care + // not to persist the adjusted balance, as the feeRate may change + // during the channel closing process. + proposedFee := uint64(btcutil.Amount(feeRate) * commitWeight / 1000) + ourBalance := lc.channelState.OurBalance + theirBalance := lc.channelState.TheirBalance + + if lc.channelState.IsInitiator { + ourBalance = ourBalance - btcutil.Amount(proposedFee) + } else { + theirBalance = theirBalance - btcutil.Amount(proposedFee) + } + // Create the transaction used to return the current settled balance // on this active channel back to both parties. In this current model, // the initiator pays full fees for the cooperative close transaction. closeTx := CreateCooperativeCloseTx(lc.fundingTxIn, lc.channelState.OurDustLimit, lc.channelState.TheirDustLimit, - lc.channelState.OurBalance, lc.channelState.TheirBalance, - lc.channelState.OurDeliveryScript, - lc.channelState.TheirDeliveryScript, - lc.channelState.IsInitiator) + ourBalance, theirBalance, lc.channelState.OurDeliveryScript, + lc.channelState.TheirDeliveryScript, lc.channelState.IsInitiator) // Ensure that the transaction doesn't explicitly validate any // consensus rules such as being too big, or having any value with a diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 6831e5fd..876dff4d 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -53,12 +53,6 @@ var ( numReqConfs = uint16(1) ) -const ( - // proposedFee is a hard-coded fee value, intended to be replaced by a - // more robust fee estimation implemention. - proposedFee = 5000 -) - type mockSigner struct { key *btcec.PrivateKey } @@ -250,7 +244,8 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan var obsfucator [StateHintSize]byte copy(obsfucator[:], aliceFirstRevoke[:]) - feePerKw := btcutil.Amount(6000) + estimator := &StaticFeeEstimator{24, 6} + feePerKw := btcutil.Amount(estimator.EstimateFeePerWeight(1) * 1000) aliceChannelState := &channeldb.OpenChannel{ IdentityPub: aliceKeyPub, ChanID: prevOut, @@ -315,7 +310,6 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan bobSigner := &mockSigner{bobKeyPriv} notifier := &mockNotfier{} - estimator := &StaticFeeEstimator{24, 6} channelAlice, err := NewLightningChannel(aliceSigner, notifier, estimator, aliceChannelState) @@ -711,28 +705,32 @@ func TestCooperativeChannelClosure(t *testing.T) { } defer cleanUp() + aliceFeeRate := uint64(aliceChannel.channelState.FeePerKw) + bobFeeRate := uint64(bobChannel.channelState.FeePerKw) + // First we test creating of cooperative close proposals. - aliceSig, err := aliceChannel.CreateCloseProposal(proposedFee) + aliceSig, _, err := aliceChannel.CreateCloseProposal( + aliceFeeRate) if err != nil { t.Fatalf("unable to create alice coop close proposal: %v", err) } aliceCloseSig := append(aliceSig, byte(txscript.SigHashAll)) - bobSig, err := bobChannel.CreateCloseProposal(proposedFee) + bobSig, _, err := bobChannel.CreateCloseProposal(bobFeeRate) if err != nil { t.Fatalf("unable to create bob coop close proposal: %v", err) } bobCloseSig := append(bobSig, byte(txscript.SigHashAll)) aliceCloseTx, err := bobChannel.CompleteCooperativeClose(bobCloseSig, - aliceCloseSig, proposedFee) + aliceCloseSig, bobFeeRate) if err != nil { t.Fatalf("unable to complete alice cooperative close: %v", err) } bobCloseSha := aliceCloseTx.TxHash() bobCloseTx, err := aliceChannel.CompleteCooperativeClose(aliceCloseSig, - bobCloseSig, proposedFee) + bobCloseSig, aliceFeeRate) if err != nil { t.Fatalf("unable to complete bob cooperative close: %v", err) } @@ -1555,6 +1553,9 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { } defer cleanUp() + aliceFeeRate := uint64(aliceChannel.channelState.FeePerKw) + bobFeeRate := uint64(bobChannel.channelState.FeePerKw) + setDustLimit := func(dustVal btcutil.Amount) { aliceChannel.channelState.OurDustLimit = dustVal aliceChannel.channelState.TheirDustLimit = dustVal @@ -1582,20 +1583,20 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { // Both sides currently have over 1 BTC settled as part of their // balances. As a result, performing a cooperative closure now result // in both sides having an output within the closure transaction. - aliceSig, err := aliceChannel.CreateCloseProposal(proposedFee) + aliceSig, _, err := aliceChannel.CreateCloseProposal(aliceFeeRate) if err != nil { t.Fatalf("unable to close channel: %v", err) } aliceCloseSig := append(aliceSig, byte(txscript.SigHashAll)) - bobSig, err := bobChannel.CreateCloseProposal(proposedFee) + bobSig, _, err := bobChannel.CreateCloseProposal(bobFeeRate) if err != nil { t.Fatalf("unable to close channel: %v", err) } bobCloseSig := append(bobSig, byte(txscript.SigHashAll)) closeTx, err := bobChannel.CompleteCooperativeClose(bobCloseSig, - aliceCloseSig, proposedFee) + aliceCloseSig, bobFeeRate) if err != nil { t.Fatalf("unable to accept channel close: %v", err) } @@ -1617,20 +1618,20 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { // Attempt another cooperative channel closure. It should succeed // without any issues. - aliceSig, err = aliceChannel.CreateCloseProposal(proposedFee) + aliceSig, _, err = aliceChannel.CreateCloseProposal(aliceFeeRate) if err != nil { t.Fatalf("unable to close channel: %v", err) } aliceCloseSig = append(aliceSig, byte(txscript.SigHashAll)) - bobSig, err = bobChannel.CreateCloseProposal(proposedFee) + bobSig, _, err = bobChannel.CreateCloseProposal(bobFeeRate) if err != nil { t.Fatalf("unable to close channel: %v", err) } bobCloseSig = append(bobSig, byte(txscript.SigHashAll)) closeTx, err = bobChannel.CompleteCooperativeClose(bobCloseSig, - aliceCloseSig, proposedFee) + aliceCloseSig, bobFeeRate) if err != nil { t.Fatalf("unable to accept channel close: %v", err) } @@ -1653,20 +1654,20 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { // Our final attempt at another cooperative channel closure. It should // succeed without any issues. - aliceSig, err = aliceChannel.CreateCloseProposal(proposedFee) + aliceSig, _, err = aliceChannel.CreateCloseProposal(aliceFeeRate) if err != nil { t.Fatalf("unable to close channel: %v", err) } aliceCloseSig = append(aliceSig, byte(txscript.SigHashAll)) - bobSig, err = bobChannel.CreateCloseProposal(proposedFee) + bobSig, _, err = bobChannel.CreateCloseProposal(bobFeeRate) if err != nil { t.Fatalf("unable to close channel: %v", err) } bobCloseSig = append(bobSig, byte(txscript.SigHashAll)) closeTx, err = bobChannel.CompleteCooperativeClose(bobCloseSig, - aliceCloseSig, proposedFee) + aliceCloseSig, bobFeeRate) if err != nil { t.Fatalf("unable to accept channel close: %v", err) } diff --git a/peer.go b/peer.go index 10c1b01f..a04488da 100644 --- a/peer.go +++ b/peer.go @@ -39,10 +39,6 @@ const ( // messages to be sent across the wire, requested by objects outside // this struct. outgoingQueueLen = 50 - - // proposedFee is a hard-coded fee value, intended to be replaced by a - // more robust fee estimation implemention. - proposedFee = 5000 ) // outgoinMsg packages an lnwire.Message to be sent out on the wire, along with @@ -938,18 +934,26 @@ func (p *peer) handleInitClosingSigned(req *closeLinkReq, msg *lnwire.ClosingSig channel := p.activeChannels[chanID] p.activeChanMtx.RUnlock() - initiatorSig, err := channel.CreateCloseProposal(proposedFee) + // Calculate a fee rate that we believe to be fair. + // TODO(bvu): with a dynamic fee implementation, we will compare this to + // the fee proposed by the responder in their ClosingSigned message. + feeRate := p.server.feeEstimator.EstimateFeePerWeight(1) * 1000 + + // We agree with the proposed channel close transaction and fee rate, + // so generate our signature. + initiatorSig, proposedFee, err := channel.CreateCloseProposal(feeRate) if err != nil { req.err <- err return } initSig := append(initiatorSig, byte(txscript.SigHashAll)) - // Complete coop close transaction. + // Complete coop close transaction with the signatures of the close + // initiator and responder. responderSig := msg.Signature respSig := append(responderSig.Serialize(), byte(txscript.SigHashAll)) closeTx, err := channel.CompleteCooperativeClose(initSig, respSig, - proposedFee) + feeRate) if err != nil { req.err <- err // TODO(roasbeef): send ErrorGeneric to other side @@ -1063,8 +1067,10 @@ func (p *peer) handleResponseClosingSigned(msg *lnwire.ClosingSigned, initSig := append(initiatorSig.Serialize(), byte(txscript.SigHashAll)) chanPoint := channel.ChannelPoint() + // Calculate our expected fee rate. + feeRate := p.server.feeEstimator.EstimateFeePerWeight(1) * 1000 closeTx, err := channel.CompleteCooperativeClose(respSig, initSig, - proposedFee) + feeRate) if err != nil { peerLog.Errorf("unable to complete cooperative "+ "close for ChannelPoint(%v): %v", @@ -1195,9 +1201,12 @@ func (p *peer) sendShutdown(channel *lnwallet.LightningChannel) error { // sendClosingSigned handles the creation and sending of proposed channel // close transactions. func (p *peer) sendClosingSigned(channel *lnwallet.LightningChannel) ([]byte, error) { - // We agree with the proposed fee, so we send back our signature - // for the proposed transaction. - closeSig, err := channel.CreateCloseProposal(proposedFee) + + // Calculate an initial proposed fee rate for the close transaction. + feeRate := p.server.feeEstimator.EstimateFeePerWeight(1) * 1000 + + // Create a proposed channel close transaction with our fee proposal. + closeSig, proposedFee, err := channel.CreateCloseProposal(feeRate) if err != nil { return nil, err }