multi: return route role from HopPayload

We need to know what role we're playing to be able to handle errors
correctly, but the information that we need for this is held by our
iterator:
- Whether we had a blinding point in update add (blinding kit)
- Whether we had a blinding point in payload

As we're now going to use the route role return value even when our
err!=nil, we rename the error to signal that we're using less
canonical golang here.

An alternative to this approach is to attach a RouteRole to our
ErrInvalidPayload. The downside of that approach is:
- Propagate context through parsing (whether we had updateAddHtlc)
- Clumsy handling for errors that are not of type ErrInvalidPayload
This commit is contained in:
Carla Kirk-Cohen 2024-04-25 09:46:31 -04:00
parent b81a6f3d2f
commit 776c889267
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
6 changed files with 110 additions and 24 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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