diff --git a/fundingmanager.go b/fundingmanager.go index b2c233e3..5682888d 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -335,6 +335,8 @@ func (f *fundingManager) handleFundingRequest(fmsg *fundingRequestMsg) { fndgLog.Infof("Recv'd fundingRequest(amt=%v, delay=%v, pendingId=%v) "+ "from peerID(%v)", amt, delay, msg.ChannelID, fmsg.peer.id) + ourDustLimit := lnwallet.DefaultDustLimit() + theirDustlimit := msg.DustLimit // Attempt to initialize a reservation within the wallet. If the wallet // has insufficient resources to create the channel, then the reservation // attempt may be rejected. Note that since we're on the responding @@ -342,7 +344,8 @@ func (f *fundingManager) handleFundingRequest(fmsg *fundingRequestMsg) { // channel ourselves. // TODO(roasbeef): passing num confs 1 is irrelevant here, make signed? reservation, err := f.wallet.InitChannelReservation(amt, 0, - fmsg.peer.addr.IdentityKey, fmsg.peer.addr.Address, 1, delay) + fmsg.peer.addr.IdentityKey, fmsg.peer.addr.Address, 1, delay, + ourDustLimit) if err != nil { // TODO(roasbeef): push ErrorGeneric message fndgLog.Errorf("Unable to initialize reservation: %v", err) @@ -350,6 +353,8 @@ func (f *fundingManager) handleFundingRequest(fmsg *fundingRequestMsg) { return } + reservation.SetTheirDustLimit(theirDustlimit) + // Once the reservation has been created successfully, we add it to this // peers map of pending reservations to track this particular reservation // until either abort or completion. @@ -396,7 +401,7 @@ func (f *fundingManager) handleFundingRequest(fmsg *fundingRequestMsg) { fundingResp := lnwire.NewSingleFundingResponse(msg.ChannelID, ourContribution.RevocationKey, ourContribution.CommitKey, ourContribution.MultiSigKey, ourContribution.CsvDelay, - deliveryScript) + deliveryScript, ourDustLimit) fmsg.peer.queueMsg(fundingResp, nil) } @@ -425,6 +430,8 @@ func (f *fundingManager) handleFundingResponse(fmsg *fundingResponseMsg) { fndgLog.Infof("Recv'd fundingResponse for pendingID(%v)", msg.ChannelID) + resCtx.reservation.SetTheirDustLimit(msg.DustLimit) + // The remote node has responded with their portion of the channel // contribution. At this point, we can process their contribution which // allows us to construct and sign both the commitment transaction, and @@ -747,22 +754,24 @@ func (f *fundingManager) initFundingWorkflow(targetPeer *peer, req *openChanReq) func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { var ( // TODO(roasbeef): add delay - nodeID = msg.peer.addr.IdentityKey - localAmt = msg.localFundingAmt - remoteAmt = msg.remoteFundingAmt - capacity = localAmt + remoteAmt - numConfs = msg.numConfs + nodeID = msg.peer.addr.IdentityKey + localAmt = msg.localFundingAmt + remoteAmt = msg.remoteFundingAmt + capacity = localAmt + remoteAmt + numConfs = msg.numConfs + ourDustLimit = lnwallet.DefaultDustLimit() ) fndgLog.Infof("Initiating fundingRequest(localAmt=%v, remoteAmt=%v, "+ - "capacity=%v, numConfs=%v, addr=%v)", localAmt, remoteAmt, - capacity, numConfs, msg.peer.addr.Address) + "capacity=%v, numConfs=%v, addr=%v, dustLimit=%v)", localAmt, + remoteAmt, ourDustLimit, capacity, numConfs, + msg.peer.addr.Address) // Initialize a funding reservation with the local wallet. If the // wallet doesn't have enough funds to commit to this channel, then // the request will fail, and be aborted. reservation, err := f.wallet.InitChannelReservation(capacity, localAmt, - nodeID, msg.peer.addr.Address, uint16(numConfs), 4) + nodeID, msg.peer.addr.Address, uint16(numConfs), 4, ourDustLimit) if err != nil { msg.err <- err return @@ -815,6 +824,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { contribution.CommitKey, contribution.MultiSigKey, deliveryScript, + ourDustLimit, ) msg.peer.queueMsg(fundingReq, nil) } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index ee6daf27..15750c61 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -881,19 +881,21 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, var selfKey *btcec.PublicKey var remoteKey *btcec.PublicKey var delay uint32 - var delayBalance, p2wkhBalance btcutil.Amount + var delayBalance, p2wkhBalance, dustLimit btcutil.Amount if remoteChain { selfKey = lc.channelState.TheirCommitKey remoteKey = lc.channelState.OurCommitKey delay = lc.channelState.RemoteCsvDelay delayBalance = theirBalance p2wkhBalance = ourBalance + dustLimit = lc.channelState.TheirDustLimit } else { selfKey = lc.channelState.OurCommitKey remoteKey = lc.channelState.TheirCommitKey delay = lc.channelState.LocalCsvDelay delayBalance = ourBalance p2wkhBalance = theirBalance + dustLimit = lc.channelState.OurDustLimit } // Generate a new commitment transaction with all the latest @@ -905,6 +907,10 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, return nil, err } for _, htlc := range filteredHTLCView.ourUpdates { + if htlc.Amount < dustLimit { + continue + } + err := lc.addHTLC(commitTx, ourCommitTx, htlc, revocationHash, delay, false) if err != nil { @@ -912,6 +918,10 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, } } for _, htlc := range filteredHTLCView.theirUpdates { + if htlc.Amount < dustLimit { + continue + } + err := lc.addHTLC(commitTx, ourCommitTx, htlc, revocationHash, delay, true) if err != nil { diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index ae48c292..ef14b01c 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -188,6 +188,8 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan channelCapacity := btcutil.Amount(10 * 1e8) channelBal := channelCapacity / 2 + aliceDustLimit := btcutil.Amount(200) + bobDustLimit := btcutil.Amount(800) csvTimeoutAlice := uint32(5) csvTimeoutBob := uint32(4) @@ -265,6 +267,8 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan TheirCurrentRevocation: bobRevokeKey, LocalElkrem: aliceElkrem, RemoteElkrem: &elkrem.ElkremReceiver{}, + TheirDustLimit: bobDustLimit, + OurDustLimit: aliceDustLimit, Db: dbAlice, } bobChannelState := &channeldb.OpenChannel{ @@ -289,6 +293,8 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan TheirCurrentRevocation: aliceRevokeKey, LocalElkrem: bobElkrem, RemoteElkrem: &elkrem.ElkremReceiver{}, + TheirDustLimit: aliceDustLimit, + OurDustLimit: bobDustLimit, Db: dbBob, } @@ -833,6 +839,122 @@ func TestCheckHTLCNumberConstraint(t *testing.T) { } +// TestCheckDustLimit checks that unsettled HTLC with dust limit not included in +// commitment transaction as output, but sender balance is decreased (thereby all +// unsettled dust HTLCs will go to miners fee). +func TestCheckDustLimit(t *testing.T) { + createHTLC := func(data, amount btcutil.Amount) (*lnwire.HTLCAddRequest, + [32]byte) { + preimage := bytes.Repeat([]byte{byte(data)}, 32) + paymentHash := fastsha256.Sum256(preimage) + + var returnPreimage [32]byte + copy(returnPreimage[:], preimage) + + return &lnwire.HTLCAddRequest{ + RedemptionHashes: [][32]byte{paymentHash}, + Amount: amount, + Expiry: uint32(5), + }, returnPreimage + } + + // Create a test channel which will be used for the duration of this + // unittest. The channel will be funded evenly with Alice having 5 BTC, + // and Bob having 5 BTC. + aliceChannel, bobChannel, cleanUp, err := createTestChannels(3) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + aliceDustLimit := aliceChannel.channelState.OurDustLimit + bobDustLimit := bobChannel.channelState.OurDustLimit + htlcAmount := btcutil.Amount(500) + + if !((htlcAmount > aliceDustLimit) && (bobDustLimit > htlcAmount)) { + t.Fatal("htlc amount needs to be above Alice's dust limit, but " + + "below Bob's dust limit .") + } + + aliceAmount := aliceChannel.channelState.OurBalance + bobAmount := bobChannel.channelState.OurBalance + + htlc, preimage := createHTLC(0, htlcAmount) + if _, err := aliceChannel.AddHTLC(htlc); err != nil { + t.Fatalf("alice unable to add htlc: %v", err) + } + if _, err := bobChannel.ReceiveHTLC(htlc); err != nil { + t.Fatalf("bob unable to receive htlc: %v", err) + } + if err := forceStateTransition(aliceChannel, bobChannel); err != nil { + t.Fatalf("Can't update the channel state: %v", err) + } + + // First two outputs are payment to them and to us. If we encounter + // third output it means that dust HTLC was included. Their channel + // balance shouldn't change because, it will be changed only after + // HTLC will be settled. + + // From Alice point of view HTLC's amount is bigger then dust limit. + commitment := aliceChannel.localCommitChain.tip() + if len(commitment.txn.TxOut) != 3 { + t.Fatal("htlc wasn't added") + } + if commitment.ourBalance != aliceAmount-htlcAmount { + t.Fatal("our balance wasn't updated") + } + if commitment.theirBalance != bobAmount { + t.Fatal("their balance was updated") + } + + // From Bob point of view HTLC's amount is lower then dust limit. + commitment = bobChannel.localCommitChain.tip() + if len(commitment.txn.TxOut) != 2 { + t.Fatal("HTLC with dust amount was added") + } + if commitment.theirBalance != aliceAmount-htlcAmount { + t.Fatal("their balance wasn't updated") + } + if commitment.ourBalance != bobAmount { + t.Fatal("our balance was updated") + } + + // Settle HTLC and sign new commitment. + settleIndex, err := bobChannel.SettleHTLC(preimage) + if err != nil { + t.Fatalf("bob unable to settle inbound htlc: %v", err) + } + err = aliceChannel.ReceiveHTLCSettle(preimage, settleIndex) + if err != nil { + t.Fatalf("alice unable to accept settle of outbound htlc: %v", err) + } + if err := forceStateTransition(aliceChannel, bobChannel); err != nil { + t.Fatalf("Can't update the channel state: %v", err) + } + + commitment = aliceChannel.localCommitChain.tip() + if len(commitment.txn.TxOut) != 2 { + t.Fatal("HTLC wasn't settled") + } + if commitment.ourBalance != aliceAmount-htlcAmount { + t.Fatal("our balance wasn't updated") + } + if commitment.theirBalance != bobAmount+htlcAmount { + t.Fatal("their balance wasn't updated") + } + + commitment = bobChannel.localCommitChain.tip() + if len(commitment.txn.TxOut) != 2 { + t.Fatal("HTLC with dust amount wasn't settled") + } + if commitment.ourBalance != bobAmount+htlcAmount { + t.Fatal("our balance wasn't updated") + } + if commitment.theirBalance != aliceAmount-htlcAmount { + t.Fatal("their balance wasn't updated") + } +} + func TestStateUpdatePersistence(t *testing.T) { // Create a test channel which will be used for the duration of this // unittest. The channel will be funded evenly with Alice having 5 BTC, diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 92aeac76..b4341ced 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -363,7 +363,7 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, wallet *lnwallet // Bob initiates a channel funded with 5 BTC for each side, so 10 // BTC total. He also generates 2 BTC in change. chanReservation, err := wallet.InitChannelReservation(fundingAmount*2, - fundingAmount, bobNode.id, bobAddr, numReqConfs, 4) + fundingAmount, bobNode.id, bobAddr, numReqConfs, 4, 540) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } @@ -522,7 +522,7 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness, // Create a single channel asking for 16 BTC total. fundingAmount := btcutil.Amount(8 * 1e8) _, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, - testPub, bobAddr, numReqConfs, 4) + testPub, bobAddr, numReqConfs, 4, 540) if err != nil { t.Fatalf("unable to initialize funding reservation 1: %v", err) } @@ -532,7 +532,7 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness, // that aren't locked, so this should fail. amt := btcutil.Amount(900 * 1e8) failedReservation, err := wallet.InitChannelReservation(amt, amt, - testPub, bobAddr, numReqConfs, 4) + testPub, bobAddr, numReqConfs, 4, 540) if err == nil { t.Fatalf("not error returned, should fail on coin selection") } @@ -552,14 +552,14 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, // Create a reservation for 44 BTC. fundingAmount := btcutil.Amount(44 * 1e8) chanReservation, err := wallet.InitChannelReservation(fundingAmount, - fundingAmount, testPub, bobAddr, numReqConfs, 4) + fundingAmount, testPub, bobAddr, numReqConfs, 4, 540) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } // Attempt to create another channel with 44 BTC, this should fail. _, err = wallet.InitChannelReservation(fundingAmount, - fundingAmount, testPub, bobAddr, numReqConfs, 4) + fundingAmount, testPub, bobAddr, numReqConfs, 4, 540) if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok { t.Fatalf("coin selection succeded should have insufficient funds: %v", err) @@ -589,7 +589,7 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, // Request to fund a new channel should now succeed. _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, - testPub, bobAddr, numReqConfs, 4) + testPub, bobAddr, numReqConfs, 4, 540) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } @@ -628,7 +628,7 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, // Initialize a reservation for a channel with 4 BTC funded solely by us. fundingAmt := btcutil.Amount(4 * 1e8) chanReservation, err := lnwallet.InitChannelReservation(fundingAmt, - fundingAmt, bobNode.id, bobAddr, numReqConfs, 4) + fundingAmt, bobNode.id, bobAddr, numReqConfs, 4, 540) if err != nil { t.Fatalf("unable to init channel reservation: %v", err) } @@ -769,7 +769,7 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, // contribution and the necessary resources. fundingAmt := btcutil.Amount(0) chanReservation, err := wallet.InitChannelReservation(capacity, - fundingAmt, bobNode.id, bobAddr, numReqConfs, 4) + fundingAmt, bobNode.id, bobAddr, numReqConfs, 4, 540) if err != nil { t.Fatalf("unable to init channel reservation: %v", err) } diff --git a/lnwallet/parameters.go b/lnwallet/parameters.go index 1b889177..dfab4128 100644 --- a/lnwallet/parameters.go +++ b/lnwallet/parameters.go @@ -1,12 +1,12 @@ package lnwallet import ( - "github.com/roasbeef/btcwallet/wallet/txrules" "github.com/roasbeef/btcutil" + "github.com/roasbeef/btcwallet/wallet/txrules" ) // DefaultDustLimit is used to calculate the dust HTLC amount which will be -// proposed to other node during channel creation. +// send to other node during funding process. func DefaultDustLimit() btcutil.Amount { return txrules.GetDustThreshold(P2WSHSize, txrules.DefaultRelayFeePerKb) } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index dfcd0f34..b816829d 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -382,6 +382,14 @@ func (r *ChannelReservation) LocalCommitTx() *wire.MsgTx { return r.partialState.OurCommitTx } +// SetDustLimit set dust limit of the remote party. +func (r *ChannelReservation) SetTheirDustLimit(dustLimit btcutil.Amount) { + r.Lock() + defer r.Unlock() + + r.partialState.TheirDustLimit = dustLimit +} + // FundingOutpoint returns the outpoint of the funding transaction. // // NOTE: The pointer returned will only be set once the .ProcesContribution() diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index ba069620..356b929a 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -100,6 +100,11 @@ type initFundingReserveMsg struct { // TODO(roasbeef): integrate fee estimation project... minFeeRate btcutil.Amount + // ourDustLimit is the threshold below which no HTLC output should be + // generated for our commitment transaction; ie. HTLCs below + // this amount are not enforceable onchain from our point of view. + ourDustLimit btcutil.Amount + // The delay on the "pay-to-self" output(s) of the commitment transaction. csvDelay uint32 @@ -484,7 +489,7 @@ out: func (l *LightningWallet) InitChannelReservation(capacity, ourFundAmt btcutil.Amount, theirID *btcec.PublicKey, theirAddr *net.TCPAddr, numConfs uint16, - csvDelay uint32) (*ChannelReservation, error) { + csvDelay uint32, ourDustLimit btcutil.Amount) (*ChannelReservation, error) { errChan := make(chan error, 1) respChan := make(chan *ChannelReservation, 1) @@ -494,6 +499,7 @@ func (l *LightningWallet) InitChannelReservation(capacity, numConfs: numConfs, fundingAmount: ourFundAmt, csvDelay: csvDelay, + ourDustLimit: ourDustLimit, nodeID: theirID, err: errChan, resp: respChan, @@ -522,11 +528,14 @@ func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg reservation.Lock() defer reservation.Unlock() - reservation.partialState.IdentityPub = req.nodeID reservation.nodeAddr = req.nodeAddr - ourContribution := reservation.ourContribution - ourContribution.CsvDelay = req.csvDelay + reservation.ourContribution.CsvDelay = req.csvDelay + + reservation.partialState.IdentityPub = req.nodeID reservation.partialState.LocalCsvDelay = req.csvDelay + reservation.partialState.OurDustLimit = req.ourDustLimit + + ourContribution := reservation.ourContribution // If we're on the receiving end of a single funder channel then we // don't need to perform any coin selection. Otherwise, attempt to diff --git a/peer.go b/peer.go index 0b91d406..2545ead8 100644 --- a/peer.go +++ b/peer.go @@ -390,13 +390,7 @@ out: // * .(CommitmentUpdater) case *lnwire.ErrorGeneric: - switch msg.Code { - case lnwire.ErrorMaxPendingChannels: - p.server.fundingMgr.processErrorGeneric(msg, p) - default: - peerLog.Warnf("ErrorGeneric(%v) handling isn't"+ - " implemented.", msg.Code) - } + p.server.fundingMgr.processErrorGeneric(msg, p) case *lnwire.HTLCAddRequest: isChanUpdate = true targetChan = msg.ChannelPoint diff --git a/server.go b/server.go index 898e87a7..11229f02 100644 --- a/server.go +++ b/server.go @@ -455,9 +455,9 @@ func (s *server) handleOpenChanReq(req *openChanReq) { } // Spawn a goroutine to send the funding workflow request to the funding - // manager. This allows the server to continue handling queries instead of - // blocking on this request which is exporeted as a synchronous request to - // the outside world. + // manager. This allows the server to continue handling queries instead + // of blocking on this request which is exported as a synchronous + // request to the outside world. // TODO(roasbeef): server semaphore to restrict num goroutines go s.fundingMgr.initFundingWorkflow(targetPeer, req) }