diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index e73e3e45b..b104f8d70 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -543,7 +543,7 @@ func (h *htlcIncomingContestResolver) decodePayload() (*hop.Payload, return nil, nil, err } - payload, err := iterator.HopPayload() + payload, _, err := iterator.HopPayload() if err != nil { return nil, nil, err } diff --git a/contractcourt/htlc_incoming_contest_resolver_test.go b/contractcourt/htlc_incoming_contest_resolver_test.go index 6c4ba6bc4..55d93a6fb 100644 --- a/contractcourt/htlc_incoming_contest_resolver_test.go +++ b/contractcourt/htlc_incoming_contest_resolver_test.go @@ -263,7 +263,7 @@ type mockHopIterator struct { hop.Iterator } -func (h *mockHopIterator) HopPayload() (*hop.Payload, error) { +func (h *mockHopIterator) HopPayload() (*hop.Payload, hop.RouteRole, error) { var nextAddress [8]byte if !h.isExit { nextAddress = [8]byte{0x01} @@ -275,7 +275,7 @@ func (h *mockHopIterator) HopPayload() (*hop.Payload, error) { ForwardAmount: 100, OutgoingCltv: 40, ExtraBytes: [12]byte{}, - }), nil + }), hop.RouteRoleCleartext, nil } func (h *mockHopIterator) EncodeNextHop(w io.Writer) error { diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index 7ba0e9dd8..c387bae3a 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -24,6 +24,62 @@ var ( "blinded hop") ) +// RouteRole represents the different types of roles a node can have as a +// recipient of a HTLC. +type RouteRole uint8 + +const ( + // RouteRoleCleartext represents a regular route hop. + RouteRoleCleartext RouteRole = iota + + // RouteRoleIntroduction represents an introduction node in a blinded + // path, characterized by a blinding point in the onion payload. + RouteRoleIntroduction + + // RouteRoleRelaying represents a relaying node in a blinded path, + // characterized by a blinding point in update_add_htlc. + RouteRoleRelaying +) + +// String representation of a role in a route. +func (h RouteRole) String() string { + switch h { + case RouteRoleCleartext: + return "cleartext" + + case RouteRoleRelaying: + return "blinded relay" + + case RouteRoleIntroduction: + return "introduction node" + + default: + return fmt.Sprintf("unknown route role: %d", h) + } +} + +// NewRouteRole returns the role we're playing in a route depending on the +// blinding points set (or not). If we are in the situation where we received +// blinding points in both the update add message and the payload: +// - We must have had a valid update add blinding point, because we were able +// to decrypt our onion to get the payload blinding point. +// - We return a relaying node role, because an introduction node (by +// definition) does not receive a blinding point in update add. +// - We assume the sending node to be buggy (including a payload blinding +// where it shouldn't), and rely on validation elsewhere to handle this. +func NewRouteRole(updateAddBlinding, payloadBlinding bool) RouteRole { + switch { + case updateAddBlinding: + return RouteRoleRelaying + + case payloadBlinding: + return RouteRoleIntroduction + + default: + return RouteRoleCleartext + } +} + // Iterator is an interface that abstracts away the routing information // included in HTLC's which includes the entirety of the payment path of an // HTLC. This interface provides two basic method which carry out: how to @@ -35,8 +91,11 @@ type Iterator interface { // information encoded within the returned ForwardingInfo is to be used // by each hop to authenticate the information given to it by the prior // hop. The payload will also contain any additional TLV fields provided - // by the sender. - HopPayload() (*Payload, error) + // by the sender. The role that this hop plays in the context of + // route blinding (regular, introduction or relaying) is returned + // whenever the payload is successfully parsed, even if we subsequently + // face a validation error. + HopPayload() (*Payload, RouteRole, error) // EncodeNextHop encodes the onion packet destined for the next hop // into the passed io.Writer. @@ -95,18 +154,21 @@ func (r *sphinxHopIterator) EncodeNextHop(w io.Writer) error { // HopPayload returns the set of fields that detail exactly _how_ this hop // should forward the HTLC to the next hop. Additionally, the information // encoded within the returned ForwardingInfo is to be used by each hop to -// authenticate the information given to it by the prior hop. The payload will -// also contain any additional TLV fields provided by the sender. +// authenticate the information given to it by the prior hop. The role that +// this hop plays in the context of route blinding (regular, introduction or +// relaying) is returned whenever the payload is successfully parsed, even if +// we subsequently face a validation error. The payload will also contain any +// additional TLV fields provided by the sender. // // NOTE: Part of the HopIterator interface. -func (r *sphinxHopIterator) HopPayload() (*Payload, error) { +func (r *sphinxHopIterator) HopPayload() (*Payload, RouteRole, error) { switch r.processedPacket.Payload.Type { // If this is the legacy payload, then we'll extract the information // directly from the pre-populated ForwardingInstructions field. case sphinx.PayloadLegacy: fwdInst := r.processedPacket.ForwardingInstructions - return NewLegacyPayload(fwdInst), nil + return NewLegacyPayload(fwdInst), RouteRoleCleartext, nil // Otherwise, if this is the TLV payload, then we'll make a new stream // to decode only what we need to make routing decisions. @@ -116,14 +178,32 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, error) { bytes.NewReader(r.processedPacket.Payload.Payload), ) if err != nil { - return nil, err + // If we couldn't even parse our payload then we do + // a best-effort of determining our role in a blinded + // route, accepting that we can't know whether we + // were the introduction node (as the payload + // is not parseable). + routeRole := RouteRoleCleartext + if r.blindingKit.UpdateAddBlinding.IsSome() { + routeRole = RouteRoleRelaying + } + + return nil, routeRole, err } + // Now that we've parsed our payload we can determine which + // role we're playing in the route. + _, payloadBlinding := parsed[record.BlindingPointOnionType] + routeRole := NewRouteRole( + r.blindingKit.UpdateAddBlinding.IsSome(), + payloadBlinding, + ) + if err := ValidateTLVPayload( parsed, isFinal, r.blindingKit.UpdateAddBlinding.IsSome(), ); err != nil { - return nil, err + return nil, routeRole, err } // If we had an encrypted data payload present, pull out our @@ -133,17 +213,18 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, error) { payload, isFinal, parsed, ) if err != nil { - return nil, err + return nil, routeRole, err } payload.FwdInfo = *fwdInfo } - return payload, err + return payload, routeRole, nil default: - return nil, fmt.Errorf("unknown sphinx payload type: %v", - r.processedPacket.Payload.Type) + return nil, RouteRoleCleartext, + fmt.Errorf("unknown sphinx payload type: %v", + r.processedPacket.Payload.Type) } } diff --git a/htlcswitch/hop/iterator_test.go b/htlcswitch/hop/iterator_test.go index a850c6dc1..e69361b0a 100644 --- a/htlcswitch/hop/iterator_test.go +++ b/htlcswitch/hop/iterator_test.go @@ -88,10 +88,10 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) { for i, testCase := range testCases { iterator.processedPacket = testCase.sphinxPacket - pld, err := iterator.HopPayload() - if err != nil { + pld, _, pldErr := iterator.HopPayload() + if pldErr != nil { t.Fatalf("#%v: unable to extract forwarding "+ - "instructions: %v", i, err) + "instructions: %v", i, pldErr) } fwdInfo := pld.ForwardingInfo() diff --git a/htlcswitch/link.go b/htlcswitch/link.go index fd87c2f29..5f250636d 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -3293,14 +3293,18 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, heightNow := l.cfg.BestHeight() - pld, err := chanIterator.HopPayload() - if err != nil { + pld, _, pldErr := chanIterator.HopPayload() + if pldErr != nil { // If we're unable to process the onion payload, or we // received invalid onion payload failure, then we // should send an error back to the caller so the HTLC // can be canceled. var failedType uint64 - if e, ok := err.(hop.ErrInvalidPayload); ok { + + // We need to get the underlying error value, so we + // can't use errors.As as suggested by the linter. + //nolint:errorlint + if e, ok := pldErr.(hop.ErrInvalidPayload); ok { failedType = uint64(e.Type) } @@ -3316,7 +3320,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, ) l.log.Errorf("unable to decode forwarding "+ - "instructions: %v", err) + "instructions: %v", pldErr) + continue } diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index dfa834c58..0dbefd45c 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -330,10 +330,10 @@ func newMockHopIterator(hops ...*hop.Payload) hop.Iterator { return &mockHopIterator{hops: hops} } -func (r *mockHopIterator) HopPayload() (*hop.Payload, error) { +func (r *mockHopIterator) HopPayload() (*hop.Payload, hop.RouteRole, error) { h := r.hops[0] r.hops = r.hops[1:] - return h, nil + return h, hop.RouteRoleCleartext, nil } func (r *mockHopIterator) ExtraOnionBlob() []byte {