-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
htlcswitch+routerrpc: support unencrypted failure reasons #7067
base: master
Are you sure you want to change the base?
Changes from all commits
7293be8
d7af064
6279e47
3d8cd01
569650f
14047d4
2f91905
58dfa61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,10 +1,12 @@ | ||||
package htlcswitch | ||||
|
||||
import ( | ||||
"bytes" | ||||
"crypto/sha256" | ||||
"fmt" | ||||
"sync" | ||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa" | ||||
"github.com/go-errors/errors" | ||||
"github.com/lightningnetwork/lnd/chainntnfs" | ||||
"github.com/lightningnetwork/lnd/channeldb/models" | ||||
|
@@ -83,6 +85,11 @@ type InterceptableSwitch struct { | |||
// currentHeight is the currently best known height. | ||||
currentHeight int32 | ||||
|
||||
// signChannelUpdate is used when an intercepting application includes | ||||
// an unsigned channel update to be signed by us. | ||||
signChannelUpdate func(u *lnwire.ChannelUpdate) (*ecdsa.Signature, | ||||
error) | ||||
|
||||
wg sync.WaitGroup | ||||
quit chan struct{} | ||||
} | ||||
|
@@ -119,9 +126,15 @@ type FwdResolution struct { | |||
// FwdActionSettle. | ||||
Preimage lntypes.Preimage | ||||
|
||||
// FailureMessage is the encrypted failure message that is to be passed | ||||
// back to the sender if action is FwdActionFail. | ||||
FailureMessage []byte | ||||
// EncryptedFailureMessage is the encrypted failure message that is to | ||||
// be passed back to the sender if action is FwdActionFail. This field | ||||
// is mutually exclusive with FailureMessage. | ||||
EncryptedFailureMessage []byte | ||||
|
||||
// FailureMessage is the decoded failure message that is to be encrypted | ||||
// for the first hop. This field is mutually exclusive with | ||||
// EncryptedFailureMessage. | ||||
FailureMessage lnwire.FailureMessage | ||||
|
||||
// FailureCode is the failure code that is to be passed back to the | ||||
// sender if action is FwdActionFail. | ||||
|
@@ -158,6 +171,11 @@ type InterceptableSwitchConfig struct { | |||
// RequireInterceptor indicates whether processing should block if no | ||||
// interceptor is connected. | ||||
RequireInterceptor bool | ||||
|
||||
// SignChannelUpdate is used when an intercepting application includes | ||||
// an unsigned channel update to be signed by us. | ||||
SignChannelUpdate func(u *lnwire.ChannelUpdate) (*ecdsa.Signature, | ||||
joostjager marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
error) | ||||
} | ||||
|
||||
// NewInterceptableSwitch returns an instance of InterceptableSwitch. | ||||
|
@@ -181,6 +199,7 @@ func NewInterceptableSwitch(cfg *InterceptableSwitchConfig) ( | |||
cltvRejectDelta: cfg.CltvRejectDelta, | ||||
cltvInterceptDelta: cfg.CltvInterceptDelta, | ||||
notifier: cfg.Notifier, | ||||
signChannelUpdate: cfg.SignChannelUpdate, | ||||
|
||||
quit: make(chan struct{}), | ||||
}, nil | ||||
|
@@ -377,17 +396,115 @@ func (s *InterceptableSwitch) resolve(res *FwdResolution) error { | |||
return intercepted.Settle(res.Preimage) | ||||
|
||||
case FwdActionFail: | ||||
if len(res.FailureMessage) > 0 { | ||||
return intercepted.Fail(res.FailureMessage) | ||||
} | ||||
switch { | ||||
// Fail with encrypted failure message. | ||||
case len(res.EncryptedFailureMessage) > 0: | ||||
return intercepted.Fail( | ||||
res.EncryptedFailureMessage, false, | ||||
) | ||||
|
||||
// Fail with known failure message that is to be encoded and | ||||
// encrypted. | ||||
case res.FailureMessage != nil: | ||||
msg := res.FailureMessage | ||||
|
||||
// Re-sign the channel update if present. Note that this | ||||
// changes the passed in FwdResolution. | ||||
update := getChannelUpdateRef(msg) | ||||
if update != nil { | ||||
err := s.validateChannelUpdate(update) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
err = s.resignChannelUpdate(update) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also commit this update to disk as well? Apologies if this was already an older review comment... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because the real node isn't aware of this virtual channel at all. It's all up to the external interceptor logic to keep track of the virtual channel properties. As mentioned in #7067 (comment), the external interceptor is taking over part of the responsibilities of the switch. |
||||
if err != nil { | ||||
return err | ||||
} | ||||
} | ||||
|
||||
return intercepted.FailWithCode(res.FailureCode) | ||||
var encodedMsg bytes.Buffer | ||||
err := lnwire.EncodeFailureMessage( | ||||
&encodedMsg, msg, 0, | ||||
) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
return intercepted.Fail( | ||||
encodedMsg.Bytes(), true, | ||||
) | ||||
|
||||
// Fail with failure code. | ||||
default: | ||||
return intercepted.FailWithCode(res.FailureCode) | ||||
} | ||||
|
||||
default: | ||||
return fmt.Errorf("unrecognized action %v", res.Action) | ||||
} | ||||
} | ||||
|
||||
func (s *InterceptableSwitch) validateChannelUpdate( | ||||
update *lnwire.ChannelUpdate) error { | ||||
|
||||
// The maxHTLC flag is mandatory. | ||||
if !update.MessageFlags.HasMaxHtlc() { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More validation is needed here, eg: max HTLC shouldn't be greater than the channel capacity, etc, etc. I think we should just call the normal validation routine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wanted to call Line 165 in bf5aab9
But that would create a circular reference. For now, I just copied over the additional If desired I can do a bigger refactor and move the validation logic into |
||||
return errors.Errorf("max htlc flag not set for channel") | ||||
} | ||||
|
||||
// Check that max htlc is at least min htlc. | ||||
maxHtlc := update.HtlcMaximumMsat | ||||
if maxHtlc == 0 || maxHtlc < update.HtlcMinimumMsat { | ||||
return errors.Errorf("invalid max htlc for channel update ") | ||||
} | ||||
|
||||
return nil | ||||
} | ||||
|
||||
// resignChannelUpdate signs the provided channel update with our node key. | ||||
func (s *InterceptableSwitch) resignChannelUpdate( | ||||
update *lnwire.ChannelUpdate) error { | ||||
|
||||
sig, err := s.signChannelUpdate(update) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
update.Signature, err = lnwire.NewSigFromSignature(sig) | ||||
if err != nil { | ||||
return nil | ||||
} | ||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't commit it. Even if we do decide to allow this, we also need to make sure that the update would actually validate in the first place (has all the needed fields, bounds respected, etc). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean commit as in write to disk? As explained in #7067 (comment), it is the intention to just pass through without persisting. Added a validation function that carries out the checks from |
||||
return nil | ||||
} | ||||
|
||||
// getChannelUpdateRef returns a reference to an embedded channel update if | ||||
// present in the failure message. | ||||
func getChannelUpdateRef(msg lnwire.FailureMessage) *lnwire.ChannelUpdate { | ||||
switch m := msg.(type) { | ||||
case *lnwire.FailFeeInsufficient: | ||||
return &m.Update | ||||
|
||||
case *lnwire.FailIncorrectCltvExpiry: | ||||
return &m.Update | ||||
|
||||
case *lnwire.FailTemporaryChannelFailure: | ||||
return m.Update | ||||
|
||||
case *lnwire.FailAmountBelowMinimum: | ||||
return &m.Update | ||||
|
||||
case *lnwire.FailExpiryTooSoon: | ||||
return &m.Update | ||||
|
||||
case *lnwire.FailChannelDisabled: | ||||
return &m.Update | ||||
} | ||||
|
||||
return nil | ||||
} | ||||
|
||||
// Resolve resolves an intercepted packet. | ||||
func (s *InterceptableSwitch) Resolve(res *FwdResolution) error { | ||||
internalRes := &fwdResolution{ | ||||
|
@@ -615,10 +732,24 @@ func (f *interceptedForward) Resume() error { | |||
return f.htlcSwitch.ForwardPackets(nil, f.packet) | ||||
} | ||||
|
||||
// Fail notifies the intention to Fail an existing hold forward with an | ||||
// encrypted failure reason. | ||||
func (f *interceptedForward) Fail(reason []byte) error { | ||||
obfuscatedReason := f.packet.obfuscator.IntermediateEncrypt(reason) | ||||
// Fail notifies the intention to Fail an existing hold forward. | ||||
func (f *interceptedForward) Fail(reason []byte, encryptFirstHop bool) error { | ||||
var ( | ||||
obfuscatedReason []byte | ||||
obfuscator = f.packet.obfuscator | ||||
) | ||||
|
||||
if encryptFirstHop { | ||||
var err error | ||||
obfuscatedReason, err = obfuscator.EncryptEncodedFirstHop( | ||||
reason, | ||||
) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
} else { | ||||
obfuscatedReason = obfuscator.IntermediateEncrypt(reason) | ||||
} | ||||
|
||||
return f.resolve(&lnwire.UpdateFailHTLC{ | ||||
Reason: obfuscatedReason, | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, shouldn't we assume that they want to instead send the latest update? Otherwise i can see this causing an error where: user sends custom channel update, the one on disk conflicts, so routing errors occur.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can prevent this in the forwarding level during the validation of the returned message. If it's an encoded channel update, then it needs to match what we'd send out anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The context in which this is to be used is to update the routing policy of a virtual channel. The channel isn't broadcasted through gossip and doesn't exist in the graph on disk. There is no 'latest update'.
The existence of the virtual channel is solely communicated to the payer through invoice route hints. If the receiver (the virtual hop) changes its policy, the channel update contained in the failure message updates the sender with the new values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming the virtual channel has a distinct pubkey (and also shared secret in the onion), then how would this work in practice? lnd would use diff logic for if it was the final hop vs the penultimate hop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If node N is the virtual node, then it is the job of node N-1 to return a channel update. The example is again
fee_insufficient
.Real node N-1 doesn't know about this channel at all (it is virtual), so what happens is that the virtual node N returns a channel update for the virtual channel to N-1, and N-1 just signs the update.