diff --git a/input/size.go b/input/size.go index 005c2273..d5f5e2b0 100644 --- a/input/size.go +++ b/input/size.go @@ -202,6 +202,23 @@ const ( // which will transition an incoming HTLC to the delay-and-claim state. HtlcSuccessWeight = 703 + // HtlcConfirmedScriptOverhead is the extra length of an HTLC script + // that requires confirmation before it can be spent. These extra bytes + // is a result of the extra CSV check. + HtlcConfirmedScriptOverhead = 3 + + // HtlcTimeoutWeightConfirmed is the weight of the HTLC timeout + // transaction which will transition an outgoing HTLC to the + // delay-and-claim state, for the confirmed HTLC outputs. It is 3 bytes + // larger because of the additional CSV check in the input script. + HtlcTimeoutWeightConfirmed = HtlcTimeoutWeight + HtlcConfirmedScriptOverhead + + // HtlcSuccessWeightCOnfirmed is the weight of the HTLC success + // transaction which will transition an incoming HTLC to the + // delay-and-claim state, for the confirmed HTLC outputs. It is 3 bytes + // larger because of the cdditional CSV check in the input script. + HtlcSuccessWeightConfirmed = HtlcSuccessWeight + HtlcConfirmedScriptOverhead + // MaxHTLCNumber is the maximum number HTLCs which can be included in a // commitment transaction. This limit was chosen such that, in the case // of a contract breach, the punishment transaction is able to sweep diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 6b072ade..aca93b99 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -596,7 +596,7 @@ func locateOutputIndex(p *PaymentDescriptor, tx *wire.MsgTx, ourCommit bool, // we need to keep track of the indexes of each HTLC in order to properly write // the current state to disk, and also to locate the PaymentDescriptor // corresponding to HTLC outputs in the commitment transaction. -func (c *commitment) populateHtlcIndexes() error { +func (c *commitment) populateHtlcIndexes(chanType channeldb.ChannelType) error { // First, we'll set up some state to allow us to locate the output // index of the all the HTLC's within the commitment transaction. We // must keep this index so we can validate the HTLC signatures sent to @@ -608,8 +608,10 @@ func (c *commitment) populateHtlcIndexes() error { // populateIndex is a helper function that populates the necessary // indexes within the commitment view for a particular HTLC. populateIndex := func(htlc *PaymentDescriptor, incoming bool) error { - isDust := htlcIsDust(incoming, c.isOurs, c.feePerKw, - htlc.Amount.ToSatoshis(), c.dustLimit) + isDust := htlcIsDust( + chanType, incoming, c.isOurs, c.feePerKw, + htlc.Amount.ToSatoshis(), c.dustLimit, + ) var err error switch { @@ -782,8 +784,10 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, // generate them in order to locate the outputs within the commitment // transaction. As we'll mark dust with a special output index in the // on-disk state snapshot. - isDustLocal := htlcIsDust(htlc.Incoming, true, feeRate, - htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit) + isDustLocal := htlcIsDust( + chanType, htlc.Incoming, true, feeRate, + htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit, + ) if !isDustLocal && localCommitKeys != nil { ourP2WSH, ourWitnessScript, err = genHtlcScript( chanType, htlc.Incoming, true, htlc.RefundTimeout, @@ -793,8 +797,10 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, return pd, err } } - isDustRemote := htlcIsDust(htlc.Incoming, false, feeRate, - htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit) + isDustRemote := htlcIsDust( + chanType, htlc.Incoming, false, feeRate, + htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit, + ) if !isDustRemote && remoteCommitKeys != nil { theirP2WSH, theirWitnessScript, err = genHtlcScript( chanType, htlc.Incoming, false, htlc.RefundTimeout, @@ -930,7 +936,8 @@ func (lc *LightningChannel) diskCommitToMemCommit(isLocal bool, // Finally, we'll re-populate the HTLC index for this state so we can // properly locate each HTLC within the commitment transaction. - if err := commit.populateHtlcIndexes(); err != nil { + err = commit.populateHtlcIndexes(lc.channelState.ChanType) + if err != nil { return nil, err } @@ -1410,8 +1417,10 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob)) copy(pd.OnionBlob[:], wireMsg.OnionBlob[:]) - isDustRemote := htlcIsDust(false, false, feeRate, - wireMsg.Amount.ToSatoshis(), remoteDustLimit) + isDustRemote := htlcIsDust( + lc.channelState.ChanType, false, false, feeRate, + wireMsg.Amount.ToSatoshis(), remoteDustLimit, + ) if !isDustRemote { theirP2WSH, theirWitnessScript, err := genHtlcScript( lc.channelState.ChanType, false, false, @@ -2168,7 +2177,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // If the HTLC is dust, then we'll skip it as it doesn't have // an output on the commitment transaction. if htlcIsDust( - htlc.Incoming, false, + chanState.ChanType, htlc.Incoming, false, chainfee.SatPerKWeight(revokedSnapshot.FeePerKw), htlc.Amt.ToSatoshis(), chanState.RemoteChanCfg.DustLimit, ) { @@ -2239,25 +2248,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, }, nil } -// htlcTimeoutFee returns the fee in satoshis required for an HTLC timeout -// transaction based on the current fee rate. -func htlcTimeoutFee(feePerKw chainfee.SatPerKWeight) btcutil.Amount { - return feePerKw.FeeForWeight(input.HtlcTimeoutWeight) -} - -// htlcSuccessFee returns the fee in satoshis required for an HTLC success -// transaction based on the current fee rate. -func htlcSuccessFee(feePerKw chainfee.SatPerKWeight) btcutil.Amount { - return feePerKw.FeeForWeight(input.HtlcSuccessWeight) -} - // htlcIsDust determines if an HTLC output is dust or not depending on two // bits: if the HTLC is incoming and if the HTLC will be placed on our // commitment transaction, or theirs. These two pieces of information are // require as we currently used second-level HTLC transactions as off-chain // covenants. Depending on the two bits, we'll either be using a timeout or // success transaction which have different weights. -func htlcIsDust(incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, +func htlcIsDust(chanType channeldb.ChannelType, + incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, htlcAmt, dustLimit btcutil.Amount) bool { // First we'll determine the fee required for this HTLC based on if this is @@ -2269,25 +2267,25 @@ func htlcIsDust(incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, // If this is an incoming HTLC on our commitment transaction, then the // second-level transaction will be a success transaction. case incoming && ourCommit: - htlcFee = htlcSuccessFee(feePerKw) + htlcFee = HtlcSuccessFee(chanType, feePerKw) // If this is an incoming HTLC on their commitment transaction, then // we'll be using a second-level timeout transaction as they've added // this HTLC. case incoming && !ourCommit: - htlcFee = htlcTimeoutFee(feePerKw) + htlcFee = HtlcTimeoutFee(chanType, feePerKw) // If this is an outgoing HTLC on our commitment transaction, then // we'll be using a timeout transaction as we're the sender of the // HTLC. case !incoming && ourCommit: - htlcFee = htlcTimeoutFee(feePerKw) + htlcFee = HtlcTimeoutFee(chanType, feePerKw) // If this is an outgoing HTLC on their commitment transaction, then // we'll be using an HTLC success transaction as they're the receiver // of this HTLC. case !incoming && !ourCommit: - htlcFee = htlcSuccessFee(feePerKw) + htlcFee = HtlcSuccessFee(chanType, feePerKw) } return (htlcAmt - htlcFee) < dustLimit @@ -2431,7 +2429,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, // Finally, we'll populate all the HTLC indexes so we can track the // locations of each HTLC in the commitment state. - if err := c.populateHtlcIndexes(); err != nil { + if err := c.populateHtlcIndexes(lc.channelState.ChanType); err != nil { return nil, err } @@ -2769,8 +2767,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // dust output after taking into account second-level HTLC fees, then a // sigJob will be generated and appended to the current batch. for _, htlc := range remoteCommitView.incomingHTLCs { - if htlcIsDust(true, false, feePerKw, htlc.Amount.ToSatoshis(), - dustLimit) { + if htlcIsDust( + chanType, true, false, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -2785,7 +2785,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // HTLC timeout transaction for them. The output of the timeout // transaction needs to account for fees, so we'll compute the // required fee and output now. - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee // With the fee calculate, we can properly create the HTLC @@ -2822,8 +2822,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, sigBatch = append(sigBatch, sigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { - if htlcIsDust(false, false, feePerKw, htlc.Amount.ToSatoshis(), - dustLimit) { + if htlcIsDust( + chanType, false, false, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -2836,7 +2838,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // HTLC success transaction for them. The output of the timeout // transaction needs to account for fees, so we'll compute the // required fee and output now. - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee // With the proper output amount calculated, we can now @@ -3785,16 +3787,20 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // weight, needed to calculate the transaction fee. var totalHtlcWeight int64 for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(remoteChain, !remoteChain, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + lc.channelState.ChanType, remoteChain, !remoteChain, + feePerKw, htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } totalHtlcWeight += input.HTLCWeight } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(!remoteChain, !remoteChain, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + lc.channelState.ChanType, !remoteChain, !remoteChain, + feePerKw, htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -3857,7 +3863,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, Index: uint32(htlc.localOutputIndex), } - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee successTx, err := createHtlcSuccessTx( @@ -3911,7 +3917,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, Index: uint32(htlc.localOutputIndex), } - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee timeoutTx, err := createHtlcTimeoutTx( @@ -5383,7 +5389,7 @@ func newOutgoingHtlcResolution(signer input.Signer, // In order to properly reconstruct the HTLC transaction, we'll need to // re-calculate the fee required at this state, so we can add the // correct output value amount to the transaction. - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee // With the fee calculated, re-construct the second level timeout @@ -5513,7 +5519,7 @@ func newIncomingHtlcResolution(signer input.Signer, // First, we'll reconstruct the original HTLC success transaction, // taking into account the fee rate used. - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee successTx, err := createHtlcSuccessTx( chanType, op, secondLevelOutputAmt, csvDelay, @@ -5637,8 +5643,10 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, // We'll skip any HTLC's which were dust on the commitment // transaction, as these don't have a corresponding output // within the commitment transaction. - if htlcIsDust(htlc.Incoming, ourCommit, feePerKw, - htlc.Amt.ToSatoshis(), dustLimit) { + if htlcIsDust( + chanType, htlc.Incoming, ourCommit, feePerKw, + htlc.Amt.ToSatoshis(), dustLimit, + ) { continue } @@ -6141,7 +6149,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, // For an extra HTLC fee to be paid on our commitment, the HTLC must be // large enough to make a non-dust HTLC timeout transaction. htlcFee := lnwire.NewMSatFromSatoshis( - htlcTimeoutFee(feePerKw), + HtlcTimeoutFee(lc.channelState.ChanType, feePerKw), ) // If we are looking at the remote commitment, we must use the remote @@ -6151,7 +6159,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, lc.channelState.RemoteChanCfg.DustLimit, ) htlcFee = lnwire.NewMSatFromSatoshis( - htlcSuccessFee(feePerKw), + HtlcSuccessFee(lc.channelState.ChanType, feePerKw), ) } diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index c4c88670..511631bb 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -1011,7 +1011,8 @@ func TestHTLCDustLimit(t *testing.T) { // The amount of the HTLC should be above Alice's dust limit and below // Bob's dust limit. - htlcSat := (btcutil.Amount(500) + htlcTimeoutFee( + htlcSat := (btcutil.Amount(500) + HtlcTimeoutFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), @@ -1119,8 +1120,12 @@ func TestHTLCSigNumber(t *testing.T) { t.Fatalf("unable to get fee: %v", err) } - belowDust := btcutil.Amount(500) + htlcTimeoutFee(feePerKw) - aboveDust := btcutil.Amount(1400) + htlcSuccessFee(feePerKw) + belowDust := btcutil.Amount(500) + HtlcTimeoutFee( + channeldb.SingleFunderTweaklessBit, feePerKw, + ) + aboveDust := btcutil.Amount(1400) + HtlcSuccessFee( + channeldb.SingleFunderTweaklessBit, feePerKw, + ) // =================================================================== // Test that Bob will reject a commitment if Alice doesn't send enough @@ -1278,7 +1283,8 @@ func TestChannelBalanceDustLimit(t *testing.T) { defaultFee := calcStaticFee(1) aliceBalance := aliceChannel.channelState.LocalCommitment.LocalBalance.ToSatoshis() htlcSat := aliceBalance - defaultFee - htlcSat += htlcSuccessFee( + htlcSat += HtlcSuccessFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), @@ -4759,10 +4765,10 @@ func TestChanAvailableBalanceNearHtlcFee(t *testing.T) { aliceChannel.channelState.LocalCommitment.CommitFee, ) htlcTimeoutFee := lnwire.NewMSatFromSatoshis( - htlcTimeoutFee(feeRate), + HtlcTimeoutFee(aliceChannel.channelState.ChanType, feeRate), ) htlcSuccessFee := lnwire.NewMSatFromSatoshis( - htlcSuccessFee(feeRate), + HtlcSuccessFee(aliceChannel.channelState.ChanType, feeRate), ) // Helper method to check the current reported balance. @@ -6273,7 +6279,8 @@ func TestChanReserveLocalInitiatorDustHtlc(t *testing.T) { // limit (1300 sat). It is considered dust if the amount remaining // after paying the HTLC fee is below the dustlimit, so we choose a // size of 500+htlcFee. - htlcSat := btcutil.Amount(500) + htlcTimeoutFee( + htlcSat := btcutil.Amount(500) + HtlcTimeoutFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index abe9288e..077b6da9 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -255,6 +255,29 @@ func CommitWeight(chanType channeldb.ChannelType) int64 { return input.CommitWeight } +// HtlcTimeoutFee returns the fee in satoshis required for an HTLC timeout +// transaction based on the current fee rate. +func HtlcTimeoutFee(chanType channeldb.ChannelType, + feePerKw chainfee.SatPerKWeight) btcutil.Amount { + + if chanType.HasAnchors() { + return feePerKw.FeeForWeight(input.HtlcTimeoutWeightConfirmed) + } + + return feePerKw.FeeForWeight(input.HtlcTimeoutWeight) +} + +// HtlcSuccessFee returns the fee in satoshis required for an HTLC success +// transaction based on the current fee rate. +func HtlcSuccessFee(chanType channeldb.ChannelType, + feePerKw chainfee.SatPerKWeight) btcutil.Amount { + + if chanType.HasAnchors() { + return feePerKw.FeeForWeight(input.HtlcSuccessWeightConfirmed) + } + return feePerKw.FeeForWeight(input.HtlcSuccessWeight) +} + // CommitScriptAnchors return the scripts to use for the local and remote // anchor. func CommitScriptAnchors(localChanCfg, @@ -373,18 +396,20 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, numHTLCs := int64(0) for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(false, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { - + if htlcIsDust( + cb.chanState.ChanType, false, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } numHTLCs++ } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(true, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { - + if htlcIsDust( + cb.chanState.ChanType, true, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -460,8 +485,10 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // purposes of sorting. cltvs := make([]uint32, len(commitTx.TxOut)) for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(false, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + cb.chanState.ChanType, false, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -475,8 +502,10 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, cltvs = append(cltvs, htlc.Timeout) } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(true, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + cb.chanState.ChanType, true, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue }