diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 0aa20772..68fac720 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -2354,228 +2354,20 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, fwdInfo := chanIterator.ForwardingInstructions() switch fwdInfo.NextHop { case exitHop: - // If hodl.ExitSettle is requested, we will not validate - // the final hop's ADD, nor will we settle the - // corresponding invoice or respond with the preimage. - if l.cfg.DebugHTLC && - l.cfg.HodlMask.Active(hodl.ExitSettle) { - l.warnf(hodl.ExitSettle.Warning()) - continue - } - - // First, we'll check the expiry of the HTLC itself - // against, the current block height. If the timeout is - // too soon, then we'll reject the HTLC. - if pd.Timeout-expiryGraceDelta <= heightNow { - log.Errorf("htlc(%x) has an expiry that's too "+ - "soon: expiry=%v, best_height=%v", - pd.RHash[:], pd.Timeout, heightNow) - - failure := lnwire.FailFinalExpiryTooSoon{} - l.sendHTLCError( - pd.HtlcIndex, &failure, obfuscator, pd.SourceRef, - ) - needUpdate = true - continue - } - - // We're the designated payment destination. Therefore - // we attempt to see if we have an invoice locally - // which'll allow us to settle this htlc. - invoiceHash := lntypes.Hash(pd.RHash) - invoice, minCltvDelta, err := l.cfg.Registry.LookupInvoice( - invoiceHash, - ) - if err != nil { - log.Errorf("unable to query invoice registry: "+ - " %v", err) - failure := lnwire.NewFailUnknownPaymentHash( - pd.Amount, - ) - l.sendHTLCError( - pd.HtlcIndex, failure, obfuscator, pd.SourceRef, - ) - - needUpdate = true - continue - } - - // Reject htlcs for canceled invoices. - if invoice.Terms.State == channeldb.ContractCanceled { - l.errorf("Rejecting htlc due to canceled " + - "invoice") - - failure := lnwire.NewFailUnknownPaymentHash( - pd.Amount, - ) - l.sendHTLCError( - pd.HtlcIndex, failure, obfuscator, - pd.SourceRef, - ) - - needUpdate = true - continue - } - - // If the invoice is already settled, we choose to - // accept the payment to simplify failure recovery. - // - // NOTE: Though our recovery and forwarding logic is - // predominately batched, settling invoices happens - // iteratively. We may reject one of two payments - // for the same rhash at first, but then restart and - // reject both after seeing that the invoice has been - // settled. Without any record of which one settles - // first, it is ambiguous as to which one actually - // settled the invoice. Thus, by accepting all - // payments, we eliminate the race condition that can - // lead to this inconsistency. - // - // TODO(conner): track ownership of settlements to - // properly recover from failures? or add batch invoice - // settlement - if invoice.Terms.State != channeldb.ContractOpen { - log.Warnf("Accepting duplicate payment for "+ - "hash=%x", pd.RHash[:]) - } - - // If we're not currently in debug mode, and the - // extended htlc doesn't meet the value requested, then - // we'll fail the htlc. Otherwise, we settle this htlc - // within our local state update log, then send the - // update entry to the remote party. - // - // NOTE: We make an exception when the value requested - // by the invoice is zero. This means the invoice - // allows the payee to specify the amount of satoshis - // they wish to send. So since we expect the htlc to - // have a different amount, we should not fail. - if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 && - pd.Amount < invoice.Terms.Value { - - log.Errorf("rejecting htlc due to incorrect "+ - "amount: expected %v, received %v", - invoice.Terms.Value, pd.Amount) - - failure := lnwire.NewFailUnknownPaymentHash( - pd.Amount, - ) - l.sendHTLCError( - pd.HtlcIndex, failure, obfuscator, pd.SourceRef, - ) - - needUpdate = true - continue - } - - // As we're the exit hop, we'll double check the - // hop-payload included in the HTLC to ensure that it - // was crafted correctly by the sender and matches the - // HTLC we were extended. - // - // NOTE: We make an exception when the value requested - // by the invoice is zero. This means the invoice - // allows the payee to specify the amount of satoshis - // they wish to send. So since we expect the htlc to - // have a different amount, we should not fail. - if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 && - fwdInfo.AmountToForward < invoice.Terms.Value { - - log.Errorf("Onion payload of incoming htlc(%x) "+ - "has incorrect value: expected %v, "+ - "got %v", pd.RHash, invoice.Terms.Value, - fwdInfo.AmountToForward) - - failure := lnwire.NewFailUnknownPaymentHash( - pd.Amount, - ) - l.sendHTLCError( - pd.HtlcIndex, failure, obfuscator, pd.SourceRef, - ) - - needUpdate = true - continue - } - - // We'll also ensure that our time-lock value has been - // computed correctly. - expectedHeight := heightNow + minCltvDelta - switch { - case !l.cfg.DebugHTLC && pd.Timeout < expectedHeight: - log.Errorf("Incoming htlc(%x) has an "+ - "expiration that is too soon: "+ - "expected at least %v, got %v", - pd.RHash[:], expectedHeight, pd.Timeout) - - failure := lnwire.FailFinalExpiryTooSoon{} - l.sendHTLCError( - pd.HtlcIndex, failure, obfuscator, - pd.SourceRef, - ) - - needUpdate = true - continue - - case !l.cfg.DebugHTLC && pd.Timeout != fwdInfo.OutgoingCTLV: - log.Errorf("HTLC(%x) has incorrect "+ - "time-lock: expected %v, got %v", - pd.RHash[:], pd.Timeout, - fwdInfo.OutgoingCTLV) - - failure := lnwire.NewFinalIncorrectCltvExpiry( - fwdInfo.OutgoingCTLV, - ) - l.sendHTLCError( - pd.HtlcIndex, failure, obfuscator, pd.SourceRef, - ) - - needUpdate = true - continue - } - - preimage := invoice.Terms.PaymentPreimage - err = l.channel.SettleHTLC( - preimage, pd.HtlcIndex, pd.SourceRef, nil, nil, + updated, err := l.processExitHop( + pd, obfuscator, fwdInfo, heightNow, ) if err != nil { l.fail(LinkFailureError{code: ErrInternalError}, - "unable to settle htlc: %v", err) + err.Error(), + ) + return false } - - // Notify the invoiceRegistry of the invoices we just - // settled (with the amount accepted at settle time) - // with this latest commitment update. - err = l.cfg.Registry.SettleInvoice( - invoiceHash, pd.Amount, - ) - if err != nil { - l.fail(LinkFailureError{code: ErrInternalError}, - "unable to settle invoice: %v", err) - return false + if updated { + needUpdate = true } - l.infof("settling %x as exit hop", pd.RHash) - - // If the link is in hodl.BogusSettle mode, replace the - // preimage with a fake one before sending it to the - // peer. - if l.cfg.DebugHTLC && - l.cfg.HodlMask.Active(hodl.BogusSettle) { - l.warnf(hodl.BogusSettle.Warning()) - preimage = [32]byte{} - copy(preimage[:], bytes.Repeat([]byte{2}, 32)) - } - - // HTLC was successfully settled locally send - // notification about it remote peer. - l.cfg.Peer.SendMessage(false, &lnwire.UpdateFulfillHTLC{ - ChanID: l.ChanID(), - ID: pd.HtlcIndex, - PaymentPreimage: preimage, - }) - needUpdate = true - // There are additional channels left within this route. So // we'll simply do some forwarding package book-keeping. default: @@ -2733,6 +2525,178 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, return needUpdate } +// processExitHop handles an htlc for which this link is the exit hop. It +// returns a boolean indicating whether the commitment tx needs an update. +func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor, + obfuscator ErrorEncrypter, fwdInfo ForwardingInfo, heightNow uint32) ( + bool, error) { + + // If hodl.ExitSettle is requested, we will not validate the final hop's + // ADD, nor will we settle the corresponding invoice or respond with the + // preimage. + if l.cfg.DebugHTLC && l.cfg.HodlMask.Active(hodl.ExitSettle) { + l.warnf(hodl.ExitSettle.Warning()) + + return false, nil + } + + // First, we'll check the expiry of the HTLC itself against, the current + // block height. If the timeout is too soon, then we'll reject the HTLC. + if pd.Timeout-expiryGraceDelta <= heightNow { + log.Errorf("htlc(%x) has an expiry that's too soon: expiry=%v"+ + ", best_height=%v", pd.RHash[:], pd.Timeout, heightNow) + + failure := lnwire.NewFinalExpiryTooSoon() + l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef) + + return true, nil + } + + // We're the designated payment destination. Therefore we attempt to + // see if we have an invoice locally which'll allow us to settle this + // htlc. + invoiceHash := lntypes.Hash(pd.RHash) + invoice, minCltvDelta, err := l.cfg.Registry.LookupInvoice(invoiceHash) + if err != nil { + log.Errorf("unable to query invoice registry: %v", err) + failure := lnwire.NewFailUnknownPaymentHash(pd.Amount) + l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef) + + return true, nil + } + + // Reject htlcs for canceled invoices. + if invoice.Terms.State == channeldb.ContractCanceled { + l.errorf("Rejecting htlc due to canceled invoice") + + failure := lnwire.NewFailUnknownPaymentHash(pd.Amount) + l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef) + + return true, nil + } + + // If the invoice is already settled, we choose to accept the payment to + // simplify failure recovery. + // + // NOTE: Though our recovery and forwarding logic is predominately + // batched, settling invoices happens iteratively. We may reject one of + // two payments for the same rhash at first, but then restart and reject + // both after seeing that the invoice has been settled. Without any + // record of which one settles first, it is ambiguous as to which one + // actually settled the invoice. Thus, by accepting all payments, we + // eliminate the race condition that can lead to this inconsistency. + // + // TODO(conner): track ownership of settlements to properly recover from + // failures? or add batch invoice settlement + if invoice.Terms.State != channeldb.ContractOpen { + log.Warnf("Accepting duplicate payment for hash=%x", + pd.RHash[:]) + } + + // If we're not currently in debug mode, and the extended htlc doesn't + // meet the value requested, then we'll fail the htlc. Otherwise, we + // settle this htlc within our local state update log, then send the + // update entry to the remote party. + // + // NOTE: We make an exception when the value requested by the invoice is + // zero. This means the invoice allows the payee to specify the amount + // of satoshis they wish to send. So since we expect the htlc to have a + // different amount, we should not fail. + if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 && + pd.Amount < invoice.Terms.Value { + + log.Errorf("rejecting htlc due to incorrect amount: expected "+ + "%v, received %v", invoice.Terms.Value, pd.Amount) + + failure := lnwire.NewFailUnknownPaymentHash(pd.Amount) + l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef) + + return true, nil + } + + // As we're the exit hop, we'll double check the hop-payload included in + // the HTLC to ensure that it was crafted correctly by the sender and + // matches the HTLC we were extended. + // + // NOTE: We make an exception when the value requested by the invoice is + // zero. This means the invoice allows the payee to specify the amount + // of satoshis they wish to send. So since we expect the htlc to have a + // different amount, we should not fail. + if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 && + fwdInfo.AmountToForward < invoice.Terms.Value { + + log.Errorf("Onion payload of incoming htlc(%x) has incorrect "+ + "value: expected %v, got %v", pd.RHash, + invoice.Terms.Value, fwdInfo.AmountToForward) + + failure := lnwire.NewFailUnknownPaymentHash(pd.Amount) + l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef) + + return true, nil + } + + // We'll also ensure that our time-lock value has been computed + // correctly. + expectedHeight := heightNow + minCltvDelta + switch { + case !l.cfg.DebugHTLC && pd.Timeout < expectedHeight: + log.Errorf("Incoming htlc(%x) has an expiration that is too "+ + "soon: expected at least %v, got %v", + pd.RHash[:], expectedHeight, pd.Timeout) + + failure := lnwire.FailFinalExpiryTooSoon{} + l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef) + + return true, nil + + case !l.cfg.DebugHTLC && pd.Timeout != fwdInfo.OutgoingCTLV: + log.Errorf("HTLC(%x) has incorrect time-lock: expected %v, "+ + "got %v", pd.RHash[:], pd.Timeout, fwdInfo.OutgoingCTLV) + + failure := lnwire.NewFinalIncorrectCltvExpiry( + fwdInfo.OutgoingCTLV, + ) + l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef) + + return true, nil + } + + preimage := invoice.Terms.PaymentPreimage + err = l.channel.SettleHTLC( + preimage, pd.HtlcIndex, pd.SourceRef, nil, nil, + ) + if err != nil { + return false, fmt.Errorf("unable to settle htlc: %v", err) + } + + // Notify the invoiceRegistry of the invoices we just settled (with the + // amount accepted at settle time) with this latest commitment update. + err = l.cfg.Registry.SettleInvoice(invoiceHash, pd.Amount) + if err != nil { + return false, fmt.Errorf("unable to settle invoice: %v", err) + } + + l.infof("settling %x as exit hop", pd.RHash) + + // If the link is in hodl.BogusSettle mode, replace the preimage with a + // fake one before sending it to the peer. + if l.cfg.DebugHTLC && l.cfg.HodlMask.Active(hodl.BogusSettle) { + l.warnf(hodl.BogusSettle.Warning()) + preimage = [32]byte{} + copy(preimage[:], bytes.Repeat([]byte{2}, 32)) + } + + // HTLC was successfully settled locally send notification about it + // remote peer. + l.cfg.Peer.SendMessage(false, &lnwire.UpdateFulfillHTLC{ + ChanID: l.ChanID(), + ID: pd.HtlcIndex, + PaymentPreimage: preimage, + }) + + return true, nil +} + // forwardBatch forwards the given htlcPackets to the switch, and waits on the // err chan for the individual responses. This method is intended to be spawned // as a goroutine so the responses can be handled in the background.