fundingmanager+lnwallet: add HTLC dust limit logic

This commit is contained in:
Andrey Samokhvalov 2016-12-06 17:05:46 +03:00 committed by Olaoluwa Osuntokun
parent c731156ac8
commit d01f1b5ff4
9 changed files with 188 additions and 35 deletions

@ -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)
}

@ -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 {

@ -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,

@ -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)
}

@ -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)
}

@ -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()

@ -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

@ -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

@ -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)
}